33 * @copyright 2017 Toru Nagashima. All rights reserved.
44 * See LICENSE file in root directory for full license.
55 */
6+ import * as lodash from "lodash"
67import { debug } from "../common/debug"
7- import { ErrorCode , ParseError , Token , VAttribute , VDocumentFragment , VElement , VNode } from "../ast"
8- import { HTML_CAN_BE_LEFT_OPEN_TAGS , HTML_NON_FHRASING_TAGS , HTML_VOID_ELEMENT_TAGS } from "./util/tag-names"
8+ import { ErrorCode , Namespace , NS , ParseError , Token , VAttribute , VDocumentFragment , VElement , VNode , VText } from "../ast"
9+ import { MATHML_ATTRIBUTE_NAME_MAP , SVG_ATTRIBUTE_NAME_MAP } from "./util/attribute-names"
10+ import { HTML_CAN_BE_LEFT_OPEN_TAGS , HTML_NON_FHRASING_TAGS , HTML_VOID_ELEMENT_TAGS , SVG_ELEMENT_NAME_MAP } from "./util/tag-names"
911import { Tokenizer , TokenType } from "./tokenizer"
1012
13+ /**
14+ * Check whether the element is a MathML text integration point or not.
15+ * @see https://html.spec.whatwg.org/multipage/parsing.html#tree-construction-dispatcher
16+ * @param element The current element.
17+ * @returns `true` if the element is a MathML text integration point.
18+ */
19+ function isMathMLIntegrationPoint ( element : VElement ) : boolean {
20+ if ( element . namespace === NS . MathML ) {
21+ const name = element . name
22+ return name === "mi" || name === "mo" || name === "mn" || name === "ms" || name === "mtext"
23+ }
24+ return false
25+ }
26+
27+ /**
28+ * Check whether the element is a HTML integration point or not.
29+ * @see https://html.spec.whatwg.org/multipage/parsing.html#tree-construction-dispatcher
30+ * @param element The current element.
31+ * @returns `true` if the element is a HTML integration point.
32+ */
33+ function isHTMLIntegrationPoint ( element : VElement ) : boolean {
34+ if ( element . namespace === NS . MathML ) {
35+ return (
36+ element . name === "annotation-xml" &&
37+ element . startTag . attributes . some ( a =>
38+ a . directive === false &&
39+ a . key . name === "encoding" &&
40+ a . value != null &&
41+ (
42+ a . value . value === "text/html" ||
43+ a . value . value === "application/xhtml+xml"
44+ )
45+ )
46+ )
47+ }
48+ if ( element . namespace === NS . SVG ) {
49+ const name = element . name
50+ return name === "foreignObject" || name === "desc" || name === "title"
51+ }
52+
53+ return false
54+ }
55+
1156/**
1257 * The parser of HTML.
58+ * This is not following to the HTML spec completely because Vue.js template spec is pretty different to HTML.
1359 */
1460export class Parser {
1561 private tokenizer : Tokenizer
1662 private tokens : Token [ ]
1763 private comments : Token [ ]
18- private errors : ParseError [ ]
1964 private currentNode : VNode
2065 private nodeStack : VNode [ ]
66+ private namespaceStack : VElement [ ]
2167
2268 /**
2369 * Initialize this parser.
@@ -28,7 +74,6 @@ export class Parser {
2874 this . tokenizer = tokenizer
2975 this . tokens = [ ]
3076 this . comments = [ ]
31- this . errors = this . tokenizer . errors
3277 this . currentNode = {
3378 type : "VDocumentFragment" ,
3479 range : [ 0 , 0 ] ,
@@ -40,9 +85,27 @@ export class Parser {
4085 children : [ ] ,
4186 tokens : this . tokens ,
4287 comments : this . comments ,
43- errors : this . errors ,
88+ errors : this . tokenizer . errors ,
4489 }
4590 this . nodeStack = [ ]
91+ this . namespaceStack = [ ]
92+ }
93+
94+ /**
95+ * The syntax errors which are found in this parsing.
96+ */
97+ get errors ( ) : ParseError [ ] {
98+ return this . tokenizer . errors
99+ }
100+
101+ /**
102+ * The current namespace.
103+ */
104+ get namespace ( ) : Namespace {
105+ return this . tokenizer . namespace
106+ }
107+ set namespace ( value : Namespace ) { //eslint-disable-line require-jsdoc
108+ this . tokenizer . namespace = value
46109 }
47110
48111 /**
@@ -79,15 +142,18 @@ export class Parser {
79142 /**
80143 * Push the given node to the current node stack.
81144 * @param node The node to push.
82- * @returns The pushed node.
83145 */
84- private pushNodeStack < T extends VNode > ( node : T ) : T {
146+ private pushNodeStack ( node : VNode ) : void {
85147 debug ( "[html] push node: %s" , node . type )
86148
87149 this . nodeStack . push ( this . currentNode )
88150 this . currentNode = node
89151
90- return node
152+ // Update the current namespace.
153+ if ( node . type === "VElement" && node . namespace !== this . namespace ) {
154+ this . namespaceStack . push ( node )
155+ this . namespace = node . namespace
156+ }
91157 }
92158
93159 /**
@@ -106,6 +172,97 @@ export class Parser {
106172 poppedNode . loc . end = node . loc . end
107173
108174 this . currentNode = poppedNode
175+
176+ // Update the current namespace.
177+ if ( node === lodash . last ( this . namespaceStack ) ) {
178+ this . namespaceStack . pop ( )
179+
180+ const namespaceElement = lodash . last ( this . namespaceStack )
181+ if ( namespaceElement != null ) {
182+ this . namespace = namespaceElement . namespace
183+ }
184+ else {
185+ this . namespace = NS . HTML
186+ }
187+ }
188+ }
189+
190+ /**
191+ * Adjust element names by the current namespace.
192+ * @param name The lowercase element name to adjust.
193+ * @returns The adjusted element name.
194+ */
195+ private adjustElementName ( name : string ) : string {
196+ if ( this . namespace === NS . SVG ) {
197+ return SVG_ELEMENT_NAME_MAP . get ( name ) || name
198+ }
199+ return name
200+ }
201+
202+ /**
203+ * Adjust attribute names by the current namespace.
204+ * @param name The lowercase attribute name to adjust.
205+ * @returns The adjusted attribute name.
206+ */
207+ private adjustAttributeName ( name : string ) : string {
208+ if ( this . namespace === NS . SVG ) {
209+ return SVG_ATTRIBUTE_NAME_MAP . get ( name ) || name
210+ }
211+ if ( this . namespace === NS . MathML ) {
212+ return MATHML_ATTRIBUTE_NAME_MAP . get ( name ) || name
213+ }
214+ return name
215+ }
216+
217+ /**
218+ * Get the current element.
219+ * @returns The current element.
220+ */
221+ private getCurrentElement ( ) : VElement | undefined {
222+ if ( this . currentNode != null && this . currentNode . type === "VElement" ) {
223+ return this . currentNode
224+ }
225+
226+ for ( let i = this . nodeStack . length - 1 ; i >= 0 ; -- i ) {
227+ const node = this . nodeStack [ i ]
228+ if ( node . type === "VElement" ) {
229+ return node
230+ }
231+ }
232+
233+ return undefined
234+ }
235+
236+ /**
237+ * Detect the namespace of the new element.
238+ * @param name The value of a HTMLTagOpen token.
239+ * @returns The namespace of the new element.
240+ */
241+ private detectNamespace ( name : string ) : Namespace {
242+ let ns = this . namespace
243+
244+ if ( ns === NS . MathML || ns === NS . SVG ) {
245+ const element = this . getCurrentElement ( )
246+ if ( element != null ) {
247+ if ( element . namespace === NS . MathML && element . name === "annotation-xml" && name === "svg" ) {
248+ return NS . SVG
249+ }
250+ if ( isHTMLIntegrationPoint ( element ) || ( isMathMLIntegrationPoint ( element ) && name !== "mglyph" && name !== "malignmark" ) ) {
251+ ns = NS . HTML
252+ }
253+ }
254+ }
255+
256+ if ( ns === NS . HTML ) {
257+ if ( name === "svg" ) {
258+ return NS . SVG
259+ }
260+ if ( name === "math" ) {
261+ return NS . MathML
262+ }
263+ }
264+
265+ return ns
109266 }
110267
111268 /**
@@ -158,14 +315,16 @@ export class Parser {
158315 }
159316
160317 const parentElement = this . currentNode
161- const text = this . pushNodeStack ( {
318+ const text : VText = {
162319 type : "VText" ,
163320 range : [ token . range [ 0 ] , token . range [ 1 ] ] ,
164321 loc : { start : token . loc . start , end : token . loc . end } ,
165322 parent : parentElement ,
166323 value : token . value ,
167- } )
324+ }
168325 parentElement . children . push ( text )
326+
327+ this . pushNodeStack ( text )
169328 }
170329
171330 /**
@@ -256,12 +415,14 @@ export class Parser {
256415
257416 // Push the end tag.
258417 const element = this . currentNode
259- element . endTag = this . pushNodeStack ( {
418+ element . endTag = {
260419 type : "VEndTag" ,
261420 range : [ token . range [ 0 ] , token . range [ 1 ] ] ,
262421 loc : { start : token . loc . start , end : token . loc . end } ,
263422 parent : element ,
264- } )
423+ }
424+
425+ this . pushNodeStack ( element . endTag )
265426 }
266427
267428 /**
@@ -295,13 +456,14 @@ export class Parser {
295456 range : [ token . range [ 0 ] , token . range [ 1 ] ] ,
296457 loc : { start : token . loc . start , end : token . loc . end } ,
297458 parent : { } as VAttribute ,
298- name : token . value ,
459+ name : this . adjustAttributeName ( token . value ) ,
299460 } ,
300461 value : null ,
301462 }
302463 attribute . key . parent = attribute
464+ startTag . attributes . push ( attribute )
303465
304- startTag . attributes . push ( this . pushNodeStack ( attribute ) )
466+ this . pushNodeStack ( attribute )
305467 }
306468
307469 /**
@@ -316,6 +478,15 @@ export class Parser {
316478 throw new Error ( "unreachable" )
317479 }
318480
481+ // Check namespace
482+ if ( attribute . key . name === "xmlns" && token . value !== this . namespace ) {
483+ this . reportParseError ( token , "x-invalid-namespace" )
484+ }
485+ if ( attribute . key . name === "xmlns:xlink" && token . value !== NS . XLink ) {
486+ this . reportParseError ( token , "x-invalid-namespace" )
487+ }
488+
489+ // Initialize the attribute value.
319490 attribute . range [ 1 ] = token . range [ 1 ]
320491 attribute . loc . end = token . loc . end
321492 attribute . value = {
@@ -422,7 +593,8 @@ export class Parser {
422593 range : [ token . range [ 0 ] , token . range [ 1 ] ] ,
423594 loc : { start : token . loc . start , end : token . loc . end } ,
424595 parent : parentElement ,
425- name : token . value ,
596+ name : this . adjustElementName ( token . value ) ,
597+ namespace : this . detectNamespace ( token . value ) ,
426598 startTag : {
427599 type : "VStartTag" ,
428600 range : [ token . range [ 0 ] , token . range [ 1 ] ] ,
0 commit comments