Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add script tag support for enclosing a template #19346

Merged
merged 13 commits into from Dec 10, 2018
Merged

Add script tag support for enclosing a template #19346

merged 13 commits into from Dec 10, 2018

Conversation

alabiaga
Copy link
Contributor

@alabiaga alabiaga commented Nov 15, 2018

fixes #19321, #11205

  • allow defining templates with a script tag. This allows text nodes used for amp-mustache templating not to be hoisted, destroying templating. See amp-mustache: Mustache tags don't work in tables #11205.
  • revamp test-amp-mustache to run test suite against template creation with supported types 'script' and 'template'.
  • add example of script type template in amp-list.amp.html example.
  • add TODO to handle nested script templates. I omitted this for now. I'll address in a following PR. This should fine as the validator won't allow for this change until I make the validator changes.

@alabiaga alabiaga changed the title [WIP] Add script tag support for enclosing a template Add script tag support for enclosing a template Nov 19, 2018
@alabiaga
Copy link
Contributor Author

cc\ @zhangsu

this.template_ = container./*OK*/innerHTML;
} else if (this.element.tagName == 'SCRIPT') {
//TODO(alabiaga): handle nested templates.
this.template_ = this.element.text.trim();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

textContent

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes that is the data type but it is the template. I'd like to keep a name that can be used for both cases when either template is defined via tag or <script> tag. In both cases, it is still just a string representing the template.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd like to keep the name as I reuse it for the 'TEMPLATE' type use case as well. The data type is text but for both cases this.template_ is a string representing the template contents. Would this.templateString_ be a better name?

this.template_ = container./*OK*/innerHTML;
} else if (this.element.tagName == 'SCRIPT') {
//TODO(alabiaga): handle nested templates.
this.template_ = this.element.text.trim();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't trim in the <template> case.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

right. mustache parsing won't care about the spaces anyway. removed in the script case.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

right. removed. mustache parse will ignore anyway

*
* @param {!Element} content
*/
processNestedScriptTemplates_(content) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are we duplicating?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Initially, what was written v0.2 was already the same logic to what was in amp-mustache v0.1 and inlined and not just extracted into its own method. I just pulled it out to its own method. I will pull it out and the template versions can share the logic once I add support for nested templates in the next PR.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, it's the diff that's screwed up. It's not showing me the old code.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function isn't called anywhere.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removed. Thanks, initially was going to try and add support for nested script templates but deferring it as it's not a priority and I filed a ticket for it as mentioned in another review comment.

'Template element must be a "template" tag %s', templateElement);
const templateTagName = templateElement.tagName;
user().assert(
templateTagName == 'TEMPLATE' || templateTagName == 'SCRIPT',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Assert script has type="template"

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the template tag also has a type that is enforced by the validator but isn't in the code base.

e.g. <template type="amp-mustache>..</template>

Maybe it is enough to do that there and the script way of defining templates is going to follow the same model. I will do the validation changes in a separate PR.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

<script type="template"> will need to be validated the same way a <template>. But, without this runtime check, I can pass a selector that will be a <script type="text/json">, which won't be validated. That would be a security issue.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can pass in json data with the following markup <script type="text/json"> but the sanitizer which runs during mustache rendering will strip out unsafe markup, script tags included. But to be on the safe side, you're right we should validate the type. Though I was pointing out that during runtime..the type for template is not validated, which it is in the validator. I will not add this change, as this will probably break clients who currently do not set this attribute.

Nested templates via <script type="template/amp-mustache"> will be whitelisted and allowed in the sanitizer in my next PR when I add support for nested script tags.

Adding validation for checking <script type="template/amp-mustache"> rather than just <script type="template"> to be more in line with the template syntax we support, . Thanks

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should use a valid MIME type string to avoid future collisions, e.g. text/plain. We can use a non-standard attribute template="amp-mustache" to denote template syntax.

https://html.spec.whatwg.org/multipage/scripting.html#attr-script-type

@@ -285,7 +288,9 @@ export class Templates {
} else if (opt_querySelector) {
return scopedQuerySelector(parent, opt_querySelector);
} else {
return childElementByTag(parent, 'template');
const template = childElementByTag(parent, 'template');
const templateByScript = childElementByTag(parent, 'script');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use a query selector to do this in one pass.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done.

* @param {!Array} values
* @param {func} func
*/
export function testCases(values, func) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Huh?

Copy link
Contributor Author

@alabiaga alabiaga Nov 20, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I use this in the tests (test-amp-mustache.js) so that I can reuse all the tests and just run them using a template creation with either in one test run and then <script> in the next.

e.g.

`testCases(['script', 'template'], templateType => {
    beforeEach(
       document.createElement(templateType);
    )
})`

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you try using describes.repeated?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did not. I was looking and briefly mentioned this in a standup and was under the impression that it didn't exist. Changed! Thanks.

Copy link
Contributor Author

@alabiaga alabiaga left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks Justin.

this.template_ = container./*OK*/innerHTML;
} else if (this.element.tagName == 'SCRIPT') {
//TODO(alabiaga): handle nested templates.
this.template_ = this.element.text.trim();
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd like to keep the name as I reuse it for the 'TEMPLATE' type use case as well. The data type is text but for both cases this.template_ is a string representing the template contents. Would this.templateString_ be a better name?

this.template_ = container./*OK*/innerHTML;
} else if (this.element.tagName == 'SCRIPT') {
//TODO(alabiaga): handle nested templates.
this.template_ = this.element.text.trim();
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

right. removed. mustache parse will ignore anyway

this.template_ = container./*OK*/innerHTML;
} else if (this.element.tagName == 'SCRIPT') {
//TODO(alabiaga): handle nested templates.
this.template_ = this.element.text;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, use element.textContent instead of .text. 😛

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah..Wish there was support for highlighting the exact code you are referring rather than just the line. Done. Thanks.

});
// This test suite runs twice. First by creating templates of type template
// and then by creating templates encapsulated within script.
testCases(['script', 'template'], templateType => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're looking for describes.repeated:

describes.repeated('toggle', {
  'name': data
}, (name, data) => {
});

*
* @param {!Element} content
*/
processNestedScriptTemplates_(content) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, it's the diff that's screwed up. It's not showing me the old code.

'Template element must be a "template" tag %s', templateElement);
const templateTagName = templateElement.tagName;
user().assert(
templateTagName == 'TEMPLATE' || templateTagName == 'SCRIPT',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

<script type="template"> will need to be validated the same way a <template>. But, without this runtime check, I can pass a selector that will be a <script type="text/json">, which won't be validated. That would be a security issue.

container.appendChild(content);
this.template_ = container./*OK*/innerHTML;
} else if (this.element.tagName == 'SCRIPT') {
//TODO(alabiaga): handle nested templates.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is lower priority, remove and file a tracking issue. No need to address yet. :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done filed #19450

@alabiaga
Copy link
Contributor Author

@choumx friendly ping since justin is away.

extensions/amp-mustache/0.1/amp-mustache.js Show resolved Hide resolved
*
* @param {!Element} content
*/
processNestedScriptTemplates_(content) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function isn't called anywhere.

const templateTagName = templateElement.tagName;
user().assert(
templateTagName == 'TEMPLATE' || templateTagName == 'SCRIPT',
'Template must be a "template" tag or defined in a "script" tag %s',

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"Template must defined in a <template> or <script type=text/plain> tag."

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done.

'Script template attribute must be "text/plain"');
user().assert(
templateElement.getAttribute('template') == 'amp-mustache',
'Script template attribute must be "amp-mustache"');

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We shouldn't couple template-impl.js to amp-mustache. Mustache is just a flavor of template.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

right. removed. In the future we can just add support for it when other template types are supported and just default to amp-mustache if omitted perhaps.

if (templateTagName == 'SCRIPT') {
user().assert(
templateElement.getAttribute('type') == 'text/plain',
'Script template attribute must be "text/plain"');

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just include this in the expression above. Also, what about case sensitivity?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done. tagName in DOM trees always return in uppercase. https://developer.mozilla.org/en-US/docs/Web/API/Element/tagName

* @param {!Array} values
* @param {func} func
*/
export function testCases(values, func) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you try using describes.repeated?

@@ -57,6 +57,24 @@
</template>
</amp-list>

<!-- ## Using a script to enclose the template. -->
<amp-list width="auto" height="100" layout="fixed-height" src="comments.example.json">
<script type="amp-mustache">

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

type="text/plain" template="amp-mustache", right? Did we test that this example works?

Let's also add an integration test to extensions/amp-list/0.1/test/integration/test-amp-list.js to make sure it doesn't stop working. :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, I did test this but this was before the whole discussion about the changes to these attributes so yes this stopped working with the recent changes. Thanks.

made test changes.

@@ -285,7 +296,7 @@ export class Templates {
} else if (opt_querySelector) {
return scopedQuerySelector(parent, opt_querySelector);
} else {
return childElementByTag(parent, 'template');
return parent.querySelector('template, script');

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why this change?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change is required to find the script or template tag defined in an amp-list.

Example

<amp-list>
  <script id="amp-template-id" type="text/plain" template="amp-mustache">
    ...
  </script>
</amp-list>

Note that it will return the first match found. We can make the validator rule changes enforce that only 1 template type is defined so that we can't do the following.

<amp-list>
  <script type="text/plain" template="amp-mustache">
    ...
  </script>
  <template type="amp-mustache">
    ...
  </template>
</amp-list>

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gotcha, missed the , script part.

Copy link
Contributor Author

@alabiaga alabiaga left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks Will!

extensions/amp-mustache/0.1/amp-mustache.js Show resolved Hide resolved
*
* @param {!Element} content
*/
processNestedScriptTemplates_(content) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removed. Thanks, initially was going to try and add support for nested script templates but deferring it as it's not a priority and I filed a ticket for it as mentioned in another review comment.

const templateTagName = templateElement.tagName;
user().assert(
templateTagName == 'TEMPLATE' || templateTagName == 'SCRIPT',
'Template must be a "template" tag or defined in a "script" tag %s',
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done.

'Script template attribute must be "text/plain"');
user().assert(
templateElement.getAttribute('template') == 'amp-mustache',
'Script template attribute must be "amp-mustache"');
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

right. removed. In the future we can just add support for it when other template types are supported and just default to amp-mustache if omitted perhaps.

@@ -285,7 +296,7 @@ export class Templates {
} else if (opt_querySelector) {
return scopedQuerySelector(parent, opt_querySelector);
} else {
return childElementByTag(parent, 'template');
return parent.querySelector('template, script');
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change is required to find the script or template tag defined in an amp-list.

Example

<amp-list>
  <script id="amp-template-id" type="text/plain" template="amp-mustache">
    ...
  </script>
</amp-list>

Note that it will return the first match found. We can make the validator rule changes enforce that only 1 template type is defined so that we can't do the following.

<amp-list>
  <script type="text/plain" template="amp-mustache">
    ...
  </script>
  <template type="amp-mustache">
    ...
  </template>
</amp-list>

* @param {!Array} values
* @param {func} func
*/
export function testCases(values, func) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did not. I was looking and briefly mentioned this in a standup and was under the impression that it didn't exist. Changed! Thanks.

if (templateTagName == 'SCRIPT') {
user().assert(
templateElement.getAttribute('type') == 'text/plain',
'Script template attribute must be "text/plain"');
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done. tagName in DOM trees always return in uppercase. https://developer.mozilla.org/en-US/docs/Web/API/Element/tagName

@@ -57,6 +57,24 @@
</template>
</amp-list>

<!-- ## Using a script to enclose the template. -->
<amp-list width="auto" height="100" layout="fixed-height" src="comments.example.json">
<script type="amp-mustache">
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, I did test this but this was before the whole discussion about the changes to these attributes so yes this stopped working with the recent changes. Thanks.

made test changes.

Copy link
Contributor

@jridgewell jridgewell left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall looks good to me. Please wait for @choumx to review.

(templateTagName == 'TEMPLATE' || (templateTagName == 'SCRIPT'
&& templateElement.getAttribute('type') === 'text/plain')),
'Template must be defined in a <template> or '
+ '<script type="text/plain"> tag');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe text/html?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Considered this but technically mustache templates aren't HTML, which is why table foster parenting semantics won't affect this etc.

Copy link

@dreamofabear dreamofabear left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please file an issue to add an integration test for amp-list for the new script case.

describe('amp-mustache 0.1', () => {
describes.repeated('amp-mustache 0.1', {
'template script': {templateType: 'script'},
'template template': {templateType: 'template'},

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

'template template' is confusing.

'with script[type=text/plain][template=amp-mustache]'
'with template[type=amp-mustache]'

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

'text before a template text after a template');
done();
} else {
done();

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Passing and calling done shouldn't be necessary here. Also, extract the isTemplateType conditional:

if (isTemplateType) {
  describe('with template[type=amp-mustache]') {
    it(...);
    it(...);
  }
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the test would hang otherwise on the the condition script[type=text/plain] [template=amp-mustache]. But extracting the condition outside and not running it at all does remove the need for done(), if that's you are saying. Thanks

done();
}
});
});

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto, except do the same for the script case.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done.

'</tr>' +
'{{/replies}}' +
'</tbody>' +
'</table>');

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we use a more minimal example than this? It's also worth running the same example on the template case to highlight the difference.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done. I removed some table data to simplify the example.

@@ -285,7 +296,7 @@ export class Templates {
} else if (opt_querySelector) {
return scopedQuerySelector(parent, opt_querySelector);
} else {
return childElementByTag(parent, 'template');
return parent.querySelector('template, script');

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gotcha, missed the , script part.

@alabiaga
Copy link
Contributor Author

alabiaga commented Dec 3, 2018

integration test issue here #19583

});
if (isTemplateTypeScript) {
it('should not foster text nodes in script template', () => {
allowConsoleError(() => {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You need to return this otherwise Mocha won't execute the embedded code.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

if (isTemplateTypeScript) {
it('should foster text nodes in template[type="amp-mustache"]'
+ 'destroying the templating', () => {
allowConsoleError(() => {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Copy link

@dreamofabear dreamofabear left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM except let's double check that one test.

});
});
}
if (isTemplateTypeScript) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be isTemplateType right?

});
}
if (isTemplateTypeScript) {
it('should foster text nodes in template[type="amp-mustache"]'

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Missing trailing space.

@@ -19,40 +19,76 @@ import * as service from '../../../../src/service';
import {AmpMustache} from '../amp-mustache';
import mustache from '../../../../third_party/mustache/mustache';

describe('amp-mustache 0.1', () => {
describes.repeated('amp-mustache 0.1', {
'with script[type=text/plain][template=amp-mustache':

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Missing trailing ].

sandbox.stub(Services, 'templatesFor').returns(templates);
describes.repeated('amp-list', {
'template type script': {templateType: 'script'},
'template type': {templateType: 'template'},

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Use the same repeated test labels as test-amp-mustache.

@alabiaga alabiaga merged commit ce617b7 into ampproject:master Dec 10, 2018
noranazmy pushed a commit to noranazmy/amphtml that referenced this pull request Mar 22, 2019
* merge

* add todo for nested script templates

* fix test

* address jridgewell comments

* lint

* address comments

* fix merge

* fix example

* lint

* address choumx comments

* address choumx comments

* address choumx comments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Support templates within script tags.
4 participants