From cb9b00254b67eb251b19c5abe2c33ac809b86a37 Mon Sep 17 00:00:00 2001 From: Rich-Harris Date: Sat, 19 Nov 2016 14:53:00 -0500 Subject: [PATCH] more attribute parsing --- compiler/generate/attributes/lookup.js | 123 ++++++++++++++++++ compiler/generate/index.js | 41 +++++- compiler/parse/state/tag.js | 7 +- .../attribute-static-boolean/_config.js | 9 ++ .../attribute-static-boolean/main.svelte | 1 + test/compiler/attribute-static/_config.js | 3 + test/compiler/attribute-static/main.svelte | 1 + test/parser/attribute-dynamic/input.svelte | 1 + test/parser/attribute-dynamic/output.json | 63 +++++++++ .../attribute-static-boolean/input.svelte | 1 + .../attribute-static-boolean/output.json | 27 ++++ 11 files changed, 273 insertions(+), 4 deletions(-) create mode 100644 compiler/generate/attributes/lookup.js create mode 100644 test/compiler/attribute-static-boolean/_config.js create mode 100644 test/compiler/attribute-static-boolean/main.svelte create mode 100644 test/compiler/attribute-static/_config.js create mode 100644 test/compiler/attribute-static/main.svelte create mode 100644 test/parser/attribute-dynamic/input.svelte create mode 100644 test/parser/attribute-dynamic/output.json create mode 100644 test/parser/attribute-static-boolean/input.svelte create mode 100644 test/parser/attribute-static-boolean/output.json diff --git a/compiler/generate/attributes/lookup.js b/compiler/generate/attributes/lookup.js new file mode 100644 index 0000000000..f31496d9e1 --- /dev/null +++ b/compiler/generate/attributes/lookup.js @@ -0,0 +1,123 @@ +// source: https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes +const lookup = { + accept: { appliesTo: [ 'form', 'input' ] }, + 'accept-charset': { propertyName: 'acceptCharset', appliesTo: [ 'form' ] }, + accesskey: { propertyName: 'accessKey' }, + action: { appliesTo: [ 'form' ] }, + align: { appliesTo: [ 'applet', 'caption', 'col', 'colgroup', 'hr', 'iframe', 'img', 'table', 'tbody', 'td', 'tfoot' , 'th', 'thead', 'tr' ] }, + allowfullscreen: { propertyName: 'allowFullscreen', appliesTo: [ 'iframe' ] }, + alt: { appliesTo: [ 'applet', 'area', 'img', 'input' ] }, + async: { appliesTo: [ 'script' ] }, + autocomplete: { appliesTo: [ 'form', 'input' ] }, + autofocus: { appliesTo: [ 'button', 'input', 'keygen', 'select', 'textarea' ] }, + autoplay: { appliesTo: [ 'audio', 'video' ] }, + autosave: { appliesTo: [ 'input' ] }, + bgcolor: { propertyName: 'bgColor', appliesTo: [ 'body', 'col', 'colgroup', 'marquee', 'table', 'tbody', 'tfoot', 'td', 'th', 'tr' ] }, + border: { appliesTo: [ 'img', 'object', 'table' ] }, + buffered: { appliesTo: [ 'audio', 'video' ] }, + challenge: { appliesTo: [ 'keygen' ] }, + charset: { appliesTo: [ 'meta', 'script' ] }, + checked: { appliesTo: [ 'command', 'input' ] }, + cite: { appliesTo: [ 'blockquote', 'del', 'ins', 'q' ] }, + class: { propertyName: 'className' }, + code: { appliesTo: [ 'applet' ] }, + codebase: { propertyName: 'codeBase', appliesTo: [ 'applet' ] }, + color: { appliesTo: [ 'basefont', 'font', 'hr' ] }, + cols: { appliesTo: [ 'textarea' ] }, + colspan: { propertyName: 'colSpan', appliesTo: [ 'td', 'th' ] }, + content: { appliesTo: [ 'meta' ] }, + contenteditable: { propertyName: 'contentEditable' }, + contextmenu: {}, + controls: { appliesTo: [ 'audio', 'video' ] }, + coords: { appliesTo: [ 'area' ] }, + data: { appliesTo: [ 'object' ] }, + datetime: { propertyName: 'dateTime', appliesTo: [ 'del', 'ins', 'time' ] }, + default: { appliesTo: [ 'track' ] }, + defer: { appliesTo: [ 'script' ] }, + dir: {}, + dirname: { propertyName: 'dirName', appliesTo: [ 'input', 'textarea' ] }, + disabled: { appliesTo: [ 'button', 'command', 'fieldset', 'input', 'keygen', 'optgroup', 'option', 'select', 'textarea' ] }, + download: { appliesTo: [ 'a', 'area' ] }, + draggable: {}, + dropzone: {}, + enctype: { appliesTo: [ 'form' ] }, + for: { propertyName: 'htmlFor', appliesTo: [ 'label', 'output' ] }, + form: { appliesTo: [ 'button', 'fieldset', 'input', 'keygen', 'label', 'meter', 'object', 'output', 'progress', 'select', 'textarea' ] }, + formaction: { appliesTo: [ 'input', 'button' ] }, + headers: { appliesTo: [ 'td', 'th' ] }, + height: { appliesTo: [ 'canvas', 'embed', 'iframe', 'img', 'input', 'object', 'video' ] }, + hidden: {}, + high: { appliesTo: [ 'meter' ] }, + href: { appliesTo: [ 'a', 'area', 'base', 'link' ] }, + hreflang: { appliesTo: [ 'a', 'area', 'link' ] }, + 'http-equiv': { propertyName: 'httpEquiv', appliesTo: [ 'meta' ] }, + icon: { appliesTo: [ 'command' ] }, + id: {}, + ismap: { propertyName: 'isMap', appliesTo: [ 'img' ] }, + itemprop: {}, + keytype: { appliesTo: [ 'keygen' ] }, + kind: { appliesTo: [ 'track' ] }, + label: { appliesTo: [ 'track' ] }, + lang: {}, + language: { appliesTo: [ 'script' ] }, + list: { appliesTo: [ 'input' ] }, + loop: { appliesTo: [ 'audio', 'bgsound', 'marquee', 'video' ] }, + low: { appliesTo: [ 'meter' ] }, + manifest: { appliesTo: [ 'html' ] }, + max: { appliesTo: [ 'input', 'meter', 'progress' ] }, + maxlength: { propertyName: 'maxLength', appliesTo: [ 'input', 'textarea' ] }, + media: { appliesTo: [ 'a', 'area', 'link', 'source', 'style' ] }, + method: { appliesTo: [ 'form' ] }, + min: { appliesTo: [ 'input', 'meter' ] }, + multiple: { appliesTo: [ 'input', 'select' ] }, + muted: { appliesTo: [ 'video' ] }, + name: { appliesTo: [ 'button', 'form', 'fieldset', 'iframe', 'input', 'keygen', 'object', 'output', 'select', 'textarea', 'map', 'meta', 'param' ] }, + novalidate: { propertyName: 'noValidate', appliesTo: [ 'form' ] }, + open: { appliesTo: [ 'details' ] }, + optimum: { appliesTo: [ 'meter' ] }, + pattern: { appliesTo: [ 'input' ] }, + ping: { appliesTo: [ 'a', 'area' ] }, + placeholder: { appliesTo: [ 'input', 'textarea' ] }, + poster: { appliesTo: [ 'video' ] }, + preload: { appliesTo: [ 'audio', 'video' ] }, + radiogroup: { appliesTo: [ 'command' ] }, + readonly: { propertyName: 'readOnly', appliesTo: [ 'input', 'textarea' ] }, + rel: { appliesTo: [ 'a', 'area', 'link' ] }, + required: { appliesTo: [ 'input', 'select', 'textarea' ] }, + reversed: { appliesTo: [ 'ol' ] }, + rows: { appliesTo: [ 'textarea' ] }, + rowspan: { propertyName: 'rowSpan', appliesTo: [ 'td', 'th' ] }, + sandbox: { appliesTo: [ 'iframe' ] }, + scope: { appliesTo: [ 'th' ] }, + scoped: { appliesTo: [ 'style' ] }, + seamless: { appliesTo: [ 'iframe' ] }, + selected: { appliesTo: [ 'option' ] }, + shape: { appliesTo: [ 'a', 'area' ] }, + size: { appliesTo: [ 'input', 'select' ] }, + sizes: { appliesTo: [ 'link', 'img', 'source' ] }, + span: { appliesTo: [ 'col', 'colgroup' ] }, + spellcheck: {}, + src: { appliesTo: [ 'audio', 'embed', 'iframe', 'img', 'input', 'script', 'source', 'track', 'video' ] }, + srcdoc: { appliesTo: [ 'iframe' ] }, + srclang: { appliesTo: [ 'track' ] }, + srcset: { appliesTo: [ 'img' ] }, + start: { appliesTo: [ 'ol' ] }, + step: { appliesTo: [ 'input' ] }, + style: {}, + summary: { appliesTo: [ 'table' ] }, + tabindex: { propertyName: 'tabIndex' }, + target: { appliesTo: [ 'a', 'area', 'base', 'form' ] }, + title: {}, + type: { appliesTo: [ 'button', 'input', 'command', 'embed', 'object', 'script', 'source', 'style', 'menu' ] }, + usemap: { propertyName: 'useMap', appliesTo: [ 'img', 'input', 'object' ] }, + value: { appliesTo: [ 'button', 'option', 'input', 'li', 'meter', 'progress', 'param' ] }, + width: { appliesTo: [ 'canvas', 'embed', 'iframe', 'img', 'input', 'object', 'video' ] }, + wrap: { appliesTo: [ 'textarea' ] } +}; + +Object.keys( lookup ).forEach( name => { + const metadata = lookup[ name ]; + if ( !metadata.propertyName ) metadata.propertyName = name; +}); + +export default lookup; diff --git a/compiler/generate/index.js b/compiler/generate/index.js index 0efeb8183a..82b38b187a 100644 --- a/compiler/generate/index.js +++ b/compiler/generate/index.js @@ -6,6 +6,7 @@ import flattenReference from './utils/flattenReference.js'; import isReference from './utils/isReference.js'; import contextualise from './utils/contextualise.js'; import counter from './utils/counter.js'; +import attributeLookup from './attributes/lookup.js'; function createRenderer ( fragment ) { return deindent` @@ -115,7 +116,45 @@ export default function generate ( parsed, template ) { const allUsedContexts = new Set(); node.attributes.forEach( attribute => { - if ( attribute.type === 'EventHandler' ) { + if ( attribute.type === 'Attribute' ) { + let metadata = attributeLookup[ attribute.name ]; + if ( metadata.appliesTo && !~metadata.appliesTo.indexOf( node.name ) ) metadata = null; + + if ( attribute.value === true ) { + // attributes without values, e.g. `, + test ( component, target ) { + const textarea = target.querySelector( 'textarea' ); + assert.ok( textarea.readOnly ); + } +}; diff --git a/test/compiler/attribute-static-boolean/main.svelte b/test/compiler/attribute-static-boolean/main.svelte new file mode 100644 index 0000000000..3ca3bfd9a8 --- /dev/null +++ b/test/compiler/attribute-static-boolean/main.svelte @@ -0,0 +1 @@ + diff --git a/test/compiler/attribute-static/_config.js b/test/compiler/attribute-static/_config.js new file mode 100644 index 0000000000..5d3b9c81a8 --- /dev/null +++ b/test/compiler/attribute-static/_config.js @@ -0,0 +1,3 @@ +export default { + html: `
` +}; diff --git a/test/compiler/attribute-static/main.svelte b/test/compiler/attribute-static/main.svelte new file mode 100644 index 0000000000..3cb2e4b233 --- /dev/null +++ b/test/compiler/attribute-static/main.svelte @@ -0,0 +1 @@ +
diff --git a/test/parser/attribute-dynamic/input.svelte b/test/parser/attribute-dynamic/input.svelte new file mode 100644 index 0000000000..84a34b91ff --- /dev/null +++ b/test/parser/attribute-dynamic/input.svelte @@ -0,0 +1 @@ +
{{color}}
diff --git a/test/parser/attribute-dynamic/output.json b/test/parser/attribute-dynamic/output.json new file mode 100644 index 0000000000..2f1cc44454 --- /dev/null +++ b/test/parser/attribute-dynamic/output.json @@ -0,0 +1,63 @@ +{ + "html": { + "start": 0, + "end": 46, + "type": "Fragment", + "children": [ + { + "start": 0, + "end": 46, + "type": "Element", + "name": "div", + "attributes": [ + { + "start": 5, + "end": 30, + "type": "Attribute", + "name": "style", + "value": [ + { + "start": 12, + "end": 19, + "type": "Text", + "data": "color: " + }, + { + "start": 19, + "end": 28, + "type": "MustacheTag", + "expression": { + "start": 21, + "end": 26, + "type": "Identifier", + "name": "color" + } + }, + { + "start": 28, + "end": 29, + "type": "Text", + "data": ";" + } + ] + } + ], + "children": [ + { + "start": 31, + "end": 40, + "type": "MustacheTag", + "expression": { + "start": 33, + "end": 38, + "type": "Identifier", + "name": "color" + } + } + ] + } + ] + }, + "css": null, + "js": null +} diff --git a/test/parser/attribute-static-boolean/input.svelte b/test/parser/attribute-static-boolean/input.svelte new file mode 100644 index 0000000000..3ca3bfd9a8 --- /dev/null +++ b/test/parser/attribute-static-boolean/input.svelte @@ -0,0 +1 @@ + diff --git a/test/parser/attribute-static-boolean/output.json b/test/parser/attribute-static-boolean/output.json new file mode 100644 index 0000000000..5a7f79bee3 --- /dev/null +++ b/test/parser/attribute-static-boolean/output.json @@ -0,0 +1,27 @@ +{ + "html": { + "start": 0, + "end": 30, + "type": "Fragment", + "children": [ + { + "start": 0, + "end": 30, + "type": "Element", + "name": "textarea", + "attributes": [ + { + "start": 10, + "end": 18, + "type": "Attribute", + "name": "readonly", + "value": true + } + ], + "children": [] + } + ] + }, + "css": null, + "js": null +}