more attribute parsing

pull/31/head
Rich-Harris 8 years ago
parent 154cd4326d
commit cb9b00254b

@ -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;

@ -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. <textarea readonly>
if ( metadata ) {
initStatements.push( deindent`
${name}.${metadata.propertyName} = true;
` );
} else {
initStatements.push( deindent`
${name}.setAttribute( '${attribute.name}', true );
` );
}
}
else if ( attribute.value.length === 1 && attribute.value[0].type === 'Text' ) {
// static attributes
const value = JSON.stringify( attribute.value[0].data );
if ( metadata ) {
initStatements.push( deindent`
${name}.${metadata.propertyName} = ${value};
` );
} else {
initStatements.push( deindent`
${name}.setAttribute( '${attribute.name}', ${value} );
` );
}
}
else {
// need to handle boolean and string attributes differently
throw new Error( 'TODO dynamic attributes' );
}
}
else if ( attribute.type === 'EventHandler' ) {
// TODO verify that it's a valid callee (i.e. built-in or declared method)
const handler = current.counter( `${attribute.name}Handler` );

@ -165,15 +165,16 @@ function readQuotedAttributeValue ( parser, quoteMark ) {
}
else {
if ( parser.match( '{{' ) ) {
const index = parser.index;
const index = parser.index;
if ( parser.eat( '{{' ) ) {
currentChunk.end = index;
if ( currentChunk.data ) {
chunks.push( currentChunk );
}
const expression = readExpression();
const expression = readExpression( parser );
parser.allowWhitespace();
if ( !parser.eat( '}}' ) ) {
parser.error( `Expected }}` );

@ -0,0 +1,9 @@
import * as assert from 'assert';
export default {
html: `<textarea readonly=""></textarea>`,
test ( component, target ) {
const textarea = target.querySelector( 'textarea' );
assert.ok( textarea.readOnly );
}
};

@ -0,0 +1 @@
<textarea readonly></textarea>

@ -0,0 +1,3 @@
export default {
html: `<div class="foo"></div>`
};

@ -0,0 +1 @@
<div style='color: {{color}};'>{{color}}</div>

@ -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
}

@ -0,0 +1 @@
<textarea readonly></textarea>

@ -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
}
Loading…
Cancel
Save