diff --git a/.gitignore b/.gitignore index 329472f106..d134f6249c 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,4 @@ coverage coverage.lcov test/sourcemaps/*/output.js test/sourcemaps/*/output.js.map +_actual.json \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e3c588804..2afff4d708 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Svelte changelog +## 1.8.0 + +* Prevent duplicate imports ([#308](https://github.com/sveltejs/svelte/issues/308)) +* Use `input` events (not `change`) for all input elements other than `checkbox` and `radio`, and textareas ([#309](https://github.com/sveltejs/svelte/pull/309)) +* Encapsulate keyframe declarations ([#245](https://github.com/sveltejs/svelte/issues/245)) + ## 1.7.1 * Deconflict imports and shared helpers ([#222](https://github.com/sveltejs/svelte/issues/222)) diff --git a/package.json b/package.json index fd06f7cd40..f9b53c0f27 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "svelte", - "version": "1.7.1", + "version": "1.8.0", "description": "The magical disappearing UI framework", "main": "compiler/svelte.js", "files": [ diff --git a/src/generators/shared/processCss.js b/src/generators/shared/processCss.js index bb0012e990..91ca055d31 100644 --- a/src/generators/shared/processCss.js +++ b/src/generators/shared/processCss.js @@ -6,6 +6,26 @@ export default function processCss ( parsed, code ) { const attr = `[svelte-${parsed.hash}]`; + const keyframes = new Map(); + + function walkKeyframes ( node ) { + if ( node.type === 'Atrule' && node.name.toLowerCase() === 'keyframes' ) { + node.expression.children.forEach( expression => { + if ( expression.type === 'Identifier' ) { + const newName = `svelte-${parsed.hash}-${expression.name}`; + code.overwrite( expression.start, expression.end, newName ); + keyframes.set( expression.name, newName ); + } + }); + } else if ( node.children ) { + node.children.forEach( walkKeyframes ); + } else if ( node.block ) { + walkKeyframes( node.block ); + } + } + + parsed.css.children.forEach( walkKeyframes ); + function transform ( rule ) { rule.selector.children.forEach( selector => { const start = selector.start - offset; @@ -29,11 +49,29 @@ export default function processCss ( parsed, code ) { code.overwrite( start + offset, end + offset, transformed ); }); + + rule.block.children.forEach( block => { + if ( block.type === 'Declaration' ) { + const property = block.property.toLowerCase(); + if ( property === 'animation' || property === 'animation-name' ) { + block.value.children.forEach( block => { + if ( block.type === 'Identifier' ) { + const name = block.name; + if ( keyframes.has( name ) ) { + code.overwrite( block.start, block.end, keyframes.get( name ) ); + } + } + }); + } + } + }); } function walk ( node ) { if ( node.type === 'Rule' ) { transform( node ); + } else if ( node.type === 'Atrule' && node.name.toLowerCase() === 'keyframes' ) { + // these have already been processed } else if ( node.children ) { node.children.forEach( walk ); } else if ( node.block ) { @@ -53,4 +91,4 @@ export default function processCss ( parsed, code ) { } return code.slice( parsed.css.content.start, parsed.css.content.end ); -} \ No newline at end of file +} diff --git a/src/parse/state/tag.js b/src/parse/state/tag.js index 72f73f0651..3b79ad8406 100644 --- a/src/parse/state/tag.js +++ b/src/parse/state/tag.js @@ -21,9 +21,46 @@ const specials = { } }; +// based on http://developers.whatwg.org/syntax.html#syntax-tag-omission +const disallowedContents = { + li: [ 'li' ], + dt: [ 'dt', 'dd' ], + dd: [ 'dt', 'dd' ], + p: 'address article aside blockquote div dl fieldset footer form h1 h2 h3 h4 h5 h6 header hgroup hr main menu nav ol p pre section table ul'.split( ' ' ), + rt: [ 'rt', 'rp' ], + rp: [ 'rt', 'rp' ], + optgroup: [ 'optgroup' ], + option: [ 'option', 'optgroup' ], + thead: [ 'tbody', 'tfoot' ], + tbody: [ 'tbody', 'tfoot' ], + tfoot: [ 'tbody' ], + tr: [ 'tr', 'tbody' ], + td: [ 'td', 'th', 'tr' ], + th: [ 'td', 'th', 'tr' ] +}; + +function stripWhitespace ( element ) { + if ( element.children.length ) { + const firstChild = element.children[0]; + const lastChild = element.children[ element.children.length - 1 ]; + + if ( firstChild.type === 'Text' ) { + firstChild.data = trimStart( firstChild.data ); + if ( !firstChild.data ) element.children.shift(); + } + + if ( lastChild.type === 'Text' ) { + lastChild.data = trimEnd( lastChild.data ); + if ( !lastChild.data ) element.children.pop(); + } + } +} + export default function tag ( parser ) { const start = parser.index++; + let parent = parser.current(); + if ( parser.eat( '!--' ) ) { const data = parser.readUntil( /-->/ ); parser.eat( '-->' ); @@ -40,8 +77,6 @@ export default function tag ( parser ) { const isClosingTag = parser.eat( '/' ); - // TODO handle cases like
  • one
  • two - const name = readTagName( parser ); parser.allowWhitespace(); @@ -53,28 +88,31 @@ export default function tag ( parser ) { if ( !parser.eat( '>' ) ) parser.error( `Expected '>'` ); - const element = parser.current(); - - // strip leading/trailing whitespace as necessary - if ( element.children.length ) { - const firstChild = element.children[0]; - const lastChild = element.children[ element.children.length - 1 ]; - - if ( firstChild.type === 'Text' ) { - firstChild.data = trimStart( firstChild.data ); - if ( !firstChild.data ) element.children.shift(); - } + // close any elements that don't have their own closing tags, e.g.

    + while ( parent.name !== name ) { + parent.end = start; + parser.stack.pop(); - if ( lastChild.type === 'Text' ) { - lastChild.data = trimEnd( lastChild.data ); - if ( !lastChild.data ) element.children.pop(); - } + parent = parser.current(); } - element.end = parser.index; + // strip leading/trailing whitespace as necessary + stripWhitespace( parent ); + + parent.end = parser.index; parser.stack.pop(); return null; + } else if ( parent.name in disallowedContents ) { + // can this be a child of the parent element, or does it implicitly + // close it, like `
  • one
  • two`? + const disallowed = disallowedContents[ parent.name ]; + if ( ~disallowed.indexOf( name ) ) { + stripWhitespace( parent ); + + parent.end = start; + parser.stack.pop(); + } } const attributes = []; diff --git a/test/parse.js b/test/parse.js index fa8cf85d8d..caa132bbdf 100644 --- a/test/parse.js +++ b/test/parse.js @@ -16,7 +16,8 @@ describe( 'parse', () => { const input = fs.readFileSync( `test/parser/${dir}/input.html`, 'utf-8' ).replace( /\s+$/, '' ); try { - const actual = JSON.parse( JSON.stringify( svelte.parse( input ) ) ); + const actual = svelte.parse( input ); + fs.writeFileSync( `test/parser/${dir}/_actual.json`, JSON.stringify( actual, null, '\t' ) ); const expected = require( `./parser/${dir}/output.json` ); assert.deepEqual( actual.html, expected.html ); diff --git a/test/parser/implicitly-closed-li/input.html b/test/parser/implicitly-closed-li/input.html new file mode 100644 index 0000000000..14fc1168ba --- /dev/null +++ b/test/parser/implicitly-closed-li/input.html @@ -0,0 +1,5 @@ + \ No newline at end of file diff --git a/test/parser/implicitly-closed-li/output.json b/test/parser/implicitly-closed-li/output.json new file mode 100644 index 0000000000..3263fa8e40 --- /dev/null +++ b/test/parser/implicitly-closed-li/output.json @@ -0,0 +1,66 @@ +{ + "hash": 3806276940, + "html": { + "start": 0, + "end": 31, + "type": "Fragment", + "children": [ + { + "start": 0, + "end": 31, + "type": "Element", + "name": "ul", + "attributes": [], + "children": [ + { + "start": 6, + "end": 13, + "type": "Element", + "name": "li", + "attributes": [], + "children": [ + { + "start": 10, + "end": 13, + "type": "Text", + "data": "a" + } + ] + }, + { + "start": 13, + "end": 20, + "type": "Element", + "name": "li", + "attributes": [], + "children": [ + { + "start": 17, + "end": 20, + "type": "Text", + "data": "b" + } + ] + }, + { + "start": 20, + "end": 26, + "type": "Element", + "name": "li", + "attributes": [], + "children": [ + { + "start": 24, + "end": 26, + "type": "Text", + "data": "c\n" + } + ] + } + ] + } + ] + }, + "css": null, + "js": null +} \ No newline at end of file