From dfa64c8dfec25a46ab13cd909edfbb8155bf0250 Mon Sep 17 00:00:00 2001 From: David Kudera Date: Fri, 13 Jan 2017 22:21:28 +0100 Subject: [PATCH 1/2] Parsing: create BrowserElementParser for faster element parsing in RootCompiler --- src/Parsers/AbstractHTMLParser.ts | 167 +++++++++++++ src/Parsers/BrowserElementParser.ts | 42 ++++ src/Parsers/HTMLParser.ts | 180 +------------- src/Templating/Compilers/ComponentCompiler.ts | 3 +- src/Templating/Compilers/RootCompiler.ts | 14 +- src/Templating/QuerySelector.ts | 2 +- tests/index.ts | 1 + tests/tests/Parsers/BrowserElementParser.ts | 219 ++++++++++++++++++ tests/tests/Parsers/HTMLParser.ts | 5 +- tests/tests/Templating/QuerySelector.ts | 3 +- 10 files changed, 450 insertions(+), 186 deletions(-) create mode 100644 src/Parsers/AbstractHTMLParser.ts create mode 100644 src/Parsers/BrowserElementParser.ts create mode 100755 tests/tests/Parsers/BrowserElementParser.ts diff --git a/src/Parsers/AbstractHTMLParser.ts b/src/Parsers/AbstractHTMLParser.ts new file mode 100644 index 0000000..7373c4b --- /dev/null +++ b/src/Parsers/AbstractHTMLParser.ts @@ -0,0 +1,167 @@ +import {TextParser} from './TextParser'; +import {Strings} from '../Util/Strings'; + + +export enum HTMLAttributeType +{ + NATIVE, + EXPRESSION, + PROPERTY, + EVENT, + EXPORT, + TEMPLATE, +} + + +export enum HTMLTokenType +{ + T_ELEMENT, + T_STRING, + T_EXPRESSION, +} + + +export declare interface AttributeToken +{ + type: HTMLAttributeType, + name: string, + originalName: string, + value: string, + preventDefault?: boolean, +} + + +export declare interface StringToken +{ + type: HTMLTokenType, + value: string, + parent: ElementToken, +} + + +export declare interface ElementToken +{ + type: HTMLTokenType, + name: string, + attributes: {[name: string]: AttributeToken}, + parent: ElementToken, + children: Array, +} + + +export abstract class AbstractHTMLParser +{ + + + private static TWO_WAY_BINDING_CHANGE = 'change'; + + + protected exports: Array = []; + + + protected parseAttribute(name: string, value: string): Array + { + let type = HTMLAttributeType.NATIVE; + let preventDefault = false; + let match; + + if (match = name.match(/^\[\((.+)\)]$/)) { + return [ + this.parseAttribute('[' + match[1] + ']', value)[0], + this.parseAttribute('(' + match[1] + '-' + AbstractHTMLParser.TWO_WAY_BINDING_CHANGE + ')!', value + '=$value')[0], + ]; + } + + if (match = name.match(/^\*(.+)/)) { + type = HTMLAttributeType.TEMPLATE; + name = match[1]; + } else if (match = name.match(/^#(.+)/)) { + type = HTMLAttributeType.EXPORT; + name = match[1]; + } else if (match = name.match(/^\[(.+)]$/)) { + type = HTMLAttributeType.PROPERTY; + name = match[1]; + } else if (match = name.match(/^\((.+)\)!?$/)) { + preventDefault = name.slice(-1) === '!'; + type = HTMLAttributeType.EVENT; + name = match[1]; + } + + if (type === HTMLAttributeType.NATIVE) { + let attr = this.parseAttributeValue(value); + type = attr.type; + value = attr.value; + } + + let attributes = []; + + if (type === HTMLAttributeType.EVENT) { + let events = name.split('|'); + for (let i = 0; i < events.length; i++) { + attributes.push({ + type: type, + name: Strings.hyphensToCamelCase(events[i]), + originalName: events[i], + value: value, + preventDefault: preventDefault, + }); + } + + } else { + let attribute: AttributeToken = { + type: type, + name: Strings.hyphensToCamelCase(name), + originalName: name, + value: value, + }; + + attributes.push(attribute); + + if (attribute.type === HTMLAttributeType.EXPORT && this.exports.indexOf(attribute.name) < 0) { + this.exports.push(attribute.name); + } + } + + return attributes; + } + + + protected parseAttributeValue(value: string): {type: HTMLAttributeType, value: string} + { + let tokens = TextParser.parse(value); + let type = HTMLAttributeType.NATIVE; + + if (tokens.length === 0) { + // skip + + } else if (tokens.length === 1) { + if (tokens[0].type === TextParser.TYPE_BINDING) { + type = HTMLAttributeType.EXPRESSION; + value = tokens[0].value; + } + + } else { + let buffer = []; + + for (let i = 0; i < tokens.length; i++) { + let token = tokens[i]; + + if (token.type === TextParser.TYPE_TEXT) { + buffer.push('"' + token.value + '"'); + + } else if (token.type === TextParser.TYPE_BINDING) { + buffer.push('(' + token.value + ')'); + } + } + + type = HTMLAttributeType.EXPRESSION; + value = buffer.join('+'); + } + + return { + type: type, + value: value, + }; + } + +} diff --git a/src/Parsers/BrowserElementParser.ts b/src/Parsers/BrowserElementParser.ts new file mode 100644 index 0000000..4f74c05 --- /dev/null +++ b/src/Parsers/BrowserElementParser.ts @@ -0,0 +1,42 @@ +import {AbstractHTMLParser, ElementToken, AttributeToken, HTMLTokenType, HTMLAttributeType} from './AbstractHTMLParser'; + + +export class BrowserElementParser extends AbstractHTMLParser +{ + + + public parse(element: Element): ElementToken + { + if (element.nodeName.toLowerCase() === 'template') { + throw new Error('BrowserElementParser: can not parse template element.'); + } + + return { + type: HTMLTokenType.T_ELEMENT, + name: element.nodeName.toLowerCase(), + attributes: this.parseAttributes(element), + parent: null, + children: [], + }; + } + + + private parseAttributes(element: Element): {[name: string]: AttributeToken} + { + let attributes = {}; + + for (let i = 0; i < element.attributes.length; i++) { + let append = this.parseAttribute(element.attributes[i].name, element.attributes[i].value); + for (let j = 0; j < append.length; j++) { + if (append[j].type === HTMLAttributeType.TEMPLATE) { + throw new Error('BrowserElementParser: can not parse template shortcut attribute "' + append[j].name + '".'); + } + + attributes[append[j].name] = append[j]; + } + } + + return attributes; + } + +} diff --git a/src/Parsers/HTMLParser.ts b/src/Parsers/HTMLParser.ts index c61d766..5cd7f1c 100644 --- a/src/Parsers/HTMLParser.ts +++ b/src/Parsers/HTMLParser.ts @@ -1,65 +1,11 @@ import {TextParser} from '../Parsers/TextParser'; -import {Strings} from '../Util/Strings'; import {TemplateAttributeParser} from '../Parsers/TemplateAttributeParser'; import {Helpers} from '../Util/Helpers'; +import {AbstractHTMLParser, HTMLAttributeType, HTMLTokenType, AttributeToken, StringToken, ElementToken} from './AbstractHTMLParser'; -export enum HTMLAttributeType +export class HTMLParser extends AbstractHTMLParser { - NATIVE, - EXPRESSION, - PROPERTY, - EVENT, - EXPORT, - TEMPLATE, -} - - -export enum HTMLTokenType -{ - T_ELEMENT, - T_STRING, - T_EXPRESSION, - T_COMMENT, -} - - -export declare interface AttributeToken -{ - type: HTMLAttributeType, - name: string, - originalName: string, - value: string, - preventDefault?: boolean, -} - - -export declare interface StringToken -{ - type: HTMLTokenType, - value: string, - parent: ElementToken, -} - - -export declare interface ElementToken -{ - type: HTMLTokenType, - name: string, - attributes: {[name: string]: AttributeToken}, - parent: ElementToken, - children: Array, -} - - -export class HTMLParser -{ - - - private static TWO_WAY_BINDING_CHANGE = 'change'; - - - private exports: Array = []; public parse(html: string): {exports: Array, tree: Array} @@ -74,12 +20,6 @@ export class HTMLParser } - public parseElement(element: Element): ElementToken - { - return this._parseElement(element, null, false); - } - - private parseBranch(node: Node, parent: ElementToken = null): Array { let branch = []; @@ -99,7 +39,7 @@ export class HTMLParser } } else if (child.nodeType === Node.ELEMENT_NODE) { - branch.push(this._parseElement(child, parent)); + branch.push(this.parseElement(child, parent)); } } @@ -158,7 +98,7 @@ export class HTMLParser } - private _parseElement(node: Element, parent: ElementToken = null, parseChildren: boolean = true): ElementToken + private parseElement(node: Element, parent: ElementToken = null): ElementToken { let attributes = this.parseAttributes(node); @@ -170,9 +110,7 @@ export class HTMLParser children: [], }; - if (parseChildren) { - nodeToken.children = this.parseBranch(node, nodeToken); - } + nodeToken.children = this.parseBranch(node, nodeToken); let rootTemplate: ElementToken; let parentTemplate: ElementToken; @@ -217,7 +155,7 @@ export class HTMLParser } - public parseAttributes(node: Element): {[name: string]: AttributeToken} + private parseAttributes(node: Element): {[name: string]: AttributeToken} { let attributes = {}; @@ -231,110 +169,4 @@ export class HTMLParser return attributes; } - - private parseAttribute(name: string, value: string): Array - { - let type = HTMLAttributeType.NATIVE; - let preventDefault = false; - let match; - - if (match = name.match(/^\[\((.+)\)]$/)) { - return [ - this.parseAttribute('[' + match[1] + ']', value)[0], - this.parseAttribute('(' + match[1] + '-' + HTMLParser.TWO_WAY_BINDING_CHANGE + ')!', value + '=$value')[0], - ]; - } - - if (match = name.match(/^\*(.+)/)) { - type = HTMLAttributeType.TEMPLATE; - name = match[1]; - } else if (match = name.match(/^#(.+)/)) { - type = HTMLAttributeType.EXPORT; - name = match[1]; - } else if (match = name.match(/^\[(.+)]$/)) { - type = HTMLAttributeType.PROPERTY; - name = match[1]; - } else if (match = name.match(/^\((.+)\)!?$/)) { - preventDefault = name.slice(-1) === '!'; - type = HTMLAttributeType.EVENT; - name = match[1]; - } - - if (type === HTMLAttributeType.NATIVE) { - let attr = this.parseAttributeValue(value); - type = attr.type; - value = attr.value; - } - - let attributes = []; - - if (type === HTMLAttributeType.EVENT) { - let events = name.split('|'); - for (let i = 0; i < events.length; i++) { - attributes.push({ - type: type, - name: Strings.hyphensToCamelCase(events[i]), - originalName: events[i], - value: value, - preventDefault: preventDefault, - }); - } - - } else { - let attribute: AttributeToken = { - type: type, - name: Strings.hyphensToCamelCase(name), - originalName: name, - value: value, - }; - - attributes.push(attribute); - - if (attribute.type === HTMLAttributeType.EXPORT && this.exports.indexOf(attribute.name) < 0) { - this.exports.push(attribute.name); - } - } - - return attributes; - } - - - private parseAttributeValue(value: string): {type: HTMLAttributeType, value: string} - { - let tokens = TextParser.parse(value); - let type = HTMLAttributeType.NATIVE; - - if (tokens.length === 0) { - // skip - - } else if (tokens.length === 1) { - if (tokens[0].type === TextParser.TYPE_BINDING) { - type = HTMLAttributeType.EXPRESSION; - value = tokens[0].value; - } - - } else { - let buffer = []; - - for (let i = 0; i < tokens.length; i++) { - let token = tokens[i]; - - if (token.type === TextParser.TYPE_TEXT) { - buffer.push('"' + token.value + '"'); - - } else if (token.type === TextParser.TYPE_BINDING) { - buffer.push('(' + token.value + ')'); - } - } - - type = HTMLAttributeType.EXPRESSION; - value = buffer.join('+'); - } - - return { - type: type, - value: value, - }; - } - } diff --git a/src/Templating/Compilers/ComponentCompiler.ts b/src/Templating/Compilers/ComponentCompiler.ts index 04d566e..81435ad 100644 --- a/src/Templating/Compilers/ComponentCompiler.ts +++ b/src/Templating/Compilers/ComponentCompiler.ts @@ -10,7 +10,8 @@ import { ChildDirectiveDefinition, ChildrenDirectiveDefinition } from '../../Entity/Metadata'; import {Annotations} from '../../Util/Annotations'; -import {HTMLParser, StringToken, ElementToken, HTMLTokenType, HTMLAttributeType, AttributeToken} from '../../Parsers/HTMLParser'; +import {HTMLAttributeType, HTMLTokenType, AttributeToken, StringToken, ElementToken} from '../../Parsers/AbstractHTMLParser'; +import {HTMLParser} from '../../Parsers/HTMLParser'; import {QuerySelector} from '../QuerySelector'; import {Container} from '../../DI/Container'; import {Dom} from '../../Util/Dom'; diff --git a/src/Templating/Compilers/RootCompiler.ts b/src/Templating/Compilers/RootCompiler.ts index 708db3d..eab3264 100644 --- a/src/Templating/Compilers/RootCompiler.ts +++ b/src/Templating/Compilers/RootCompiler.ts @@ -2,7 +2,8 @@ import {Container, CustomServiceDefinition} from '../../DI/Container'; import {DirectiveDefinition} from '../../Entity/DirectiveParser'; import {AbstractCompiler} from './AbstractCompiler'; import {ComponentCompiler} from './ComponentCompiler'; -import {HTMLParser, HTMLAttributeType} from '../../Parsers/HTMLParser'; +import {HTMLAttributeType, AttributeToken} from '../../Parsers/AbstractHTMLParser'; +import {BrowserElementParser} from '../../Parsers/BrowserElementParser'; import {ElementRef} from '../ElementRef'; import {AbstractComponentTemplate} from '../Templates/AbstractComponentTemplate'; import {ParametersList, OnInit, OnDestroy} from '../../Interfaces'; @@ -51,12 +52,13 @@ export class RootCompiler extends AbstractCompiler { let elementRef = ElementRef.get(el); let directive = this.template.attachDirective(this.directiveType, elementRef); + let node = (new BrowserElementParser).parse(el); if (this.definition.parentComponent) { throw Errors.parentComponentInRoot(this.definition.name, this.definition.parentComponent.property); } - this.processInputs(this.template, el, directive); + this.processInputs(this.template, el, node.attributes, directive); this.processElements(elementRef, directive); this.processEvents(el, elementRef, directive); @@ -72,7 +74,7 @@ export class RootCompiler extends AbstractCompiler { let compiler = new ComponentCompiler(this.container, this.templatesStorage, this.directiveType); let elementRef = ElementRef.get(el); - let node = (new HTMLParser).parseElement(el); + let node = (new BrowserElementParser).parse(el); if (this.definition.parentComponent) { throw Errors.parentComponentInRoot(this.definition.name, this.definition.parentComponent.property); @@ -93,7 +95,7 @@ export class RootCompiler extends AbstractCompiler let TemplateType = compiler.compile(); let template: AbstractComponentTemplate = new TemplateType(this.template, this.directiveType, elementRef, this.container, this.extensions, parameters, null, this.definition.metadata.controllerAs, use); - this.processInputs(template, el, template.component); + this.processInputs(template, el, node.attributes, template.component); Helpers.each(this.definition.elements, (property: string, el: HostElementMetadataDefinition) => { if (!el.selector) { @@ -127,10 +129,8 @@ export class RootCompiler extends AbstractCompiler } - private processInputs(template: AbstractTemplate, el: HTMLElement, directive: any): void + private processInputs(template: AbstractTemplate, el: HTMLElement, attributes: {[name: string]: AttributeToken}, directive: any): void { - let attributes = (new HTMLParser).parseAttributes(el); - Helpers.each(this.definition.inputs, (name: string, input: InputMetadataDefinition) => { let attributeName = input.name === null ? name : input.name; let attribute = attributes[attributeName]; diff --git a/src/Templating/QuerySelector.ts b/src/Templating/QuerySelector.ts index a98d39a..72dc7b2 100644 --- a/src/Templating/QuerySelector.ts +++ b/src/Templating/QuerySelector.ts @@ -1,5 +1,5 @@ import {SelectorParser, SelectorType, SelectorItem, ChildType, ElementSelector} from '../Parsers/SelectorParser'; -import {ElementToken, AttributeToken} from '../Parsers/HTMLParser'; +import {AttributeToken, ElementToken} from '../Parsers/AbstractHTMLParser'; export class QuerySelector diff --git a/tests/index.ts b/tests/index.ts index 68352b3..bf3b03e 100644 --- a/tests/index.ts +++ b/tests/index.ts @@ -36,6 +36,7 @@ import './tests/Parsers/ExpressionParser.variableProvider'; import './tests/Parsers/ExpressionParser.filterProvider'; import './tests/Parsers/TemplateAttributeParser'; import './tests/Parsers/TextParser'; +import './tests/Parsers/BrowserElementParser'; import './tests/Parsers/HTMLParser'; import './tests/Parsers/SelectorParser'; diff --git a/tests/tests/Parsers/BrowserElementParser.ts b/tests/tests/Parsers/BrowserElementParser.ts new file mode 100755 index 0000000..cf32d36 --- /dev/null +++ b/tests/tests/Parsers/BrowserElementParser.ts @@ -0,0 +1,219 @@ +import {BrowserElementParser} from '../../../src/Parsers/BrowserElementParser'; +import {HTMLAttributeType, HTMLTokenType, ElementToken} from '../../../src/Parsers/AbstractHTMLParser'; + +import chai = require('chai'); + + +let expect = chai.expect; + + +let parse = (html: string): ElementToken => { + let parent = document.createElement('div'); + parent.innerHTML = html; + + return (new BrowserElementParser).parse(parent.children[0]); +}; + + +describe('#Parsers/BrowserElementParser', () => { + + describe('parse()', () => { + + it('should throw an error when parsing template element', () => { + expect(() => { + parse(''); + }).to.throw(Error, 'BrowserElementParser: can not parse template element.'); + }); + + it('should throw an error when parsing template shortcut attributes', () => { + expect(() => { + parse('
'); + }).to.throw(Error, 'BrowserElementParser: can not parse template shortcut attribute "s:for".'); + }); + + it('should parse element', () => { + expect(parse('
')).to.be.eql({ + type: HTMLTokenType.T_ELEMENT, + name: 'div', + attributes: {}, + parent: null, + children: [], + }); + }); + + it('should parse element with attributes', () => { + expect(parse('')).to.be.eql({ + type: HTMLTokenType.T_ELEMENT, + name: 'div', + attributes: { + hidden: { + type: HTMLAttributeType.NATIVE, + name: 'hidden', + originalName: 'hidden', + value: '', + }, + 'class': { + type: HTMLAttributeType.NATIVE, + name: 'class', + originalName: 'class', + value: 'alert', + }, + id: { + type: HTMLAttributeType.NATIVE, + name: 'id', + originalName: 'id', + value: 'div', + }, + data: { + type: HTMLAttributeType.PROPERTY, + name: 'data', + originalName: 'data', + value: 'data', + }, + div: { + type: HTMLAttributeType.EXPORT, + name: 'div', + originalName: 'div', + value: '', + }, + click: { + type: HTMLAttributeType.EVENT, + name: 'click', + originalName: 'click', + preventDefault: false, + value: 'click()', + }, + }, + parent: null, + children: [], + }); + }); + + it('should convert attributes with hyphens to camel cased names', () => { + expect(parse('
')).to.be.eql({ + type: HTMLTokenType.T_ELEMENT, + name: 'div', + attributes: { + dataAttr: { + type: HTMLAttributeType.NATIVE, + name: 'dataAttr', + originalName: 'data-attr', + value: 'data', + }, + }, + parent: null, + children: [], + }); + }); + + it('should parse element with single expression in attribute', () => { + expect(parse('
')).to.be.eql({ + type: HTMLTokenType.T_ELEMENT, + name: 'div', + attributes: { + 'class': { + type: HTMLAttributeType.EXPRESSION, + name: 'class', + originalName: 'class', + value: 'type', + }, + }, + parent: null, + children: [], + }); + }); + + it('should parse element with expression in attribute', () => { + expect(parse('
')).to.be.eql({ + type: HTMLTokenType.T_ELEMENT, + name: 'div', + attributes: { + 'class': { + type: HTMLAttributeType.EXPRESSION, + name: 'class', + originalName: 'class', + value: '"alert alert-"+(type)', + }, + }, + parent: null, + children: [], + }); + }); + + it('should expand events', () => { + expect(parse('')).to.be.eql({ + type: HTMLTokenType.T_ELEMENT, + name: 'input', + attributes: { + keydown: { + name: 'keydown', + originalName: 'keydown', + type: HTMLAttributeType.EVENT, + preventDefault: false, + value: 'press()', + }, + keypress: { + name: 'keypress', + originalName: 'keypress', + type: HTMLAttributeType.EVENT, + preventDefault: false, + value: 'press()', + }, + }, + parent: null, + children: [], + }); + }); + + it('should parse events with preventDefault option', () => { + expect(parse('')).to.be.eql({ + type: HTMLTokenType.T_ELEMENT, + name: 'input', + attributes: { + keydown: { + name: 'keydown', + originalName: 'keydown', + type: HTMLAttributeType.EVENT, + preventDefault: true, + value: 'press()', + }, + keypress: { + name: 'keypress', + originalName: 'keypress', + type: HTMLAttributeType.EVENT, + preventDefault: true, + value: 'press()', + }, + }, + parent: null, + children: [], + }); + }); + + it('should parse two way data binding', () => { + expect(parse('
')).to.be.eql({ + type: HTMLTokenType.T_ELEMENT, + name: 'div', + attributes: { + articleTitle: { + name: 'articleTitle', + originalName: 'article-title', + type: HTMLAttributeType.PROPERTY, + value: 'title', + }, + articleTitleChange: { + name: 'articleTitleChange', + originalName: 'article-title-change', + preventDefault: true, + type: HTMLAttributeType.EVENT, + value: 'title=$value', + }, + }, + parent: null, + children: [], + }); + }); + + }); + +}); diff --git a/tests/tests/Parsers/HTMLParser.ts b/tests/tests/Parsers/HTMLParser.ts index d9d5183..92b2465 100755 --- a/tests/tests/Parsers/HTMLParser.ts +++ b/tests/tests/Parsers/HTMLParser.ts @@ -1,4 +1,5 @@ -import {HTMLParser, HTMLTokenType, HTMLAttributeType, StringToken, ElementToken} from '../../../src/Parsers/HTMLParser'; +import {HTMLParser} from '../../../src/Parsers/HTMLParser'; +import {HTMLAttributeType, HTMLTokenType, StringToken, ElementToken} from '../../../src/Parsers/AbstractHTMLParser'; import chai = require('chai'); @@ -36,7 +37,7 @@ let parse = (html: string): {exports: Array, tree: Array { +describe('#Parsers/HTMLParser', () => { describe('parse()', () => { diff --git a/tests/tests/Templating/QuerySelector.ts b/tests/tests/Templating/QuerySelector.ts index 06492f1..183faca 100644 --- a/tests/tests/Templating/QuerySelector.ts +++ b/tests/tests/Templating/QuerySelector.ts @@ -1,5 +1,6 @@ import {QuerySelector} from '../../../src/Templating/QuerySelector'; -import {HTMLParser, ElementToken} from '../../../src/Parsers/HTMLParser'; +import {HTMLParser} from '../../../src/Parsers/HTMLParser'; +import {ElementToken} from '../../../src/Parsers/AbstractHTMLParser'; import chai = require('chai'); From fa47a25d8fcf43a6967533659a8cdcb6edfa0257 Mon Sep 17 00:00:00 2001 From: David Kudera Date: Sat, 14 Jan 2017 13:14:03 +0100 Subject: [PATCH 2/2] Parsing: using parse5 for HTML parsing --- package.json | 3 +- src/Parsers/HTMLParser.ts | 72 +++++++++++++++++++++++---------------- 2 files changed, 44 insertions(+), 31 deletions(-) diff --git a/package.json b/package.json index dc3d05f..5ca7097 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,8 @@ "dependencies": { "zone.js": "~0.7.0", "core-js": "~2.4.0", - "rxjs": "~5.0.0" + "rxjs": "~5.0.0", + "parse5": "~3.0.0" }, "devDependencies": { "gulp": "~3.9.0", diff --git a/src/Parsers/HTMLParser.ts b/src/Parsers/HTMLParser.ts index 5cd7f1c..ff91bbf 100644 --- a/src/Parsers/HTMLParser.ts +++ b/src/Parsers/HTMLParser.ts @@ -1,3 +1,4 @@ +import * as parse5 from 'parse5'; import {TextParser} from '../Parsers/TextParser'; import {TemplateAttributeParser} from '../Parsers/TemplateAttributeParser'; import {Helpers} from '../Util/Helpers'; @@ -10,37 +11,49 @@ export class HTMLParser extends AbstractHTMLParser public parse(html: string): {exports: Array, tree: Array} { - let parent = document.createElement('div'); - parent.innerHTML = html; - return { exports: this.exports, - tree: this.parseBranch(parent), + tree: this.parseDocument(parse5.parseFragment(html)), }; } - private parseBranch(node: Node, parent: ElementToken = null): Array + private parseDocument(document: parse5.AST.Default.DocumentFragment): Array { - let branch = []; - let child: Node; + return this.parseBranch(document.childNodes); + } + - if (node.nodeName.toLowerCase() === 'template' && typeof node['content'] !== 'undefined') { - node = document.importNode(node, true)['content']; + private parseChildren(element: parse5.AST.Default.Element, parent: ElementToken = null): Array + { + if (element.nodeName.toLowerCase() === 'template') { + return this.parseDocument(element['content']); } - for (let i = 0; i < node.childNodes.length; i++) { - child = node.childNodes[i]; + return this.parseBranch(element.childNodes, parent); + } + - if (child.nodeType === Node.TEXT_NODE) { - let items = this.parseText(child, parent); - for (let i = 0; i < items.length; i++) { - branch.push(items[i]); - } + private parseBranch(children: Array, parent: ElementToken = null): Array + { + let branch = []; + let child: parse5.AST.Default.Node; + + for (let i = 0; i < children.length; i++) { + child = children[i]; + + if (child.nodeName === '#comment') { + continue; + } - } else if (child.nodeType === Node.ELEMENT_NODE) { - branch.push(this.parseElement(child, parent)); + if (child.nodeName === '#text') { + let items = this.parseText(child, parent); + for (let j = 0; j < items.length; j++) { + branch.push(items[j]); + } + } else { + branch.push(this.parseElement(child, parent)); } } @@ -48,9 +61,9 @@ export class HTMLParser extends AbstractHTMLParser } - private parseText(node: Text, parent: ElementToken = null): Array + private parseText(node: parse5.AST.Default.TextNode, parent: ElementToken = null): Array { - let tokens = TextParser.parse(node.nodeValue); + let tokens = TextParser.parse(node.value); if (tokens.length === 0) { // skip @@ -66,11 +79,10 @@ export class HTMLParser extends AbstractHTMLParser } else { return [{ type: HTMLTokenType.T_STRING, - value: node.nodeValue, + value: node.value, parent: parent, }]; } - } else { let buffer: Array = []; @@ -98,19 +110,19 @@ export class HTMLParser extends AbstractHTMLParser } - private parseElement(node: Element, parent: ElementToken = null): ElementToken + private parseElement(element: parse5.AST.Default.Element, parent: ElementToken = null): ElementToken { - let attributes = this.parseAttributes(node); + let attributes = this.parseAttributes(element); let nodeToken: ElementToken = { type: HTMLTokenType.T_ELEMENT, - name: node.nodeName.toLowerCase(), + name: element.nodeName.toLowerCase(), attributes: {}, parent: !parent || parent.name === 'template' ? null : parent, children: [], }; - nodeToken.children = this.parseBranch(node, nodeToken); + nodeToken.children = this.parseChildren(element, nodeToken); let rootTemplate: ElementToken; let parentTemplate: ElementToken; @@ -155,18 +167,18 @@ export class HTMLParser extends AbstractHTMLParser } - private parseAttributes(node: Element): {[name: string]: AttributeToken} + private parseAttributes(element: parse5.AST.Default.Element): {[name: string]: AttributeToken} { let attributes = {}; - for (let i = 0; i < node.attributes.length; i++) { - let append = this.parseAttribute(node.attributes[i].name, node.attributes[i].value); + for (let i = 0; i < element.attrs.length; i++) { + let append = this.parseAttribute(element.attrs[i].name, element.attrs[i].value); for (let j = 0; j < append.length; j++) { attributes[append[j].name] = append[j]; } } - return attributes; + return attributes; } }