]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
feat(compiler-core): support specifying root namespace when parsing
authorEvan You <yyx990803@gmail.com>
Sun, 19 Nov 2023 03:20:05 +0000 (11:20 +0800)
committerEvan You <yyx990803@gmail.com>
Sat, 25 Nov 2023 08:18:29 +0000 (16:18 +0800)
packages/compiler-core/src/ast.ts
packages/compiler-core/src/options.ts
packages/compiler-core/src/parser/index.ts
packages/compiler-dom/__tests__/parse.spec.ts
packages/compiler-dom/src/parserOptions.ts
packages/compiler-dom/src/transforms/stringifyStatic.ts

index cc581fff13a9e8d18393e97eb42268454c240d51..b7fb571582b480d6d714311b0486492fa4357102 100644 (file)
@@ -16,12 +16,13 @@ import { PropsExpression } from './transforms/transformElement'
 import { ImportItem, TransformContext } from './transform'
 
 // Vue template is a platform-agnostic superset of HTML (syntax only).
-// More namespaces like SVG and MathML are declared by platform specific
-// compilers.
+// More namespaces can be declared by platform specific compilers.
 export type Namespace = number
 
 export const enum Namespaces {
-  HTML
+  HTML,
+  SVG,
+  MATH_ML
 }
 
 export const enum NodeTypes {
index a85842754de72a6d62ae03dfb9cef339b5771e68..4b67ab0dd76224b22d65ae91c55607e507d1dfb9 100644 (file)
@@ -1,4 +1,10 @@
-import { ElementNode, Namespace, TemplateChildNode, ParentNode } from './ast'
+import {
+  ElementNode,
+  Namespace,
+  TemplateChildNode,
+  ParentNode,
+  Namespaces
+} from './ast'
 import { CompilerError } from './errors'
 import {
   NodeTransform,
@@ -16,7 +22,24 @@ export interface ErrorHandlingOptions {
 export interface ParserOptions
   extends ErrorHandlingOptions,
     CompilerCompatOptions {
+  /**
+   * Base mode is platform agnostic and only parses HTML-like template syntax,
+   * treating all tags the same way. Specific tag parsing behavior can be
+   * configured by higher-level compilers.
+   *
+   * HTML mode adds additional logic for handling special parsing behavior in
+   * `<script>`, `<style>`,`<title>` and `<html>`, plus SVG and MathML
+   * namespaces. The logic is handled inside compiler-core for efficiency.
+   *
+   * SFC mode treats content of all root-level tags except `<template>` as plain
+   * text.
+   */
   parseMode?: 'base' | 'html' | 'sfc'
+  /**
+   * Specify the root namepsace to use when parsing a tempalte.
+   * Defaults to `Namepsaces.HTML` (0).
+   */
+  ns?: Namespaces
   /**
    * e.g. platform native elements, e.g. `<div>` for browsers
    */
@@ -40,7 +63,11 @@ export interface ParserOptions
   /**
    * Get tag namespace
    */
-  getNamespace?: (tag: string, parent: ElementNode | undefined) => Namespace
+  getNamespace?: (
+    tag: string,
+    parent: ElementNode | undefined,
+    rootNamespace: Namespace
+  ) => Namespace
   /**
    * @default ['{{', '}}']
    */
index 981d39c3be8d02b8b4ea4b118a8cc5da6abedec7..967dfc18738139e31d749c5ea173f9dc59672314 100644 (file)
@@ -40,6 +40,7 @@ type MergedParserOptions = Omit<Required<ParserOptions>, OptionalOptions> &
 
 export const defaultParserOptions: MergedParserOptions = {
   parseMode: 'base',
+  ns: Namespaces.HTML,
   delimiters: [`{{`, `}}`],
   getNamespace: () => Namespaces.HTML,
   isVoidTag: NO,
@@ -107,7 +108,7 @@ const tokenizer = new Tokenizer(stack, {
     currentElement = {
       type: NodeTypes.ELEMENT,
       tag: name,
-      ns: currentOptions.getNamespace(name, stack[0]),
+      ns: currentOptions.getNamespace(name, stack[0], currentOptions.ns),
       tagType: ElementTypes.ELEMENT, // will be refined on tag close
       props: [],
       children: [],
index 5f15fa7c97ca978d79f4cb8afd1e208f10607937..bda24656b6fa2e582a380335f88a2dfa396b1cce 100644 (file)
@@ -6,9 +6,10 @@ import {
   ElementTypes,
   InterpolationNode,
   AttributeNode,
-  ConstantTypes
+  ConstantTypes,
+  Namespaces
 } from '@vue/compiler-core'
-import { parserOptions, DOMNamespaces } from '../src/parserOptions'
+import { parserOptions } from '../src/parserOptions'
 
 describe('DOM parser', () => {
   describe('Text', () => {
@@ -264,7 +265,7 @@ describe('DOM parser', () => {
 
       expect(element).toStrictEqual({
         type: NodeTypes.ELEMENT,
-        ns: DOMNamespaces.HTML,
+        ns: Namespaces.HTML,
         tag: 'img',
         tagType: ElementTypes.ELEMENT,
         props: [],
@@ -324,21 +325,21 @@ describe('DOM parser', () => {
       const ast = parse('<html>test</html>', parserOptions)
       const element = ast.children[0] as ElementNode
 
-      expect(element.ns).toBe(DOMNamespaces.HTML)
+      expect(element.ns).toBe(Namespaces.HTML)
     })
 
     test('SVG namespace', () => {
       const ast = parse('<svg>test</svg>', parserOptions)
       const element = ast.children[0] as ElementNode
 
-      expect(element.ns).toBe(DOMNamespaces.SVG)
+      expect(element.ns).toBe(Namespaces.SVG)
     })
 
     test('MATH_ML namespace', () => {
       const ast = parse('<math>test</math>', parserOptions)
       const element = ast.children[0] as ElementNode
 
-      expect(element.ns).toBe(DOMNamespaces.MATH_ML)
+      expect(element.ns).toBe(Namespaces.MATH_ML)
     })
 
     test('SVG in MATH_ML namespace', () => {
@@ -350,8 +351,8 @@ describe('DOM parser', () => {
       const elementAnnotation = elementMath.children[0] as ElementNode
       const elementSvg = elementAnnotation.children[0] as ElementNode
 
-      expect(elementMath.ns).toBe(DOMNamespaces.MATH_ML)
-      expect(elementSvg.ns).toBe(DOMNamespaces.SVG)
+      expect(elementMath.ns).toBe(Namespaces.MATH_ML)
+      expect(elementSvg.ns).toBe(Namespaces.SVG)
     })
 
     test('html text/html in MATH_ML namespace', () => {
@@ -364,8 +365,8 @@ describe('DOM parser', () => {
       const elementAnnotation = elementMath.children[0] as ElementNode
       const element = elementAnnotation.children[0] as ElementNode
 
-      expect(elementMath.ns).toBe(DOMNamespaces.MATH_ML)
-      expect(element.ns).toBe(DOMNamespaces.HTML)
+      expect(elementMath.ns).toBe(Namespaces.MATH_ML)
+      expect(element.ns).toBe(Namespaces.HTML)
     })
 
     test('html application/xhtml+xml in MATH_ML namespace', () => {
@@ -377,8 +378,8 @@ describe('DOM parser', () => {
       const elementAnnotation = elementMath.children[0] as ElementNode
       const element = elementAnnotation.children[0] as ElementNode
 
-      expect(elementMath.ns).toBe(DOMNamespaces.MATH_ML)
-      expect(element.ns).toBe(DOMNamespaces.HTML)
+      expect(elementMath.ns).toBe(Namespaces.MATH_ML)
+      expect(element.ns).toBe(Namespaces.HTML)
     })
 
     test('mtext malignmark in MATH_ML namespace', () => {
@@ -390,8 +391,8 @@ describe('DOM parser', () => {
       const elementText = elementMath.children[0] as ElementNode
       const element = elementText.children[0] as ElementNode
 
-      expect(elementMath.ns).toBe(DOMNamespaces.MATH_ML)
-      expect(element.ns).toBe(DOMNamespaces.MATH_ML)
+      expect(elementMath.ns).toBe(Namespaces.MATH_ML)
+      expect(element.ns).toBe(Namespaces.MATH_ML)
     })
 
     test('mtext and not malignmark tag in MATH_ML namespace', () => {
@@ -400,8 +401,8 @@ describe('DOM parser', () => {
       const elementText = elementMath.children[0] as ElementNode
       const element = elementText.children[0] as ElementNode
 
-      expect(elementMath.ns).toBe(DOMNamespaces.MATH_ML)
-      expect(element.ns).toBe(DOMNamespaces.HTML)
+      expect(elementMath.ns).toBe(Namespaces.MATH_ML)
+      expect(element.ns).toBe(Namespaces.HTML)
     })
 
     test('foreignObject tag in SVG namespace', () => {
@@ -413,8 +414,8 @@ describe('DOM parser', () => {
       const elementForeignObject = elementSvg.children[0] as ElementNode
       const element = elementForeignObject.children[0] as ElementNode
 
-      expect(elementSvg.ns).toBe(DOMNamespaces.SVG)
-      expect(element.ns).toBe(DOMNamespaces.HTML)
+      expect(elementSvg.ns).toBe(Namespaces.SVG)
+      expect(element.ns).toBe(Namespaces.HTML)
     })
 
     test('desc tag in SVG namespace', () => {
@@ -423,8 +424,8 @@ describe('DOM parser', () => {
       const elementDesc = elementSvg.children[0] as ElementNode
       const element = elementDesc.children[0] as ElementNode
 
-      expect(elementSvg.ns).toBe(DOMNamespaces.SVG)
-      expect(element.ns).toBe(DOMNamespaces.HTML)
+      expect(elementSvg.ns).toBe(Namespaces.SVG)
+      expect(element.ns).toBe(Namespaces.HTML)
     })
 
     test('title tag in SVG namespace', () => {
@@ -433,8 +434,8 @@ describe('DOM parser', () => {
       const elementTitle = elementSvg.children[0] as ElementNode
       const element = elementTitle.children[0] as ElementNode
 
-      expect(elementSvg.ns).toBe(DOMNamespaces.SVG)
-      expect(element.ns).toBe(DOMNamespaces.HTML)
+      expect(elementSvg.ns).toBe(Namespaces.SVG)
+      expect(element.ns).toBe(Namespaces.HTML)
     })
 
     test('SVG in HTML namespace', () => {
@@ -442,8 +443,8 @@ describe('DOM parser', () => {
       const elementHtml = ast.children[0] as ElementNode
       const element = elementHtml.children[0] as ElementNode
 
-      expect(elementHtml.ns).toBe(DOMNamespaces.HTML)
-      expect(element.ns).toBe(DOMNamespaces.SVG)
+      expect(elementHtml.ns).toBe(Namespaces.HTML)
+      expect(element.ns).toBe(Namespaces.SVG)
     })
 
     test('MATH in HTML namespace', () => {
@@ -451,8 +452,20 @@ describe('DOM parser', () => {
       const elementHtml = ast.children[0] as ElementNode
       const element = elementHtml.children[0] as ElementNode
 
-      expect(elementHtml.ns).toBe(DOMNamespaces.HTML)
-      expect(element.ns).toBe(DOMNamespaces.MATH_ML)
+      expect(elementHtml.ns).toBe(Namespaces.HTML)
+      expect(element.ns).toBe(Namespaces.MATH_ML)
+    })
+
+    test('root ns', () => {
+      const ast = parse('<foreignObject><test/></foreignObject>', {
+        ...parserOptions,
+        ns: Namespaces.SVG
+      })
+      const elementForieng = ast.children[0] as ElementNode
+      const element = elementForieng.children[0] as ElementNode
+
+      expect(elementForieng.ns).toBe(Namespaces.SVG)
+      expect(element.ns).toBe(Namespaces.HTML)
     })
   })
 })
index 29f76b7fde6291d6c436d562677d34bfd92c34e0..2b5e9f0108413af7eceea0254b8ae58b4bbd04d9 100644 (file)
@@ -1,14 +1,8 @@
-import { ParserOptions, ElementNode, NodeTypes } from '@vue/compiler-core'
+import { ParserOptions, NodeTypes, Namespaces } from '@vue/compiler-core'
 import { isVoidTag, isHTMLTag, isSVGTag } from '@vue/shared'
 import { TRANSITION, TRANSITION_GROUP } from './runtimeHelpers'
 import { decodeHtmlBrowser } from './decodeHtmlBrowser'
 
-export const enum DOMNamespaces {
-  HTML = 0 /* Namespaces.HTML */,
-  SVG,
-  MATH_ML
-}
-
 export const parserOptions: ParserOptions = {
   parseMode: 'html',
   isVoidTag,
@@ -16,7 +10,7 @@ export const parserOptions: ParserOptions = {
   isPreTag: tag => tag === 'pre',
   decodeEntities: __BROWSER__ ? decodeHtmlBrowser : undefined,
 
-  isBuiltInComponent: (tag: string): symbol | undefined => {
+  isBuiltInComponent: tag => {
     if (tag === 'Transition' || tag === 'transition') {
       return TRANSITION
     } else if (tag === 'TransitionGroup' || tag === 'transition-group') {
@@ -25,12 +19,12 @@ export const parserOptions: ParserOptions = {
   },
 
   // https://html.spec.whatwg.org/multipage/parsing.html#tree-construction-dispatcher
-  getNamespace(tag: string, parent: ElementNode | undefined): DOMNamespaces {
-    let ns = parent ? parent.ns : DOMNamespaces.HTML
-    if (parent && ns === DOMNamespaces.MATH_ML) {
+  getNamespace(tag, parent, rootNamespace) {
+    let ns = parent ? parent.ns : rootNamespace
+    if (parent && ns === Namespaces.MATH_ML) {
       if (parent.tag === 'annotation-xml') {
         if (tag === 'svg') {
-          return DOMNamespaces.SVG
+          return Namespaces.SVG
         }
         if (
           parent.props.some(
@@ -42,31 +36,31 @@ export const parserOptions: ParserOptions = {
                 a.value.content === 'application/xhtml+xml')
           )
         ) {
-          ns = DOMNamespaces.HTML
+          ns = Namespaces.HTML
         }
       } else if (
         /^m(?:[ions]|text)$/.test(parent.tag) &&
         tag !== 'mglyph' &&
         tag !== 'malignmark'
       ) {
-        ns = DOMNamespaces.HTML
+        ns = Namespaces.HTML
       }
-    } else if (parent && ns === DOMNamespaces.SVG) {
+    } else if (parent && ns === Namespaces.SVG) {
       if (
         parent.tag === 'foreignObject' ||
         parent.tag === 'desc' ||
         parent.tag === 'title'
       ) {
-        ns = DOMNamespaces.HTML
+        ns = Namespaces.HTML
       }
     }
 
-    if (ns === DOMNamespaces.HTML) {
+    if (ns === Namespaces.HTML) {
       if (tag === 'svg') {
-        return DOMNamespaces.SVG
+        return Namespaces.SVG
       }
       if (tag === 'math') {
-        return DOMNamespaces.MATH_ML
+        return Namespaces.MATH_ML
       }
     }
     return ns
index 67042b2d7f3c7b5caa0e88ff8e541e46ea4b3021..02a7cb031bffe94cebe86bf8a398e1d0cc60c65a 100644 (file)
@@ -15,7 +15,8 @@ import {
   PlainElementNode,
   JSChildNode,
   TextCallNode,
-  ConstantTypes
+  ConstantTypes,
+  Namespaces
 } from '@vue/compiler-core'
 import {
   isVoidTag,
@@ -31,7 +32,6 @@ import {
   isKnownSvgAttr,
   isBooleanAttr
 } from '@vue/shared'
-import { DOMNamespaces } from '../parserOptions'
 
 export const enum StringifyThresholds {
   ELEMENT_WITH_BINDING_COUNT = 5,
@@ -148,11 +148,11 @@ const getHoistedNode = (node: TemplateChildNode) =>
   node.codegenNode.hoisted
 
 const dataAriaRE = /^(data|aria)-/
-const isStringifiableAttr = (name: string, ns: DOMNamespaces) => {
+const isStringifiableAttr = (name: string, ns: Namespaces) => {
   return (
-    (ns === DOMNamespaces.HTML
+    (ns === Namespaces.HTML
       ? isKnownHtmlAttr(name)
-      : ns === DOMNamespaces.SVG
+      : ns === Namespaces.SVG
         ? isKnownSvgAttr(name)
         : false) || dataAriaRE.test(name)
   )