Skip to content

Change to parse expressions in style vars #118

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Jul 18, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ For example:
"vueFeatures": {
"filter": true,
"interpolationAsNonHTML": false,
"styleCSSVariableInjection": true,
}
}
}
Expand Down Expand Up @@ -189,6 +190,12 @@ The following template can be parsed well.

But, it cannot be parsed with Vue 2.

### parserOptions.vueFeatures.styleCSSVariableInjection

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`.

See also to [here](https://github.com/vuejs/rfcs/blob/master/active-rfcs/0043-sfc-style-variables.md).

## 🎇 Usage for custom rules / plugins

- This parser provides `parserServices` to traverse `<template>`.
Expand Down
2 changes: 1 addition & 1 deletion docs/ast.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ interface VFilter <: Node {
}
```

- This is mustaches or directive values.
- This is mustaches, directive values, or `v-bind()` in `<style>`.
- If syntax errors exist, `VExpressionContainer#expression` is `null`.
- If it's an empty mustache, `VExpressionContainer#expression` is `null`. (e.g., `{{ /* a comment */ }}`)
- `Reference` is objects but not `Node`. Those are external references which are in the expression.
Expand Down
17 changes: 11 additions & 6 deletions scripts/update-fixtures-document-fragment.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ function replacer(key, value) {
return undefined
}
if (key === "errors" && Array.isArray(value)) {
return value.map(e => ({
return value.map((e) => ({
message: e.message,
index: e.index,
lineNumber: e.lineNumber,
Expand All @@ -51,13 +51,18 @@ function replacer(key, value) {
for (const name of TARGETS) {
const sourceFileName = fs
.readdirSync(path.join(ROOT, name))
.find(f => f.startsWith("source."))
.find((f) => f.startsWith("source."))
const sourcePath = path.join(ROOT, `${name}/${sourceFileName}`)
const optionsPath = path.join(ROOT, `${name}/parser-options.json`)
const source = fs.readFileSync(sourcePath, "utf8")
const result = parser.parseForESLint(
source,
Object.assign({ filePath: sourcePath }, PARSER_OPTIONS)
const options = Object.assign(
{ filePath: sourcePath },
PARSER_OPTIONS,
fs.existsSync(optionsPath)
? JSON.parse(fs.readFileSync(optionsPath, "utf8"))
: {}
)
const result = parser.parseForESLint(source, options)
const actual = result.services.getDocumentFragment()

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

console.log("Update:", name)

const tokenRanges = getAllTokens(actual).map(t =>
const tokenRanges = getAllTokens(actual).map((t) =>
source.slice(t.range[0], t.range[1])
)
const tree = getTree(source, actual)
Expand Down
12 changes: 11 additions & 1 deletion src/ast/nodes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -859,5 +859,15 @@ export interface VDocumentFragment
HasConcreteInfo {
type: "VDocumentFragment"
parent: null
children: (VElement | VText | VExpressionContainer)[]
children: (VElement | VText | VExpressionContainer | VStyleElement)[]
}

/**
* Style element nodes.
*/
export interface VStyleElement extends VElement {
type: "VElement"
name: "style"
style: true
children: (VText | VExpressionContainer)[]
}
30 changes: 29 additions & 1 deletion src/common/ast-utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import type { VAttribute, VDirective, VElement, VNode } from "../ast"
import type {
VAttribute,
VDirective,
VDocumentFragment,
VElement,
VNode,
} from "../ast"

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

/**
* Check whether the node is a `<style>` element.
* @param node The node to check.
* @returns `true` if the node is a `<style>` element.
*/
export function isStyleElement(node: VNode): node is VElement {
return node.type === "VElement" && node.name === "style"
}

/**
* Get the belonging document of the given node.
* @param leafNode The node to get.
* @returns The belonging document.
*/
export function getOwnerDocument(leafNode: VNode): VDocumentFragment | null {
let node: VNode | null = leafNode
while (node != null && node.type !== "VDocumentFragment") {
node = node.parent
}
return node
}

/**
* Check whether the attribute node is a `lang` attribute.
* @param attribute The attribute node to check.
Expand Down
27 changes: 27 additions & 0 deletions src/common/error-utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import type { ParseError, VDocumentFragment } from "../ast"
import sortedIndexBy from "lodash/sortedIndexBy"
/**
* Insert the given error.
* @param document The document that the node is belonging to.
* @param error The error to insert.
*/
export function insertError(
document: VDocumentFragment | null,
error: ParseError,
): void {
if (document == null) {
return
}

const index = sortedIndexBy(document.errors, error, byIndex)
document.errors.splice(index, 0, error)
}

/**
* Get `x.pos`.
* @param x The object to get.
* @returns `x.pos`.
*/
function byIndex(x: ParseError): number {
return x.index
}
12 changes: 3 additions & 9 deletions src/common/location-calculator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ export class LocationCalculatorForHtml
* @returns The location of the index.
*/
public getLocation(index: number): Location {
return this.getLocFromIndex(this.baseOffset + index + this.shiftOffset)
return this.getLocFromIndex(this.getOffsetWithGap(index))
}

/**
Expand All @@ -130,13 +130,7 @@ export class LocationCalculatorForHtml
* @returns The offset of the index.
*/
public getOffsetWithGap(index: number): number {
const shiftOffset = this.shiftOffset
return (
this.baseOffset +
index +
shiftOffset +
this._getGap(index + shiftOffset)
)
return index + this.getFixOffset(index)
}

/**
Expand All @@ -146,6 +140,6 @@ export class LocationCalculatorForHtml
public getFixOffset(offset: number): number {
const shiftOffset = this.shiftOffset
const gap = this._getGap(offset + shiftOffset)
return this.baseOffset + Math.max(0, gap) + shiftOffset
return this.baseOffset + gap + shiftOffset
}
}
1 change: 1 addition & 0 deletions src/common/parser-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export interface ParserOptions {
vueFeatures?: {
interpolationAsNonHTML?: boolean // default false
filter?: boolean // default true
styleCSSVariableInjection?: boolean // default true
}

// espree options
Expand Down
159 changes: 159 additions & 0 deletions src/common/token-utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
import sortedIndexBy from "lodash/sortedIndexBy"
import sortedLastIndexBy from "lodash/sortedLastIndexBy"
import type { LocationRange, Token, VDocumentFragment } from "../ast"
import type { LinesAndColumns } from "./lines-and-columns"

interface HasRange {
range: [number, number]
}
/**
* Replace the tokens in the given range.
* @param document The document that the node is belonging to.
* @param node The node to specify the range of replacement.
* @param newTokens The new tokens.
*/
export function replaceTokens(
document: VDocumentFragment | null,
node: HasRange,
newTokens: Token[],
): void {
if (document == null) {
return
}

const index = sortedIndexBy(document.tokens, node, byRange0)
const count = sortedLastIndexBy(document.tokens, node, byRange1) - index
document.tokens.splice(index, count, ...newTokens)
}

/**
* Replace and split the tokens in the given range.
* @param document The document that the node is belonging to.
* @param node The node to specify the range of replacement.
* @param newTokens The new tokens.
*/
export function replaceAndSplitTokens(
document: VDocumentFragment | null,
node: HasRange & {
loc: LocationRange
},
newTokens: Token[],
): void {
if (document == null) {
return
}

const index = sortedIndexBy(document.tokens, node, byRange0)
if (
document.tokens.length === index ||
node.range[0] < document.tokens[index].range[0]
) {
// split
const beforeToken = document.tokens[index - 1]
const value = beforeToken.value
const splitOffset = node.range[0] - beforeToken.range[0]
const afterToken: Token = {
type: beforeToken.type,
range: [node.range[0], beforeToken.range[1]],
loc: {
start: { ...node.loc.start },
end: { ...beforeToken.loc.end },
},
value: value.slice(splitOffset),
}
beforeToken.range[1] = node.range[0]
beforeToken.loc.end = { ...node.loc.start }
beforeToken.value = value.slice(0, splitOffset)
document.tokens.splice(index, 0, afterToken)
}
let lastIndex = sortedLastIndexBy(document.tokens, node, byRange1)
if (
lastIndex === 0 ||
node.range[1] < document.tokens[lastIndex].range[1]
) {
// split
const beforeToken = document.tokens[lastIndex]
const value = beforeToken.value
const splitOffset =
beforeToken.range[1] -
beforeToken.range[0] -
(beforeToken.range[1] - node.range[1])
const afterToken: Token = {
type: beforeToken.type,
range: [node.range[1], beforeToken.range[1]],
loc: {
start: { ...node.loc.end },
end: { ...beforeToken.loc.end },
},
value: value.slice(splitOffset),
}
beforeToken.range[1] = node.range[1]
beforeToken.loc.end = { ...node.loc.end }
beforeToken.value = value.slice(0, splitOffset)
document.tokens.splice(lastIndex + 1, 0, afterToken)
lastIndex++
}
const count = lastIndex - index
document.tokens.splice(index, count, ...newTokens)
}

/**
* Insert the given comment tokens.
* @param document The document that the node is belonging to.
* @param newComments The comments to insert.
*/
export function insertComments(
document: VDocumentFragment | null,
newComments: Token[],
): void {
if (document == null || newComments.length === 0) {
return
}

const index = sortedIndexBy(document.comments, newComments[0], byRange0)
document.comments.splice(index, 0, ...newComments)
}

/**
* Create a simple token.
* @param type The type of new token.
* @param start The offset of the start position of new token.
* @param end The offset of the end position of new token.
* @param value The value of new token.
* @returns The new token.
*/
export function createSimpleToken(
type: string,
start: number,
end: number,
value: string,
linesAndColumns: LinesAndColumns,
): Token {
return {
type,
range: [start, end],
loc: {
start: linesAndColumns.getLocFromIndex(start),
end: linesAndColumns.getLocFromIndex(end),
},
value,
}
}

/**
* Get `x.range[0]`.
* @param x The object to get.
* @returns `x.range[0]`.
*/
function byRange0(x: HasRange): number {
return x.range[0]
}

/**
* Get `x.range[1]`.
* @param x The object to get.
* @returns `x.range[1]`.
*/
function byRange1(x: HasRange): number {
return x.range[1]
}
10 changes: 10 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,10 @@ import {
getLang,
isScriptElement,
isScriptSetupElement,
isStyleElement,
isTemplateElement,
} from "./common/ast-utils"
import { parseStyleElements } from "./style"

const STARTS_WITH_LT = /^\s*</u

Expand Down Expand Up @@ -120,6 +122,14 @@ export function parseForESLint(
})
}

if (options.vueFeatures?.styleCSSVariableInjection ?? true) {
const styles = rootAST.children.filter(isStyleElement)
parseStyleElements(styles, locationCalculator, {
...options,
parser: getScriptParser(options.parser, rootAST, "template"),
})
}

result.ast.templateBody = templateBody
document = rootAST
}
Expand Down
2 changes: 1 addition & 1 deletion src/parser-services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,11 @@ import type {
import {
createCustomBlockSharedContext,
getCustomBlocks,
getLang,
parseCustomBlockElement,
} from "./sfc/custom-block"
import type { ParserOptions } from "./common/parser-options"
import { isSFCFile } from "./common/parser-options"
import { getLang } from "./common/ast-utils"

//------------------------------------------------------------------------------
// Helpers
Expand Down
Loading