diff --git a/compiler/generate/css/process.js b/compiler/generate/css/process.js new file mode 100644 index 0000000000..bf26549341 --- /dev/null +++ b/compiler/generate/css/process.js @@ -0,0 +1,18 @@ +import deindent from '../utils/deindent.js'; +import spaces from '../../utils/spaces.js'; +import transform from './transform.js'; + +export default function process ( parsed ) { + const scoped = transform( spaces( parsed.css.content.start ) + parsed.css.content.styles, parsed.hash ); + + return deindent` + let addedCss = false; + function addCss () { + var style = document.createElement( 'style' ); + style.textContent = ${JSON.stringify( scoped )}; + document.head.appendChild( style ); + + addedCss = true; + } + `; +} diff --git a/compiler/generate/css/transform.js b/compiler/generate/css/transform.js new file mode 100644 index 0000000000..803c16d47a --- /dev/null +++ b/compiler/generate/css/transform.js @@ -0,0 +1,58 @@ +// largely borrowed from Ractive – https://github.com/ractivejs/ractive/blob/2ec648aaf5296bb88c21812e947e0e42fcc456e3/src/Ractive/config/custom/css/transform.js +const selectorsPattern = /(?:^|\})?\s*([^\{\}]+)\s*\{/g; +const commentsPattern = /\/\*.*?\*\//g; +const selectorUnitPattern = /((?:(?:\[[^\]+]\])|(?:[^\s\+\>~:]))+)((?:::?[^\s\+\>\~\(:]+(?:\([^\)]+\))?)*\s*[\s\+\>\~]?)\s*/g; +const excludePattern = /^(?:@|\d+%)/; + +function transformSelector ( selector, parent ) { + const selectorUnits = []; + let match; + + while ( match = selectorUnitPattern.exec( selector ) ) { + selectorUnits.push({ + str: match[0], + base: match[1], + modifiers: match[2] + }); + } + + // For each simple selector within the selector, we need to create a version + // that a) combines with the id, and b) is inside the id + const base = selectorUnits.map( unit => unit.str ); + + const transformed = []; + let i = selectorUnits.length; + + while ( i-- ) { + const appended = base.slice(); + + // Pseudo-selectors should go after the attribute selector + const unit = selectorUnits[i]; + appended[i] = unit.base + parent + unit.modifiers || ''; + + const prepended = base.slice(); + prepended[i] = parent + ' ' + prepended[i]; + + transformed.push( appended.join( ' ' ), prepended.join( ' ' ) ); + } + + return transformed.join( ', ' ); +} + +export default function transformCss ( css, hash ) { + const attr = `[svelte-${hash}]`; + + return css + .replace( commentsPattern, '' ) + .replace( selectorsPattern, ( match, $1 ) => { + // don't transform at-rules and keyframe declarations + if ( excludePattern.test( $1 ) ) return match; + + const selectors = $1.split( ',' ).map( selector => selector.trim() ); + const transformed = selectors + .map( selector => transformSelector( selector, attr ) ) + .join( ', ' ) + ' '; + + return match.replace( $1, transformed ); + }); +} diff --git a/compiler/generate/index.js b/compiler/generate/index.js index 5af327eb3f..da70291449 100644 --- a/compiler/generate/index.js +++ b/compiler/generate/index.js @@ -5,13 +5,12 @@ import isReference from './utils/isReference.js'; import counter from './utils/counter.js'; import flattenReference from './utils/flattenReference.js'; import visitors from './visitors/index.js'; +import processCss from './css/process.js'; export default function generate ( parsed, template, options = {} ) { const renderers = []; const generator = { - code: new MagicString( template ), - addRenderer ( fragment ) { if ( fragment.autofocus ) { fragment.initStatements.push( `${fragment.autofocus}.focus();` ); @@ -43,6 +42,10 @@ export default function generate ( parsed, template, options = {} ) { }); }, + code: new MagicString( template ), + + components: {}, + contextualise ( expression, isEventHandler ) { const usedContexts = []; @@ -81,18 +84,20 @@ export default function generate ( parsed, template, options = {} ) { return usedContexts; }, - helpers: {}, - events: {}, - components: {}, - - getName: counter(), - // TODO use getName instead of counters counters: { if: 0, each: 0 }, + events: {}, + + getName: counter(), + + cssId: parsed.css ? `svelte-${parsed.hash}` : '', + + helpers: {}, + usesRefs: false, template @@ -127,6 +132,7 @@ export default function generate ( parsed, template, options = {} ) { name: 'renderMainFragment', namespace: null, target: 'target', + elementDepth: 0, initStatements: [], updateStatements: [], @@ -204,11 +210,15 @@ export default function generate ( parsed, template, options = {} ) { dispatchObservers( observers.deferred, newState, oldState ); ` ); + const addCss = parsed.css ? processCss( parsed ) : null; + const constructorName = options.name || 'SvelteComponent'; const result = deindent` ${parsed.js ? `[✂${parsed.js.content.start}-${parsed.js.content.end}✂]` : ``} + ${parsed.css ? addCss : ``} + ${renderers.reverse().join( '\n\n' )} export default function ${constructorName} ( options ) { @@ -292,6 +302,7 @@ export default function generate ( parsed, template, options = {} ) { this.fire( 'teardown' );${templateProperties.onteardown ? `\ntemplate.onteardown.call( this );` : ``} }; + ${parsed.css ? `if ( !addedCss ) addCss();` : ''} let mainFragment = renderMainFragment( this, options.target ); this.set( ${templateProperties.data ? `Object.assign( template.data(), options.data )` : `options.data`} ); diff --git a/compiler/generate/visitors/Element.js b/compiler/generate/visitors/Element.js index d84235457e..1d91061ab5 100644 --- a/compiler/generate/visitors/Element.js +++ b/compiler/generate/visitors/Element.js @@ -68,12 +68,15 @@ export default { local.update.push( declarations ); } - local.init.unshift( - local.namespace ? - `var ${name} = document.createElementNS( '${local.namespace}', '${node.name}' );` : - `var ${name} = document.createElement( '${node.name}' );` - ); + let render = local.namespace ? + `var ${name} = document.createElementNS( '${local.namespace}', '${node.name}' );` : + `var ${name} = document.createElement( '${node.name}' );`; + if ( generator.cssId && !generator.current.elementDepth ) { + render += `\n${name}.setAttribute( '${generator.cssId}', '' );`; + } + + local.init.unshift( render ); local.teardown.push( `${name}.parentNode.removeChild( ${name} );` ); } @@ -85,7 +88,8 @@ export default { isComponent, namespace: local.namespace, target: name, - parent: generator.current + parent: generator.current, + elementDepth: generator.current.elementDepth + 1 }); }, diff --git a/compiler/parse/index.js b/compiler/parse/index.js index 03b02e3697..17bbfb6bdb 100644 --- a/compiler/parse/index.js +++ b/compiler/parse/index.js @@ -2,7 +2,8 @@ import { locate } from 'locate-character'; import fragment from './state/fragment.js'; import { whitespace } from './patterns.js'; import { trimStart, trimEnd } from './utils/trim.js'; -import spaces from './utils/spaces.js'; +import spaces from '../utils/spaces.js'; +import hash from './utils/hash.js'; function tabsToSpaces ( str ) { return str.replace( /^\t+/, match => match.split( '\t' ).join( ' ' ) ); @@ -165,6 +166,7 @@ export default function parse ( template ) { } return { + hash: hash( template ), html: parser.html, css: parser.css, js: parser.js diff --git a/compiler/parse/read/script.js b/compiler/parse/read/script.js index 2f91e323ea..b0c8cf197b 100644 --- a/compiler/parse/read/script.js +++ b/compiler/parse/read/script.js @@ -1,5 +1,5 @@ import { parse, tokenizer } from 'acorn'; -import spaces from '../utils/spaces.js'; +import spaces from '../../utils/spaces.js'; export default function readScript ( parser, start, attributes ) { const scriptStart = parser.index; diff --git a/compiler/parse/read/style.js b/compiler/parse/read/style.js index bab3d2e7b6..5da3cc2d4c 100644 --- a/compiler/parse/read/style.js +++ b/compiler/parse/read/style.js @@ -1,3 +1,19 @@ -export default function readStyle () { - throw new Error( 'TODO ', true ); + const end = parser.index; + + return { + start, + end, + attributes, + content: { + start: contentStart, + end: contentEnd, + styles + } + }; } diff --git a/compiler/parse/utils/hash.js b/compiler/parse/utils/hash.js new file mode 100644 index 0000000000..02dc221523 --- /dev/null +++ b/compiler/parse/utils/hash.js @@ -0,0 +1,8 @@ +// https://github.com/darkskyapp/string-hash/blob/master/index.js +export default function hash ( str ) { + let hash = 5381; + let i = str.length; + + while ( i-- ) hash = ( hash * 33 ) ^ str.charCodeAt( i ); + return hash >>> 0; +} diff --git a/compiler/parse/utils/spaces.js b/compiler/utils/spaces.js similarity index 100% rename from compiler/parse/utils/spaces.js rename to compiler/utils/spaces.js diff --git a/test/compiler/css/Widget.html b/test/compiler/css/Widget.html new file mode 100644 index 0000000000..1d893b8bcd --- /dev/null +++ b/test/compiler/css/Widget.html @@ -0,0 +1,7 @@ +

test

+ + diff --git a/test/compiler/css/_config.js b/test/compiler/css/_config.js new file mode 100644 index 0000000000..af689c32f6 --- /dev/null +++ b/test/compiler/css/_config.js @@ -0,0 +1,10 @@ +import * as assert from 'assert'; + +export default { + test ( component, target, window ) { + const [ control, test ] = target.querySelectorAll( 'p' ); + + assert.equal( window.getComputedStyle( control ).color, '' ); + assert.equal( window.getComputedStyle( test ).color, 'red' ); + } +}; diff --git a/test/compiler/css/main.html b/test/compiler/css/main.html new file mode 100644 index 0000000000..70e77bd59b --- /dev/null +++ b/test/compiler/css/main.html @@ -0,0 +1,10 @@ +

control

+ + + diff --git a/test/parser/css/input.html b/test/parser/css/input.html new file mode 100644 index 0000000000..659c4343f6 --- /dev/null +++ b/test/parser/css/input.html @@ -0,0 +1,7 @@ +
foo
+ + diff --git a/test/parser/css/output.json b/test/parser/css/output.json new file mode 100644 index 0000000000..1e78e04cf5 --- /dev/null +++ b/test/parser/css/output.json @@ -0,0 +1,35 @@ +{ + "html": { + "start": 0, + "end": 14, + "type": "Fragment", + "children": [ + { + "start": 0, + "end": 14, + "type": "Element", + "name": "div", + "attributes": [], + "children": [ + { + "start": 5, + "end": 8, + "type": "Text", + "data": "foo" + } + ] + } + ] + }, + "css": { + "start": 16, + "end": 56, + "attributes": [], + "content": { + "start": 23, + "end": 48, + "styles": "\n\tdiv {\n\t\tcolor: red;\n\t}\n" + } + }, + "js": null +} diff --git a/test/test.js b/test/test.js index e4d6849cc1..7521cf29d0 100644 --- a/test/test.js +++ b/test/test.js @@ -35,7 +35,9 @@ describe( 'svelte', () => { const actual = parse( input ); const expected = require( `./parser/${dir}/output.json` ); - assert.deepEqual( actual, expected ); + assert.deepEqual( actual.html, expected.html ); + assert.deepEqual( actual.css, expected.css ); + assert.deepEqual( actual.js, expected.js ); } catch ( err ) { if ( err.name !== 'ParseError' ) throw err;