From 5ccc2002221e0b74dd47a171d43f03297b750196 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 27 May 2017 12:29:01 -0400 Subject: [PATCH 1/8] separate parsing rules for textareas --- src/parse/state/tag.ts | 91 +++++++++++-------- .../samples/textarea-children/input.html | 3 + .../samples/textarea-children/output.json | 44 +++++++++ 3 files changed, 99 insertions(+), 39 deletions(-) create mode 100644 test/parser/samples/textarea-children/input.html create mode 100644 test/parser/samples/textarea-children/output.json diff --git a/src/parse/state/tag.ts b/src/parse/state/tag.ts index 97d34db372..47b8b1c7d6 100644 --- a/src/parse/state/tag.ts +++ b/src/parse/state/tag.ts @@ -9,7 +9,6 @@ import { Parser } from '../index'; import { Node } from '../../interfaces'; const validTagName = /^\!?[a-zA-Z]{1,}:?[a-zA-Z0-9\-]*/; -const invalidUnquotedAttributeCharacters = /[\s"'=<>\/`]/; const SELF = ':Self'; @@ -181,6 +180,11 @@ export default function tag ( parser: Parser ) { if ( selfClosing ) { element.end = parser.index; + } else if ( name === 'textarea' ) { + // special case + element.children = readSequence( parser, () => parser.template.slice( parser.index, parser.index + 11 ) === '' ); + parser.read( /<\/textarea>/ ); + element.end = parser.index; } else { // don't push self-closing elements onto the stack parser.stack.push( element ); @@ -280,11 +284,41 @@ function readAttribute ( parser: Parser, uniqueNames ) { } function readAttributeValue ( parser: Parser ) { - let quoteMark; + const quoteMark = ( + parser.eat( `'` ) ? `'` : + parser.eat( `"` ) ? `"` : + null + ); + + const regex = ( + quoteMark === `'` ? /'/ : + quoteMark === `"` ? /"/ : + /[\s"'=<>\/`]/ + ); + + const value = readSequence( parser, () => regex.test( parser.template[ parser.index ] ) ); + + if ( quoteMark ) parser.index += 1; + return value; +} - if ( parser.eat( `'` ) ) quoteMark = `'`; - if ( parser.eat( `"` ) ) quoteMark = `"`; +function getShorthandValue ( start: number, name: string ) { + const end = start + name.length; + return [{ + type: 'AttributeShorthand', + start, + end, + expression: { + type: 'Identifier', + start, + end, + name + } + }]; +} + +function readSequence ( parser: Parser, done: () => boolean ) { let currentChunk: Node = { start: parser.index, end: null, @@ -292,16 +326,24 @@ function readAttributeValue ( parser: Parser ) { data: '' }; - const done = quoteMark ? - char => char === quoteMark : - char => invalidUnquotedAttributeCharacters.test( char ); - const chunks = []; while ( parser.index < parser.template.length ) { const index = parser.index; - if ( parser.eat( '{{' ) ) { + if ( done() ) { + currentChunk.end = parser.index; + + if ( currentChunk.data ) chunks.push( currentChunk ); + + chunks.forEach( chunk => { + if ( chunk.type === 'Text' ) chunk.data = decodeCharacterReferences( chunk.data ); + }); + + return chunks; + } + + else if ( parser.eat( '{{' ) ) { if ( currentChunk.data ) { currentChunk.end = index; chunks.push( currentChunk ); @@ -328,39 +370,10 @@ function readAttributeValue ( parser: Parser ) { }; } - else if ( done( parser.template[ parser.index ] ) ) { - currentChunk.end = parser.index; - if ( quoteMark ) parser.index += 1; - - if ( currentChunk.data ) chunks.push( currentChunk ); - - chunks.forEach( chunk => { - if ( chunk.type === 'Text' ) chunk.data = decodeCharacterReferences( chunk.data ); - }); - - return chunks; - } - else { currentChunk.data += parser.template[ parser.index++ ]; } } parser.error( `Unexpected end of input` ); -} - -function getShorthandValue ( start: number, name: string ) { - const end = start + name.length; - - return [{ - type: 'AttributeShorthand', - start, - end, - expression: { - type: 'Identifier', - start, - end, - name - } - }]; -} +} \ No newline at end of file diff --git a/test/parser/samples/textarea-children/input.html b/test/parser/samples/textarea-children/input.html new file mode 100644 index 0000000000..de50c807d6 --- /dev/null +++ b/test/parser/samples/textarea-children/input.html @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/test/parser/samples/textarea-children/output.json b/test/parser/samples/textarea-children/output.json new file mode 100644 index 0000000000..4b67ac108a --- /dev/null +++ b/test/parser/samples/textarea-children/output.json @@ -0,0 +1,44 @@ +{ + "hash": 3618147195, + "html": { + "start": 0, + "end": 63, + "type": "Fragment", + "children": [ + { + "start": 0, + "end": 63, + "type": "Element", + "name": "textarea", + "attributes": [], + "children": [ + { + "start": 10, + "end": 40, + "type": "Text", + "data": "\n\t

not actually an element. " + }, + { + "start": 40, + "end": 47, + "type": "MustacheTag", + "expression": { + "type": "Identifier", + "start": 42, + "end": 45, + "name": "foo" + } + }, + { + "start": 47, + "end": 52, + "type": "Text", + "data": "

\n" + } + ] + } + ] + }, + "css": null, + "js": null +} \ No newline at end of file From b2ea03dde036fa73a863419ea44ec8ab4381ecf0 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 27 May 2017 12:29:44 -0400 Subject: [PATCH 2/8] prevent textarea from having both value and children --- src/validate/html/validateElement.ts | 8 ++++++++ .../validator/samples/textarea-value-children/errors.json | 8 ++++++++ test/validator/samples/textarea-value-children/input.html | 3 +++ 3 files changed, 19 insertions(+) create mode 100644 test/validator/samples/textarea-value-children/errors.json create mode 100644 test/validator/samples/textarea-value-children/input.html diff --git a/src/validate/html/validateElement.ts b/src/validate/html/validateElement.ts index 6610e7ee69..18c906b298 100644 --- a/src/validate/html/validateElement.ts +++ b/src/validate/html/validateElement.ts @@ -77,6 +77,14 @@ export default function validateElement ( validator: Validator, node: Node ) { validator.error( `Missing transition '${attribute.name}'`, attribute.start ); } } + + else if ( attribute.type === 'Attribute' ) { + if ( attribute.name === 'value' && node.name === 'textarea' ) { + if ( node.children.length ) { + validator.error( `A \ No newline at end of file From 70431dd794d75b39d2e9d61e120c366d69673711 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 27 May 2017 12:30:29 -0400 Subject: [PATCH 3/8] use value property for textareas --- src/generators/dom/visitors/Element/lookup.ts | 2 +- test/runtime/samples/textarea-value/_config.js | 17 +++++++++++++++++ test/runtime/samples/textarea-value/main.html | 1 + 3 files changed, 19 insertions(+), 1 deletion(-) create mode 100644 test/runtime/samples/textarea-value/_config.js create mode 100644 test/runtime/samples/textarea-value/main.html diff --git a/src/generators/dom/visitors/Element/lookup.ts b/src/generators/dom/visitors/Element/lookup.ts index 41fc249e61..de48507fea 100644 --- a/src/generators/dom/visitors/Element/lookup.ts +++ b/src/generators/dom/visitors/Element/lookup.ts @@ -109,7 +109,7 @@ const lookup = { 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', 'select' ] }, + value: { appliesTo: [ 'button', 'option', 'input', 'li', 'meter', 'progress', 'param', 'select', 'textarea' ] }, width: { appliesTo: [ 'canvas', 'embed', 'iframe', 'img', 'input', 'object', 'video' ] }, wrap: { appliesTo: [ 'textarea' ] } }; diff --git a/test/runtime/samples/textarea-value/_config.js b/test/runtime/samples/textarea-value/_config.js new file mode 100644 index 0000000000..89ff63508c --- /dev/null +++ b/test/runtime/samples/textarea-value/_config.js @@ -0,0 +1,17 @@ +export default { + 'skip-ssr': true, // SSR behaviour is awkwardly different + + data: { + foo: 42 + }, + + html: ``, + + test ( assert, component, target ) { + const textarea = target.querySelector( 'textarea' ); + assert.strictEqual( textarea.value, '42' ); + + component.set({ foo: 43 }); + assert.strictEqual( textarea.value, '43' ); + } +}; \ No newline at end of file diff --git a/test/runtime/samples/textarea-value/main.html b/test/runtime/samples/textarea-value/main.html new file mode 100644 index 0000000000..215166ab10 --- /dev/null +++ b/test/runtime/samples/textarea-value/main.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/test/server-side-rendering/samples/textarea-children/main.html b/test/server-side-rendering/samples/textarea-children/main.html new file mode 100644 index 0000000000..7209683448 --- /dev/null +++ b/test/server-side-rendering/samples/textarea-children/main.html @@ -0,0 +1,11 @@ + + + \ No newline at end of file diff --git a/test/server-side-rendering/samples/textarea-value/_expected.html b/test/server-side-rendering/samples/textarea-value/_expected.html new file mode 100644 index 0000000000..cd39aabde5 --- /dev/null +++ b/test/server-side-rendering/samples/textarea-value/_expected.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/test/server-side-rendering/samples/textarea-value/main.html b/test/server-side-rendering/samples/textarea-value/main.html new file mode 100644 index 0000000000..33375b5785 --- /dev/null +++ b/test/server-side-rendering/samples/textarea-value/main.html @@ -0,0 +1,9 @@ +`, + + test ( assert, component, target ) { + const textarea = target.querySelector( 'textarea' ); + assert.strictEqual( textarea.value, `\n\t

not actually an element. 42

\n` ); + + component.set({ foo: 43 }); + assert.strictEqual( textarea.value, `\n\t

not actually an element. 43

\n` ); + } +}; \ No newline at end of file diff --git a/test/runtime/samples/textarea-children/main.html b/test/runtime/samples/textarea-children/main.html new file mode 100644 index 0000000000..de50c807d6 --- /dev/null +++ b/test/runtime/samples/textarea-children/main.html @@ -0,0 +1,3 @@ + \ No newline at end of file From 75ea52754d6b961927fbd80c1b8ce4330499d42b Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 27 May 2017 13:22:56 -0400 Subject: [PATCH 6/8] always use helpers if referenced, not just for CallExpressions, and warn on context clashes (#575) --- src/generators/Generator.ts | 10 +++++----- src/validate/html/index.ts | 11 +++++++++++ .../helpers-not-call-expression/_config.js | 3 +++ .../helpers-not-call-expression/main.html | 15 +++++++++++++++ .../samples/helper-clash-context/input.html | 17 +++++++++++++++++ .../samples/helper-clash-context/warnings.json | 8 ++++++++ 6 files changed, 59 insertions(+), 5 deletions(-) create mode 100644 test/runtime/samples/helpers-not-call-expression/_config.js create mode 100644 test/runtime/samples/helpers-not-call-expression/main.html create mode 100644 test/validator/samples/helper-clash-context/input.html create mode 100644 test/validator/samples/helper-clash-context/warnings.json diff --git a/src/generators/Generator.ts b/src/generators/Generator.ts index fc87740e5a..9fe322e7f9 100644 --- a/src/generators/Generator.ts +++ b/src/generators/Generator.ts @@ -117,11 +117,7 @@ export default class Generator { const { name } = flattenReference( node ); if ( scope.has( name ) ) return; - if ( parent && parent.type === 'CallExpression' && node === parent.callee && helpers.has( name ) ) { - code.prependRight( node.start, `${self.alias( 'template' )}.helpers.` ); - } - - else if ( name === 'event' && isEventHandler ) { + if ( name === 'event' && isEventHandler ) { // noop } @@ -135,6 +131,10 @@ export default class Generator { if ( !~usedContexts.indexOf( name ) ) usedContexts.push( name ); } + else if ( helpers.has( name ) ) { + code.prependRight( node.start, `${self.alias( 'template' )}.helpers.` ); + } + else if ( indexes.has( name ) ) { const context = indexes.get( name ); if ( !~usedContexts.indexOf( context ) ) usedContexts.push( context ); diff --git a/src/validate/html/index.ts b/src/validate/html/index.ts index 4256ca3951..b091a90db3 100644 --- a/src/validate/html/index.ts +++ b/src/validate/html/index.ts @@ -26,6 +26,17 @@ export default function validateHtml ( validator: Validator, html: Node ) { elementDepth += 1; validateElement( validator, node ); + } else if ( node.type === 'EachBlock' ) { + if ( validator.helpers.has( node.context ) ) { + let c = node.expression.end; + + // find start of context + while ( /\s/.test( validator.source[c] ) ) c += 1; + c += 2; + while ( /\s/.test( validator.source[c] ) ) c += 1; + + validator.warn( `Context clashes with a helper. Rename one or the other to eliminate any ambiguity`, c ); + } } if ( node.children ) { diff --git a/test/runtime/samples/helpers-not-call-expression/_config.js b/test/runtime/samples/helpers-not-call-expression/_config.js new file mode 100644 index 0000000000..0b65615c81 --- /dev/null +++ b/test/runtime/samples/helpers-not-call-expression/_config.js @@ -0,0 +1,3 @@ +export default { + html: '

1,4,9

' +}; diff --git a/test/runtime/samples/helpers-not-call-expression/main.html b/test/runtime/samples/helpers-not-call-expression/main.html new file mode 100644 index 0000000000..e59a83eab8 --- /dev/null +++ b/test/runtime/samples/helpers-not-call-expression/main.html @@ -0,0 +1,15 @@ +

{{numbers.map(square)}}

+ + diff --git a/test/validator/samples/helper-clash-context/input.html b/test/validator/samples/helper-clash-context/input.html new file mode 100644 index 0000000000..85f6faf117 --- /dev/null +++ b/test/validator/samples/helper-clash-context/input.html @@ -0,0 +1,17 @@ +{{#each things as thing}} + {{thing}} +{{/each}} + + \ No newline at end of file diff --git a/test/validator/samples/helper-clash-context/warnings.json b/test/validator/samples/helper-clash-context/warnings.json new file mode 100644 index 0000000000..e71b4edb59 --- /dev/null +++ b/test/validator/samples/helper-clash-context/warnings.json @@ -0,0 +1,8 @@ +[{ + "message": "Context clashes with a helper. Rename one or the other to eliminate any ambiguity", + "loc": { + "line": 1, + "column": 18 + }, + "pos": 18 +}] \ No newline at end of file From bf78dcc86abc2ea861043be49e11fe845891cc64 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 27 May 2017 13:49:17 -0400 Subject: [PATCH 7/8] remove generated shared.ts file from repo --- .gitignore | 1 + src/generators/dom/shared.ts | 35 ----------------------------------- 2 files changed, 1 insertion(+), 35 deletions(-) delete mode 100644 src/generators/dom/shared.ts diff --git a/.gitignore b/.gitignore index 8975ad857c..c51cf048c9 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,4 @@ coverage.lcov test/sourcemaps/samples/*/output.js test/sourcemaps/samples/*/output.js.map _actual.* +src/generators/dom/shared.ts \ No newline at end of file diff --git a/src/generators/dom/shared.ts b/src/generators/dom/shared.ts deleted file mode 100644 index 906b6222ad..0000000000 --- a/src/generators/dom/shared.ts +++ /dev/null @@ -1,35 +0,0 @@ -// this file is auto-generated, do not edit it -export default { - "appendNode": "function appendNode ( node, target ) {\n\ttarget.appendChild( node );\n}", - "insertNode": "function insertNode ( node, target, anchor ) {\n\ttarget.insertBefore( node, anchor );\n}", - "detachNode": "function detachNode ( node ) {\n\tnode.parentNode.removeChild( node );\n}", - "detachBetween": "function detachBetween ( before, after ) {\n\twhile ( before.nextSibling && before.nextSibling !== after ) {\n\t\tbefore.parentNode.removeChild( before.nextSibling );\n\t}\n}", - "destroyEach": "function destroyEach ( iterations, detach, start ) {\n\tfor ( var i = start; i < iterations.length; i += 1 ) {\n\t\tif ( iterations[i] ) iterations[i].destroy( detach );\n\t}\n}", - "createElement": "function createElement ( name ) {\n\treturn document.createElement( name );\n}", - "createSvgElement": "function createSvgElement ( name ) {\n\treturn document.createElementNS( 'http://www.w3.org/2000/svg', name );\n}", - "createText": "function createText ( data ) {\n\treturn document.createTextNode( data );\n}", - "createComment": "function createComment () {\n\treturn document.createComment( '' );\n}", - "addEventListener": "function addEventListener ( node, event, handler ) {\n\tnode.addEventListener( event, handler, false );\n}", - "removeEventListener": "function removeEventListener ( node, event, handler ) {\n\tnode.removeEventListener( event, handler, false );\n}", - "setAttribute": "function setAttribute ( node, attribute, value ) {\n\tnode.setAttribute( attribute, value );\n}", - "setXlinkAttribute": "function setXlinkAttribute ( node, attribute, value ) {\n\tnode.setAttributeNS( 'http://www.w3.org/1999/xlink', attribute, value );\n}", - "getBindingGroupValue": "function getBindingGroupValue ( group ) {\n\tvar value = [];\n\tfor ( var i = 0; i < group.length; i += 1 ) {\n\t\tif ( group[i].checked ) value.push( group[i].__value );\n\t}\n\treturn value;\n}", - "differs": "function differs ( a, b ) {\n\treturn ( a !== b ) || ( a && ( typeof a === 'object' ) || ( typeof a === 'function' ) );\n}", - "dispatchObservers": "function dispatchObservers ( component, group, newState, oldState ) {\n\tfor ( var key in group ) {\n\t\tif ( !( key in newState ) ) continue;\n\n\t\tvar newValue = newState[ key ];\n\t\tvar oldValue = oldState[ key ];\n\n\t\tif ( differs( newValue, oldValue ) ) {\n\t\t\tvar callbacks = group[ key ];\n\t\t\tif ( !callbacks ) continue;\n\n\t\t\tfor ( var i = 0; i < callbacks.length; i += 1 ) {\n\t\t\t\tvar callback = callbacks[i];\n\t\t\t\tif ( callback.__calling ) continue;\n\n\t\t\t\tcallback.__calling = true;\n\t\t\t\tcallback.call( component, newValue, oldValue );\n\t\t\t\tcallback.__calling = false;\n\t\t\t}\n\t\t}\n\t}\n}", - "get": "function get ( key ) {\n\treturn key ? this._state[ key ] : this._state;\n}", - "fire": "function fire ( eventName, data ) {\n\tvar handlers = eventName in this._handlers && this._handlers[ eventName ].slice();\n\tif ( !handlers ) return;\n\n\tfor ( var i = 0; i < handlers.length; i += 1 ) {\n\t\thandlers[i].call( this, data );\n\t}\n}", - "observe": "function observe ( key, callback, options ) {\n\tvar group = ( options && options.defer ) ? this._observers.post : this._observers.pre;\n\n\t( group[ key ] || ( group[ key ] = [] ) ).push( callback );\n\n\tif ( !options || options.init !== false ) {\n\t\tcallback.__calling = true;\n\t\tcallback.call( this, this._state[ key ] );\n\t\tcallback.__calling = false;\n\t}\n\n\treturn {\n\t\tcancel: function () {\n\t\t\tvar index = group[ key ].indexOf( callback );\n\t\t\tif ( ~index ) group[ key ].splice( index, 1 );\n\t\t}\n\t};\n}", - "observeDev": "function observeDev ( key, callback, options ) {\n\tvar c = ( key = '' + key ).search( /[^\\w]/ );\n\tif ( c > -1 ) {\n\t\tvar message = \"The first argument to component.observe(...) must be the name of a top-level property\";\n\t\tif ( c > 0 ) message += \", i.e. '\" + key.slice( 0, c ) + \"' rather than '\" + key + \"'\";\n\n\t\tthrow new Error( message );\n\t}\n\n\treturn observe.call( this, key, callback, options );\n}", - "on": "function on ( eventName, handler ) {\n\tif ( eventName === 'teardown' ) return this.on( 'destroy', handler );\n\n\tvar handlers = this._handlers[ eventName ] || ( this._handlers[ eventName ] = [] );\n\thandlers.push( handler );\n\n\treturn {\n\t\tcancel: function () {\n\t\t\tvar index = handlers.indexOf( handler );\n\t\t\tif ( ~index ) handlers.splice( index, 1 );\n\t\t}\n\t};\n}", - "onDev": "function onDev ( eventName, handler ) {\n\tif ( eventName === 'teardown' ) {\n\t\tconsole.warn( \"Use component.on('destroy', ...) instead of component.on('teardown', ...) which has been deprecated and will be unsupported in Svelte 2\" );\n\t\treturn this.on( 'destroy', handler );\n\t}\n\n\treturn on.call( this, eventName, handler );\n}", - "set": "function set ( newState ) {\n\tthis._set( assign( {}, newState ) );\n\tthis._root._flush();\n}", - "_flush": "function _flush () {\n\tif ( !this._renderHooks ) return;\n\n\twhile ( this._renderHooks.length ) {\n\t\tthis._renderHooks.pop()();\n\t}\n}", - "proto": "{\n\tget: get,\n\tfire: fire,\n\tobserve: observe,\n\ton: on,\n\tset: set,\n\t_flush: _flush\n}", - "protoDev": "{\n\tget: get,\n\tfire: fire,\n\tobserve: observeDev,\n\ton: onDev,\n\tset: set,\n\t_flush: _flush\n}", - "linear": "function linear ( t ) {\n\treturn t;\n}", - "generateKeyframes": "function generateKeyframes ( a, b, delta, duration, ease, fn, node, style ) {\n\tvar id = '__svelte' + ~~( Math.random() * 1e9 ); // TODO make this more robust\n\tvar keyframes = '@keyframes ' + id + '{\\n';\n\n\tfor ( var p = 0; p <= 1; p += 16.666 / duration ) {\n\t\tvar t = a + delta * ease( p );\n\t\tkeyframes += ( p * 100 ) + '%{' + fn( t ) + '}\\n';\n\t}\n\n\tkeyframes += '100% {' + fn( b ) + '}\\n}';\n\tstyle.textContent += keyframes;\n\n\tdocument.head.appendChild( style );\n\n\tnode.style.animation = ( node.style.animation || '' ).split( ',' )\n\t\t.filter( function ( anim ) {\n\t\t\t// when introing, discard old animations if there are any\n\t\t\treturn anim && ( delta < 0 || !/__svelte/.test( anim ) );\n\t\t})\n\t\t.concat( id + ' ' + duration + 'ms linear 1 forwards' )\n\t\t.join( ', ' );\n}", - "wrapTransition": "function wrapTransition ( node, fn, params, intro, outgroup ) {\n\tvar obj = fn( node, params );\n\tvar duration = obj.duration || 300;\n\tvar ease = obj.easing || linear;\n\n\t// TODO share