Skip to content

Commit 53f246a

Browse files
committed
💥 support dynamic argument [RFC0003] (refs #39)
This commit contains a breaking change about the tokens of VDirectiveKey nodes.
1 parent 809009d commit 53f246a

File tree

74 files changed

+8683
-169
lines changed

Some content is hidden

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

74 files changed

+8683
-169
lines changed

‎docs/ast.md

+4-1
Original file line numberDiff line numberDiff line change
@@ -141,15 +141,17 @@ interface VFilter <: Node {
141141
interface VDirectiveKey <: Node {
142142
type: "VDirectiveKey"
143143
name: string
144-
argument: string | null
144+
argument: VExpressionContainer | string | null
145145
modifiers: [ string ]
146146
shorthand: boolean
147147
}
148148
```
149149

150150
- The `name` property doesn't have `v-` prefix. It's dropped.
151+
- The `argument` property is a `VExpressionContainer` node if it's a [dynamic argument].
151152
- In the shorthand of `v-bind` cases, the `name` property is `"bind"` and the `shorthand` property is `true`.
152153
- In the shorthand of `v-on` cases, the `name` property is `"on"` and the `shorthand` property is `true`.
154+
- In the shorthand of `v-slot` cases, the `name` property is `"slot"` and the `shorthand` property is `true`.
153155
- Otherwise, `shorthand` property is always `false`.
154156

155157
## VLiteral
@@ -276,3 +278,4 @@ This supports only HTML for now. However, I'm going to add other languages Vue.j
276278
[`v-slot` directives]: https://vuejs.org/v2/api/#v-slot
277279
[scope]: https://vuejs.org/v2/guide/components.html#Scoped-Slots
278280
[`slot-scope` attributes]: https://vuejs.org/v2/guide/components.html#Scoped-Slots
281+
[dynamic argument]: https://vuejs.org/v2/guide/syntax.html#Dynamic-Arguments

‎src/ast/nodes.ts

+5-5
Original file line numberDiff line numberDiff line change
@@ -741,7 +741,7 @@ export interface VText extends HasLocation, HasParent {
741741
*/
742742
export interface VExpressionContainer extends HasLocation, HasParent {
743743
type: "VExpressionContainer"
744-
parent: VDocumentFragment | VElement | VDirective
744+
parent: VDocumentFragment | VElement | VDirective | VDirectiveKey
745745
expression:
746746
| ESLintExpression
747747
| VFilterSequenceExpression
@@ -771,12 +771,12 @@ export interface DirectiveKeyParts {
771771
/**
772772
* Attribute name nodes.
773773
*/
774-
export interface VDirectiveKey
775-
extends HasLocation,
776-
HasParent,
777-
DirectiveKeyParts {
774+
export interface VDirectiveKey extends HasLocation, HasParent {
778775
type: "VDirectiveKey"
779776
parent: VAttribute
777+
name: string
778+
argument: VExpressionContainer | string | null
779+
modifiers: string[]
780780
shorthand: boolean
781781
raw: DirectiveKeyParts
782782
}

‎src/ast/traverse.ts

+12-3
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import { Node } from "./nodes"
1212

1313
const KEYS = Evk.unionWith({
1414
VAttribute: ["key", "value"],
15-
VDirectiveKey: [],
15+
VDirectiveKey: ["argument"],
1616
VDocumentFragment: ["children"],
1717
VElement: ["startTag", "children", "endTag"],
1818
VEndTag: [],
@@ -59,6 +59,15 @@ function getFallbackKeys(node: Node): string[] {
5959
return Object.keys(node).filter(fallbackKeysFilter, node)
6060
}
6161

62+
/**
63+
* Check wheather a given value is a node.
64+
* @param x The value to check.
65+
* @returns `true` if the value is a node.
66+
*/
67+
function isNode(x: any): x is Node {
68+
return x !== null && typeof x === "object" && typeof x.type === "string"
69+
}
70+
6271
/**
6372
* Traverse the given node.
6473
* @param node The node to traverse.
@@ -78,11 +87,11 @@ function traverse(node: Node, parent: Node | null, visitor: Visitor): void {
7887

7988
if (Array.isArray(child)) {
8089
for (j = 0; j < child.length; ++j) {
81-
if (child[j]) {
90+
if (isNode(child[j])) {
8291
traverse(child[j], node, visitor)
8392
}
8493
}
85-
} else if (child) {
94+
} else if (isNode(child)) {
8695
traverse(child, node, visitor)
8796
}
8897
}

‎src/template/index.ts

+225-14
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,10 @@ import {
3535
parseSlotScopeExpression,
3636
} from "../script"
3737

38+
const shorthandSign = /^[:@#]/u
39+
const shorthandNameMap = { ":": "bind", "@": "on", "#": "slot" }
40+
const shorthandSignMap = { bind: ":", on: "@", slot: "#" }
41+
3842
/**
3943
* Get the belonging document of the given node.
4044
* @param leafNode The node to get.
@@ -79,7 +83,7 @@ function createSimpleToken(
7983
* @param node The identifier node to parse.
8084
* @returns The directive key node.
8185
*/
82-
function createDirectiveKey(node: VIdentifier): VDirectiveKey {
86+
function parseDirectiveKeyStatically(node: VIdentifier): VDirectiveKey {
8387
const raw: DirectiveKeyParts = {
8488
name: "",
8589
argument: null,
@@ -100,16 +104,10 @@ function createDirectiveKey(node: VIdentifier): VDirectiveKey {
100104
const rawId = node.rawName
101105
let i = 0
102106

103-
if (node.name.startsWith(":")) {
104-
ret.name = raw.name = "bind"
105-
ret.shorthand = true
106-
i = 1
107-
} else if (id.startsWith("@")) {
108-
ret.name = raw.name = "on"
109-
ret.shorthand = true
110-
i = 1
111-
} else if (id.startsWith("#")) {
112-
ret.name = raw.name = "slot"
107+
// Parse.
108+
if (shorthandSign.test(id)) {
109+
const sign = id[0] as ":" | "@" | "#"
110+
ret.name = raw.name = shorthandNameMap[sign]
113111
ret.shorthand = true
114112
i = 1
115113
} else {
@@ -133,11 +131,219 @@ function createDirectiveKey(node: VIdentifier): VDirectiveKey {
133131
ret.modifiers = dotSplit.slice(1)
134132
raw.modifiers = dotSplitRaw.slice(1)
135133

134+
return ret
135+
}
136+
137+
/**
138+
* Parse the tokens of a given key node.
139+
* @param node The key node to parse.
140+
*/
141+
function parseDirectiveKeyTokens(
142+
node: VDirectiveKey,
143+
locationCalculator: LocationCalculator,
144+
): Token[] {
145+
const raw = node.raw
146+
const tokens: Token[] = []
147+
let i = 0
148+
149+
if (node.shorthand) {
150+
const name = raw.name as "bind" | "on" | "slot"
151+
tokens.push(
152+
createSimpleToken(
153+
"Punctuator",
154+
node.range[0],
155+
node.range[0] + 1,
156+
shorthandSignMap[name],
157+
locationCalculator,
158+
),
159+
)
160+
i = 1
161+
} else if (raw.name) {
162+
tokens.push(
163+
createSimpleToken(
164+
"HTMLIdentifier",
165+
node.range[0],
166+
node.range[0] + raw.name.length,
167+
raw.name,
168+
locationCalculator,
169+
),
170+
)
171+
i = raw.name.length
172+
173+
if (raw.argument) {
174+
tokens.push(
175+
createSimpleToken(
176+
"Punctuator",
177+
node.range[0] + i,
178+
node.range[0] + i + 1,
179+
":",
180+
locationCalculator,
181+
),
182+
)
183+
i += 1
184+
}
185+
}
186+
187+
if (raw.argument) {
188+
tokens.push(
189+
createSimpleToken(
190+
"HTMLIdentifier",
191+
node.range[0] + i,
192+
node.range[0] + i + raw.argument.length,
193+
raw.argument,
194+
locationCalculator,
195+
),
196+
)
197+
i += raw.argument.length
198+
}
199+
200+
for (const modifier of raw.modifiers) {
201+
tokens.push(
202+
createSimpleToken(
203+
"Punctuator",
204+
node.range[0] + i,
205+
node.range[0] + i + 1,
206+
".",
207+
locationCalculator,
208+
),
209+
createSimpleToken(
210+
"HTMLIdentifier",
211+
node.range[0] + i + 1,
212+
node.range[0] + i + 1 + modifier.length,
213+
modifier,
214+
locationCalculator,
215+
),
216+
)
217+
i += 1 + modifier.length
218+
}
219+
220+
return tokens
221+
}
222+
223+
/**
224+
* Convert `node.argument` property to a `VExpressionContainer` node if it's a dynamic argument.
225+
* @param text The source code text of the directive key node.
226+
* @param node The directive key node to convert.
227+
* @param document The belonging document node.
228+
* @param parserOptions The parser options to parse.
229+
* @param locationCalculator The location calculator to parse.
230+
*/
231+
function convertDynamicArgument(
232+
text: string,
233+
node: VDirectiveKey,
234+
document: VDocumentFragment | null,
235+
parserOptions: any,
236+
locationCalculator: LocationCalculator,
237+
): void {
238+
const argument = node.raw.argument
239+
if (
240+
typeof argument !== "string" ||
241+
!argument.startsWith("[") ||
242+
!argument.endsWith("]")
243+
) {
244+
return
245+
}
246+
247+
const start = node.range[0] + text.indexOf(argument)
248+
const end = start + argument.length
249+
try {
250+
const { comments, expression, references, tokens } = parseExpression(
251+
argument.slice(1, -1),
252+
locationCalculator.getSubCalculatorAfter(start + 1),
253+
parserOptions,
254+
)
255+
256+
node.argument = {
257+
type: "VExpressionContainer",
258+
range: [start, end],
259+
loc: {
260+
start: locationCalculator.getLocation(start),
261+
end: locationCalculator.getLocation(end),
262+
},
263+
parent: node,
264+
expression,
265+
references,
266+
}
267+
268+
if (expression != null) {
269+
expression.parent = node.argument
270+
}
271+
272+
// Add tokens of `[` and `]`.
273+
tokens.unshift(
274+
createSimpleToken(
275+
"Punctuator",
276+
start,
277+
start + 1,
278+
"[",
279+
locationCalculator,
280+
),
281+
)
282+
tokens.push(
283+
createSimpleToken(
284+
"Punctuator",
285+
end - 1,
286+
end,
287+
"]",
288+
locationCalculator,
289+
),
290+
)
291+
292+
replaceTokens(document, node.argument, tokens)
293+
insertComments(document, comments)
294+
} catch (error) {
295+
debug("[template] Parse error: %s", error)
296+
297+
if (ParseError.isParseError(error)) {
298+
node.argument = {
299+
type: "VExpressionContainer",
300+
range: [start, end],
301+
loc: {
302+
start: locationCalculator.getLocation(start),
303+
end: locationCalculator.getLocation(end),
304+
},
305+
parent: node,
306+
expression: null,
307+
references: [],
308+
}
309+
insertError(document, error)
310+
} else {
311+
throw error
312+
}
313+
}
314+
}
315+
316+
/**
317+
* Parse the given attribute name as a directive key.
318+
* @param node The identifier node to parse.
319+
* @returns The directive key node.
320+
*/
321+
function createDirectiveKey(
322+
node: VIdentifier,
323+
document: VDocumentFragment | null,
324+
parserOptions: any,
325+
locationCalculator: LocationCalculator,
326+
): VDirectiveKey {
327+
// Parse node and tokens.
328+
const ret = parseDirectiveKeyStatically(node)
329+
const tokens = parseDirectiveKeyTokens(ret, locationCalculator)
330+
replaceTokens(document, ret, tokens)
331+
332+
// Drop `v-` prefix.
136333
if (ret.name.startsWith("v-")) {
137334
ret.name = ret.name.slice(2)
138-
raw.name = raw.name.slice(2)
335+
ret.raw.name = ret.raw.name.slice(2)
139336
}
140337

338+
// Parse dynamic argument.
339+
convertDynamicArgument(
340+
node.rawName,
341+
ret,
342+
document,
343+
parserOptions,
344+
locationCalculator,
345+
)
346+
141347
return ret
142348
}
143349

@@ -382,14 +588,19 @@ export function convertToDirective(
382588
node.range,
383589
)
384590

591+
const document = getOwnerDocument(node)
385592
const directive: VDirective = node as any
386593
directive.directive = true
387-
directive.key = createDirectiveKey(node.key)
594+
directive.key = createDirectiveKey(
595+
node.key,
596+
document,
597+
parserOptions,
598+
locationCalculator,
599+
)
388600

389601
if (node.value == null) {
390602
return
391603
}
392-
const document = getOwnerDocument(node)
393604

394605
try {
395606
const ret = parseAttributeValue(

0 commit comments

Comments
 (0)