Skip to content

Commit 40f72d5

Browse files
committed
feat(compiler-core): support specifying root namespace when parsing
1 parent a1b10a2 commit 40f72d5

File tree

6 files changed

+92
-56
lines changed

6 files changed

+92
-56
lines changed

packages/compiler-core/src/ast.ts

+4-3
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,13 @@ import { PropsExpression } from './transforms/transformElement'
1616
import { ImportItem, TransformContext } from './transform'
1717

1818
// Vue template is a platform-agnostic superset of HTML (syntax only).
19-
// More namespaces like SVG and MathML are declared by platform specific
20-
// compilers.
19+
// More namespaces can be declared by platform specific compilers.
2120
export type Namespace = number
2221

2322
export const enum Namespaces {
24-
HTML
23+
HTML,
24+
SVG,
25+
MATH_ML
2526
}
2627

2728
export const enum NodeTypes {

packages/compiler-core/src/options.ts

+29-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
1-
import { ElementNode, Namespace, TemplateChildNode, ParentNode } from './ast'
1+
import {
2+
ElementNode,
3+
Namespace,
4+
TemplateChildNode,
5+
ParentNode,
6+
Namespaces
7+
} from './ast'
28
import { CompilerError } from './errors'
39
import {
410
NodeTransform,
@@ -16,7 +22,24 @@ export interface ErrorHandlingOptions {
1622
export interface ParserOptions
1723
extends ErrorHandlingOptions,
1824
CompilerCompatOptions {
25+
/**
26+
* Base mode is platform agnostic and only parses HTML-like template syntax,
27+
* treating all tags the same way. Specific tag parsing behavior can be
28+
* configured by higher-level compilers.
29+
*
30+
* HTML mode adds additional logic for handling special parsing behavior in
31+
* `<script>`, `<style>`,`<title>` and `<html>`, plus SVG and MathML
32+
* namespaces. The logic is handled inside compiler-core for efficiency.
33+
*
34+
* SFC mode treats content of all root-level tags except `<template>` as plain
35+
* text.
36+
*/
1937
parseMode?: 'base' | 'html' | 'sfc'
38+
/**
39+
* Specify the root namepsace to use when parsing a tempalte.
40+
* Defaults to `Namepsaces.HTML` (0).
41+
*/
42+
ns?: Namespaces
2043
/**
2144
* e.g. platform native elements, e.g. `<div>` for browsers
2245
*/
@@ -40,7 +63,11 @@ export interface ParserOptions
4063
/**
4164
* Get tag namespace
4265
*/
43-
getNamespace?: (tag: string, parent: ElementNode | undefined) => Namespace
66+
getNamespace?: (
67+
tag: string,
68+
parent: ElementNode | undefined,
69+
rootNamespace: Namespace
70+
) => Namespace
4471
/**
4572
* @default ['{{', '}}']
4673
*/

packages/compiler-core/src/parser/index.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ type MergedParserOptions = Omit<Required<ParserOptions>, OptionalOptions> &
4040

4141
export const defaultParserOptions: MergedParserOptions = {
4242
parseMode: 'base',
43+
ns: Namespaces.HTML,
4344
delimiters: [`{{`, `}}`],
4445
getNamespace: () => Namespaces.HTML,
4546
isVoidTag: NO,
@@ -107,7 +108,7 @@ const tokenizer = new Tokenizer(stack, {
107108
currentElement = {
108109
type: NodeTypes.ELEMENT,
109110
tag: name,
110-
ns: currentOptions.getNamespace(name, stack[0]),
111+
ns: currentOptions.getNamespace(name, stack[0], currentOptions.ns),
111112
tagType: ElementTypes.ELEMENT, // will be refined on tag close
112113
props: [],
113114
children: [],

packages/compiler-dom/__tests__/parse.spec.ts

+39-26
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,10 @@ import {
66
ElementTypes,
77
InterpolationNode,
88
AttributeNode,
9-
ConstantTypes
9+
ConstantTypes,
10+
Namespaces
1011
} from '@vue/compiler-core'
11-
import { parserOptions, DOMNamespaces } from '../src/parserOptions'
12+
import { parserOptions } from '../src/parserOptions'
1213

1314
describe('DOM parser', () => {
1415
describe('Text', () => {
@@ -264,7 +265,7 @@ describe('DOM parser', () => {
264265

265266
expect(element).toStrictEqual({
266267
type: NodeTypes.ELEMENT,
267-
ns: DOMNamespaces.HTML,
268+
ns: Namespaces.HTML,
268269
tag: 'img',
269270
tagType: ElementTypes.ELEMENT,
270271
props: [],
@@ -324,21 +325,21 @@ describe('DOM parser', () => {
324325
const ast = parse('<html>test</html>', parserOptions)
325326
const element = ast.children[0] as ElementNode
326327

327-
expect(element.ns).toBe(DOMNamespaces.HTML)
328+
expect(element.ns).toBe(Namespaces.HTML)
328329
})
329330

330331
test('SVG namespace', () => {
331332
const ast = parse('<svg>test</svg>', parserOptions)
332333
const element = ast.children[0] as ElementNode
333334

334-
expect(element.ns).toBe(DOMNamespaces.SVG)
335+
expect(element.ns).toBe(Namespaces.SVG)
335336
})
336337

337338
test('MATH_ML namespace', () => {
338339
const ast = parse('<math>test</math>', parserOptions)
339340
const element = ast.children[0] as ElementNode
340341

341-
expect(element.ns).toBe(DOMNamespaces.MATH_ML)
342+
expect(element.ns).toBe(Namespaces.MATH_ML)
342343
})
343344

344345
test('SVG in MATH_ML namespace', () => {
@@ -350,8 +351,8 @@ describe('DOM parser', () => {
350351
const elementAnnotation = elementMath.children[0] as ElementNode
351352
const elementSvg = elementAnnotation.children[0] as ElementNode
352353

353-
expect(elementMath.ns).toBe(DOMNamespaces.MATH_ML)
354-
expect(elementSvg.ns).toBe(DOMNamespaces.SVG)
354+
expect(elementMath.ns).toBe(Namespaces.MATH_ML)
355+
expect(elementSvg.ns).toBe(Namespaces.SVG)
355356
})
356357

357358
test('html text/html in MATH_ML namespace', () => {
@@ -364,8 +365,8 @@ describe('DOM parser', () => {
364365
const elementAnnotation = elementMath.children[0] as ElementNode
365366
const element = elementAnnotation.children[0] as ElementNode
366367

367-
expect(elementMath.ns).toBe(DOMNamespaces.MATH_ML)
368-
expect(element.ns).toBe(DOMNamespaces.HTML)
368+
expect(elementMath.ns).toBe(Namespaces.MATH_ML)
369+
expect(element.ns).toBe(Namespaces.HTML)
369370
})
370371

371372
test('html application/xhtml+xml in MATH_ML namespace', () => {
@@ -377,8 +378,8 @@ describe('DOM parser', () => {
377378
const elementAnnotation = elementMath.children[0] as ElementNode
378379
const element = elementAnnotation.children[0] as ElementNode
379380

380-
expect(elementMath.ns).toBe(DOMNamespaces.MATH_ML)
381-
expect(element.ns).toBe(DOMNamespaces.HTML)
381+
expect(elementMath.ns).toBe(Namespaces.MATH_ML)
382+
expect(element.ns).toBe(Namespaces.HTML)
382383
})
383384

384385
test('mtext malignmark in MATH_ML namespace', () => {
@@ -390,8 +391,8 @@ describe('DOM parser', () => {
390391
const elementText = elementMath.children[0] as ElementNode
391392
const element = elementText.children[0] as ElementNode
392393

393-
expect(elementMath.ns).toBe(DOMNamespaces.MATH_ML)
394-
expect(element.ns).toBe(DOMNamespaces.MATH_ML)
394+
expect(elementMath.ns).toBe(Namespaces.MATH_ML)
395+
expect(element.ns).toBe(Namespaces.MATH_ML)
395396
})
396397

397398
test('mtext and not malignmark tag in MATH_ML namespace', () => {
@@ -400,8 +401,8 @@ describe('DOM parser', () => {
400401
const elementText = elementMath.children[0] as ElementNode
401402
const element = elementText.children[0] as ElementNode
402403

403-
expect(elementMath.ns).toBe(DOMNamespaces.MATH_ML)
404-
expect(element.ns).toBe(DOMNamespaces.HTML)
404+
expect(elementMath.ns).toBe(Namespaces.MATH_ML)
405+
expect(element.ns).toBe(Namespaces.HTML)
405406
})
406407

407408
test('foreignObject tag in SVG namespace', () => {
@@ -413,8 +414,8 @@ describe('DOM parser', () => {
413414
const elementForeignObject = elementSvg.children[0] as ElementNode
414415
const element = elementForeignObject.children[0] as ElementNode
415416

416-
expect(elementSvg.ns).toBe(DOMNamespaces.SVG)
417-
expect(element.ns).toBe(DOMNamespaces.HTML)
417+
expect(elementSvg.ns).toBe(Namespaces.SVG)
418+
expect(element.ns).toBe(Namespaces.HTML)
418419
})
419420

420421
test('desc tag in SVG namespace', () => {
@@ -423,8 +424,8 @@ describe('DOM parser', () => {
423424
const elementDesc = elementSvg.children[0] as ElementNode
424425
const element = elementDesc.children[0] as ElementNode
425426

426-
expect(elementSvg.ns).toBe(DOMNamespaces.SVG)
427-
expect(element.ns).toBe(DOMNamespaces.HTML)
427+
expect(elementSvg.ns).toBe(Namespaces.SVG)
428+
expect(element.ns).toBe(Namespaces.HTML)
428429
})
429430

430431
test('title tag in SVG namespace', () => {
@@ -433,26 +434,38 @@ describe('DOM parser', () => {
433434
const elementTitle = elementSvg.children[0] as ElementNode
434435
const element = elementTitle.children[0] as ElementNode
435436

436-
expect(elementSvg.ns).toBe(DOMNamespaces.SVG)
437-
expect(element.ns).toBe(DOMNamespaces.HTML)
437+
expect(elementSvg.ns).toBe(Namespaces.SVG)
438+
expect(element.ns).toBe(Namespaces.HTML)
438439
})
439440

440441
test('SVG in HTML namespace', () => {
441442
const ast = parse('<html><svg></svg></html>', parserOptions)
442443
const elementHtml = ast.children[0] as ElementNode
443444
const element = elementHtml.children[0] as ElementNode
444445

445-
expect(elementHtml.ns).toBe(DOMNamespaces.HTML)
446-
expect(element.ns).toBe(DOMNamespaces.SVG)
446+
expect(elementHtml.ns).toBe(Namespaces.HTML)
447+
expect(element.ns).toBe(Namespaces.SVG)
447448
})
448449

449450
test('MATH in HTML namespace', () => {
450451
const ast = parse('<html><math></math></html>', parserOptions)
451452
const elementHtml = ast.children[0] as ElementNode
452453
const element = elementHtml.children[0] as ElementNode
453454

454-
expect(elementHtml.ns).toBe(DOMNamespaces.HTML)
455-
expect(element.ns).toBe(DOMNamespaces.MATH_ML)
455+
expect(elementHtml.ns).toBe(Namespaces.HTML)
456+
expect(element.ns).toBe(Namespaces.MATH_ML)
457+
})
458+
459+
test('root ns', () => {
460+
const ast = parse('<foreignObject><test/></foreignObject>', {
461+
...parserOptions,
462+
ns: Namespaces.SVG
463+
})
464+
const elementForieng = ast.children[0] as ElementNode
465+
const element = elementForieng.children[0] as ElementNode
466+
467+
expect(elementForieng.ns).toBe(Namespaces.SVG)
468+
expect(element.ns).toBe(Namespaces.HTML)
456469
})
457470
})
458471
})

packages/compiler-dom/src/parserOptions.ts

+13-19
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,16 @@
1-
import { ParserOptions, ElementNode, NodeTypes } from '@vue/compiler-core'
1+
import { ParserOptions, NodeTypes, Namespaces } from '@vue/compiler-core'
22
import { isVoidTag, isHTMLTag, isSVGTag } from '@vue/shared'
33
import { TRANSITION, TRANSITION_GROUP } from './runtimeHelpers'
44
import { decodeHtmlBrowser } from './decodeHtmlBrowser'
55

6-
export const enum DOMNamespaces {
7-
HTML = 0 /* Namespaces.HTML */,
8-
SVG,
9-
MATH_ML
10-
}
11-
126
export const parserOptions: ParserOptions = {
137
parseMode: 'html',
148
isVoidTag,
159
isNativeTag: tag => isHTMLTag(tag) || isSVGTag(tag),
1610
isPreTag: tag => tag === 'pre',
1711
decodeEntities: __BROWSER__ ? decodeHtmlBrowser : undefined,
1812

19-
isBuiltInComponent: (tag: string): symbol | undefined => {
13+
isBuiltInComponent: tag => {
2014
if (tag === 'Transition' || tag === 'transition') {
2115
return TRANSITION
2216
} else if (tag === 'TransitionGroup' || tag === 'transition-group') {
@@ -25,12 +19,12 @@ export const parserOptions: ParserOptions = {
2519
},
2620

2721
// https://html.spec.whatwg.org/multipage/parsing.html#tree-construction-dispatcher
28-
getNamespace(tag: string, parent: ElementNode | undefined): DOMNamespaces {
29-
let ns = parent ? parent.ns : DOMNamespaces.HTML
30-
if (parent && ns === DOMNamespaces.MATH_ML) {
22+
getNamespace(tag, parent, rootNamespace) {
23+
let ns = parent ? parent.ns : rootNamespace
24+
if (parent && ns === Namespaces.MATH_ML) {
3125
if (parent.tag === 'annotation-xml') {
3226
if (tag === 'svg') {
33-
return DOMNamespaces.SVG
27+
return Namespaces.SVG
3428
}
3529
if (
3630
parent.props.some(
@@ -42,31 +36,31 @@ export const parserOptions: ParserOptions = {
4236
a.value.content === 'application/xhtml+xml')
4337
)
4438
) {
45-
ns = DOMNamespaces.HTML
39+
ns = Namespaces.HTML
4640
}
4741
} else if (
4842
/^m(?:[ions]|text)$/.test(parent.tag) &&
4943
tag !== 'mglyph' &&
5044
tag !== 'malignmark'
5145
) {
52-
ns = DOMNamespaces.HTML
46+
ns = Namespaces.HTML
5347
}
54-
} else if (parent && ns === DOMNamespaces.SVG) {
48+
} else if (parent && ns === Namespaces.SVG) {
5549
if (
5650
parent.tag === 'foreignObject' ||
5751
parent.tag === 'desc' ||
5852
parent.tag === 'title'
5953
) {
60-
ns = DOMNamespaces.HTML
54+
ns = Namespaces.HTML
6155
}
6256
}
6357

64-
if (ns === DOMNamespaces.HTML) {
58+
if (ns === Namespaces.HTML) {
6559
if (tag === 'svg') {
66-
return DOMNamespaces.SVG
60+
return Namespaces.SVG
6761
}
6862
if (tag === 'math') {
69-
return DOMNamespaces.MATH_ML
63+
return Namespaces.MATH_ML
7064
}
7165
}
7266
return ns

packages/compiler-dom/src/transforms/stringifyStatic.ts

+5-5
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ import {
1515
PlainElementNode,
1616
JSChildNode,
1717
TextCallNode,
18-
ConstantTypes
18+
ConstantTypes,
19+
Namespaces
1920
} from '@vue/compiler-core'
2021
import {
2122
isVoidTag,
@@ -31,7 +32,6 @@ import {
3132
isKnownSvgAttr,
3233
isBooleanAttr
3334
} from '@vue/shared'
34-
import { DOMNamespaces } from '../parserOptions'
3535

3636
export const enum StringifyThresholds {
3737
ELEMENT_WITH_BINDING_COUNT = 5,
@@ -148,11 +148,11 @@ const getHoistedNode = (node: TemplateChildNode) =>
148148
node.codegenNode.hoisted
149149

150150
const dataAriaRE = /^(data|aria)-/
151-
const isStringifiableAttr = (name: string, ns: DOMNamespaces) => {
151+
const isStringifiableAttr = (name: string, ns: Namespaces) => {
152152
return (
153-
(ns === DOMNamespaces.HTML
153+
(ns === Namespaces.HTML
154154
? isKnownHtmlAttr(name)
155-
: ns === DOMNamespaces.SVG
155+
: ns === Namespaces.SVG
156156
? isKnownSvgAttr(name)
157157
: false) || dataAriaRE.test(name)
158158
)

0 commit comments

Comments
 (0)