From eaf4a2e8bda90409b57d5daed16c9ec8d69c59d0 Mon Sep 17 00:00:00 2001 From: Rich-Harris Date: Fri, 17 Mar 2017 21:55:30 -0400 Subject: [PATCH 1/6] parse <:Window> tags --- src/parse/index.js | 1 + src/parse/state/tag.js | 23 +++++++++++++++++++ .../samples/error-window-children/error.json | 8 +++++++ .../samples/error-window-children/input.html | 1 + .../samples/error-window-duplicate/error.json | 8 +++++++ .../samples/error-window-duplicate/input.html | 2 ++ .../error-window-inside-block/error.json | 8 +++++++ .../error-window-inside-block/input.html | 3 +++ .../error-window-inside-element/error.json | 8 +++++++ .../error-window-inside-element/input.html | 3 +++ 10 files changed, 65 insertions(+) create mode 100644 test/parser/samples/error-window-children/error.json create mode 100644 test/parser/samples/error-window-children/input.html create mode 100644 test/parser/samples/error-window-duplicate/error.json create mode 100644 test/parser/samples/error-window-duplicate/input.html create mode 100644 test/parser/samples/error-window-inside-block/error.json create mode 100644 test/parser/samples/error-window-inside-block/input.html create mode 100644 test/parser/samples/error-window-inside-element/error.json create mode 100644 test/parser/samples/error-window-inside-element/input.html diff --git a/src/parse/index.js b/src/parse/index.js index ee0c1d8dfb..29122d69bc 100644 --- a/src/parse/index.js +++ b/src/parse/index.js @@ -32,6 +32,7 @@ export default function parse ( template, options = {} ) { index: 0, template, stack: [], + metaTags: {}, current () { return this.stack[ this.stack.length - 1 ]; diff --git a/src/parse/state/tag.js b/src/parse/state/tag.js index f315ef69e4..396bd46244 100644 --- a/src/parse/state/tag.js +++ b/src/parse/state/tag.js @@ -11,6 +11,10 @@ const invalidUnquotedAttributeCharacters = /[\s"'=<>\/`]/; const SELF = ':Self'; +const metaTags = { + ':Window': true +}; + const specials = { script: { read: readScript, @@ -81,6 +85,22 @@ export default function tag ( parser ) { const name = readTagName( parser ); + if ( name in metaTags ) { + if ( name in parser.metaTags ) { + if ( isClosingTag && parser.current().children.length ) { + parser.error( `<${name}> cannot have children`, parser.current().children[0].start ); + } + + parser.error( `A component can only have one <${name}> tag`, start ); + } + + parser.metaTags[ name ] = true; + + if ( parser.stack.length > 1 ) { + parser.error( `<${name}> tags cannot be inside elements or blocks`, start ); + } + } + parser.allowWhitespace(); if ( isClosingTag ) { @@ -194,6 +214,9 @@ function readTagName ( parser ) { } const name = parser.readUntil( /(\s|\/|>)/ ); + + if ( name in metaTags ) return name; + if ( !validTagName.test( name ) ) { parser.error( `Expected valid tag name`, start ); } diff --git a/test/parser/samples/error-window-children/error.json b/test/parser/samples/error-window-children/error.json new file mode 100644 index 0000000000..8ff51ca8fb --- /dev/null +++ b/test/parser/samples/error-window-children/error.json @@ -0,0 +1,8 @@ +{ + "message": "<:Window> cannot have children", + "loc": { + "line": 1, + "column": 9 + }, + "pos": 9 +} \ No newline at end of file diff --git a/test/parser/samples/error-window-children/input.html b/test/parser/samples/error-window-children/input.html new file mode 100644 index 0000000000..eff4bebe50 --- /dev/null +++ b/test/parser/samples/error-window-children/input.html @@ -0,0 +1 @@ +<:Window>contents \ No newline at end of file diff --git a/test/parser/samples/error-window-duplicate/error.json b/test/parser/samples/error-window-duplicate/error.json new file mode 100644 index 0000000000..1e86fafea2 --- /dev/null +++ b/test/parser/samples/error-window-duplicate/error.json @@ -0,0 +1,8 @@ +{ + "message": "A component can only have one <:Window> tag", + "loc": { + "line": 2, + "column": 0 + }, + "pos": 11 +} \ No newline at end of file diff --git a/test/parser/samples/error-window-duplicate/input.html b/test/parser/samples/error-window-duplicate/input.html new file mode 100644 index 0000000000..683468e436 --- /dev/null +++ b/test/parser/samples/error-window-duplicate/input.html @@ -0,0 +1,2 @@ +<:Window/> +<:Window/> \ No newline at end of file diff --git a/test/parser/samples/error-window-inside-block/error.json b/test/parser/samples/error-window-inside-block/error.json new file mode 100644 index 0000000000..d6f6bd2f33 --- /dev/null +++ b/test/parser/samples/error-window-inside-block/error.json @@ -0,0 +1,8 @@ +{ + "message": "<:Window> tags cannot be inside elements or blocks", + "loc": { + "line": 2, + "column": 1 + }, + "pos": 13 +} \ No newline at end of file diff --git a/test/parser/samples/error-window-inside-block/input.html b/test/parser/samples/error-window-inside-block/input.html new file mode 100644 index 0000000000..b361517056 --- /dev/null +++ b/test/parser/samples/error-window-inside-block/input.html @@ -0,0 +1,3 @@ +{{#if foo}} + <:Window/> +{{/if}} \ No newline at end of file diff --git a/test/parser/samples/error-window-inside-element/error.json b/test/parser/samples/error-window-inside-element/error.json new file mode 100644 index 0000000000..5c28694d61 --- /dev/null +++ b/test/parser/samples/error-window-inside-element/error.json @@ -0,0 +1,8 @@ +{ + "message": "<:Window> tags cannot be inside elements or blocks", + "loc": { + "line": 2, + "column": 1 + }, + "pos": 7 +} \ No newline at end of file diff --git a/test/parser/samples/error-window-inside-element/input.html b/test/parser/samples/error-window-inside-element/input.html new file mode 100644 index 0000000000..edda05bfbb --- /dev/null +++ b/test/parser/samples/error-window-inside-element/input.html @@ -0,0 +1,3 @@ +
+ <:Window/> +
> \ No newline at end of file From 958ed7409f8fbb908999102e93494e1c4b298b46 Mon Sep 17 00:00:00 2001 From: Rich-Harris Date: Sun, 19 Mar 2017 12:06:21 -0400 Subject: [PATCH 2/6] implement :Window events --- src/generators/dom/visitors/Element.js | 13 ++++++++ src/generators/dom/visitors/meta/Window.js | 32 +++++++++++++++++++ test/generator/index.js | 2 ++ .../generator/samples/window-event/_config.js | 26 +++++++++++++++ test/generator/samples/window-event/main.html | 3 ++ 5 files changed, 76 insertions(+) create mode 100644 src/generators/dom/visitors/meta/Window.js create mode 100644 test/generator/samples/window-event/_config.js create mode 100644 test/generator/samples/window-event/main.html diff --git a/src/generators/dom/visitors/Element.js b/src/generators/dom/visitors/Element.js index 0e76e0d708..7b359b1d90 100644 --- a/src/generators/dom/visitors/Element.js +++ b/src/generators/dom/visitors/Element.js @@ -2,9 +2,18 @@ import CodeBuilder from '../../../utils/CodeBuilder.js'; import deindent from '../../../utils/deindent.js'; import addElementAttributes from './attributes/addElementAttributes.js'; import Component from './Component.js'; +import Window from './meta/Window.js'; + +const meta = { + ':Window': Window +}; export default { enter ( generator, node ) { + if ( node.name in meta ) { + return meta[ node.name ].enter( generator, node ); + } + const isComponent = node.name in generator.components || node.name === ':Self'; if ( isComponent ) { return Component.enter( generator, node ); @@ -100,6 +109,10 @@ export default { }, leave ( generator, node ) { + if ( node.name in meta ) { + return meta[ node.name ].leave( generator, node ); + } + const isComponent = node.name in generator.components; if ( isComponent ) { return Component.leave( generator, node ); diff --git a/src/generators/dom/visitors/meta/Window.js b/src/generators/dom/visitors/meta/Window.js new file mode 100644 index 0000000000..dcda89c247 --- /dev/null +++ b/src/generators/dom/visitors/meta/Window.js @@ -0,0 +1,32 @@ +import flattenReference from '../../../../utils/flattenReference.js'; +import deindent from '../../../../utils/deindent.js'; + +export default { + enter ( generator, node ) { + node.attributes.forEach( attribute => { + if ( attribute.type === 'EventHandler' ) { + // TODO verify that it's a valid callee (i.e. built-in or declared method) + generator.addSourcemapLocations( attribute.expression ); + + const flattened = flattenReference( attribute.expression.callee ); + if ( flattened.name !== 'event' && flattened.name !== 'this' ) { + // allow event.stopPropagation(), this.select() etc + generator.code.prependRight( attribute.expression.start, 'component.' ); + } + + const handlerName = generator.current.getUniqueName( `onwindow${attribute.name}` ); + + generator.current.builders.init.addBlock( deindent` + var ${handlerName} = function ( event ) { + [✂${attribute.expression.start}-${attribute.expression.end}✂]; + }; + window.addEventListener( '${attribute.name}', ${handlerName} ); + ` ); + + generator.current.builders.teardown.addBlock( deindent` + window.removeEventListener( '${attribute.name}', ${handlerName} ); + ` ); + } + }); + } +}; diff --git a/test/generator/index.js b/test/generator/index.js index f52831b7c2..04599c6f2b 100644 --- a/test/generator/index.js +++ b/test/generator/index.js @@ -93,6 +93,8 @@ describe( 'generate', () => { return env() .then( window => { + global.window = window; + // Put the constructor on window for testing window.SvelteComponent = SvelteComponent; diff --git a/test/generator/samples/window-event/_config.js b/test/generator/samples/window-event/_config.js new file mode 100644 index 0000000000..89dfd2057c --- /dev/null +++ b/test/generator/samples/window-event/_config.js @@ -0,0 +1,26 @@ +export default { + html: `
undefinedxundefined
`, + + test ( assert, component, target, window ) { + const event = new window.Event( 'resize' ); + + // JSDOM executes window event listeners with `global` rather than + // `window` (bug?) so we need to do this + Object.defineProperties( global, { + innerWidth: { + value: 567, + configurable: true + }, + innerHeight: { + value: 456, + configurable: true + } + }); + + window.dispatchEvent( event ); + + assert.htmlEqual( target.innerHTML, ` +
567x456
+ `); + } +}; \ No newline at end of file diff --git a/test/generator/samples/window-event/main.html b/test/generator/samples/window-event/main.html new file mode 100644 index 0000000000..b6679a0e44 --- /dev/null +++ b/test/generator/samples/window-event/main.html @@ -0,0 +1,3 @@ +<:Window on:resize='set({ width: this.innerWidth, height: this.innerHeight })'/> + +
{{width}}x{{height}}
\ No newline at end of file From ab03bef8cdd57e479f5f795ba5af7349a63d2c8b Mon Sep 17 00:00:00 2001 From: Rich-Harris Date: Sun, 19 Mar 2017 12:25:47 -0400 Subject: [PATCH 3/6] SSR support for <:Window> (albeit impossible to test for some reason) --- src/generators/dom/visitors/Element.js | 3 ++- .../server-side-rendering/visitors/Element.js | 14 ++++++++++++++ .../server-side-rendering/visitors/meta/Window.js | 9 +++++++++ test/generator/samples/window-event/_config.js | 2 ++ 4 files changed, 27 insertions(+), 1 deletion(-) create mode 100644 src/generators/server-side-rendering/visitors/meta/Window.js diff --git a/src/generators/dom/visitors/Element.js b/src/generators/dom/visitors/Element.js index 7b359b1d90..524a979aa9 100644 --- a/src/generators/dom/visitors/Element.js +++ b/src/generators/dom/visitors/Element.js @@ -110,7 +110,8 @@ export default { leave ( generator, node ) { if ( node.name in meta ) { - return meta[ node.name ].leave( generator, node ); + if ( meta[ node.name ].leave ) meta[ node.name ].leave( generator, node ); + return; } const isComponent = node.name in generator.components; diff --git a/src/generators/server-side-rendering/visitors/Element.js b/src/generators/server-side-rendering/visitors/Element.js index 4b750bf7b1..c8bfd1f902 100644 --- a/src/generators/server-side-rendering/visitors/Element.js +++ b/src/generators/server-side-rendering/visitors/Element.js @@ -1,8 +1,17 @@ import Component from './Component.js'; import isVoidElementName from '../../../utils/isVoidElementName.js'; +import Window from './meta/Window.js'; + +const meta = { + ':Window': Window +}; export default { enter ( generator, node ) { + if ( node.name in meta ) { + return meta[ node.name ].enter( generator, node ); + } + if ( node.name in generator.components || node.name === ':Self' ) { Component.enter( generator, node ); return; @@ -39,6 +48,11 @@ export default { }, leave ( generator, node ) { + if ( node.name in meta ) { + if ( meta[ node.name ].leave ) meta[ node.name ].leave( generator, node ); + return; + } + if ( node.name in generator.components || node.name === ':Self' ) { Component.leave( generator, node ); return; diff --git a/src/generators/server-side-rendering/visitors/meta/Window.js b/src/generators/server-side-rendering/visitors/meta/Window.js new file mode 100644 index 0000000000..7df7d8bdaf --- /dev/null +++ b/src/generators/server-side-rendering/visitors/meta/Window.js @@ -0,0 +1,9 @@ +export default { + enter () { + // noop + }, + + leave () { + // noop + } +}; diff --git a/test/generator/samples/window-event/_config.js b/test/generator/samples/window-event/_config.js index 89dfd2057c..d79202f67a 100644 --- a/test/generator/samples/window-event/_config.js +++ b/test/generator/samples/window-event/_config.js @@ -1,6 +1,8 @@ export default { html: `
undefinedxundefined
`, + 'skip-ssr': true, // there's some kind of weird bug with this test... it compiles with the wrong require.extensions hook for some bizarre reason + test ( assert, component, target, window ) { const event = new window.Event( 'resize' ); From f5c612520eee5fba977b68e61eb4fb5f4616f3ed Mon Sep 17 00:00:00 2001 From: Rich-Harris Date: Sun, 19 Mar 2017 12:36:02 -0400 Subject: [PATCH 4/6] ugh node 4 --- test/generator/samples/window-event/_config.js | 1 + 1 file changed, 1 insertion(+) diff --git a/test/generator/samples/window-event/_config.js b/test/generator/samples/window-event/_config.js index d79202f67a..8d805ba59e 100644 --- a/test/generator/samples/window-event/_config.js +++ b/test/generator/samples/window-event/_config.js @@ -1,6 +1,7 @@ export default { html: `
undefinedxundefined
`, + skip: /^v4/.test( process.version ), // node 4 apparently does some dumb stuff 'skip-ssr': true, // there's some kind of weird bug with this test... it compiles with the wrong require.extensions hook for some bizarre reason test ( assert, component, target, window ) { From 4949edfe55b272ea63e24ba923783490983c7a42 Mon Sep 17 00:00:00 2001 From: Rich-Harris Date: Sun, 19 Mar 2017 14:33:52 -0400 Subject: [PATCH 5/6] add infrastructure for bindings on <:Window> --- src/generators/dom/index.js | 9 ++++ src/generators/dom/visitors/meta/Window.js | 52 +++++++++++++++++++ .../samples/window-binding-resize/_config.js | 29 +++++++++++ .../samples/window-binding-resize/main.html | 3 ++ 4 files changed, 93 insertions(+) create mode 100644 test/generator/samples/window-binding-resize/_config.js create mode 100644 test/generator/samples/window-binding-resize/main.html diff --git a/src/generators/dom/index.js b/src/generators/dom/index.js index ebf478f5a2..004cb6b763 100644 --- a/src/generators/dom/index.js +++ b/src/generators/dom/index.js @@ -19,6 +19,11 @@ class DomGenerator extends Generator { this.importedNames = {}; this.aliases = {}; + // initial values for e.g. window.innerWidth, if there's a <:Window> meta tag + this.builders = { + metaBindings: new CodeBuilder() + }; + this.importedComponents = {}; } @@ -354,6 +359,10 @@ export default function dom ( parsed, source, options, names ) { `this._state = ${templateProperties.data ? `Object.assign( template.data(), options.data )` : `options.data || {}`};` ); + if ( !generator.builders.metaBindings.isEmpty() ) { + constructorBlock.addBlock( generator.builders.metaBindings ); + } + if ( templateProperties.computed ) { constructorBlock.addLine( `${generator.alias( 'applyComputations' )}( this._state, this._state, {}, true );` diff --git a/src/generators/dom/visitors/meta/Window.js b/src/generators/dom/visitors/meta/Window.js index dcda89c247..e1d5cb7b43 100644 --- a/src/generators/dom/visitors/meta/Window.js +++ b/src/generators/dom/visitors/meta/Window.js @@ -1,8 +1,20 @@ import flattenReference from '../../../../utils/flattenReference.js'; import deindent from '../../../../utils/deindent.js'; +const associatedEvents = { + innerWidth: 'resize', + innerHeight: 'resize', + outerWidth: 'resize', + outerHeight: 'resize', + + scrollX: 'scroll', + scrollY: 'scroll' +}; + export default { enter ( generator, node ) { + const events = {}; + node.attributes.forEach( attribute => { if ( attribute.type === 'EventHandler' ) { // TODO verify that it's a valid callee (i.e. built-in or declared method) @@ -27,6 +39,46 @@ export default { window.removeEventListener( '${attribute.name}', ${handlerName} ); ` ); } + + if ( attribute.type === 'Binding' ) { + const associatedEvent = associatedEvents[ attribute.name ]; + + if ( !associatedEvent ) { + throw new Error( `Cannot bind to ${attribute.name} on <:Window>` ); + } + + if ( attribute.value.type !== 'Identifier' ) { + const { parts, keypath } = flattenReference( attribute.value ); + throw new Error( `Bindings on <:Window/> must be to top-level properties, e.g. '${parts.pop()}' rather than '${keypath}'` ); + } + + if ( !events[ associatedEvent ] ) events[ associatedEvent ] = []; + events[ associatedEvent ].push( `${attribute.value.name}: this.${attribute.name}` ); + + // add initial value + generator.builders.metaBindings.addLine( + `this._state.${attribute.value.name} = window.${attribute.name};` + ); + } + }); + + Object.keys( events ).forEach( event => { + const handlerName = generator.current.getUniqueName( `onwindow${event}` ); + + const props = events[ event ].join( ',\n' ); + + generator.current.builders.init.addBlock( deindent` + var ${handlerName} = function ( event ) { + component.set({ + ${props} + }); + }; + window.addEventListener( '${event}', ${handlerName} ); + ` ); + + generator.current.builders.teardown.addBlock( deindent` + window.removeEventListener( '${event}', ${handlerName} ); + ` ); }); } }; diff --git a/test/generator/samples/window-binding-resize/_config.js b/test/generator/samples/window-binding-resize/_config.js new file mode 100644 index 0000000000..ee991552a7 --- /dev/null +++ b/test/generator/samples/window-binding-resize/_config.js @@ -0,0 +1,29 @@ +export default { + html: `
1024x768
`, + + skip: /^v4/.test( process.version ), // node 4 apparently does some dumb stuff + 'skip-ssr': true, // there's some kind of weird bug with this test... it compiles with the wrong require.extensions hook for some bizarre reason + + test ( assert, component, target, window ) { + const event = new window.Event( 'resize' ); + + // JSDOM executes window event listeners with `global` rather than + // `window` (bug?) so we need to do this + Object.defineProperties( global, { + innerWidth: { + value: 567, + configurable: true + }, + innerHeight: { + value: 456, + configurable: true + } + }); + + window.dispatchEvent( event ); + + assert.htmlEqual( target.innerHTML, ` +
567x456
+ `); + } +}; \ No newline at end of file diff --git a/test/generator/samples/window-binding-resize/main.html b/test/generator/samples/window-binding-resize/main.html new file mode 100644 index 0000000000..e7e682a10c --- /dev/null +++ b/test/generator/samples/window-binding-resize/main.html @@ -0,0 +1,3 @@ +<:Window bind:innerWidth='width' bind:innerHeight='height'/> + +
{{width}}x{{height}}
\ No newline at end of file From 1fea246acde2e299b96eb9c4944fb901a114b0bf Mon Sep 17 00:00:00 2001 From: Rich-Harris Date: Sun, 26 Mar 2017 09:50:30 -0400 Subject: [PATCH 6/6] fix bad merge --- src/generators/dom/index.js | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/generators/dom/index.js b/src/generators/dom/index.js index 0174bd6d4e..38c5f81ec9 100644 --- a/src/generators/dom/index.js +++ b/src/generators/dom/index.js @@ -14,11 +14,6 @@ class DomGenerator extends Generator { this.renderers = []; this.uses = new Set(); - // allow compiler to deconflict user's `import { get } from 'whatever'` and - // Svelte's builtin `import { get, ... } from 'svelte/shared.js'`; - this.importedNames = {}; - this.aliases = {}; - // initial values for e.g. window.innerWidth, if there's a <:Window> meta tag this.builders = { metaBindings: new CodeBuilder()