From eb95fd1ebb86e0f8cd42b007e7e5d20b671c42db Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 24 Feb 2017 12:49:44 -0500 Subject: [PATCH] use css-tree to generate CSS AST and walk it --- package.json | 1 + src/generators/dom/index.js | 4 +- src/generators/server-side-rendering/index.js | 4 +- src/generators/shared/css/process.js | 5 -- src/generators/shared/css/transform.js | 58 ------------------- src/generators/shared/processCss.js | 51 ++++++++++++++++ 6 files changed, 56 insertions(+), 67 deletions(-) delete mode 100644 src/generators/shared/css/process.js delete mode 100644 src/generators/shared/css/transform.js create mode 100644 src/generators/shared/processCss.js diff --git a/package.json b/package.json index 8845b478d7..b1fdd90af1 100644 --- a/package.json +++ b/package.json @@ -49,6 +49,7 @@ "babel-register": "^6.18.0", "codecov": "^1.0.1", "console-group": "^0.3.2", + "css-tree": "^1.0.0-alpha16", "eslint": "^3.12.2", "eslint-plugin-import": "^2.2.0", "estree-walker": "^0.3.0", diff --git a/src/generators/dom/index.js b/src/generators/dom/index.js index 773a28f8d4..c2a1cacb56 100644 --- a/src/generators/dom/index.js +++ b/src/generators/dom/index.js @@ -2,7 +2,7 @@ import deindent from '../../utils/deindent.js'; import getBuilders from './utils/getBuilders.js'; import CodeBuilder from '../../utils/CodeBuilder.js'; import namespaces from '../../utils/namespaces.js'; -import processCss from '../shared/css/process.js'; +import processCss from '../shared/processCss.js'; import visitors from './visitors/index.js'; import Generator from '../Generator.js'; import * as shared from '../../shared/index.js'; @@ -221,7 +221,7 @@ export default function dom ( parsed, source, options, names ) { let addedCss = false; function addCss () { var style = createElement( 'style' ); - style.textContent = ${JSON.stringify( processCss( parsed ) )}; + style.textContent = ${JSON.stringify( processCss( parsed, generator.code ) )}; appendNode( style, document.head ); addedCss = true; diff --git a/src/generators/server-side-rendering/index.js b/src/generators/server-side-rendering/index.js index d2e8cd961c..a12366cc2d 100644 --- a/src/generators/server-side-rendering/index.js +++ b/src/generators/server-side-rendering/index.js @@ -1,6 +1,6 @@ import deindent from '../../utils/deindent.js'; import CodeBuilder from '../../utils/CodeBuilder.js'; -import processCss from '../shared/css/process.js'; +import processCss from '../shared/processCss.js'; import visitors from './visitors/index.js'; import Generator from '../Generator.js'; @@ -94,7 +94,7 @@ export default function ssr ( parsed, source, options, names ) { builders.renderCss.addBlock( deindent` components.push({ filename: ${name}.filename, - css: ${JSON.stringify( processCss( parsed ) )}, + css: ${JSON.stringify( processCss( parsed, generator.code ) )}, map: null // TODO }); ` ); diff --git a/src/generators/shared/css/process.js b/src/generators/shared/css/process.js deleted file mode 100644 index beeb4353ad..0000000000 --- a/src/generators/shared/css/process.js +++ /dev/null @@ -1,5 +0,0 @@ -import transform from './transform.js'; - -export default function process ( parsed ) { - return transform( parsed.css.content.styles, parsed.hash ); -} diff --git a/src/generators/shared/css/transform.js b/src/generators/shared/css/transform.js deleted file mode 100644 index 12ae0981e5..0000000000 --- a/src/generators/shared/css/transform.js +++ /dev/null @@ -1,58 +0,0 @@ -// 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 = /\/\*[\s\S]*?\*\//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/src/generators/shared/processCss.js b/src/generators/shared/processCss.js new file mode 100644 index 0000000000..4f57e4db70 --- /dev/null +++ b/src/generators/shared/processCss.js @@ -0,0 +1,51 @@ +import parse from 'css-tree/lib/parser/index.js'; +import walk from 'css-tree/lib/utils/walk.js'; + +const commentsPattern = /\/\*[\s\S]*?\*\//g; + +export default function processCss ( parsed, code ) { + const css = parsed.css.content.styles; + const offset = parsed.css.content.start; + + const ast = parse( css, { + positions: true + }); + + const attr = `[svelte-${parsed.hash}]`; + + walk.rules( ast, rule => { + rule.selector.children.each( selector => { + const start = selector.loc.start.offset; + const end = selector.loc.end.offset; + + const selectorString = css.slice( start, end ); + + const firstToken = selector.children.head; + + let transformed; + + if ( firstToken.data.type === 'TypeSelector' ) { + const insert = firstToken.data.loc.end.offset; + const head = css.slice( start, insert ); + const tail = css.slice( insert, end ); + + transformed = `${head}${attr}${tail}, ${attr} ${selectorString}`; + } else { + transformed = `${attr}${selectorString}, ${attr} ${selectorString}`; + } + + code.overwrite( start + offset, end + offset, transformed ); + }); + }); + + // remove comments. TODO would be nice if this was exposed in css-tree + let match; + while ( match = commentsPattern.exec( css ) ) { + const start = match.index + offset; + const end = start + match[0].length; + + code.remove( start, end ); + } + + return code.slice( parsed.css.content.start, parsed.css.content.end ); +} \ No newline at end of file