From 82559c37750f04b7a9dad66ee257ff425e97e215 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sun, 30 Jul 2017 14:28:08 -0400 Subject: [PATCH] support ref:foo as a CSS selector (#693) --- src/css/Selector.ts | 24 ++++- .../dom/visitors/Element/Element.ts | 6 ++ .../server-side-rendering/visitors/Element.ts | 4 + src/parse/read/style.ts | 45 +++++++-- test/css/index.js | 4 +- test/css/samples/refs-qualified/_config.js | 23 +++++ test/css/samples/refs-qualified/expected.css | 1 + test/css/samples/refs-qualified/expected.html | 1 + test/css/samples/refs-qualified/input.html | 15 +++ test/css/samples/refs/_config.js | 19 ++++ test/css/samples/refs/expected.css | 1 + test/css/samples/refs/expected.html | 3 + test/css/samples/refs/input.html | 17 ++++ .../samples/css-ref-selector/input.html | 7 ++ .../samples/css-ref-selector/output.json | 96 +++++++++++++++++++ yarn.lock | 6 +- 16 files changed, 258 insertions(+), 14 deletions(-) create mode 100644 test/css/samples/refs-qualified/_config.js create mode 100644 test/css/samples/refs-qualified/expected.css create mode 100644 test/css/samples/refs-qualified/expected.html create mode 100644 test/css/samples/refs-qualified/input.html create mode 100644 test/css/samples/refs/_config.js create mode 100644 test/css/samples/refs/expected.css create mode 100644 test/css/samples/refs/expected.html create mode 100644 test/css/samples/refs/input.html create mode 100644 test/parser/samples/css-ref-selector/input.html create mode 100644 test/parser/samples/css-ref-selector/output.json diff --git a/src/css/Selector.ts b/src/css/Selector.ts index 07a0fb9786..b4ebc087f5 100644 --- a/src/css/Selector.ts +++ b/src/css/Selector.ts @@ -51,7 +51,7 @@ export default class Selector { }); } - transform(code: MagicString, attr: string) { + transform(code: MagicString, attr: string, id: string) { function encapsulateBlock(block: Block) { let i = block.selectors.length; while (i--) { @@ -64,7 +64,19 @@ export default class Selector { code.appendLeft(selector.end, attr); } - return; + break; + } + + i = block.selectors.length; + while (i--) { + const selector = block.selectors[i]; + + if (selector.type === 'RefSelector') { + code.overwrite(selector.start, selector.end, `[svelte-ref-${selector.name}]`, { + contentOnly: true, + storeName: false + }); + } } } @@ -154,6 +166,14 @@ function selectorAppliesTo(blocks: Block[], node: Node, stack: Node[]): boolean if (node.name !== selector.name && selector.name !== '*') return false; } + else if (selector.type === 'RefSelector') { + if (node.attributes.some((attr: Node) => attr.type === 'Ref' && attr.name === selector.name)) { + node._cssRefAttribute = selector.name; + return true; + } + return; + } + else { // bail. TODO figure out what these could be return true; diff --git a/src/generators/dom/visitors/Element/Element.ts b/src/generators/dom/visitors/Element/Element.ts index 19743bb3ce..c20ee06abc 100644 --- a/src/generators/dom/visitors/Element/Element.ts +++ b/src/generators/dom/visitors/Element/Element.ts @@ -87,6 +87,12 @@ export default function visitElement( block.builders.hydrate.addLine( `@encapsulateStyles( ${name} );` ); + + if (node._cssRefAttribute) { + block.builders.hydrate.addLine( + `@setAttribute( ${name}, 'svelte-ref-${node._cssRefAttribute}', '' );` + ) + } } function visitAttributesAndAddProps() { diff --git a/src/generators/server-side-rendering/visitors/Element.ts b/src/generators/server-side-rendering/visitors/Element.ts index 059e8bced2..1dcded426f 100644 --- a/src/generators/server-side-rendering/visitors/Element.ts +++ b/src/generators/server-side-rendering/visitors/Element.ts @@ -58,6 +58,10 @@ export default function visitElement( if (node._needsCssAttribute) { openingTag += ` ${generator.stylesheet.id}`; + + if (node._cssRefAttribute) { + openingTag += ` svelte-ref-${node._cssRefAttribute}`; + } } openingTag += '>'; diff --git a/src/parse/read/style.ts b/src/parse/read/style.ts index d12cd0ab41..d8f40e9588 100644 --- a/src/parse/read/style.ts +++ b/src/parse/read/style.ts @@ -1,5 +1,5 @@ import parse from 'css-tree/lib/parser/index.js'; -import walk from 'css-tree/lib/utils/walk.js'; +import { walk } from 'estree-walker'; import { Parser } from '../index'; import { Node } from '../../interfaces'; @@ -23,12 +23,33 @@ export default function readStyle(parser: Parser, start: number, attributes: Nod } } + ast = JSON.parse(JSON.stringify(ast)); + // tidy up AST - walk.all(ast, (node: Node) => { - if (node.loc) { - node.start = node.loc.start.offset; - node.end = node.loc.end.offset; - delete node.loc; + walk(ast, { + enter: (node: Node) => { + // replace `ref:a` nodes + if (node.type === 'Selector') { + for (let i = 0; i < node.children.length; i += 1) { + const a = node.children[i]; + const b = node.children[i + 1]; + + if (isRefSelector(a, b)) { + node.children.splice(i, 2, { + type: 'RefSelector', + start: a.loc.start.offset, + end: b.loc.end.offset, + name: b.name + }); + } + } + } + + if (node.loc) { + node.start = node.loc.start.offset; + node.end = node.loc.end.offset; + delete node.loc; + } } }); @@ -39,7 +60,7 @@ export default function readStyle(parser: Parser, start: number, attributes: Nod start, end, attributes, - children: JSON.parse(JSON.stringify(ast.children)), + children: ast.children, content: { start: contentStart, end: contentEnd, @@ -47,3 +68,13 @@ export default function readStyle(parser: Parser, start: number, attributes: Nod }, }; } + +function isRefSelector(a: Node, b: Node) { + if (!b) return false; + + return ( + a.type === 'TypeSelector' && + a.name === 'ref' && + b.type === 'PseudoClassSelector' + ); +} \ No newline at end of file diff --git a/test/css/index.js b/test/css/index.js index 0d0b0e1b4a..d595d5766d 100644 --- a/test/css/index.js +++ b/test/css/index.js @@ -84,7 +84,7 @@ describe("css", () => { // dom assert.equal( - normalizeHtml(window, html).replace(/svelte-\d+/g, 'svelte-xyz'), + normalizeHtml(window, html.replace(/svelte-\d+/g, 'svelte-xyz')), normalizeHtml(window, expected.html) ); @@ -92,7 +92,7 @@ describe("css", () => { const component = eval(`(function () { ${ssr.code}; return SvelteComponent; }())`); assert.equal( - normalizeHtml(window, component.render(config.data)).replace(/svelte-\d+/g, 'svelte-xyz'), + normalizeHtml(window, component.render(config.data).replace(/svelte-\d+/g, 'svelte-xyz')), normalizeHtml(window, expected.html) ); }); diff --git a/test/css/samples/refs-qualified/_config.js b/test/css/samples/refs-qualified/_config.js new file mode 100644 index 0000000000..7a68dd30ec --- /dev/null +++ b/test/css/samples/refs-qualified/_config.js @@ -0,0 +1,23 @@ +export default { + cascade: false, + + data: { + active: true + }, + + warnings: [{ + message: 'Unused CSS selector', + loc: { + column: 1, + line: 12 + }, + pos: 174, + frame: ` + 10: } + 11: + 12: ref:button.inactive { + ^ + 13: color: green; + 14: }` + }] +}; \ No newline at end of file diff --git a/test/css/samples/refs-qualified/expected.css b/test/css/samples/refs-qualified/expected.css new file mode 100644 index 0000000000..005306d25f --- /dev/null +++ b/test/css/samples/refs-qualified/expected.css @@ -0,0 +1 @@ +[svelte-ref-button].active[svelte-xyz]{color:red} \ No newline at end of file diff --git a/test/css/samples/refs-qualified/expected.html b/test/css/samples/refs-qualified/expected.html new file mode 100644 index 0000000000..8805a8a3a2 --- /dev/null +++ b/test/css/samples/refs-qualified/expected.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/test/css/samples/refs-qualified/input.html b/test/css/samples/refs-qualified/input.html new file mode 100644 index 0000000000..44ff33a3ce --- /dev/null +++ b/test/css/samples/refs-qualified/input.html @@ -0,0 +1,15 @@ +{{#if active}} + +{{else}} + +{{/if}} + + \ No newline at end of file diff --git a/test/css/samples/refs/_config.js b/test/css/samples/refs/_config.js new file mode 100644 index 0000000000..995ae66b4e --- /dev/null +++ b/test/css/samples/refs/_config.js @@ -0,0 +1,19 @@ +export default { + cascade: false, + + warnings: [{ + message: 'Unused CSS selector', + loc: { + column: 1, + line: 14 + }, + pos: 120, + frame: ` + 12: } + 13: + 14: ref:d { + ^ + 15: color: blue; + 16: }` + }] +}; \ No newline at end of file diff --git a/test/css/samples/refs/expected.css b/test/css/samples/refs/expected.css new file mode 100644 index 0000000000..9c5055a064 --- /dev/null +++ b/test/css/samples/refs/expected.css @@ -0,0 +1 @@ +[svelte-ref-a][svelte-xyz]{color:red}[svelte-ref-b][svelte-xyz]{color:green} \ No newline at end of file diff --git a/test/css/samples/refs/expected.html b/test/css/samples/refs/expected.html new file mode 100644 index 0000000000..cb16a55879 --- /dev/null +++ b/test/css/samples/refs/expected.html @@ -0,0 +1,3 @@ +
+
+
\ No newline at end of file diff --git a/test/css/samples/refs/input.html b/test/css/samples/refs/input.html new file mode 100644 index 0000000000..a5d1156c5a --- /dev/null +++ b/test/css/samples/refs/input.html @@ -0,0 +1,17 @@ +
+
+
+ + \ No newline at end of file diff --git a/test/parser/samples/css-ref-selector/input.html b/test/parser/samples/css-ref-selector/input.html new file mode 100644 index 0000000000..07ab3790f7 --- /dev/null +++ b/test/parser/samples/css-ref-selector/input.html @@ -0,0 +1,7 @@ +
+ + diff --git a/test/parser/samples/css-ref-selector/output.json b/test/parser/samples/css-ref-selector/output.json new file mode 100644 index 0000000000..2ddafc4aae --- /dev/null +++ b/test/parser/samples/css-ref-selector/output.json @@ -0,0 +1,96 @@ +{ + "hash": 1104014177, + "html": { + "start": 0, + "end": 14, + "type": "Fragment", + "children": [ + { + "start": 0, + "end": 14, + "type": "Element", + "name": "div", + "attributes": [ + { + "start": 5, + "end": 12, + "type": "Ref", + "name": "foo" + } + ], + "children": [] + }, + { + "start": 14, + "end": 16, + "type": "Text", + "data": "\n\n" + } + ] + }, + "css": { + "start": 16, + "end": 60, + "attributes": [], + "children": [ + { + "type": "Rule", + "selector": { + "type": "SelectorList", + "children": [ + { + "type": "Selector", + "children": [ + { + "type": "RefSelector", + "start": 25, + "end": 32, + "name": "foo" + } + ], + "start": 25, + "end": 32 + } + ], + "start": 25, + "end": 32 + }, + "block": { + "type": "Block", + "children": [ + { + "type": "Declaration", + "important": false, + "property": "color", + "value": { + "type": "Value", + "children": [ + { + "type": "Identifier", + "name": "red", + "start": 44, + "end": 47 + } + ], + "start": 43, + "end": 47 + }, + "start": 37, + "end": 47 + } + ], + "start": 33, + "end": 51 + }, + "start": 25, + "end": 51 + } + ], + "content": { + "start": 23, + "end": 52, + "styles": "\n\tref:foo {\n\t\tcolor: red;\n\t}\n" + } + }, + "js": null +} \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 45c20cdf10..fb04ab8c43 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1976,9 +1976,9 @@ magic-string@^0.19.0, magic-string@~0.19.0: dependencies: vlq "^0.2.1" -magic-string@^0.22.1: - version "0.22.1" - resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.22.1.tgz#a1bda64dfd4ae6c63797a45a67ee473b1f8d0e0f" +magic-string@^0.22.3: + version "0.22.3" + resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.22.3.tgz#047989d99bfc7cbdefba1604adc8912551cd7ef1" dependencies: vlq "^0.2.1"