Skip to content

Commit

Permalink
amp-mustache extended template implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
Dima Voytenko committed Nov 12, 2015
1 parent b48ca65 commit a379179
Show file tree
Hide file tree
Showing 7 changed files with 229 additions and 8 deletions.
2 changes: 2 additions & 0 deletions extensions/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,5 @@ Current list of extended templates:

| Component | Description |
| --------------------------------------------- | -------------------------------------------------------------------------------------------
|
| [`amp-mustache`](amp-mustache/amp-mustache.md) | Mustache template. |
83 changes: 83 additions & 0 deletions extensions/amp-mustache/0.1/amp-mustache.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/**
* Copyright 2015 The AMP HTML Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS-IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import {childElementByTag} from '../../../src/dom';
import {isExperimentOn} from '../../../src/experiments';
import {log} from '../../../src/log';
import {parse as mustacheParse, render as mustacheRender,
setUnescapedSanitizier} from '../../../third_party/mustache/mustache';
import {sanitizeHtml, sanitizeFormattingHtml} from '../../../src/sanitizer';

/** @const */
const assert = AMP.assert;

/** @const */
const EXPERIMENT = 'mustache';

/** @const */
const TAG = 'AmpMustache';


// Configure inline sanitizer for unescaped values.
setUnescapedSanitizier(sanitizeFormattingHtml);


/**
* Implements an AMP template for Mustache.js.
* See {@link https://github.com/janl/mustache.js/}.
*
* @private Visible for testing.
*/
export class AmpMustache extends AMP.BaseTemplate {

/**
* @return {boolean}
* @private
*/
isExperimentOn_() {
return isExperimentOn(this.getWin(), EXPERIMENT);
}

/** @override */
compileCallback() {
if (!this.isExperimentOn_()) {
return;
}
/** @private @const {string} */
this.template_ = this.element./*OK*/innerHTML;
mustacheParse(this.template_);
}

/** @override */
render(data) {
if (!this.isExperimentOn_()) {
const m = `Experiment "${EXPERIMENT}" disabled`;
log.warn(TAG, m, this.element);
const fallback = document.createElement('div');
fallback.textContent = m;
return fallback;
}

const html = mustacheRender(this.template_, data);
const sanitized = sanitizeHtml(html);
const root = this.getWin().document.createElement('div');
root./*OK*/innerHTML = sanitized;
return this.unwrap(root);
}
}


AMP.registerTemplate('amp-mustache', AmpMustache);
72 changes: 72 additions & 0 deletions extensions/amp-mustache/0.1/test/test-amp-mustache.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/**
* Copyright 2015 The AMP HTML Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS-IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import {AmpMustache}
from '../../../../build/all/v0/amp-mustache-0.1.max';

describe('amp-mustache template', () => {

it('should be blocked by the experiment', () => {
const templateElement = document.createElement('div');
const template = new AmpMustache(templateElement);
template.isExperimentOn_ = () => false;
const result = template.render({});
expect(result./*OK*/innerHTML).to.equal('Experiment "mustache" disabled');
});

it('should render with the experiment', () => {
const templateElement = document.createElement('div');
templateElement.textContent = 'value = {{value}}';
const template = new AmpMustache(templateElement);
template.isExperimentOn_ = () => true;
template.compileCallback();
const result = template.render({value: 'abc'});
expect(result./*OK*/innerHTML).to.equal('value = abc');
});

it('should sanitize output', () => {
const templateElement = document.createElement('div');
templateElement./*OK*/innerHTML = 'value = <a href="{{value}}">abc</a>';
const template = new AmpMustache(templateElement);
template.isExperimentOn_ = () => true;
template.compileCallback();
const result = template.render({
value: /*eslint no-script-url: 0*/ 'javascript:alert();'
});
expect(result./*OK*/innerHTML).to.equal('value = <a>abc</a>');
});

it('should sanitize triple-mustache', () => {
const templateElement = document.createElement('div');
templateElement.textContent = 'value = {{{value}}}';
const template = new AmpMustache(templateElement);
template.isExperimentOn_ = () => true;
template.compileCallback();
const result = template.render({value: '<b>abc</b><img><div>def</div>'});
expect(result./*OK*/innerHTML).to.equal('value = <b>abc</b>');
});

it('should unwrap output', () => {
const templateElement = document.createElement('div');
templateElement./*OK*/innerHTML = '<a>abc</a>';
const template = new AmpMustache(templateElement);
template.isExperimentOn_ = () => true;
template.compileCallback();
const result = template.render({});
expect(result.tagName).to.equal('A');
expect(result./*OK*/innerHTML).to.equal('abc');
});
});
61 changes: 61 additions & 0 deletions extensions/amp-mustache/amp-mustache.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
<!---
Copyright 2015 The AMP HTML Authors. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS-IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->

### <a name="amp-mustache"></a> `amp-mustache`

The `amp-mustache` allows rendering of [Mustache.js](https://github.com/janl/mustache.js/) templates.

#### Syntax

Mustache is a logic-less template syntax. See [Mustache.js docs](https://github.com/janl/mustache.js/)
for more details. Some of the core Mustache tags are:

- `{{variable}}` - variable tag. It outputs the the HTML-escaped value of a variable;
- `{{#section}}{{/section}}` - section tag. It can test existance of a variable and iterate over it if
it's an array;
- `{{^section}}{{/section}}` - inverted tag. It can test non-existance of a variable.

#### Usage

The `amp-mustache` template has to be defined and used according to the
[AMP Template Spec](../../spec/amp-html-templates.md).

First, the `amp-mustache` has to be declared/loaded like this:

```html
<script async custom-template="amp-mustache" src="https://cdn.ampproject.org/v0/amp-mustache-0.1.js"></script>
```

Then, the Mustache templates can be defined in the `template` tags like this:

```html
<template type="amp-mustache">
Hello {{world!}}
</template>
```

How templates are discovered, when they are rendered, how data is provided - all decided by the
target AMP element that uses this template to render its content.

#### Restrictions

Like all AMP templates, `amp-mustache` templates are required to be well-formed DOM fragments. This means
that among other things, you can't use `amp-mustache` to:

- Calculate tag name. E.g. `<{{tagName}}` is not allowed.
- Calculate attribute name. E.g. `<div {{class=my-class}}>` is not allowed.
- Output arbitrary HTML using `{{{unescaped}}}`. The output of "triple-mustache" is sanitized to only allow
formatting tags such as `<b>`, `<i>`, and so on.
1 change: 1 addition & 0 deletions gulpfile.js
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ function buildExtensions(options) {
buildExtension('amp-image-lightbox', '0.1', true, options);
buildExtension('amp-instagram', '0.1', false, options);
buildExtension('amp-lightbox', '0.1', false, options);
buildExtension('amp-mustache', '0.1', false, options);
buildExtension('amp-pinterest', '0.1', true, options);
/**
* @deprecated `amp-slides` is deprecated and will be deleted before 1.0.
Expand Down
15 changes: 8 additions & 7 deletions test/functional/test-mustache.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,29 +14,30 @@
* limitations under the License.
*/

import {render, setUnescapedSanitizier} from
'../../third_party/mustache/mustache';

const mustache = require('../../third_party/mustache/mustache');

describe('Mustache', () => {

let savedSanitizer;

beforeEach(() => {
setUnescapedSanitizier(function(value) {
savedSanitizer = mustache.sanitizeUnescaped;
mustache.setUnescapedSanitizier(function(value) {
return value.toUpperCase();
});
});

afterEach(() => {
setUnescapedSanitizier(null);
mustache.setUnescapedSanitizier(savedSanitizer);
});

it('should escape html', () => {
expect(render('{{value}}', {value: '<b>abc</b>'})).to.equal(
expect(mustache.render('{{value}}', {value: '<b>abc</b>'})).to.equal(
'&lt;b&gt;abc&lt;&#x2F;b&gt;');
});

it('should transform unescaped html', () => {
expect(render('{{{value}}}', {value: '<b>abc</b>'})).to.equal(
expect(mustache.render('{{{value}}}', {value: '<b>abc</b>'})).to.equal(
'<B>ABC</B>');
});
});
3 changes: 2 additions & 1 deletion test/size.txt
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@
226.87 kB | 73.15 kB | 18.14 kB | v0/amp-image-lightbox-0.1.js
22.32 kB | 6.07 kB | 2.52 kB | v0/amp-instagram-0.1.js
152.02 kB | 49.8 kB | 11.96 kB | v0/amp-lightbox-0.1.js
124.08 kB | 35.05 kB | 11.83 kB | v0/amp-mustache-0.1.js
47.33 kB | 19.34 kB | 6.27 kB | v0/amp-pinterest-0.1.js
102.38 kB | 30.51 kB | 8.97 kB | v0/amp-slides-0.1.js
50.58 kB | 14.9 kB | 5.4 kB | v0/amp-twitter-0.1.js
22.43 kB | 6.29 kB | 2.61 kB | v0/amp-youtube-0.1.js
34.79 kB | 10.73 kB | 4.03 kB | current-min/f.js / current/integration.js
34.79 kB | 10.73 kB | 4.03 kB | current-min/f.js / current/integration.js

0 comments on commit a379179

Please sign in to comment.