angular-html-parser
Advanced tools
Comparing version 4.0.1 to 5.0.0
@@ -32,2 +32,6 @@ import { TagContentType } from '../../compiler/src/ml_parser/tags.js'; | ||
}>) => void | TagContentType; | ||
/** | ||
* tokenize blocks (Angular Control Flow Syntax) | ||
*/ | ||
tokenizeBlocks?: boolean; | ||
} | ||
@@ -34,0 +38,0 @@ export declare function parse(input: string, options?: ParseOptions): ParseTreeResult; |
@@ -11,3 +11,3 @@ import { HtmlParser } from "../../compiler/src/ml_parser/html_parser.js"; | ||
export function parse(input, options = {}) { | ||
const { canSelfClose = false, allowHtmComponentClosingTags = false, isTagNameCaseSensitive = false, getTagContentType, } = options; | ||
const { canSelfClose = false, allowHtmComponentClosingTags = false, isTagNameCaseSensitive = false, getTagContentType, tokenizeBlocks = false, } = options; | ||
return getParser().parse(input, "angular-html-parser", { | ||
@@ -18,2 +18,3 @@ tokenizeExpansionForms: false, | ||
allowHtmComponentClosingTags, | ||
tokenizeBlocks, | ||
}, isTagNameCaseSensitive, getTagContentType); | ||
@@ -20,0 +21,0 @@ } |
@@ -0,0 +0,0 @@ /** |
@@ -0,0 +0,0 @@ /** |
@@ -0,0 +0,0 @@ /** |
@@ -0,0 +0,0 @@ /** |
@@ -19,6 +19,8 @@ /** | ||
export interface Input { | ||
bindingPropertyName?: string; | ||
alias?: string; | ||
required?: boolean; | ||
transform?: (value: any) => any; | ||
} | ||
export interface Output { | ||
bindingPropertyName?: string; | ||
alias?: string; | ||
} | ||
@@ -25,0 +27,0 @@ export interface HostBinding { |
@@ -0,0 +0,0 @@ /** |
@@ -79,6 +79,6 @@ /** | ||
sourceSpan: ParseSourceSpan; | ||
expressionPlaceholder: string; | ||
expressionPlaceholder?: string; | ||
constructor(expression: string, type: string, cases: { | ||
[k: string]: Node; | ||
}, sourceSpan: ParseSourceSpan); | ||
}, sourceSpan: ParseSourceSpan, expressionPlaceholder?: string); | ||
visit(visitor: Visitor, context?: any): any; | ||
@@ -85,0 +85,0 @@ } |
@@ -24,5 +24,5 @@ /** | ||
this.customId = customId; | ||
this.id = this.customId; | ||
/** The ids to use if there are no custom id and if `i18nLegacyMessageIdFormat` is not empty */ | ||
this.legacyIds = []; | ||
this.id = this.customId; | ||
this.messageString = serializeMessage(this.nodes); | ||
@@ -63,3 +63,3 @@ if (nodes.length) { | ||
export class Icu { | ||
constructor(expression, type, cases, sourceSpan) { | ||
constructor(expression, type, cases, sourceSpan, expressionPlaceholder) { | ||
this.expression = expression; | ||
@@ -69,2 +69,3 @@ this.type = type; | ||
this.sourceSpan = sourceSpan; | ||
this.expressionPlaceholder = expressionPlaceholder; | ||
} | ||
@@ -125,4 +126,3 @@ visit(visitor, context) { | ||
Object.keys(icu.cases).forEach(key => cases[key] = icu.cases[key].visit(this, context)); | ||
const msg = new Icu(icu.expression, icu.type, cases, icu.sourceSpan); | ||
msg.expressionPlaceholder = icu.expressionPlaceholder; | ||
const msg = new Icu(icu.expression, icu.type, cases, icu.sourceSpan, icu.expressionPlaceholder); | ||
return msg; | ||
@@ -129,0 +129,0 @@ } |
@@ -15,3 +15,3 @@ /** | ||
} | ||
export type Node = Attribute | CDATA | Comment | DocType | Element | Text; | ||
export type Node = Attribute | CDATA | Comment | DocType | Element | Text | Block | BlockParameter; | ||
export declare abstract class NodeWithI18n implements BaseNode { | ||
@@ -90,2 +90,20 @@ sourceSpan: ParseSourceSpan; | ||
} | ||
export declare class Block implements BaseNode { | ||
name: string; | ||
parameters: BlockParameter[]; | ||
children: Node[]; | ||
sourceSpan: ParseSourceSpan; | ||
startSourceSpan: ParseSourceSpan; | ||
endSourceSpan: ParseSourceSpan | null; | ||
constructor(name: string, parameters: BlockParameter[], children: Node[], sourceSpan: ParseSourceSpan, startSourceSpan: ParseSourceSpan, endSourceSpan?: ParseSourceSpan | null); | ||
visit(visitor: Visitor, context: any): any; | ||
readonly type = "block"; | ||
} | ||
export declare class BlockParameter implements BaseNode { | ||
expression: string; | ||
sourceSpan: ParseSourceSpan; | ||
constructor(expression: string, sourceSpan: ParseSourceSpan); | ||
visit(visitor: Visitor, context: any): any; | ||
readonly type = "blockParameter"; | ||
} | ||
export interface Visitor { | ||
@@ -101,2 +119,4 @@ visit?(node: Node, context: any): any; | ||
visitExpansionCase(expansionCase: ExpansionCase, context: any): any; | ||
visitBlock(block: Block, context: any): any; | ||
visitBlockParameter(parameter: BlockParameter, context: any): any; | ||
} | ||
@@ -114,4 +134,6 @@ export declare function visitAll(visitor: Visitor, nodes: Node[], context?: any): any[]; | ||
visitExpansionCase(ast: ExpansionCase, context: any): any; | ||
visitBlock(block: Block, context: any): any; | ||
visitBlockParameter(ast: BlockParameter, context: any): any; | ||
private visitChildren; | ||
} | ||
export {}; |
@@ -114,2 +114,26 @@ /** | ||
} | ||
export class Block { | ||
constructor(name, parameters, children, sourceSpan, startSourceSpan, endSourceSpan = null) { | ||
this.name = name; | ||
this.parameters = parameters; | ||
this.children = children; | ||
this.sourceSpan = sourceSpan; | ||
this.startSourceSpan = startSourceSpan; | ||
this.endSourceSpan = endSourceSpan; | ||
this.type = 'block'; | ||
} | ||
visit(visitor, context) { | ||
return visitor.visitBlock(this, context); | ||
} | ||
} | ||
export class BlockParameter { | ||
constructor(expression, sourceSpan) { | ||
this.expression = expression; | ||
this.sourceSpan = sourceSpan; | ||
this.type = 'blockParameter'; | ||
} | ||
visit(visitor, context) { | ||
return visitor.visitBlockParameter(this, context); | ||
} | ||
} | ||
export function visitAll(visitor, nodes, context = null) { | ||
@@ -147,2 +171,9 @@ const result = []; | ||
visitExpansionCase(ast, context) { } | ||
visitBlock(block, context) { | ||
this.visitChildren(context, visit => { | ||
visit(block.parameters); | ||
visit(block.children); | ||
}); | ||
} | ||
visitBlockParameter(ast, context) { } | ||
visitChildren(context, cb) { | ||
@@ -149,0 +180,0 @@ let results = []; |
@@ -0,0 +0,0 @@ /** |
@@ -2139,5 +2139,6 @@ /** | ||
}; | ||
// The &ngsp; pseudo-entity is denoting a space. see: | ||
// https://github.com/dart-lang/angular/blob/0bb611387d29d65b5af7f9d2515ab571fd3fbee4/_tests/test/compiler/preserve_whitespace_test.dart | ||
// The &ngsp; pseudo-entity is denoting a space. | ||
// 0xE500 is a PUA (Private Use Areas) unicode character | ||
// This is inspired by the Angular Dart implementation. | ||
export const NGSP_UNICODE = '\uE500'; | ||
NAMED_ENTITIES['ngsp'] = NGSP_UNICODE; |
@@ -0,0 +0,0 @@ /** |
@@ -0,0 +0,0 @@ /** |
@@ -0,0 +0,0 @@ /** |
@@ -43,3 +43,3 @@ /** | ||
DEFAULT_TAG_DEFINITION = new HtmlTagDefinition({ canSelfClose: true }); | ||
TAG_DEFINITIONS = { | ||
TAG_DEFINITIONS = Object.assign(Object.create(null), { | ||
'base': new HtmlTagDefinition({ isVoid: true }), | ||
@@ -106,5 +106,5 @@ 'meta': new HtmlTagDefinition({ isVoid: true }), | ||
'textarea': new HtmlTagDefinition({ contentType: TagContentType.ESCAPABLE_RAW_TEXT, ignoreFirstLf: true }), | ||
}; | ||
}); | ||
new DomElementSchemaRegistry().allKnownElementNames().forEach(knownTagName => { | ||
if (!TAG_DEFINITIONS.hasOwnProperty(knownTagName) && getNsPrefix(knownTagName) === null) { | ||
if (!TAG_DEFINITIONS[knownTagName] && getNsPrefix(knownTagName) === null) { | ||
TAG_DEFINITIONS[knownTagName] = new HtmlTagDefinition({ canSelfClose: false }); | ||
@@ -116,4 +116,5 @@ } | ||
// HTML tag names are case insensitive, whereas some SVG tags are case sensitive. | ||
return TAG_DEFINITIONS[tagName] ?? // TAG_DEFINITIONS[tagName.toLowerCase()] ?? -- angular-html-parser modification | ||
return TAG_DEFINITIONS[tagName] ?? // TAG_DEFINITIONS[tagName.toLowerCase()] ?? -- | ||
// angular-html-parser modification | ||
DEFAULT_TAG_DEFINITION; | ||
} |
@@ -0,0 +0,0 @@ /** |
@@ -0,0 +0,0 @@ /** |
@@ -84,2 +84,7 @@ /** | ||
preserveLineEndings?: boolean; | ||
/** | ||
* Whether to tokenize @ block syntax. Otherwise considered text, | ||
* or ICU tokens if `tokenizeExpansionForms` is enabled. | ||
*/ | ||
tokenizeBlocks?: boolean; | ||
canSelfClose?: boolean; | ||
@@ -86,0 +91,0 @@ allowHtmComponentClosingTags?: boolean; |
@@ -12,3 +12,3 @@ /** | ||
import { DEFAULT_INTERPOLATION_CONFIG } from "./interpolation_config.js"; | ||
import { TagContentType, mergeNsAndName } from "./tags.js"; | ||
import { mergeNsAndName, TagContentType } from "./tags.js"; | ||
export class TokenError extends ParseError { | ||
@@ -57,3 +57,4 @@ constructor(errorMsg, tokenType, span) { | ||
* @param _file The html source file being tokenized. | ||
* @param _getTagContentType A function that will retrieve a tag content type for a given tag name. | ||
* @param _getTagContentType A function that will retrieve a tag content type for a given tag | ||
* name. | ||
* @param options Configuration of the tokenization. | ||
@@ -81,4 +82,4 @@ */ | ||
this._preserveLineEndings = options.preserveLineEndings || false; | ||
this._escapedString = options.escapedString || false; | ||
this._i18nNormalizeLineEndingsInICUs = options.i18nNormalizeLineEndingsInICUs || false; | ||
this._tokenizeBlocks = options.tokenizeBlocks ?? true; | ||
try { | ||
@@ -134,2 +135,9 @@ this._cursor.init(); | ||
} | ||
else if (this._tokenizeBlocks && this._attemptCharCode(chars.$AT)) { | ||
this._consumeBlockStart(start); | ||
} | ||
else if (this._tokenizeBlocks && !this._inInterpolation && !this._isInExpansionCase() && | ||
!this._isInExpansionForm() && this._attemptCharCode(chars.$RBRACE)) { | ||
this._consumeBlockEnd(start); | ||
} | ||
else if (!(this._tokenizeIcu && this._tokenizeExpansionForm())) { | ||
@@ -145,5 +153,93 @@ // In (possibly interpolated) text the end of the text is given by `isTextEnd()`, while | ||
} | ||
this._beginToken(25 /* TokenType.EOF */); | ||
this._beginToken(30 /* TokenType.EOF */); | ||
this._endToken([]); | ||
} | ||
_getBlockName() { | ||
// This allows us to capture up something like `@else if`, but not `@ if`. | ||
let spacesInNameAllowed = false; | ||
const nameCursor = this._cursor.clone(); | ||
this._attemptCharCodeUntilFn(code => { | ||
if (chars.isWhitespace(code)) { | ||
return !spacesInNameAllowed; | ||
} | ||
if (isBlockNameChar(code)) { | ||
spacesInNameAllowed = true; | ||
return false; | ||
} | ||
return true; | ||
}); | ||
return this._cursor.getChars(nameCursor).trim(); | ||
} | ||
_consumeBlockStart(start) { | ||
this._beginToken(25 /* TokenType.BLOCK_OPEN_START */, start); | ||
const startToken = this._endToken([this._getBlockName()]); | ||
if (this._cursor.peek() === chars.$LPAREN) { | ||
// Advance past the opening paren. | ||
this._cursor.advance(); | ||
// Capture the parameters. | ||
this._consumeBlockParameters(); | ||
// Allow spaces before the closing paren. | ||
this._attemptCharCodeUntilFn(isNotWhitespace); | ||
if (this._attemptCharCode(chars.$RPAREN)) { | ||
// Allow spaces after the paren. | ||
this._attemptCharCodeUntilFn(isNotWhitespace); | ||
} | ||
else { | ||
startToken.type = 29 /* TokenType.INCOMPLETE_BLOCK_OPEN */; | ||
return; | ||
} | ||
} | ||
if (this._attemptCharCode(chars.$LBRACE)) { | ||
this._beginToken(26 /* TokenType.BLOCK_OPEN_END */); | ||
this._endToken([]); | ||
} | ||
else { | ||
startToken.type = 29 /* TokenType.INCOMPLETE_BLOCK_OPEN */; | ||
} | ||
} | ||
_consumeBlockEnd(start) { | ||
this._beginToken(27 /* TokenType.BLOCK_CLOSE */, start); | ||
this._endToken([]); | ||
} | ||
_consumeBlockParameters() { | ||
// Trim the whitespace until the first parameter. | ||
this._attemptCharCodeUntilFn(isBlockParameterChar); | ||
while (this._cursor.peek() !== chars.$RPAREN && this._cursor.peek() !== chars.$EOF) { | ||
this._beginToken(28 /* TokenType.BLOCK_PARAMETER */); | ||
const start = this._cursor.clone(); | ||
let inQuote = null; | ||
let openParens = 0; | ||
// Consume the parameter until the next semicolon or brace. | ||
// Note that we skip over semicolons/braces inside of strings. | ||
while ((this._cursor.peek() !== chars.$SEMICOLON && this._cursor.peek() !== chars.$EOF) || | ||
inQuote !== null) { | ||
const char = this._cursor.peek(); | ||
// Skip to the next character if it was escaped. | ||
if (char === chars.$BACKSLASH) { | ||
this._cursor.advance(); | ||
} | ||
else if (char === inQuote) { | ||
inQuote = null; | ||
} | ||
else if (inQuote === null && chars.isQuote(char)) { | ||
inQuote = char; | ||
} | ||
else if (char === chars.$LPAREN && inQuote === null) { | ||
openParens++; | ||
} | ||
else if (char === chars.$RPAREN && inQuote === null) { | ||
if (openParens === 0) { | ||
break; | ||
} | ||
else if (openParens > 0) { | ||
openParens--; | ||
} | ||
} | ||
this._cursor.advance(); | ||
} | ||
this._endToken([this._cursor.getChars(start)]); | ||
// Skip to the next parameter. | ||
this._attemptCharCodeUntilFn(isBlockParameterChar); | ||
} | ||
} | ||
/** | ||
@@ -460,3 +556,4 @@ * @returns whether an ICU token has been created | ||
} | ||
if (this._canSelfClose && this.tokens[this.tokens.length - 1].type === 2 /* TokenType.TAG_OPEN_END_VOID */) { | ||
if (this._canSelfClose && | ||
this.tokens[this.tokens.length - 1].type === 2 /* TokenType.TAG_OPEN_END_VOID */) { | ||
return; | ||
@@ -721,2 +818,6 @@ } | ||
} | ||
if (this._tokenizeBlocks && !this._inInterpolation && !this._isInExpansion() && | ||
(this._isBlockStart() || this._cursor.peek() === chars.$RBRACE)) { | ||
return true; | ||
} | ||
return false; | ||
@@ -742,2 +843,13 @@ } | ||
} | ||
_isBlockStart() { | ||
if (this._tokenizeBlocks && this._cursor.peek() === chars.$AT) { | ||
const tmp = this._cursor.clone(); | ||
// If it is, also verify that the next character is a valid block identifier. | ||
tmp.advance(); | ||
if (isBlockNameChar(tmp.peek())) { | ||
return true; | ||
} | ||
} | ||
return false; | ||
} | ||
_readUntil(char) { | ||
@@ -748,2 +860,5 @@ const start = this._cursor.clone(); | ||
} | ||
_isInExpansion() { | ||
return this._isInExpansionCase() || this._isInExpansionForm(); | ||
} | ||
_isInExpansionCase() { | ||
@@ -773,3 +888,4 @@ return this._expansionCaseStack.length > 0 && | ||
const fullName = mergeNsAndName(prefix, tagName); | ||
if (this._fullNameStack.length === 0 || this._fullNameStack[this._fullNameStack.length - 1] === fullName) { | ||
if (this._fullNameStack.length === 0 || | ||
this._fullNameStack[this._fullNameStack.length - 1] === fullName) { | ||
this._fullNameStack.push(fullName); | ||
@@ -780,3 +896,4 @@ } | ||
const fullName = mergeNsAndName(prefix, tagName); | ||
if (this._fullNameStack.length !== 0 && this._fullNameStack[this._fullNameStack.length - 1] === fullName) { | ||
if (this._fullNameStack.length !== 0 && | ||
this._fullNameStack[this._fullNameStack.length - 1] === fullName) { | ||
this._fullNameStack.pop(); | ||
@@ -813,2 +930,8 @@ } | ||
} | ||
function isBlockNameChar(code) { | ||
return chars.isAsciiLetter(code) || chars.isDigit(code) || code === chars.$_; | ||
} | ||
function isBlockParameterChar(code) { | ||
return code !== chars.$SEMICOLON && isNotWhitespace(code); | ||
} | ||
function mergeTextTokens(srcTokens) { | ||
@@ -815,0 +938,0 @@ const dstTokens = []; |
@@ -0,0 +0,0 @@ /** |
@@ -37,8 +37,8 @@ /** | ||
const getTagContentTypeWithProcessedTagName = isTagNameCaseSensitive ? getTagContentType : lowercasify(getTagContentType); | ||
const _getTagContentType = getTagContentType | ||
? (tagName, prefix, hasParent, attrs) => { | ||
const _getTagContentType = getTagContentType ? | ||
(tagName, prefix, hasParent, attrs) => { | ||
const contentType = getTagContentTypeWithProcessedTagName(tagName, prefix, hasParent, attrs); | ||
return contentType !== undefined ? contentType : getDefaultTagContentType(tagName); | ||
} | ||
: getDefaultTagContentType; | ||
} : | ||
getDefaultTagContentType; | ||
const tokenizeResult = tokenize(source, url, _getTagContentType, options); | ||
@@ -60,3 +60,3 @@ const canSelfClose = (options && options.canSelfClose) || false; | ||
this._index = -1; | ||
this._elementStack = []; | ||
this._containerStack = []; | ||
this.rootNodes = []; | ||
@@ -67,3 +67,3 @@ this.errors = []; | ||
build() { | ||
while (this._peek.type !== 25 /* TokenType.EOF */) { | ||
while (this._peek.type !== 30 /* TokenType.EOF */) { | ||
if (this._peek.type === 0 /* TokenType.TAG_OPEN_START */ || | ||
@@ -93,2 +93,14 @@ this._peek.type === 4 /* TokenType.INCOMPLETE_TAG_OPEN */) { | ||
} | ||
else if (this._peek.type === 25 /* TokenType.BLOCK_OPEN_START */) { | ||
this._closeVoidElement(); | ||
this._consumeBlockOpen(this._advance()); | ||
} | ||
else if (this._peek.type === 27 /* TokenType.BLOCK_CLOSE */) { | ||
this._closeVoidElement(); | ||
this._consumeBlockClose(this._advance()); | ||
} | ||
else if (this._peek.type === 29 /* TokenType.INCOMPLETE_BLOCK_OPEN */) { | ||
this._closeVoidElement(); | ||
this._consumeIncompleteBlock(this._advance()); | ||
} | ||
else if (this._peek.type === 18 /* TokenType.DOC_TYPE_START */) { | ||
@@ -102,2 +114,8 @@ this._consumeDocType(this._advance()); | ||
} | ||
for (const leftoverContainer of this._containerStack) { | ||
// Unlike HTML elements, blocks aren't closed implicitly by the end of the file. | ||
if (leftoverContainer instanceof html.Block) { | ||
this.errors.push(TreeError.create(leftoverContainer.name, leftoverContainer.sourceSpan, `Unclosed block "${leftoverContainer.name}"`)); | ||
} | ||
} | ||
} | ||
@@ -172,3 +190,3 @@ _advance() { | ||
const end = this._advance(); | ||
exp.push({ type: 25 /* TokenType.EOF */, parts: [], sourceSpan: end.sourceSpan }); | ||
exp.push({ type: 30 /* TokenType.EOF */, parts: [], sourceSpan: end.sourceSpan }); | ||
// parse everything in between { and } | ||
@@ -213,3 +231,3 @@ const expansionCaseParser = new _TreeBuilder(exp, this.getTagDefinition, this.canSelfClose, this.allowHtmComponentClosingTags, this.isTagNameCaseSensitive); | ||
} | ||
if (this._peek.type === 25 /* TokenType.EOF */) { | ||
if (this._peek.type === 30 /* TokenType.EOF */) { | ||
this.errors.push(TreeError.create(null, start.sourceSpan, `Invalid ICU message. Missing '}'.`)); | ||
@@ -224,3 +242,3 @@ return null; | ||
if (text.length > 0 && text[0] == '\n') { | ||
const parent = this._getParentElement(); | ||
const parent = this._getClosestParentElement(); | ||
if (parent != null && parent.children.length == 0 && | ||
@@ -238,3 +256,3 @@ this.getTagDefinition(parent.name).ignoreFirstLf) { | ||
if (text.length > 0 && text[0] === '\n') { | ||
const parent = this._getParentElement(); | ||
const parent = this._getContainer(); | ||
if (parent != null && parent.children.length === 0 && | ||
@@ -270,5 +288,5 @@ this.getTagDefinition(parent.name).ignoreFirstLf) { | ||
_closeVoidElement() { | ||
const el = this._getParentElement(); | ||
if (el && this.getTagDefinition(el.name).isVoid) { | ||
this._elementStack.pop(); | ||
const el = this._getContainer(); | ||
if (el instanceof html.Element && this.getTagDefinition(el.name).isVoid) { | ||
this._containerStack.pop(); | ||
} | ||
@@ -282,3 +300,3 @@ } | ||
} | ||
const fullName = this._getElementFullName(prefix, name, this._getParentElement()); | ||
const fullName = this._getElementFullName(prefix, name, this._getClosestParentElement()); | ||
let selfClosing = false; | ||
@@ -291,3 +309,4 @@ // Note: There could have been a tokenizer error | ||
const tagDef = this.getTagDefinition(fullName); | ||
if (!(this.canSelfClose || tagDef.canSelfClose || getNsPrefix(fullName) !== null || tagDef.isVoid)) { | ||
if (!(this.canSelfClose || tagDef.canSelfClose || getNsPrefix(fullName) !== null || | ||
tagDef.isVoid)) { | ||
this.errors.push(TreeError.create(fullName, startTagToken.sourceSpan, `Only void, custom and foreign elements can be self closed "${startTagToken.parts[1]}"`)); | ||
@@ -306,7 +325,9 @@ } | ||
const el = new html.Element(fullName, attrs, [], span, startSpan, undefined, nameSpan); | ||
this._pushElement(el); | ||
const parentEl = this._getContainer(); | ||
this._pushContainer(el, parentEl instanceof html.Element && | ||
this.getTagDefinition(parentEl.name).isClosedByChild(el.name)); | ||
if (selfClosing) { | ||
// Elements that are self-closed have their `endSourceSpan` set to the full span, as the | ||
// element start tag also represents the end tag. | ||
this._popElement(fullName, span); | ||
this._popContainer(fullName, html.Element, span); | ||
} | ||
@@ -316,23 +337,23 @@ else if (startTagToken.type === 4 /* TokenType.INCOMPLETE_TAG_OPEN */) { | ||
// close tag. Let's optimistically parse it as a full element and emit an error. | ||
this._popElement(fullName, null); | ||
this._popContainer(fullName, html.Element, null); | ||
this.errors.push(TreeError.create(fullName, span, `Opening tag "${fullName}" not terminated.`)); | ||
} | ||
} | ||
_pushElement(el) { | ||
const parentEl = this._getParentElement(); | ||
if (parentEl && this.getTagDefinition(parentEl.name).isClosedByChild(el.name)) { | ||
this._elementStack.pop(); | ||
_pushContainer(node, isClosedByChild) { | ||
if (isClosedByChild) { | ||
this._containerStack.pop(); | ||
} | ||
this._addToParent(el); | ||
this._elementStack.push(el); | ||
this._addToParent(node); | ||
this._containerStack.push(node); | ||
} | ||
_consumeEndTag(endTagToken) { | ||
// @ts-expect-error -- in angular-html-parser endTagToken.parts.length can be 0 (HTM component end-tags) | ||
const fullName = this.allowHtmComponentClosingTags && endTagToken.parts.length === 0 | ||
? null | ||
: this._getElementFullName(endTagToken.parts[0], endTagToken.parts[1], this._getParentElement()); | ||
// @ts-expect-error -- in angular-html-parser endTagToken.parts.length can be 0 (HTM component | ||
// end-tags) | ||
const fullName = this.allowHtmComponentClosingTags && endTagToken.parts.length === 0 ? | ||
null : | ||
this._getElementFullName(endTagToken.parts[0], endTagToken.parts[1], this._getClosestParentElement()); | ||
if (fullName && this.getTagDefinition(fullName).isVoid) { | ||
this.errors.push(TreeError.create(fullName, endTagToken.sourceSpan, `Void elements do not have end tags "${endTagToken.parts[1]}"`)); | ||
} | ||
else if (!this._popElement(fullName, endTagToken.sourceSpan)) { | ||
else if (!this._popContainer(fullName, html.Element, endTagToken.sourceSpan)) { | ||
const errMsg = `Unexpected closing tag "${fullName}". It may happen when the tag has already been closed by another tag. For more info see https://www.w3.org/TR/html5/syntax.html#closing-elements-that-have-implied-end-tags`; | ||
@@ -348,17 +369,19 @@ this.errors.push(TreeError.create(fullName, endTagToken.sourceSpan, errMsg)); | ||
*/ | ||
_popElement(fullName, endSourceSpan) { | ||
_popContainer(expectedName, expectedType, endSourceSpan) { | ||
let unexpectedCloseTagDetected = false; | ||
for (let stackIndex = this._elementStack.length - 1; stackIndex >= 0; stackIndex--) { | ||
const el = this._elementStack[stackIndex]; | ||
if (!fullName || | ||
( /* isForeignElement */(getNsPrefix(el.name) ? el.name == fullName : el.name.toLowerCase() == fullName.toLowerCase()))) { | ||
for (let stackIndex = this._containerStack.length - 1; stackIndex >= 0; stackIndex--) { | ||
const node = this._containerStack[stackIndex]; | ||
if (( /* isForeignElement */(getNsPrefix(node.name) ? node.name === expectedName : (expectedName == null || node.name.toLowerCase() === expectedName.toLowerCase()) && | ||
node instanceof expectedType))) { | ||
// Record the parse span with the element that is being closed. Any elements that are | ||
// removed from the element stack at this point are closed implicitly, so they won't get | ||
// an end source span (as there is no explicit closing element). | ||
el.endSourceSpan = endSourceSpan; | ||
el.sourceSpan.end = endSourceSpan !== null ? endSourceSpan.end : el.sourceSpan.end; | ||
this._elementStack.splice(stackIndex, this._elementStack.length - stackIndex); | ||
node.endSourceSpan = endSourceSpan; | ||
node.sourceSpan.end = endSourceSpan !== null ? endSourceSpan.end : node.sourceSpan.end; | ||
this._containerStack.splice(stackIndex, this._containerStack.length - stackIndex); | ||
return !unexpectedCloseTagDetected; | ||
} | ||
if (!this.getTagDefinition(el.name).closedByParent) { | ||
// Blocks and most elements are not self closing. | ||
if (node instanceof html.Block || | ||
node instanceof html.Element && !this.getTagDefinition(node.name).closedByParent) { | ||
// Note that we encountered an unexpected close tag but continue processing the element | ||
@@ -423,12 +446,61 @@ // stack so we can assign an `endSourceSpan` if there is a corresponding start tag for this | ||
} | ||
_getParentElement() { | ||
return this._elementStack.length > 0 ? this._elementStack[this._elementStack.length - 1] : null; | ||
_consumeBlockOpen(token) { | ||
const parameters = []; | ||
while (this._peek.type === 28 /* TokenType.BLOCK_PARAMETER */) { | ||
const paramToken = this._advance(); | ||
parameters.push(new html.BlockParameter(paramToken.parts[0], paramToken.sourceSpan)); | ||
} | ||
if (this._peek.type === 26 /* TokenType.BLOCK_OPEN_END */) { | ||
this._advance(); | ||
} | ||
const end = this._peek.sourceSpan.fullStart; | ||
const span = new ParseSourceSpan(token.sourceSpan.start, end, token.sourceSpan.fullStart); | ||
// Create a separate `startSpan` because `span` will be modified when there is an `end` span. | ||
const startSpan = new ParseSourceSpan(token.sourceSpan.start, end, token.sourceSpan.fullStart); | ||
const block = new html.Block(token.parts[0], parameters, [], span, startSpan); | ||
this._pushContainer(block, false); | ||
} | ||
_consumeBlockClose(token) { | ||
if (!this._popContainer(null, html.Block, token.sourceSpan)) { | ||
this.errors.push(TreeError.create(null, token.sourceSpan, `Unexpected closing block. The block may have been closed earlier. ` + | ||
`If you meant to write the } character, you should use the "}" ` + | ||
`HTML entity instead.`)); | ||
} | ||
} | ||
_consumeIncompleteBlock(token) { | ||
const parameters = []; | ||
while (this._peek.type === 28 /* TokenType.BLOCK_PARAMETER */) { | ||
const paramToken = this._advance(); | ||
parameters.push(new html.BlockParameter(paramToken.parts[0], paramToken.sourceSpan)); | ||
} | ||
const end = this._peek.sourceSpan.fullStart; | ||
const span = new ParseSourceSpan(token.sourceSpan.start, end, token.sourceSpan.fullStart); | ||
// Create a separate `startSpan` because `span` will be modified when there is an `end` span. | ||
const startSpan = new ParseSourceSpan(token.sourceSpan.start, end, token.sourceSpan.fullStart); | ||
const block = new html.Block(token.parts[0], parameters, [], span, startSpan); | ||
this._pushContainer(block, false); | ||
// Incomplete blocks don't have children so we close them immediately and report an error. | ||
this._popContainer(null, html.Block, null); | ||
this.errors.push(TreeError.create(token.parts[0], span, `Incomplete block "${token.parts[0]}". If you meant to write the @ character, ` + | ||
`you should use the "@" HTML entity instead.`)); | ||
} | ||
_getContainer() { | ||
return this._containerStack.length > 0 ? this._containerStack[this._containerStack.length - 1] : | ||
null; | ||
} | ||
_getClosestParentElement() { | ||
for (let i = this._containerStack.length - 1; i > -1; i--) { | ||
if (this._containerStack[i] instanceof html.Element) { | ||
return this._containerStack[i]; | ||
} | ||
} | ||
return null; | ||
} | ||
_addToParent(node) { | ||
const parent = this._getParentElement(); | ||
if (parent != null) { | ||
parent.children.push(node); | ||
const parent = this._getContainer(); | ||
if (parent === null) { | ||
this.rootNodes.push(node); | ||
} | ||
else { | ||
this.rootNodes.push(node); | ||
parent.children.push(node); | ||
} | ||
@@ -435,0 +507,0 @@ } |
@@ -0,0 +0,0 @@ /** |
@@ -0,0 +0,0 @@ /** |
@@ -35,5 +35,10 @@ /** | ||
EXPANSION_FORM_END = 24, | ||
EOF = 25 | ||
BLOCK_OPEN_START = 25, | ||
BLOCK_OPEN_END = 26, | ||
BLOCK_CLOSE = 27, | ||
BLOCK_PARAMETER = 28, | ||
INCOMPLETE_BLOCK_OPEN = 29, | ||
EOF = 30 | ||
} | ||
export type Token = TagOpenStartToken | TagOpenEndToken | TagOpenEndVoidToken | TagCloseToken | IncompleteTagOpenToken | TextToken | InterpolationToken | EncodedEntityToken | CommentStartToken | CommentEndToken | CdataStartToken | CdataEndToken | AttributeNameToken | AttributeQuoteToken | AttributeValueTextToken | AttributeValueInterpolationToken | DocTypeStartToken | DocTypeEndToken | ExpansionFormStartToken | ExpansionCaseValueToken | ExpansionCaseExpressionStartToken | ExpansionCaseExpressionEndToken | ExpansionFormEndToken | EndOfFileToken; | ||
export type Token = TagOpenStartToken | TagOpenEndToken | TagOpenEndVoidToken | TagCloseToken | IncompleteTagOpenToken | TextToken | InterpolationToken | EncodedEntityToken | CommentStartToken | CommentEndToken | CdataStartToken | CdataEndToken | AttributeNameToken | AttributeQuoteToken | AttributeValueTextToken | AttributeValueInterpolationToken | DocTypeStartToken | DocTypeEndToken | ExpansionFormStartToken | ExpansionCaseValueToken | ExpansionCaseExpressionStartToken | ExpansionCaseExpressionEndToken | ExpansionFormEndToken | EndOfFileToken | BlockParameterToken | BlockOpenStartToken | BlockOpenEndToken | BlockCloseToken | IncompleteBlockOpenToken; | ||
export type InterpolatedTextToken = TextToken | InterpolationToken | EncodedEntityToken; | ||
@@ -148,1 +153,21 @@ export type InterpolatedAttributeToken = AttributeValueTextToken | AttributeValueInterpolationToken | EncodedEntityToken; | ||
} | ||
export interface BlockParameterToken extends TokenBase { | ||
type: TokenType.BLOCK_PARAMETER; | ||
parts: [expression: string]; | ||
} | ||
export interface BlockOpenStartToken extends TokenBase { | ||
type: TokenType.BLOCK_OPEN_START; | ||
parts: [name: string]; | ||
} | ||
export interface BlockOpenEndToken extends TokenBase { | ||
type: TokenType.BLOCK_OPEN_END; | ||
parts: []; | ||
} | ||
export interface BlockCloseToken extends TokenBase { | ||
type: TokenType.BLOCK_CLOSE; | ||
parts: []; | ||
} | ||
export interface IncompleteBlockOpenToken extends TokenBase { | ||
type: TokenType.INCOMPLETE_BLOCK_OPEN; | ||
parts: [name: string]; | ||
} |
@@ -0,0 +0,0 @@ /** |
@@ -0,0 +0,0 @@ export declare class ParseLocation { |
@@ -0,0 +0,0 @@ /** |
@@ -0,0 +0,0 @@ /** |
@@ -0,0 +0,0 @@ /** |
@@ -0,0 +0,0 @@ /** |
@@ -0,0 +0,0 @@ /** |
@@ -0,0 +0,0 @@ /** |
@@ -0,0 +0,0 @@ /** |
@@ -0,0 +0,0 @@ /** |
@@ -0,0 +0,0 @@ /** |
@@ -0,0 +0,0 @@ /** |
@@ -99,9 +99,3 @@ /** | ||
} | ||
// Check `global` first, because in Node tests both `global` and `window` may be defined and our | ||
// `_global` variable should point to the NodeJS `global` in that case. Note: Typeof/Instanceof | ||
// checks are considered side-effects in Terser. We explicitly mark this as side-effect free: | ||
// https://github.com/terser/terser/issues/250. | ||
const _global = ( /* @__PURE__ */((() => (typeof global !== 'undefined' && global) || (typeof window !== 'undefined' && window) || | ||
(typeof self !== 'undefined' && typeof WorkerGlobalScope !== 'undefined' && | ||
self instanceof WorkerGlobalScope && self))())); | ||
const _global = globalThis; | ||
export { _global as global }; | ||
@@ -108,0 +102,0 @@ export function newArray(size, value) { |
{ | ||
"name": "angular-html-parser", | ||
"version": "4.0.1", | ||
"version": "5.0.0", | ||
"description": "A HTML parser extracted from Angular with some modifications", | ||
@@ -19,3 +19,3 @@ "main": "./lib/angular-html-parser/src/index.js", | ||
"build": "tsc -p tsconfig.build.json", | ||
"postbuild": "jscodeshift -t postbuild-codemod.ts lib --extensions=js,ts --parser=ts", | ||
"postbuild": "node ./node_modules/.bin/jscodeshift -t postbuild-codemod.ts lib --extensions=js,ts --parser=ts", | ||
"test": "ts-node --project tsconfig.test.json -r tsconfig-paths/register node_modules/jasmine/bin/jasmine.js ../compiler/test/ml_parser/*_spec.ts ./test/*_spec.ts", | ||
@@ -22,0 +22,0 @@ "release": "standard-version" |
@@ -72,2 +72,6 @@ # angular-html-parser | ||
) => void | ng.TagContentType; | ||
/** | ||
* tokenize angular control flow block syntax | ||
*/ | ||
tokenizeAngularBlocks?: boolean, | ||
} | ||
@@ -74,0 +78,0 @@ ``` |
@@ -0,0 +0,0 @@ This project incorporates third party material from the projects listed below. |
Sorry, the diff of this file is not supported yet
New author
Supply chain riskA new npm collaborator published a version of the package for the first time. New collaborators are usually benign additions to a project, but do indicate a change to the security surface area of a package.
Found 1 instance in 1 package
7306
104
273151
101666
44
1