From a796b91e84d864733e17c3d4d9faddd1bff30ce1 Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Wed, 12 Jan 2022 21:12:50 +0100 Subject: [PATCH] [fix] adjust style directive AST (#7127) - deduplicate type name: Is now "StyleDirective" - Don't transform value array to template literal in the AST phase but in the compiler phase. This ensures other tools know what the raw output was and that start/end positions are available --- src/compiler/compile/nodes/Element.ts | 8 +- src/compiler/compile/nodes/Style.ts | 22 -- src/compiler/compile/nodes/StyleDirective.ts | 39 ++ src/compiler/compile/nodes/interfaces.ts | 4 +- .../utils/nodes_to_template_literal.ts | 37 ++ src/compiler/interfaces.ts | 11 +- src/compiler/parse/state/tag.ts | 63 +--- .../output.json | 12 +- .../input.svelte | 8 +- .../output.json | 346 +++++++++++++++++- .../attribute-style-directive/output.json | 40 +- 11 files changed, 474 insertions(+), 116 deletions(-) delete mode 100644 src/compiler/compile/nodes/Style.ts create mode 100644 src/compiler/compile/nodes/StyleDirective.ts create mode 100644 src/compiler/compile/utils/nodes_to_template_literal.ts diff --git a/src/compiler/compile/nodes/Element.ts b/src/compiler/compile/nodes/Element.ts index c6b53b2e0d96..92a87dd6f8ae 100644 --- a/src/compiler/compile/nodes/Element.ts +++ b/src/compiler/compile/nodes/Element.ts @@ -7,7 +7,7 @@ import Transition from './Transition'; import Animation from './Animation'; import Action from './Action'; import Class from './Class'; -import Style from './Style'; +import StyleDirective from './StyleDirective'; import Text from './Text'; import { namespaces } from '../../utils/namespaces'; import map_children from './shared/map_children'; @@ -181,7 +181,7 @@ export default class Element extends Node { actions: Action[] = []; bindings: Binding[] = []; classes: Class[] = []; - styles: Style[] = []; + styles: StyleDirective[] = []; handlers: EventHandler[] = []; lets: Let[] = []; intro?: Transition = null; @@ -265,8 +265,8 @@ export default class Element extends Node { this.classes.push(new Class(component, this, scope, node)); break; - case 'Style': - this.styles.push(new Style(component, this, scope, node)); + case 'StyleDirective': + this.styles.push(new StyleDirective(component, this, scope, node)); break; case 'EventHandler': diff --git a/src/compiler/compile/nodes/Style.ts b/src/compiler/compile/nodes/Style.ts deleted file mode 100644 index e922f6f5783a..000000000000 --- a/src/compiler/compile/nodes/Style.ts +++ /dev/null @@ -1,22 +0,0 @@ -import Node from './shared/Node'; -import Expression from './shared/Expression'; -import { TemplateNode } from '../../interfaces'; -import TemplateScope from './shared/TemplateScope'; -import Component from '../Component'; - -export default class Style extends Node { - type: 'Style'; - name: string; - expression: Expression; - should_cache: boolean; - - constructor(component: Component, parent: Node, scope: TemplateScope, info: TemplateNode) { - super(component, parent, scope, info); - - this.name = info.name; - - this.expression = new Expression(component, this, scope, info.expression); - - this.should_cache = info.expression.type === 'TemplateLiteral' && info.expression.expressions.length > 0; - } -} diff --git a/src/compiler/compile/nodes/StyleDirective.ts b/src/compiler/compile/nodes/StyleDirective.ts new file mode 100644 index 000000000000..ea340cf2dc23 --- /dev/null +++ b/src/compiler/compile/nodes/StyleDirective.ts @@ -0,0 +1,39 @@ +import { TemplateNode } from '../../interfaces'; +import Component from '../Component'; +import { nodes_to_template_literal } from '../utils/nodes_to_template_literal'; +import Expression from './shared/Expression'; +import Node from './shared/Node'; +import TemplateScope from './shared/TemplateScope'; + +export default class StyleDirective extends Node { + type: 'StyleDirective'; + name: string; + expression: Expression; + should_cache: boolean; + + constructor(component: Component, parent: Node, scope: TemplateScope, info: TemplateNode) { + super(component, parent, scope, info); + + this.name = info.name; + + // Convert the value array to an expression so it's easier to handle + // the StyleDirective going forward. + if (info.value === true || (info.value.length === 1 && info.value[0].type === 'MustacheTag')) { + const identifier = info.value === true + ? { + type: 'Identifier', + start: info.end - info.name.length, + end: info.end, + name: info.name + } as any + : info.value[0].expression; + this.expression = new Expression(component, this, scope, identifier); + this.should_cache = false; + } else { + const raw_expression = nodes_to_template_literal(info.value); + this.expression = new Expression(component, this, scope, raw_expression); + this.should_cache = raw_expression.expressions.length > 0; + } + + } +} diff --git a/src/compiler/compile/nodes/interfaces.ts b/src/compiler/compile/nodes/interfaces.ts index b038cf62a75b..2c636b36f38b 100644 --- a/src/compiler/compile/nodes/interfaces.ts +++ b/src/compiler/compile/nodes/interfaces.ts @@ -8,7 +8,7 @@ import Binding from './Binding'; import Body from './Body'; import CatchBlock from './CatchBlock'; import Class from './Class'; -import Style from './Style'; +import StyleDirective from './StyleDirective'; import Comment from './Comment'; import ConstTag from './ConstTag'; import DebugTag from './DebugTag'; @@ -63,7 +63,7 @@ export type INode = Action | RawMustacheTag | Slot | SlotTemplate -| Style +| StyleDirective | Tag | Text | ThenBlock diff --git a/src/compiler/compile/utils/nodes_to_template_literal.ts b/src/compiler/compile/utils/nodes_to_template_literal.ts new file mode 100644 index 000000000000..46da9532616d --- /dev/null +++ b/src/compiler/compile/utils/nodes_to_template_literal.ts @@ -0,0 +1,37 @@ +import { TemplateElement, TemplateLiteral } from 'estree'; +import { MustacheTag, Text } from '../../interfaces'; + +/** + * Transforms a list of Text and MustacheTags into a TemplateLiteral expression. + * Start/End positions on the elements of the expression are not set. + */ +export function nodes_to_template_literal(value: Array): TemplateLiteral { + const literal: TemplateLiteral = { + type: 'TemplateLiteral', + expressions: [], + quasis: [] + }; + + let quasi: TemplateElement = { + type: 'TemplateElement', + value: { raw: '', cooked: null }, + tail: false + }; + + value.forEach((node) => { + if (node.type === 'Text') { + quasi.value.raw += node.raw; + } else if (node.type === 'MustacheTag') { + literal.quasis.push(quasi); + literal.expressions.push(node.expression as any); + quasi = { + type: 'TemplateElement', + value: { raw: '', cooked: null }, + tail: false + }; + } + }); + quasi.tail = true; + literal.quasis.push(quasi); + return literal; +} diff --git a/src/compiler/interfaces.ts b/src/compiler/interfaces.ts index 63dc29eec630..a9920750f96d 100644 --- a/src/compiler/interfaces.ts +++ b/src/compiler/interfaces.ts @@ -44,13 +44,18 @@ export type DirectiveType = 'Action' | 'Animation' | 'Binding' | 'Class' -| 'Style' +| 'StyleDirective' | 'EventHandler' | 'Let' | 'Ref' | 'Transition'; interface BaseDirective extends BaseNode { + type: DirectiveType; + name: string; +} + +interface BaseExpressionDirective extends BaseDirective { type: DirectiveType; expression: null | Node; name: string; @@ -74,13 +79,13 @@ export interface SpreadAttribute extends BaseNode { expression: Node; } -export interface Transition extends BaseDirective { +export interface Transition extends BaseExpressionDirective { type: 'Transition'; intro: boolean; outro: boolean; } -export type Directive = BaseDirective | Transition; +export type Directive = BaseDirective | BaseExpressionDirective | Transition; export type TemplateNode = Text | ConstTag diff --git a/src/compiler/parse/state/tag.ts b/src/compiler/parse/state/tag.ts index 81d8fff1c476..7de0f88413b7 100644 --- a/src/compiler/parse/state/tag.ts +++ b/src/compiler/parse/state/tag.ts @@ -1,14 +1,13 @@ -import { TemplateLiteral, TemplateElement, Expression } from 'estree'; +import { Directive, DirectiveType, TemplateNode, Text } from '../../interfaces'; +import { extract_svelte_ignore } from '../../utils/extract_svelte_ignore'; +import fuzzymatch from '../../utils/fuzzymatch'; +import { is_void } from '../../utils/names'; +import parser_errors from '../errors'; +import { Parser } from '../index'; import read_expression from '../read/expression'; import read_script from '../read/script'; import read_style from '../read/style'; -import { decode_character_references, closing_tag_omitted } from '../utils/html'; -import { is_void } from '../../utils/names'; -import { Parser } from '../index'; -import { Directive, DirectiveType, TemplateNode, Text, MustacheTag } from '../../interfaces'; -import fuzzymatch from '../../utils/fuzzymatch'; -import { extract_svelte_ignore } from '../../utils/extract_svelte_ignore'; -import parser_errors from '../errors'; +import { closing_tag_omitted, decode_character_references } from '../utils/html'; // eslint-disable-next-line no-useless-escape const valid_tag_name = /^\!?[a-zA-Z]{1,}:?[a-zA-Z0-9\-]*/; @@ -271,36 +270,6 @@ function read_tag_name(parser: Parser) { return name; } -function node_to_template_literal(value: Array): TemplateLiteral { - let quasi: TemplateElement = { - type: 'TemplateElement', - value: { raw: '', cooked: null }, - tail: false - }; - const literal: TemplateLiteral = { - type: 'TemplateLiteral', - expressions: [], - quasis: [] - }; - - value.forEach((node) => { - if (node.type === 'Text') { - quasi.value.raw += node.raw; - } else if (node.type === 'MustacheTag') { - literal.quasis.push(quasi); - literal.expressions.push(node.expression as Expression); - quasi = { - type: 'TemplateElement', - value: { raw: '', cooked: null }, - tail: false - }; - } - }); - quasi.tail = true; - literal.quasis.push(quasi); - return literal; -} - function read_attribute(parser: Parser, unique_names: Set) { const start = parser.index; @@ -396,14 +365,22 @@ function read_attribute(parser: Parser, unique_names: Set) { parser.error(parser_errors.invalid_ref_directive(directive_name), start); } + if (type === 'StyleDirective') { + return { + start, + end, + type, + name: directive_name, + value + }; + } + const first_value = value[0]; let expression = null; if (first_value) { const attribute_contains_text = (value as any[]).length > 1 || first_value.type === 'Text'; - if (type === 'Style') { - expression = attribute_contains_text ? node_to_template_literal(value as Array) : first_value.expression; - } else if (attribute_contains_text) { + if (attribute_contains_text) { parser.error(parser_errors.invalid_directive_value, first_value.start); } else { expression = first_value.expression; @@ -426,7 +403,7 @@ function read_attribute(parser: Parser, unique_names: Set) { } // Directive name is expression, e.g.

- if (!directive.expression && (type === 'Binding' || type === 'Class' || type === 'Style')) { + if (!directive.expression && (type === 'Binding' || type === 'Class')) { directive.expression = { start: directive.start + colon_index + 1, end: directive.end, @@ -454,7 +431,7 @@ function get_directive_type(name: string): DirectiveType { if (name === 'animate') return 'Animation'; if (name === 'bind') return 'Binding'; if (name === 'class') return 'Class'; - if (name === 'style') return 'Style'; + if (name === 'style') return 'StyleDirective'; if (name === 'on') return 'EventHandler'; if (name === 'let') return 'Let'; if (name === 'ref') return 'Ref'; diff --git a/test/parser/samples/attribute-style-directive-shorthand/output.json b/test/parser/samples/attribute-style-directive-shorthand/output.json index d4e81b6e4be2..ac658408e60b 100644 --- a/test/parser/samples/attribute-style-directive-shorthand/output.json +++ b/test/parser/samples/attribute-style-directive-shorthand/output.json @@ -13,19 +13,13 @@ { "start": 5, "end": 16, - "type": "Style", + "type": "StyleDirective", "name": "color", - "modifiers": [], - "expression": { - "start": 11, - "end": 16, - "name": "color", - "type": "Identifier" - } + "value": true } ], "children": [] } ] } -} +} \ No newline at end of file diff --git a/test/parser/samples/attribute-style-directive-string/input.svelte b/test/parser/samples/attribute-style-directive-string/input.svelte index b2eb6bfef8ce..dd8ec8dc94c8 100644 --- a/test/parser/samples/attribute-style-directive-string/input.svelte +++ b/test/parser/samples/attribute-style-directive-string/input.svelte @@ -1 +1,7 @@ -

\ No newline at end of file +
+
+
+
+
+
+
\ No newline at end of file diff --git a/test/parser/samples/attribute-style-directive-string/output.json b/test/parser/samples/attribute-style-directive-string/output.json index 4cf2bd30c2e5..e81e7be3c9fc 100644 --- a/test/parser/samples/attribute-style-directive-string/output.json +++ b/test/parser/samples/attribute-style-directive-string/output.json @@ -1,7 +1,7 @@ { "html": { "start": 0, - "end": 29, + "end": 252, "type": "Fragment", "children": [ { @@ -13,23 +13,339 @@ { "start": 5, "end": 22, - "type": "Style", + "type": "StyleDirective", "name": "color", - "modifiers": [], - "expression": { - "type": "TemplateLiteral", - "expressions": [], - "quasis": [ - { - "type": "TemplateElement", - "value": { - "raw": "red", - "cooked": null + "value": [ + { + "start": 18, + "end": 21, + "type": "Text", + "raw": "red", + "data": "red" + } + ] + } + ], + "children": [] + }, + { + "start": 29, + "end": 30, + "type": "Text", + "raw": "\n", + "data": "\n" + }, + { + "start": 30, + "end": 59, + "type": "Element", + "name": "div", + "attributes": [ + { + "start": 35, + "end": 52, + "type": "StyleDirective", + "name": "color", + "value": [ + { + "start": 48, + "end": 51, + "type": "Text", + "raw": "red", + "data": "red" + } + ] + } + ], + "children": [] + }, + { + "start": 59, + "end": 60, + "type": "Text", + "raw": "\n", + "data": "\n" + }, + { + "start": 60, + "end": 87, + "type": "Element", + "name": "div", + "attributes": [ + { + "start": 65, + "end": 80, + "type": "StyleDirective", + "name": "color", + "value": [ + { + "start": 77, + "end": 80, + "type": "Text", + "raw": "red", + "data": "red" + } + ] + } + ], + "children": [] + }, + { + "start": 87, + "end": 88, + "type": "Text", + "raw": "\n", + "data": "\n" + }, + { + "start": 88, + "end": 127, + "type": "Element", + "name": "div", + "attributes": [ + { + "start": 93, + "end": 120, + "type": "StyleDirective", + "name": "color", + "value": [ + { + "start": 106, + "end": 109, + "type": "Text", + "raw": "red", + "data": "red" + }, + { + "start": 109, + "end": 119, + "type": "MustacheTag", + "expression": { + "type": "Identifier", + "start": 110, + "end": 118, + "loc": { + "start": { + "line": 4, + "column": 22 + }, + "end": { + "line": 4, + "column": 30 + } + }, + "name": "variable" + } + } + ] + } + ], + "children": [] + }, + { + "start": 127, + "end": 128, + "type": "Text", + "raw": "\n", + "data": "\n" + }, + { + "start": 128, + "end": 167, + "type": "Element", + "name": "div", + "attributes": [ + { + "start": 133, + "end": 160, + "type": "StyleDirective", + "name": "color", + "value": [ + { + "start": 146, + "end": 149, + "type": "Text", + "raw": "red", + "data": "red" + }, + { + "start": 149, + "end": 159, + "type": "MustacheTag", + "expression": { + "type": "Identifier", + "start": 150, + "end": 158, + "loc": { + "start": { + "line": 5, + "column": 22 + }, + "end": { + "line": 5, + "column": 30 + } + }, + "name": "variable" + } + } + ] + } + ], + "children": [] + }, + { + "start": 167, + "end": 168, + "type": "Text", + "raw": "\n", + "data": "\n" + }, + { + "start": 168, + "end": 205, + "type": "Element", + "name": "div", + "attributes": [ + { + "start": 173, + "end": 198, + "type": "StyleDirective", + "name": "color", + "value": [ + { + "start": 185, + "end": 188, + "type": "Text", + "raw": "red", + "data": "red" + }, + { + "start": 188, + "end": 198, + "type": "MustacheTag", + "expression": { + "type": "Identifier", + "start": 189, + "end": 197, + "loc": { + "start": { + "line": 6, + "column": 21 + }, + "end": { + "line": 6, + "column": 29 + } + }, + "name": "variable" + } + } + ] + } + ], + "children": [] + }, + { + "start": 205, + "end": 206, + "type": "Text", + "raw": "\n", + "data": "\n" + }, + { + "start": 206, + "end": 252, + "type": "Element", + "name": "div", + "attributes": [ + { + "start": 211, + "end": 245, + "type": "StyleDirective", + "name": "color", + "value": [ + { + "start": 223, + "end": 245, + "type": "MustacheTag", + "expression": { + "type": "TemplateLiteral", + "start": 224, + "end": 244, + "loc": { + "start": { + "line": 7, + "column": 18 + }, + "end": { + "line": 7, + "column": 38 + } }, - "tail": true + "expressions": [ + { + "type": "Identifier", + "start": 235, + "end": 242, + "loc": { + "start": { + "line": 7, + "column": 29 + }, + "end": { + "line": 7, + "column": 36 + } + }, + "name": "literal" + } + ], + "quasis": [ + { + "type": "TemplateElement", + "start": 225, + "end": 233, + "loc": { + "start": { + "line": 7, + "column": 19 + }, + "end": { + "line": 7, + "column": 27 + } + }, + "value": { + "raw": "template", + "cooked": "template" + }, + "tail": false + }, + { + "type": "TemplateElement", + "start": 243, + "end": 243, + "loc": { + "start": { + "line": 7, + "column": 37 + }, + "end": { + "line": 7, + "column": 37 + } + }, + "value": { + "raw": "", + "cooked": "" + }, + "tail": true + } + ] } - ] - } + } + ] } ], "children": [] diff --git a/test/parser/samples/attribute-style-directive/output.json b/test/parser/samples/attribute-style-directive/output.json index 36906045ce16..f36ce6b01e65 100644 --- a/test/parser/samples/attribute-style-directive/output.json +++ b/test/parser/samples/attribute-style-directive/output.json @@ -13,25 +13,31 @@ { "start": 5, "end": 26, - "type": "Style", + "type": "StyleDirective", "name": "color", - "modifiers": [], - "expression": { - "type": "Identifier", - "start": 18, - "end": 25, - "loc": { - "start": { - "line": 1, - "column": 18 - }, - "end": { - "line": 1, - "column": 25 + "value": [ + { + "start": 17, + "end": 26, + "type": "MustacheTag", + "expression": { + "type": "Identifier", + "start": 18, + "end": 25, + "loc": { + "start": { + "line": 1, + "column": 18 + }, + "end": { + "line": 1, + "column": 25 + } + }, + "name": "myColor" } - }, - "name": "myColor" - } + } + ] } ], "children": []