Skip to content

Commit 0078cdc

Browse files
authored
Change to parse expressions in style vars (#118)
* Change to parse expressions in style vars Also added an option to decide whether to enable this feature. The default is enable. See also to [here](https://github.com/vuejs/rfcs/blob/master/active-rfcs/0043-sfc-style-variables.md). I tried to handle this feature with a check rule instead of a parser, but it was difficult to get the correct location, so I implement it in the parser. * fix * fix * Add test cases * update testcase * add testcases and upadte * Add test cases * update doc * update doc * refactor
1 parent c36717c commit 0078cdc

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

66 files changed

+21918
-160
lines changed

Diff for: README.md

+7
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,7 @@ For example:
124124
"vueFeatures": {
125125
"filter": true,
126126
"interpolationAsNonHTML": false,
127+
"styleCSSVariableInjection": true,
127128
}
128129
}
129130
}
@@ -189,6 +190,12 @@ The following template can be parsed well.
189190

190191
But, it cannot be parsed with Vue 2.
191192

193+
### parserOptions.vueFeatures.styleCSSVariableInjection
194+
195+
If set to `true`, to parse expressions in `v-bind` CSS functions inside `<style>` tags. `v-bind()` is parsed into the `VExpressionContainer` AST node and held in the `VElement` of `<style>`. Default is `true`.
196+
197+
See also to [here](https://github.com/vuejs/rfcs/blob/master/active-rfcs/0043-sfc-style-variables.md).
198+
192199
## 🎇 Usage for custom rules / plugins
193200

194201
- This parser provides `parserServices` to traverse `<template>`.

Diff for: docs/ast.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ interface VFilter <: Node {
124124
}
125125
```
126126

127-
- This is mustaches or directive values.
127+
- This is mustaches, directive values, or `v-bind()` in `<style>`.
128128
- If syntax errors exist, `VExpressionContainer#expression` is `null`.
129129
- If it's an empty mustache, `VExpressionContainer#expression` is `null`. (e.g., `{{ /* a comment */ }}`)
130130
- `Reference` is objects but not `Node`. Those are external references which are in the expression.

Diff for: scripts/update-fixtures-document-fragment.js

+11-6
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ function replacer(key, value) {
3434
return undefined
3535
}
3636
if (key === "errors" && Array.isArray(value)) {
37-
return value.map(e => ({
37+
return value.map((e) => ({
3838
message: e.message,
3939
index: e.index,
4040
lineNumber: e.lineNumber,
@@ -51,13 +51,18 @@ function replacer(key, value) {
5151
for (const name of TARGETS) {
5252
const sourceFileName = fs
5353
.readdirSync(path.join(ROOT, name))
54-
.find(f => f.startsWith("source."))
54+
.find((f) => f.startsWith("source."))
5555
const sourcePath = path.join(ROOT, `${name}/${sourceFileName}`)
56+
const optionsPath = path.join(ROOT, `${name}/parser-options.json`)
5657
const source = fs.readFileSync(sourcePath, "utf8")
57-
const result = parser.parseForESLint(
58-
source,
59-
Object.assign({ filePath: sourcePath }, PARSER_OPTIONS)
58+
const options = Object.assign(
59+
{ filePath: sourcePath },
60+
PARSER_OPTIONS,
61+
fs.existsSync(optionsPath)
62+
? JSON.parse(fs.readFileSync(optionsPath, "utf8"))
63+
: {}
6064
)
65+
const result = parser.parseForESLint(source, options)
6166
const actual = result.services.getDocumentFragment()
6267

6368
const resultPath = path.join(ROOT, `${name}/document-fragment.json`)
@@ -66,7 +71,7 @@ for (const name of TARGETS) {
6671

6772
console.log("Update:", name)
6873

69-
const tokenRanges = getAllTokens(actual).map(t =>
74+
const tokenRanges = getAllTokens(actual).map((t) =>
7075
source.slice(t.range[0], t.range[1])
7176
)
7277
const tree = getTree(source, actual)

Diff for: src/ast/nodes.ts

+11-1
Original file line numberDiff line numberDiff line change
@@ -859,5 +859,15 @@ export interface VDocumentFragment
859859
HasConcreteInfo {
860860
type: "VDocumentFragment"
861861
parent: null
862-
children: (VElement | VText | VExpressionContainer)[]
862+
children: (VElement | VText | VExpressionContainer | VStyleElement)[]
863+
}
864+
865+
/**
866+
* Style element nodes.
867+
*/
868+
export interface VStyleElement extends VElement {
869+
type: "VElement"
870+
name: "style"
871+
style: true
872+
children: (VText | VExpressionContainer)[]
863873
}

Diff for: src/common/ast-utils.ts

+29-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
1-
import type { VAttribute, VDirective, VElement, VNode } from "../ast"
1+
import type {
2+
VAttribute,
3+
VDirective,
4+
VDocumentFragment,
5+
VElement,
6+
VNode,
7+
} from "../ast"
28

39
/**
410
* Check whether the node is a `<script>` element.
@@ -30,6 +36,28 @@ export function isTemplateElement(node: VNode): node is VElement {
3036
return node.type === "VElement" && node.name === "template"
3137
}
3238

39+
/**
40+
* Check whether the node is a `<style>` element.
41+
* @param node The node to check.
42+
* @returns `true` if the node is a `<style>` element.
43+
*/
44+
export function isStyleElement(node: VNode): node is VElement {
45+
return node.type === "VElement" && node.name === "style"
46+
}
47+
48+
/**
49+
* Get the belonging document of the given node.
50+
* @param leafNode The node to get.
51+
* @returns The belonging document.
52+
*/
53+
export function getOwnerDocument(leafNode: VNode): VDocumentFragment | null {
54+
let node: VNode | null = leafNode
55+
while (node != null && node.type !== "VDocumentFragment") {
56+
node = node.parent
57+
}
58+
return node
59+
}
60+
3361
/**
3462
* Check whether the attribute node is a `lang` attribute.
3563
* @param attribute The attribute node to check.

Diff for: src/common/error-utils.ts

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import type { ParseError, VDocumentFragment } from "../ast"
2+
import sortedIndexBy from "lodash/sortedIndexBy"
3+
/**
4+
* Insert the given error.
5+
* @param document The document that the node is belonging to.
6+
* @param error The error to insert.
7+
*/
8+
export function insertError(
9+
document: VDocumentFragment | null,
10+
error: ParseError,
11+
): void {
12+
if (document == null) {
13+
return
14+
}
15+
16+
const index = sortedIndexBy(document.errors, error, byIndex)
17+
document.errors.splice(index, 0, error)
18+
}
19+
20+
/**
21+
* Get `x.pos`.
22+
* @param x The object to get.
23+
* @returns `x.pos`.
24+
*/
25+
function byIndex(x: ParseError): number {
26+
return x.index
27+
}

Diff for: src/common/location-calculator.ts

+3-9
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ export class LocationCalculatorForHtml
121121
* @returns The location of the index.
122122
*/
123123
public getLocation(index: number): Location {
124-
return this.getLocFromIndex(this.baseOffset + index + this.shiftOffset)
124+
return this.getLocFromIndex(this.getOffsetWithGap(index))
125125
}
126126

127127
/**
@@ -130,13 +130,7 @@ export class LocationCalculatorForHtml
130130
* @returns The offset of the index.
131131
*/
132132
public getOffsetWithGap(index: number): number {
133-
const shiftOffset = this.shiftOffset
134-
return (
135-
this.baseOffset +
136-
index +
137-
shiftOffset +
138-
this._getGap(index + shiftOffset)
139-
)
133+
return index + this.getFixOffset(index)
140134
}
141135

142136
/**
@@ -146,6 +140,6 @@ export class LocationCalculatorForHtml
146140
public getFixOffset(offset: number): number {
147141
const shiftOffset = this.shiftOffset
148142
const gap = this._getGap(offset + shiftOffset)
149-
return this.baseOffset + Math.max(0, gap) + shiftOffset
143+
return this.baseOffset + gap + shiftOffset
150144
}
151145
}

Diff for: src/common/parser-options.ts

+1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ export interface ParserOptions {
88
vueFeatures?: {
99
interpolationAsNonHTML?: boolean // default false
1010
filter?: boolean // default true
11+
styleCSSVariableInjection?: boolean // default true
1112
}
1213

1314
// espree options

Diff for: src/common/token-utils.ts

+159
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
import sortedIndexBy from "lodash/sortedIndexBy"
2+
import sortedLastIndexBy from "lodash/sortedLastIndexBy"
3+
import type { LocationRange, Token, VDocumentFragment } from "../ast"
4+
import type { LinesAndColumns } from "./lines-and-columns"
5+
6+
interface HasRange {
7+
range: [number, number]
8+
}
9+
/**
10+
* Replace the tokens in the given range.
11+
* @param document The document that the node is belonging to.
12+
* @param node The node to specify the range of replacement.
13+
* @param newTokens The new tokens.
14+
*/
15+
export function replaceTokens(
16+
document: VDocumentFragment | null,
17+
node: HasRange,
18+
newTokens: Token[],
19+
): void {
20+
if (document == null) {
21+
return
22+
}
23+
24+
const index = sortedIndexBy(document.tokens, node, byRange0)
25+
const count = sortedLastIndexBy(document.tokens, node, byRange1) - index
26+
document.tokens.splice(index, count, ...newTokens)
27+
}
28+
29+
/**
30+
* Replace and split the tokens in the given range.
31+
* @param document The document that the node is belonging to.
32+
* @param node The node to specify the range of replacement.
33+
* @param newTokens The new tokens.
34+
*/
35+
export function replaceAndSplitTokens(
36+
document: VDocumentFragment | null,
37+
node: HasRange & {
38+
loc: LocationRange
39+
},
40+
newTokens: Token[],
41+
): void {
42+
if (document == null) {
43+
return
44+
}
45+
46+
const index = sortedIndexBy(document.tokens, node, byRange0)
47+
if (
48+
document.tokens.length === index ||
49+
node.range[0] < document.tokens[index].range[0]
50+
) {
51+
// split
52+
const beforeToken = document.tokens[index - 1]
53+
const value = beforeToken.value
54+
const splitOffset = node.range[0] - beforeToken.range[0]
55+
const afterToken: Token = {
56+
type: beforeToken.type,
57+
range: [node.range[0], beforeToken.range[1]],
58+
loc: {
59+
start: { ...node.loc.start },
60+
end: { ...beforeToken.loc.end },
61+
},
62+
value: value.slice(splitOffset),
63+
}
64+
beforeToken.range[1] = node.range[0]
65+
beforeToken.loc.end = { ...node.loc.start }
66+
beforeToken.value = value.slice(0, splitOffset)
67+
document.tokens.splice(index, 0, afterToken)
68+
}
69+
let lastIndex = sortedLastIndexBy(document.tokens, node, byRange1)
70+
if (
71+
lastIndex === 0 ||
72+
node.range[1] < document.tokens[lastIndex].range[1]
73+
) {
74+
// split
75+
const beforeToken = document.tokens[lastIndex]
76+
const value = beforeToken.value
77+
const splitOffset =
78+
beforeToken.range[1] -
79+
beforeToken.range[0] -
80+
(beforeToken.range[1] - node.range[1])
81+
const afterToken: Token = {
82+
type: beforeToken.type,
83+
range: [node.range[1], beforeToken.range[1]],
84+
loc: {
85+
start: { ...node.loc.end },
86+
end: { ...beforeToken.loc.end },
87+
},
88+
value: value.slice(splitOffset),
89+
}
90+
beforeToken.range[1] = node.range[1]
91+
beforeToken.loc.end = { ...node.loc.end }
92+
beforeToken.value = value.slice(0, splitOffset)
93+
document.tokens.splice(lastIndex + 1, 0, afterToken)
94+
lastIndex++
95+
}
96+
const count = lastIndex - index
97+
document.tokens.splice(index, count, ...newTokens)
98+
}
99+
100+
/**
101+
* Insert the given comment tokens.
102+
* @param document The document that the node is belonging to.
103+
* @param newComments The comments to insert.
104+
*/
105+
export function insertComments(
106+
document: VDocumentFragment | null,
107+
newComments: Token[],
108+
): void {
109+
if (document == null || newComments.length === 0) {
110+
return
111+
}
112+
113+
const index = sortedIndexBy(document.comments, newComments[0], byRange0)
114+
document.comments.splice(index, 0, ...newComments)
115+
}
116+
117+
/**
118+
* Create a simple token.
119+
* @param type The type of new token.
120+
* @param start The offset of the start position of new token.
121+
* @param end The offset of the end position of new token.
122+
* @param value The value of new token.
123+
* @returns The new token.
124+
*/
125+
export function createSimpleToken(
126+
type: string,
127+
start: number,
128+
end: number,
129+
value: string,
130+
linesAndColumns: LinesAndColumns,
131+
): Token {
132+
return {
133+
type,
134+
range: [start, end],
135+
loc: {
136+
start: linesAndColumns.getLocFromIndex(start),
137+
end: linesAndColumns.getLocFromIndex(end),
138+
},
139+
value,
140+
}
141+
}
142+
143+
/**
144+
* Get `x.range[0]`.
145+
* @param x The object to get.
146+
* @returns `x.range[0]`.
147+
*/
148+
function byRange0(x: HasRange): number {
149+
return x.range[0]
150+
}
151+
152+
/**
153+
* Get `x.range[1]`.
154+
* @param x The object to get.
155+
* @returns `x.range[1]`.
156+
*/
157+
function byRange1(x: HasRange): number {
158+
return x.range[1]
159+
}

Diff for: src/index.ts

+10
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,10 @@ import {
1919
getLang,
2020
isScriptElement,
2121
isScriptSetupElement,
22+
isStyleElement,
2223
isTemplateElement,
2324
} from "./common/ast-utils"
25+
import { parseStyleElements } from "./style"
2426

2527
const STARTS_WITH_LT = /^\s*</u
2628

@@ -120,6 +122,14 @@ export function parseForESLint(
120122
})
121123
}
122124

125+
if (options.vueFeatures?.styleCSSVariableInjection ?? true) {
126+
const styles = rootAST.children.filter(isStyleElement)
127+
parseStyleElements(styles, locationCalculator, {
128+
...options,
129+
parser: getScriptParser(options.parser, rootAST, "template"),
130+
})
131+
}
132+
123133
result.ast.templateBody = templateBody
124134
document = rootAST
125135
}

Diff for: src/parser-services.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,11 @@ import type {
2222
import {
2323
createCustomBlockSharedContext,
2424
getCustomBlocks,
25-
getLang,
2625
parseCustomBlockElement,
2726
} from "./sfc/custom-block"
2827
import type { ParserOptions } from "./common/parser-options"
2928
import { isSFCFile } from "./common/parser-options"
29+
import { getLang } from "./common/ast-utils"
3030

3131
//------------------------------------------------------------------------------
3232
// Helpers

0 commit comments

Comments
 (0)