angular-html-parser
Advanced tools
Comparing version
@@ -15,3 +15,3 @@ /** | ||
} | ||
export type Node = Attribute | CDATA | Comment | DocType | Element | Text | Block | BlockParameter | LetDeclaration; | ||
export type Node = Attribute | CDATA | Comment | DocType | Element | Text | Block | BlockParameter | LetDeclaration | Component | Directive; | ||
export declare abstract class NodeWithI18n implements BaseNode { | ||
@@ -69,2 +69,3 @@ sourceSpan: ParseSourceSpan; | ||
attrs: Attribute[]; | ||
readonly directives: Directive[]; | ||
children: Node[]; | ||
@@ -74,3 +75,3 @@ startSourceSpan: ParseSourceSpan; | ||
nameSpan: ParseSourceSpan | null; | ||
constructor(name: string, attrs: Attribute[], children: Node[], sourceSpan: ParseSourceSpan, startSourceSpan: ParseSourceSpan, endSourceSpan?: ParseSourceSpan | null, nameSpan?: ParseSourceSpan | null, i18n?: I18nMeta); | ||
constructor(name: string, attrs: Attribute[], directives: Directive[], children: Node[], sourceSpan: ParseSourceSpan, startSourceSpan: ParseSourceSpan, endSourceSpan?: ParseSourceSpan | null, nameSpan?: ParseSourceSpan | null, i18n?: I18nMeta); | ||
visit(visitor: Visitor, context: any): any; | ||
@@ -104,2 +105,25 @@ readonly type = "element"; | ||
} | ||
export declare class Component extends NodeWithI18n { | ||
readonly componentName: string; | ||
readonly tagName: string | null; | ||
readonly fullName: string; | ||
attrs: Attribute[]; | ||
readonly directives: Directive[]; | ||
readonly children: Node[]; | ||
readonly startSourceSpan: ParseSourceSpan; | ||
endSourceSpan: ParseSourceSpan | null; | ||
constructor(componentName: string, tagName: string | null, fullName: string, attrs: Attribute[], directives: Directive[], children: Node[], sourceSpan: ParseSourceSpan, startSourceSpan: ParseSourceSpan, endSourceSpan?: ParseSourceSpan | null, i18n?: I18nMeta); | ||
visit(visitor: Visitor, context: any): any; | ||
readonly type = "component"; | ||
} | ||
export declare class Directive implements BaseNode { | ||
readonly name: string; | ||
readonly attrs: Attribute[]; | ||
readonly sourceSpan: ParseSourceSpan; | ||
readonly startSourceSpan: ParseSourceSpan; | ||
readonly endSourceSpan: ParseSourceSpan | null; | ||
constructor(name: string, attrs: Attribute[], sourceSpan: ParseSourceSpan, startSourceSpan: ParseSourceSpan, endSourceSpan?: ParseSourceSpan | null); | ||
visit(visitor: Visitor, context: any): any; | ||
readonly type = "directive"; | ||
} | ||
export declare class BlockParameter implements BaseNode { | ||
@@ -139,2 +163,4 @@ expression: string; | ||
visitLetDeclaration(decl: LetDeclaration, context: any): any; | ||
visitComponent(component: Component, context: any): any; | ||
visitDirective(directive: Directive, context: any): any; | ||
} | ||
@@ -155,4 +181,6 @@ export declare function visitAll(visitor: Visitor, nodes: Node[], context?: any): any[]; | ||
visitLetDeclaration(decl: LetDeclaration, context: any): void; | ||
visitComponent(component: Component, context: any): void; | ||
visitDirective(directive: Directive, context: any): void; | ||
private visitChildren; | ||
} | ||
export {}; |
@@ -8,3 +8,2 @@ /** | ||
*/ | ||
// Expansion|ExpansionCase -- not used by angular-html-parser, should be removed on publish | ||
export class NodeWithI18n { | ||
@@ -82,6 +81,7 @@ constructor(sourceSpan, i18n) { | ||
export class Element extends NodeWithI18n { | ||
constructor(name, attrs, children, sourceSpan, startSourceSpan, endSourceSpan = null, nameSpan = null, i18n) { | ||
constructor(name, attrs, directives, children, sourceSpan, startSourceSpan, endSourceSpan = null, nameSpan = null, i18n) { | ||
super(sourceSpan, i18n); | ||
this.name = name; | ||
this.attrs = attrs; | ||
this.directives = directives; | ||
this.children = children; | ||
@@ -132,2 +132,32 @@ this.startSourceSpan = startSourceSpan; | ||
} | ||
export class Component extends NodeWithI18n { | ||
constructor(componentName, tagName, fullName, attrs, directives, children, sourceSpan, startSourceSpan, endSourceSpan = null, i18n) { | ||
super(sourceSpan, i18n); | ||
this.componentName = componentName; | ||
this.tagName = tagName; | ||
this.fullName = fullName; | ||
this.attrs = attrs; | ||
this.directives = directives; | ||
this.children = children; | ||
this.startSourceSpan = startSourceSpan; | ||
this.endSourceSpan = endSourceSpan; | ||
this.type = 'component'; | ||
} | ||
visit(visitor, context) { | ||
return visitor.visitComponent(this, context); | ||
} | ||
} | ||
export class Directive { | ||
constructor(name, attrs, sourceSpan, startSourceSpan, endSourceSpan = null) { | ||
this.name = name; | ||
this.attrs = attrs; | ||
this.sourceSpan = sourceSpan; | ||
this.startSourceSpan = startSourceSpan; | ||
this.endSourceSpan = endSourceSpan; | ||
this.type = 'directive'; | ||
} | ||
visit(visitor, context) { | ||
return visitor.visitDirective(this, context); | ||
} | ||
} | ||
export class BlockParameter { | ||
@@ -178,2 +208,3 @@ constructor(expression, sourceSpan) { | ||
visit(ast.attrs); | ||
visit(ast.directives); | ||
visit(ast.children); | ||
@@ -201,2 +232,13 @@ }); | ||
visitLetDeclaration(decl, context) { } | ||
visitComponent(component, context) { | ||
this.visitChildren(context, (visit) => { | ||
visit(component.attrs); | ||
visit(component.children); | ||
}); | ||
} | ||
visitDirective(directive, context) { | ||
this.visitChildren(context, (visit) => { | ||
visit(directive.attrs); | ||
}); | ||
} | ||
visitChildren(context, cb) { | ||
@@ -203,0 +245,0 @@ let results = []; |
@@ -94,2 +94,4 @@ /** | ||
tokenizeLet?: boolean; | ||
/** Whether the selectorless syntax is enabled. */ | ||
selectorlessEnabled?: boolean; | ||
canSelfClose?: boolean; | ||
@@ -96,0 +98,0 @@ allowHtmComponentClosingTags?: boolean; |
@@ -65,2 +65,3 @@ /** | ||
this._expansionCaseStack = []; | ||
this._openDirectiveCount = 0; | ||
this._inInterpolation = false; | ||
@@ -90,2 +91,3 @@ this._fullNameStack = []; | ||
this._tokenizeLet = options.tokenizeLet ?? true; | ||
this._selectorlessEnabled = options.selectorlessEnabled ?? false; | ||
try { | ||
@@ -169,3 +171,3 @@ this._cursor.init(); | ||
} | ||
this._beginToken(34 /* TokenType.EOF */); | ||
this._beginToken(42 /* TokenType.EOF */); | ||
this._endToken([]); | ||
@@ -519,3 +521,3 @@ } | ||
this._cursor.advance(); | ||
const char = NAMED_ENTITIES[name]; | ||
const char = NAMED_ENTITIES.hasOwnProperty(name) && NAMED_ENTITIES[name]; | ||
if (!char) { | ||
@@ -583,3 +585,3 @@ throw this._createError(_unknownEntityErrorMsg(name), this._cursor.getSpan(start)); | ||
} | ||
_consumePrefixAndName() { | ||
_consumePrefixAndName(endPredicate) { | ||
const nameOrPrefixStart = this._cursor.clone(); | ||
@@ -599,3 +601,3 @@ let prefix = ''; | ||
} | ||
this._requireCharCodeUntilFn(isNameEnd, prefix === '' ? 0 : 1); | ||
this._requireCharCodeUntilFn(endPredicate, prefix === '' ? 0 : 1); | ||
const name = this._cursor.getChars(nameStart); | ||
@@ -607,35 +609,55 @@ return [prefix, name]; | ||
let prefix; | ||
let openTagToken; | ||
let closingTagName; | ||
let openToken; | ||
const attrs = []; | ||
try { | ||
if (!chars.isAsciiLetter(this._cursor.peek())) { | ||
throw this._createError(_unexpectedCharacterErrorMsg(this._cursor.peek()), this._cursor.getSpan(start)); | ||
if (this._selectorlessEnabled && isSelectorlessNameStart(this._cursor.peek())) { | ||
openToken = this._consumeComponentOpenStart(start); | ||
[closingTagName, prefix, tagName] = openToken.parts; | ||
if (prefix) { | ||
closingTagName += `:${prefix}`; | ||
} | ||
if (tagName) { | ||
closingTagName += `:${tagName}`; | ||
} | ||
this._attemptCharCodeUntilFn(isNotWhitespace); | ||
} | ||
openTagToken = this._consumeTagOpenStart(start); | ||
prefix = openTagToken.parts[0]; | ||
tagName = openTagToken.parts[1]; | ||
this._attemptCharCodeUntilFn(isNotWhitespace); | ||
while (this._cursor.peek() !== chars.$SLASH && | ||
this._cursor.peek() !== chars.$GT && | ||
this._cursor.peek() !== chars.$LT && | ||
this._cursor.peek() !== chars.$EOF) { | ||
const [prefix, name] = this._consumeAttributeName(); | ||
else { | ||
if (!chars.isAsciiLetter(this._cursor.peek())) { | ||
throw this._createError(_unexpectedCharacterErrorMsg(this._cursor.peek()), this._cursor.getSpan(start)); | ||
} | ||
openToken = this._consumeTagOpenStart(start); | ||
prefix = openToken.parts[0]; | ||
tagName = closingTagName = openToken.parts[1]; | ||
this._attemptCharCodeUntilFn(isNotWhitespace); | ||
if (this._attemptCharCode(chars.$EQ)) { | ||
this._attemptCharCodeUntilFn(isNotWhitespace); | ||
const value = this._consumeAttributeValue(); | ||
attrs.push({ prefix, name, value }); | ||
} | ||
while (!isAttributeTerminator(this._cursor.peek())) { | ||
if (this._selectorlessEnabled && this._cursor.peek() === chars.$AT) { | ||
const start = this._cursor.clone(); | ||
const nameStart = start.clone(); | ||
nameStart.advance(); | ||
if (isSelectorlessNameStart(nameStart.peek())) { | ||
this._consumeDirective(start, nameStart); | ||
} | ||
} | ||
else { | ||
attrs.push({ prefix, name }); | ||
const attr = this._consumeAttribute(); | ||
attrs.push(attr); | ||
} | ||
this._attemptCharCodeUntilFn(isNotWhitespace); | ||
} | ||
this._consumeTagOpenEnd(); | ||
if (openToken.type === 34 /* TokenType.COMPONENT_OPEN_START */) { | ||
this._consumeComponentOpenEnd(); | ||
} | ||
else { | ||
this._consumeTagOpenEnd(); | ||
} | ||
} | ||
catch (e) { | ||
if (e instanceof _ControlFlowError) { | ||
if (openTagToken) { | ||
if (openToken) { | ||
// We errored before we could close the opening tag, so it is incomplete. | ||
openTagToken.type = 4 /* TokenType.INCOMPLETE_TAG_OPEN */; | ||
openToken.type = | ||
openToken.type === 34 /* TokenType.COMPONENT_OPEN_START */ | ||
? 38 /* TokenType.INCOMPLETE_COMPONENT_OPEN */ | ||
: 4 /* TokenType.INCOMPLETE_TAG_OPEN */; | ||
} | ||
@@ -659,9 +681,9 @@ else { | ||
if (contentTokenType === TagContentType.RAW_TEXT) { | ||
this._consumeRawTextWithTagClose(prefix, tagName, false); | ||
this._consumeRawTextWithTagClose(prefix, openToken, closingTagName, false); | ||
} | ||
else if (contentTokenType === TagContentType.ESCAPABLE_RAW_TEXT) { | ||
this._consumeRawTextWithTagClose(prefix, tagName, true); | ||
this._consumeRawTextWithTagClose(prefix, openToken, closingTagName, true); | ||
} | ||
} | ||
_consumeRawTextWithTagClose(prefix, tagName, consumeEntities) { | ||
_consumeRawTextWithTagClose(prefix, openToken, tagName, consumeEntities) { | ||
this._consumeRawText(consumeEntities, () => { | ||
@@ -673,3 +695,3 @@ if (!this._attemptCharCode(chars.$LT)) | ||
this._attemptCharCodeUntilFn(isNotWhitespace); | ||
if (!this._attemptStrCaseInsensitive(prefix ? `${prefix}:${tagName}` : tagName)) | ||
if (!this._attemptStrCaseInsensitive(tagName)) | ||
return false; | ||
@@ -679,6 +701,8 @@ this._attemptCharCodeUntilFn(isNotWhitespace); | ||
}); | ||
this._beginToken(3 /* TokenType.TAG_CLOSE */); | ||
this._beginToken(openToken.type === 34 /* TokenType.COMPONENT_OPEN_START */ | ||
? 37 /* TokenType.COMPONENT_CLOSE */ | ||
: 3 /* TokenType.TAG_CLOSE */); | ||
this._requireCharCodeUntilFn((code) => code === chars.$GT, 3); | ||
this._cursor.advance(); // Consume the `>` | ||
this._endToken([prefix, tagName]); | ||
this._endToken(openToken.parts); | ||
this._handleFullNameStackForTagClose(prefix, tagName); | ||
@@ -688,5 +712,35 @@ } | ||
this._beginToken(0 /* TokenType.TAG_OPEN_START */, start); | ||
const parts = this._consumePrefixAndName(); | ||
const parts = this._consumePrefixAndName(isNameEnd); | ||
return this._endToken(parts); | ||
} | ||
_consumeComponentOpenStart(start) { | ||
this._beginToken(34 /* TokenType.COMPONENT_OPEN_START */, start); | ||
const parts = this._consumeComponentName(); | ||
return this._endToken(parts); | ||
} | ||
_consumeComponentName() { | ||
const nameStart = this._cursor.clone(); | ||
while (isSelectorlessNameChar(this._cursor.peek())) { | ||
this._cursor.advance(); | ||
} | ||
const name = this._cursor.getChars(nameStart); | ||
let prefix = ''; | ||
let tagName = ''; | ||
if (this._cursor.peek() === chars.$COLON) { | ||
this._cursor.advance(); | ||
[prefix, tagName] = this._consumePrefixAndName(isNameEnd); | ||
} | ||
return [name, prefix, tagName]; | ||
} | ||
_consumeAttribute() { | ||
const [prefix, name] = this._consumeAttributeName(); | ||
let value; | ||
this._attemptCharCodeUntilFn(isNotWhitespace); | ||
if (this._attemptCharCode(chars.$EQ)) { | ||
this._attemptCharCodeUntilFn(isNotWhitespace); | ||
value = this._consumeAttributeValue(); | ||
} | ||
this._attemptCharCodeUntilFn(isNotWhitespace); | ||
return { prefix, name, value }; | ||
} | ||
_consumeAttributeName() { | ||
@@ -698,3 +752,29 @@ const attrNameStart = this._cursor.peek(); | ||
this._beginToken(14 /* TokenType.ATTR_NAME */); | ||
const prefixAndName = this._consumePrefixAndName(); | ||
let nameEndPredicate; | ||
if (this._openDirectiveCount > 0) { | ||
// If we're parsing attributes inside of directive syntax, we have to terminate the name | ||
// on the first non-matching closing paren. For example, if we have `@Dir(someAttr)`, | ||
// `@Dir` and `(` will have already been captured as `DIRECTIVE_NAME` and `DIRECTIVE_OPEN` | ||
// respectively, but the `)` will get captured as a part of the name for `someAttr` | ||
// because normally that would be an event binding. | ||
let openParens = 0; | ||
nameEndPredicate = (code) => { | ||
if (this._openDirectiveCount > 0) { | ||
if (code === chars.$LPAREN) { | ||
openParens++; | ||
} | ||
else if (code === chars.$RPAREN) { | ||
if (openParens === 0) { | ||
return true; | ||
} | ||
openParens--; | ||
} | ||
} | ||
return isNameEnd(code); | ||
}; | ||
} | ||
else { | ||
nameEndPredicate = isNameEnd; | ||
} | ||
const prefixAndName = this._consumePrefixAndName(nameEndPredicate); | ||
this._endToken(prefixAndName); | ||
@@ -733,3 +813,25 @@ return prefixAndName; | ||
} | ||
_consumeComponentOpenEnd() { | ||
const tokenType = this._attemptCharCode(chars.$SLASH) | ||
? 36 /* TokenType.COMPONENT_OPEN_END_VOID */ | ||
: 35 /* TokenType.COMPONENT_OPEN_END */; | ||
this._beginToken(tokenType); | ||
this._requireCharCode(chars.$GT); | ||
this._endToken([]); | ||
} | ||
_consumeTagClose(start) { | ||
if (this._selectorlessEnabled) { | ||
const clone = start.clone(); | ||
while (clone.peek() !== chars.$GT && !isSelectorlessNameStart(clone.peek())) { | ||
clone.advance(); | ||
} | ||
if (isSelectorlessNameStart(clone.peek())) { | ||
this._beginToken(37 /* TokenType.COMPONENT_CLOSE */, start); | ||
const parts = this._consumeComponentName(); | ||
this._attemptCharCodeUntilFn(isNotWhitespace); | ||
this._requireCharCode(chars.$GT); | ||
this._endToken(parts); | ||
return; | ||
} | ||
} | ||
this._beginToken(3 /* TokenType.TAG_CLOSE */, start); | ||
@@ -744,3 +846,3 @@ this._attemptCharCodeUntilFn(isNotWhitespace); | ||
else { | ||
const [prefix, name] = this._consumePrefixAndName(); | ||
const [prefix, name] = this._consumePrefixAndName(isNameEnd); | ||
this._attemptCharCodeUntilFn(isNotWhitespace); | ||
@@ -905,2 +1007,46 @@ this._requireCharCode(chars.$GT); | ||
} | ||
_consumeDirective(start, nameStart) { | ||
this._requireCharCode(chars.$AT); | ||
// Skip over the @ since it's not part of the name. | ||
this._cursor.advance(); | ||
// Capture the rest of the name. | ||
while (isSelectorlessNameChar(this._cursor.peek())) { | ||
this._cursor.advance(); | ||
} | ||
// Capture the opening token. | ||
this._beginToken(39 /* TokenType.DIRECTIVE_NAME */, start); | ||
const name = this._cursor.getChars(nameStart); | ||
this._endToken([name]); | ||
this._attemptCharCodeUntilFn(isNotWhitespace); | ||
// Optionally there might be attributes bound to the specific directive. | ||
// Stop parsing if there's no opening character for them. | ||
if (this._cursor.peek() !== chars.$LPAREN) { | ||
return; | ||
} | ||
this._openDirectiveCount++; | ||
this._beginToken(40 /* TokenType.DIRECTIVE_OPEN */); | ||
this._cursor.advance(); | ||
this._endToken([]); | ||
this._attemptCharCodeUntilFn(isNotWhitespace); | ||
// Capture all the attributes until we hit a closing paren. | ||
while (!isAttributeTerminator(this._cursor.peek()) && this._cursor.peek() !== chars.$RPAREN) { | ||
this._consumeAttribute(); | ||
} | ||
// Trim any trailing whitespace. | ||
this._attemptCharCodeUntilFn(isNotWhitespace); | ||
this._openDirectiveCount--; | ||
if (this._cursor.peek() !== chars.$RPAREN) { | ||
// Stop parsing, instead of throwing, if we've hit the end of the tag. | ||
// This can be handled better later when turning the tokens into AST. | ||
if (this._cursor.peek() === chars.$GT || this._cursor.peek() === chars.$SLASH) { | ||
return; | ||
} | ||
throw this._createError(_unexpectedCharacterErrorMsg(this._cursor.peek()), this._cursor.getSpan(start)); | ||
} | ||
// Capture the closing token. | ||
this._beginToken(41 /* TokenType.DIRECTIVE_CLOSE */); | ||
this._cursor.advance(); | ||
this._endToken([]); | ||
this._attemptCharCodeUntilFn(isNotWhitespace); | ||
} | ||
_getProcessedChars(start, end) { | ||
@@ -1044,2 +1190,11 @@ return this._processCarriageReturns(end.getChars(start)); | ||
} | ||
function isSelectorlessNameStart(code) { | ||
return code === chars.$_ || (code >= chars.$A && code <= chars.$Z); | ||
} | ||
function isSelectorlessNameChar(code) { | ||
return chars.isAsciiLetter(code) || chars.isDigit(code) || code === chars.$_; | ||
} | ||
function isAttributeTerminator(code) { | ||
return code === chars.$SLASH || code === chars.$GT || code === chars.$LT || code === chars.$EOF; | ||
} | ||
function mergeTextTokens(srcTokens) { | ||
@@ -1046,0 +1201,0 @@ const dstTokens = []; |
@@ -14,873 +14,694 @@ /** | ||
export class TreeError extends ParseError { | ||
static create(elementName, span, msg) { | ||
return new TreeError(elementName, span, msg); | ||
} | ||
constructor(elementName, span, msg) { | ||
super(span, msg); | ||
this.elementName = elementName; | ||
} | ||
static create(elementName, span, msg) { | ||
return new TreeError(elementName, span, msg); | ||
} | ||
constructor(elementName, span, msg) { | ||
super(span, msg); | ||
this.elementName = elementName; | ||
} | ||
} | ||
export class ParseTreeResult { | ||
constructor(rootNodes, errors) { | ||
this.rootNodes = rootNodes; | ||
this.errors = errors; | ||
} | ||
constructor(rootNodes, errors) { | ||
this.rootNodes = rootNodes; | ||
this.errors = errors; | ||
} | ||
} | ||
export class Parser { | ||
constructor(getTagDefinition) { | ||
this.getTagDefinition = getTagDefinition; | ||
} | ||
parse( | ||
source, | ||
url, | ||
options, | ||
isTagNameCaseSensitive = false, | ||
getTagContentType, | ||
) { | ||
const lowercasify = | ||
(fn) => | ||
(x, ...args) => | ||
fn(x.toLowerCase(), ...args); | ||
const getTagDefinition = isTagNameCaseSensitive | ||
? this.getTagDefinition | ||
: lowercasify(this.getTagDefinition); | ||
const getDefaultTagContentType = (tagName) => | ||
getTagDefinition(tagName).getContentType(); | ||
const getTagContentTypeWithProcessedTagName = isTagNameCaseSensitive | ||
? getTagContentType | ||
: lowercasify(getTagContentType); | ||
const _getTagContentType = getTagContentType | ||
? (tagName, prefix, hasParent, attrs) => { | ||
const contentType = getTagContentTypeWithProcessedTagName( | ||
tagName, | ||
prefix, | ||
hasParent, | ||
attrs, | ||
); | ||
return contentType !== undefined | ||
? contentType | ||
: getDefaultTagContentType(tagName); | ||
} | ||
: getDefaultTagContentType; | ||
const tokenizeResult = tokenize(source, url, _getTagContentType, options); | ||
const canSelfClose = (options && options.canSelfClose) || false; | ||
const allowHtmComponentClosingTags = | ||
(options && options.allowHtmComponentClosingTags) || false; | ||
const parser = new _TreeBuilder( | ||
tokenizeResult.tokens, | ||
getTagDefinition, | ||
canSelfClose, | ||
allowHtmComponentClosingTags, | ||
isTagNameCaseSensitive, | ||
); | ||
parser.build(); | ||
return new ParseTreeResult( | ||
parser.rootNodes, | ||
tokenizeResult.errors.concat(parser.errors), | ||
); | ||
} | ||
constructor(getTagDefinition) { | ||
this.getTagDefinition = getTagDefinition; | ||
} | ||
parse(source, url, options, isTagNameCaseSensitive = false, getTagContentType) { | ||
const lowercasify = (fn) => (x, ...args) => fn(x.toLowerCase(), ...args); | ||
const getTagDefinition = isTagNameCaseSensitive ? this.getTagDefinition : lowercasify(this.getTagDefinition); | ||
const getDefaultTagContentType = (tagName) => getTagDefinition(tagName).getContentType(); | ||
const getTagContentTypeWithProcessedTagName = isTagNameCaseSensitive ? getTagContentType : lowercasify(getTagContentType); | ||
const _getTagContentType = getTagContentType ? | ||
(tagName, prefix, hasParent, attrs) => { | ||
const contentType = getTagContentTypeWithProcessedTagName(tagName, prefix, hasParent, attrs); | ||
return contentType !== undefined ? contentType : getDefaultTagContentType(tagName); | ||
} : | ||
getDefaultTagContentType; | ||
const tokenizeResult = tokenize(source, url, _getTagContentType, options); | ||
const canSelfClose = (options && options.canSelfClose) || false; | ||
const allowHtmComponentClosingTags = (options && options.allowHtmComponentClosingTags) || false; | ||
const parser = new _TreeBuilder(tokenizeResult.tokens, getTagDefinition, canSelfClose, allowHtmComponentClosingTags, isTagNameCaseSensitive); | ||
parser.build(); | ||
return new ParseTreeResult(parser.rootNodes, tokenizeResult.errors.concat(parser.errors)); | ||
} | ||
} | ||
class _TreeBuilder { | ||
constructor( | ||
tokens, | ||
getTagDefinition, | ||
canSelfClose, | ||
allowHtmComponentClosingTags, | ||
isTagNameCaseSensitive, | ||
) { | ||
this.tokens = tokens; | ||
this.getTagDefinition = getTagDefinition; | ||
this.canSelfClose = canSelfClose; | ||
this.allowHtmComponentClosingTags = allowHtmComponentClosingTags; | ||
this.isTagNameCaseSensitive = isTagNameCaseSensitive; | ||
this._index = -1; | ||
this._containerStack = []; | ||
this.rootNodes = []; | ||
this.errors = []; | ||
this._advance(); | ||
} | ||
build() { | ||
while (this._peek.type !== 34 /* TokenType.EOF */) { | ||
if ( | ||
this._peek.type === 0 /* TokenType.TAG_OPEN_START */ || | ||
this._peek.type === 4 /* TokenType.INCOMPLETE_TAG_OPEN */ | ||
) { | ||
this._consumeStartTag(this._advance()); | ||
} else if (this._peek.type === 3 /* TokenType.TAG_CLOSE */) { | ||
this._closeVoidElement(); | ||
this._consumeEndTag(this._advance()); | ||
} else if (this._peek.type === 12 /* TokenType.CDATA_START */) { | ||
this._closeVoidElement(); | ||
this._consumeCdata(this._advance()); | ||
} else if (this._peek.type === 10 /* TokenType.COMMENT_START */) { | ||
this._closeVoidElement(); | ||
this._consumeComment(this._advance()); | ||
} else if ( | ||
this._peek.type === 5 /* TokenType.TEXT */ || | ||
this._peek.type === 7 /* TokenType.RAW_TEXT */ || | ||
this._peek.type === 6 /* TokenType.ESCAPABLE_RAW_TEXT */ | ||
) { | ||
this._closeVoidElement(); | ||
this._consumeText(this._advance()); | ||
} else if (this._peek.type === 20 /* TokenType.EXPANSION_FORM_START */) { | ||
this._consumeExpansion(this._advance()); | ||
} 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 === 30 /* TokenType.LET_START */) { | ||
this._closeVoidElement(); | ||
this._consumeLet(this._advance()); | ||
} else if (this._peek.type === 18 /* TokenType.DOC_TYPE_START */) { | ||
this._consumeDocType(this._advance()); | ||
} else if (this._peek.type === 33 /* TokenType.INCOMPLETE_LET */) { | ||
this._closeVoidElement(); | ||
this._consumeIncompleteLet(this._advance()); | ||
} else { | ||
// Skip all other tokens... | ||
constructor(tokens, tagDefinitionResolver, canSelfClose, allowHtmComponentClosingTags, isTagNameCaseSensitive) { | ||
this.tokens = tokens; | ||
this.tagDefinitionResolver = tagDefinitionResolver; | ||
this.canSelfClose = canSelfClose; | ||
this.allowHtmComponentClosingTags = allowHtmComponentClosingTags; | ||
this.isTagNameCaseSensitive = isTagNameCaseSensitive; | ||
this._index = -1; | ||
this._containerStack = []; | ||
this.rootNodes = []; | ||
this.errors = []; | ||
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}"`, | ||
), | ||
); | ||
} | ||
build() { | ||
while (this._peek.type !== 42 /* TokenType.EOF */) { | ||
if (this._peek.type === 0 /* TokenType.TAG_OPEN_START */ || | ||
this._peek.type === 4 /* TokenType.INCOMPLETE_TAG_OPEN */) { | ||
this._consumeElementStartTag(this._advance()); | ||
} | ||
else if (this._peek.type === 3 /* TokenType.TAG_CLOSE */) { | ||
this._closeVoidElement(); | ||
this._consumeElementEndTag(this._advance()); | ||
} | ||
else if (this._peek.type === 12 /* TokenType.CDATA_START */) { | ||
this._closeVoidElement(); | ||
this._consumeCdata(this._advance()); | ||
} | ||
else if (this._peek.type === 10 /* TokenType.COMMENT_START */) { | ||
this._closeVoidElement(); | ||
this._consumeComment(this._advance()); | ||
} | ||
else if (this._peek.type === 5 /* TokenType.TEXT */ || | ||
this._peek.type === 7 /* TokenType.RAW_TEXT */ || | ||
this._peek.type === 6 /* TokenType.ESCAPABLE_RAW_TEXT */) { | ||
this._closeVoidElement(); | ||
this._consumeText(this._advance()); | ||
} | ||
else if (this._peek.type === 20 /* TokenType.EXPANSION_FORM_START */) { | ||
this._consumeExpansion(this._advance()); | ||
} | ||
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 === 30 /* TokenType.LET_START */) { | ||
this._closeVoidElement(); | ||
this._consumeLet(this._advance()); | ||
} | ||
else if (this._peek.type === 18 /* TokenType.DOC_TYPE_START */) { | ||
this._consumeDocType(this._advance()); | ||
} | ||
else if (this._peek.type === 33 /* TokenType.INCOMPLETE_LET */) { | ||
this._closeVoidElement(); | ||
this._consumeIncompleteLet(this._advance()); | ||
} | ||
else if (this._peek.type === 34 /* TokenType.COMPONENT_OPEN_START */ || | ||
this._peek.type === 38 /* TokenType.INCOMPLETE_COMPONENT_OPEN */) { | ||
this._consumeComponentStartTag(this._advance()); | ||
} | ||
else if (this._peek.type === 37 /* TokenType.COMPONENT_CLOSE */) { | ||
this._consumeComponentEndTag(this._advance()); | ||
} | ||
else { | ||
// Skip all other tokens... | ||
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}"`)); | ||
} | ||
} | ||
} | ||
} | ||
_advance() { | ||
const prev = this._peek; | ||
if (this._index < this.tokens.length - 1) { | ||
// Note: there is always an EOF token at the end | ||
this._index++; | ||
_advance() { | ||
const prev = this._peek; | ||
if (this._index < this.tokens.length - 1) { | ||
// Note: there is always an EOF token at the end | ||
this._index++; | ||
} | ||
this._peek = this.tokens[this._index]; | ||
return prev; | ||
} | ||
this._peek = this.tokens[this._index]; | ||
return prev; | ||
} | ||
_advanceIf(type) { | ||
if (this._peek.type === type) { | ||
return this._advance(); | ||
_advanceIf(type) { | ||
if (this._peek.type === type) { | ||
return this._advance(); | ||
} | ||
return null; | ||
} | ||
return null; | ||
} | ||
_consumeCdata(startToken) { | ||
const text = this._advance(); | ||
const value = this._getText(text); | ||
const endToken = this._advanceIf(13 /* TokenType.CDATA_END */); | ||
this._addToParent( | ||
new html.CDATA( | ||
value, | ||
new ParseSourceSpan( | ||
startToken.sourceSpan.start, | ||
(endToken || text).sourceSpan.end, | ||
), | ||
[text], | ||
), | ||
); | ||
} | ||
_consumeComment(token) { | ||
const text = this._advanceIf(7 /* TokenType.RAW_TEXT */); | ||
const endToken = this._advanceIf(11 /* TokenType.COMMENT_END */); | ||
const value = text != null ? text.parts[0].trim() : null; | ||
const sourceSpan = | ||
endToken == null | ||
? token.sourceSpan | ||
: new ParseSourceSpan( | ||
token.sourceSpan.start, | ||
endToken.sourceSpan.end, | ||
token.sourceSpan.fullStart, | ||
); | ||
this._addToParent(new html.Comment(value, sourceSpan)); | ||
} | ||
_consumeDocType(startToken) { | ||
const text = this._advanceIf(7 /* TokenType.RAW_TEXT */); | ||
const endToken = this._advanceIf(19 /* TokenType.DOC_TYPE_END */); | ||
const value = text != null ? text.parts[0].trim() : null; | ||
const sourceSpan = new ParseSourceSpan( | ||
startToken.sourceSpan.start, | ||
(endToken || text || startToken).sourceSpan.end, | ||
); | ||
this._addToParent(new html.DocType(value, sourceSpan)); | ||
} | ||
_consumeExpansion(token) { | ||
const switchValue = this._advance(); | ||
const type = this._advance(); | ||
const cases = []; | ||
// read = | ||
while (this._peek.type === 21 /* TokenType.EXPANSION_CASE_VALUE */) { | ||
const expCase = this._parseExpansionCase(); | ||
if (!expCase) return; // error | ||
cases.push(expCase); | ||
_consumeCdata(startToken) { | ||
const text = this._advance(); | ||
const value = this._getText(text); | ||
const endToken = this._advanceIf(13 /* TokenType.CDATA_END */); | ||
this._addToParent(new html.CDATA(value, new ParseSourceSpan(startToken.sourceSpan.start, (endToken || text).sourceSpan.end), [text])); | ||
} | ||
// read the final } | ||
if (this._peek.type !== 24 /* TokenType.EXPANSION_FORM_END */) { | ||
this.errors.push( | ||
TreeError.create( | ||
null, | ||
this._peek.sourceSpan, | ||
`Invalid ICU message. Missing '}'.`, | ||
), | ||
); | ||
return; | ||
_consumeComment(token) { | ||
const text = this._advanceIf(7 /* TokenType.RAW_TEXT */); | ||
const endToken = this._advanceIf(11 /* TokenType.COMMENT_END */); | ||
const value = text != null ? text.parts[0].trim() : null; | ||
const sourceSpan = endToken == null | ||
? token.sourceSpan | ||
: new ParseSourceSpan(token.sourceSpan.start, endToken.sourceSpan.end, token.sourceSpan.fullStart); | ||
this._addToParent(new html.Comment(value, sourceSpan)); | ||
} | ||
const sourceSpan = new ParseSourceSpan( | ||
token.sourceSpan.start, | ||
this._peek.sourceSpan.end, | ||
token.sourceSpan.fullStart, | ||
); | ||
this._addToParent( | ||
new html.Expansion( | ||
switchValue.parts[0], | ||
type.parts[0], | ||
cases, | ||
sourceSpan, | ||
switchValue.sourceSpan, | ||
), | ||
); | ||
this._advance(); | ||
} | ||
_parseExpansionCase() { | ||
const value = this._advance(); | ||
// read { | ||
if (this._peek.type !== 22 /* TokenType.EXPANSION_CASE_EXP_START */) { | ||
this.errors.push( | ||
TreeError.create( | ||
null, | ||
this._peek.sourceSpan, | ||
`Invalid ICU message. Missing '{'.`, | ||
), | ||
); | ||
return null; | ||
_consumeDocType(startToken) { | ||
const text = this._advanceIf(7 /* TokenType.RAW_TEXT */); | ||
const endToken = this._advanceIf(19 /* TokenType.DOC_TYPE_END */); | ||
const value = text != null ? text.parts[0].trim() : null; | ||
const sourceSpan = new ParseSourceSpan(startToken.sourceSpan.start, (endToken || text || startToken).sourceSpan.end); | ||
this._addToParent(new html.DocType(value, sourceSpan)); | ||
} | ||
// read until } | ||
const start = this._advance(); | ||
const exp = this._collectExpansionExpTokens(start); | ||
if (!exp) return null; | ||
const end = this._advance(); | ||
exp.push({ | ||
type: 34 /* TokenType.EOF */, | ||
parts: [], | ||
sourceSpan: end.sourceSpan, | ||
}); | ||
// parse everything in between { and } | ||
const expansionCaseParser = new _TreeBuilder( | ||
exp, | ||
this.getTagDefinition, | ||
this.canSelfClose, | ||
this.allowHtmComponentClosingTags, | ||
this.isTagNameCaseSensitive, | ||
); | ||
expansionCaseParser.build(); | ||
if (expansionCaseParser.errors.length > 0) { | ||
this.errors = this.errors.concat(expansionCaseParser.errors); | ||
return null; | ||
_consumeExpansion(token) { | ||
const switchValue = this._advance(); | ||
const type = this._advance(); | ||
const cases = []; | ||
// read = | ||
while (this._peek.type === 21 /* TokenType.EXPANSION_CASE_VALUE */) { | ||
const expCase = this._parseExpansionCase(); | ||
if (!expCase) | ||
return; // error | ||
cases.push(expCase); | ||
} | ||
// read the final } | ||
if (this._peek.type !== 24 /* TokenType.EXPANSION_FORM_END */) { | ||
this.errors.push(TreeError.create(null, this._peek.sourceSpan, `Invalid ICU message. Missing '}'.`)); | ||
return; | ||
} | ||
const sourceSpan = new ParseSourceSpan(token.sourceSpan.start, this._peek.sourceSpan.end, token.sourceSpan.fullStart); | ||
this._addToParent(new html.Expansion(switchValue.parts[0], type.parts[0], cases, sourceSpan, switchValue.sourceSpan)); | ||
this._advance(); | ||
} | ||
const sourceSpan = new ParseSourceSpan( | ||
value.sourceSpan.start, | ||
end.sourceSpan.end, | ||
value.sourceSpan.fullStart, | ||
); | ||
const expSourceSpan = new ParseSourceSpan( | ||
start.sourceSpan.start, | ||
end.sourceSpan.end, | ||
start.sourceSpan.fullStart, | ||
); | ||
return new html.ExpansionCase( | ||
value.parts[0], | ||
expansionCaseParser.rootNodes, | ||
sourceSpan, | ||
value.sourceSpan, | ||
expSourceSpan, | ||
); | ||
} | ||
_collectExpansionExpTokens(start) { | ||
const exp = []; | ||
const expansionFormStack = [22 /* TokenType.EXPANSION_CASE_EXP_START */]; | ||
while (true) { | ||
if ( | ||
this._peek.type === 20 /* TokenType.EXPANSION_FORM_START */ || | ||
this._peek.type === 22 /* TokenType.EXPANSION_CASE_EXP_START */ | ||
) { | ||
expansionFormStack.push(this._peek.type); | ||
} | ||
if (this._peek.type === 23 /* TokenType.EXPANSION_CASE_EXP_END */) { | ||
if ( | ||
lastOnStack( | ||
expansionFormStack, | ||
22 /* TokenType.EXPANSION_CASE_EXP_START */, | ||
) | ||
) { | ||
expansionFormStack.pop(); | ||
if (expansionFormStack.length === 0) return exp; | ||
} else { | ||
this.errors.push( | ||
TreeError.create( | ||
null, | ||
start.sourceSpan, | ||
`Invalid ICU message. Missing '}'.`, | ||
), | ||
); | ||
return null; | ||
_parseExpansionCase() { | ||
const value = this._advance(); | ||
// read { | ||
if (this._peek.type !== 22 /* TokenType.EXPANSION_CASE_EXP_START */) { | ||
this.errors.push(TreeError.create(null, this._peek.sourceSpan, `Invalid ICU message. Missing '{'.`)); | ||
return null; | ||
} | ||
} | ||
if (this._peek.type === 24 /* TokenType.EXPANSION_FORM_END */) { | ||
if ( | ||
lastOnStack( | ||
expansionFormStack, | ||
20 /* TokenType.EXPANSION_FORM_START */, | ||
) | ||
) { | ||
expansionFormStack.pop(); | ||
} else { | ||
this.errors.push( | ||
TreeError.create( | ||
null, | ||
start.sourceSpan, | ||
`Invalid ICU message. Missing '}'.`, | ||
), | ||
); | ||
return null; | ||
// read until } | ||
const start = this._advance(); | ||
const exp = this._collectExpansionExpTokens(start); | ||
if (!exp) | ||
return null; | ||
const end = this._advance(); | ||
exp.push({ type: 42 /* TokenType.EOF */, parts: [], sourceSpan: end.sourceSpan }); | ||
// parse everything in between { and } | ||
const expansionCaseParser = new _TreeBuilder(exp, this.tagDefinitionResolver, this.canSelfClose, this.allowHtmComponentClosingTags, this.isTagNameCaseSensitive); | ||
expansionCaseParser.build(); | ||
if (expansionCaseParser.errors.length > 0) { | ||
this.errors = this.errors.concat(expansionCaseParser.errors); | ||
return null; | ||
} | ||
} | ||
if (this._peek.type === 34 /* TokenType.EOF */) { | ||
this.errors.push( | ||
TreeError.create( | ||
null, | ||
start.sourceSpan, | ||
`Invalid ICU message. Missing '}'.`, | ||
), | ||
); | ||
return null; | ||
} | ||
exp.push(this._advance()); | ||
const sourceSpan = new ParseSourceSpan(value.sourceSpan.start, end.sourceSpan.end, value.sourceSpan.fullStart); | ||
const expSourceSpan = new ParseSourceSpan(start.sourceSpan.start, end.sourceSpan.end, start.sourceSpan.fullStart); | ||
return new html.ExpansionCase(value.parts[0], expansionCaseParser.rootNodes, sourceSpan, value.sourceSpan, expSourceSpan); | ||
} | ||
} | ||
_getText(token) { | ||
let text = token.parts[0]; | ||
if (text.length > 0 && text[0] == "\n") { | ||
const parent = this._getClosestParentElement(); | ||
if ( | ||
parent != null && | ||
parent.children.length == 0 && | ||
this.getTagDefinition(parent.name).ignoreFirstLf | ||
) { | ||
text = text.substring(1); | ||
} | ||
_collectExpansionExpTokens(start) { | ||
const exp = []; | ||
const expansionFormStack = [22 /* TokenType.EXPANSION_CASE_EXP_START */]; | ||
while (true) { | ||
if (this._peek.type === 20 /* TokenType.EXPANSION_FORM_START */ || | ||
this._peek.type === 22 /* TokenType.EXPANSION_CASE_EXP_START */) { | ||
expansionFormStack.push(this._peek.type); | ||
} | ||
if (this._peek.type === 23 /* TokenType.EXPANSION_CASE_EXP_END */) { | ||
if (lastOnStack(expansionFormStack, 22 /* TokenType.EXPANSION_CASE_EXP_START */)) { | ||
expansionFormStack.pop(); | ||
if (expansionFormStack.length === 0) | ||
return exp; | ||
} | ||
else { | ||
this.errors.push(TreeError.create(null, start.sourceSpan, `Invalid ICU message. Missing '}'.`)); | ||
return null; | ||
} | ||
} | ||
if (this._peek.type === 24 /* TokenType.EXPANSION_FORM_END */) { | ||
if (lastOnStack(expansionFormStack, 20 /* TokenType.EXPANSION_FORM_START */)) { | ||
expansionFormStack.pop(); | ||
} | ||
else { | ||
this.errors.push(TreeError.create(null, start.sourceSpan, `Invalid ICU message. Missing '}'.`)); | ||
return null; | ||
} | ||
} | ||
if (this._peek.type === 42 /* TokenType.EOF */) { | ||
this.errors.push(TreeError.create(null, start.sourceSpan, `Invalid ICU message. Missing '}'.`)); | ||
return null; | ||
} | ||
exp.push(this._advance()); | ||
} | ||
} | ||
return text; | ||
} | ||
_consumeText(token) { | ||
const tokens = [token]; | ||
const startSpan = token.sourceSpan; | ||
let text = token.parts[0]; | ||
if (text.length > 0 && text[0] === "\n") { | ||
const parent = this._getContainer(); | ||
if ( | ||
parent != null && | ||
parent.children.length === 0 && | ||
this.getTagDefinition(parent.name).ignoreFirstLf | ||
) { | ||
text = text.substring(1); | ||
tokens[0] = { | ||
type: token.type, | ||
sourceSpan: token.sourceSpan, | ||
parts: [text], | ||
}; | ||
} | ||
_getText(token) { | ||
let text = token.parts[0]; | ||
if (text.length > 0 && text[0] == '\n') { | ||
const parent = this._getClosestElementLikeParent(); | ||
if (parent != null && parent.children.length == 0 && | ||
this._getTagDefinition(parent)?.ignoreFirstLf) { | ||
text = text.substring(1); | ||
} | ||
} | ||
return text; | ||
} | ||
while ( | ||
this._peek.type === 8 /* TokenType.INTERPOLATION */ || | ||
this._peek.type === 5 /* TokenType.TEXT */ || | ||
this._peek.type === 9 /* TokenType.ENCODED_ENTITY */ | ||
) { | ||
token = this._advance(); | ||
tokens.push(token); | ||
if (token.type === 8 /* TokenType.INTERPOLATION */) { | ||
// For backward compatibility we decode HTML entities that appear in interpolation | ||
// expressions. This is arguably a bug, but it could be a considerable breaking change to | ||
// fix it. It should be addressed in a larger project to refactor the entire parser/lexer | ||
// chain after View Engine has been removed. | ||
text += token.parts.join("").replace(/&([^;]+);/g, decodeEntity); | ||
} else if (token.type === 9 /* TokenType.ENCODED_ENTITY */) { | ||
text += token.parts[0]; | ||
} else { | ||
text += token.parts.join(""); | ||
} | ||
_consumeText(token) { | ||
const tokens = [token]; | ||
const startSpan = token.sourceSpan; | ||
let text = token.parts[0]; | ||
if (text.length > 0 && text[0] === '\n') { | ||
const parent = this._getContainer(); | ||
if (parent != null && | ||
parent.children.length === 0 && | ||
this._getTagDefinition(parent)?.ignoreFirstLf) { | ||
text = text.substring(1); | ||
tokens[0] = { type: token.type, sourceSpan: token.sourceSpan, parts: [text] }; | ||
} | ||
} | ||
while (this._peek.type === 8 /* TokenType.INTERPOLATION */ || | ||
this._peek.type === 5 /* TokenType.TEXT */ || | ||
this._peek.type === 9 /* TokenType.ENCODED_ENTITY */) { | ||
token = this._advance(); | ||
tokens.push(token); | ||
if (token.type === 8 /* TokenType.INTERPOLATION */) { | ||
// For backward compatibility we decode HTML entities that appear in interpolation | ||
// expressions. This is arguably a bug, but it could be a considerable breaking change to | ||
// fix it. It should be addressed in a larger project to refactor the entire parser/lexer | ||
// chain after View Engine has been removed. | ||
text += token.parts.join('').replace(/&([^;]+);/g, decodeEntity); | ||
} | ||
else if (token.type === 9 /* TokenType.ENCODED_ENTITY */) { | ||
text += token.parts[0]; | ||
} | ||
else { | ||
text += token.parts.join(''); | ||
} | ||
} | ||
if (text.length > 0) { | ||
const endSpan = token.sourceSpan; | ||
this._addToParent(new html.Text(text, new ParseSourceSpan(startSpan.start, endSpan.end, startSpan.fullStart, startSpan.details), tokens)); | ||
} | ||
} | ||
if (text.length > 0) { | ||
const endSpan = token.sourceSpan; | ||
this._addToParent( | ||
new html.Text( | ||
text, | ||
new ParseSourceSpan( | ||
startSpan.start, | ||
endSpan.end, | ||
startSpan.fullStart, | ||
startSpan.details, | ||
), | ||
tokens, | ||
), | ||
); | ||
_closeVoidElement() { | ||
const el = this._getContainer(); | ||
if (el !== null && this._getTagDefinition(el)?.isVoid) { | ||
this._containerStack.pop(); | ||
} | ||
} | ||
} | ||
_closeVoidElement() { | ||
const el = this._getContainer(); | ||
if (el instanceof html.Element && this.getTagDefinition(el.name).isVoid) { | ||
this._containerStack.pop(); | ||
_consumeElementStartTag(startTagToken) { | ||
const attrs = []; | ||
const directives = []; | ||
this._consumeAttributesAndDirectives(attrs, directives); | ||
const fullName = this._getElementFullName(startTagToken, this._getClosestElementLikeParent()); | ||
let selfClosing = false; | ||
// Note: There could have been a tokenizer error | ||
// so that we don't get a token for the end tag... | ||
if (this._peek.type === 2 /* TokenType.TAG_OPEN_END_VOID */) { | ||
this._advance(); | ||
selfClosing = true; | ||
const tagDef = this._getTagDefinition(fullName); | ||
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]}"`)); | ||
} | ||
} | ||
else if (this._peek.type === 1 /* TokenType.TAG_OPEN_END */) { | ||
this._advance(); | ||
selfClosing = false; | ||
} | ||
const end = this._peek.sourceSpan.fullStart; | ||
const span = new ParseSourceSpan(startTagToken.sourceSpan.start, end, startTagToken.sourceSpan.fullStart); | ||
// Create a separate `startSpan` because `span` will be modified when there is an `end` span. | ||
const startSpan = new ParseSourceSpan(startTagToken.sourceSpan.start, end, startTagToken.sourceSpan.fullStart); | ||
const nameSpan = new ParseSourceSpan(startTagToken.sourceSpan.start.moveBy(1), startTagToken.sourceSpan.end); | ||
const el = new html.Element(fullName, attrs, directives, [], span, startSpan, undefined, nameSpan); | ||
const parent = this._getContainer(); | ||
const isClosedByChild = parent !== null && !!this._getTagDefinition(parent)?.isClosedByChild(el.name); | ||
this._pushContainer(el, isClosedByChild); | ||
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._popContainer(fullName, html.Element, span); | ||
} | ||
else if (startTagToken.type === 4 /* TokenType.INCOMPLETE_TAG_OPEN */) { | ||
// We already know the opening tag is not complete, so it is unlikely it has a corresponding | ||
// close tag. Let's optimistically parse it as a full element and emit an error. | ||
this._popContainer(fullName, html.Element, null); | ||
this.errors.push(TreeError.create(fullName, span, `Opening tag "${fullName}" not terminated.`)); | ||
} | ||
} | ||
} | ||
_consumeStartTag(startTagToken) { | ||
const [prefix, name] = startTagToken.parts; | ||
const attrs = []; | ||
while (this._peek.type === 14 /* TokenType.ATTR_NAME */) { | ||
attrs.push(this._consumeAttr(this._advance())); | ||
_consumeComponentStartTag(startToken) { | ||
const componentName = startToken.parts[0]; | ||
const attrs = []; | ||
const directives = []; | ||
this._consumeAttributesAndDirectives(attrs, directives); | ||
const closestElement = this._getClosestElementLikeParent(); | ||
const tagName = this._getComponentTagName(startToken, closestElement); | ||
const fullName = this._getComponentFullName(startToken, closestElement); | ||
const selfClosing = this._peek.type === 36 /* TokenType.COMPONENT_OPEN_END_VOID */; | ||
this._advance(); | ||
const end = this._peek.sourceSpan.fullStart; | ||
const span = new ParseSourceSpan(startToken.sourceSpan.start, end, startToken.sourceSpan.fullStart); | ||
const startSpan = new ParseSourceSpan(startToken.sourceSpan.start, end, startToken.sourceSpan.fullStart); | ||
const node = new html.Component(componentName, tagName, fullName, attrs, directives, [], span, startSpan, undefined); | ||
const parent = this._getContainer(); | ||
const isClosedByChild = parent !== null && | ||
node.tagName !== null && | ||
!!this._getTagDefinition(parent)?.isClosedByChild(node.tagName); | ||
this._pushContainer(node, isClosedByChild); | ||
if (selfClosing) { | ||
this._popContainer(fullName, html.Component, span); | ||
} | ||
else if (startToken.type === 38 /* TokenType.INCOMPLETE_COMPONENT_OPEN */) { | ||
this._popContainer(fullName, html.Component, null); | ||
this.errors.push(TreeError.create(fullName, span, `Opening tag "${fullName}" not terminated.`)); | ||
} | ||
} | ||
const fullName = this._getElementFullName( | ||
prefix, | ||
name, | ||
this._getClosestParentElement(), | ||
); | ||
console.log({ prefix, name, fullName }); | ||
let selfClosing = false; | ||
// Note: There could have been a tokenizer error | ||
// so that we don't get a token for the end tag... | ||
if (this._peek.type === 2 /* TokenType.TAG_OPEN_END_VOID */) { | ||
this._advance(); | ||
selfClosing = true; | ||
const tagDef = this.getTagDefinition(fullName); | ||
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]}"`, | ||
), | ||
); | ||
} | ||
} else if (this._peek.type === 1 /* TokenType.TAG_OPEN_END */) { | ||
this._advance(); | ||
selfClosing = false; | ||
_consumeAttributesAndDirectives(attributesResult, directivesResult) { | ||
while (this._peek.type === 14 /* TokenType.ATTR_NAME */ || | ||
this._peek.type === 39 /* TokenType.DIRECTIVE_NAME */) { | ||
if (this._peek.type === 39 /* TokenType.DIRECTIVE_NAME */) { | ||
directivesResult.push(this._consumeDirective(this._peek)); | ||
} | ||
else { | ||
attributesResult.push(this._consumeAttr(this._advance())); | ||
} | ||
} | ||
} | ||
const end = this._peek.sourceSpan.fullStart; | ||
const span = new ParseSourceSpan( | ||
startTagToken.sourceSpan.start, | ||
end, | ||
startTagToken.sourceSpan.fullStart, | ||
); | ||
// Create a separate `startSpan` because `span` will be modified when there is an `end` span. | ||
const startSpan = new ParseSourceSpan( | ||
startTagToken.sourceSpan.start, | ||
end, | ||
startTagToken.sourceSpan.fullStart, | ||
); | ||
const nameSpan = new ParseSourceSpan( | ||
startTagToken.sourceSpan.start.moveBy(1), | ||
startTagToken.sourceSpan.end, | ||
); | ||
const el = new html.Element( | ||
fullName, | ||
attrs, | ||
[], | ||
span, | ||
startSpan, | ||
undefined, | ||
nameSpan, | ||
); | ||
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._popContainer(fullName, html.Element, span); | ||
} else if (startTagToken.type === 4 /* TokenType.INCOMPLETE_TAG_OPEN */) { | ||
// We already know the opening tag is not complete, so it is unlikely it has a corresponding | ||
// close tag. Let's optimistically parse it as a full element and emit an error. | ||
this._popContainer(fullName, html.Element, null); | ||
this.errors.push( | ||
TreeError.create( | ||
fullName, | ||
span, | ||
`Opening tag "${fullName}" not terminated.`, | ||
), | ||
); | ||
_consumeComponentEndTag(endToken) { | ||
const fullName = this._getComponentFullName(endToken, this._getClosestElementLikeParent()); | ||
if (!this._popContainer(fullName, html.Component, endToken.sourceSpan)) { | ||
const container = this._containerStack[this._containerStack.length - 1]; | ||
let suffix; | ||
if (container instanceof html.Component && container.componentName === endToken.parts[0]) { | ||
suffix = `, did you mean "${container.fullName}"?`; | ||
} | ||
else { | ||
suffix = '. It may happen when the tag has already been closed by another tag.'; | ||
} | ||
const errMsg = `Unexpected closing tag "${fullName}"${suffix}`; | ||
this.errors.push(TreeError.create(fullName, endToken.sourceSpan, errMsg)); | ||
} | ||
} | ||
} | ||
_pushContainer(node, isClosedByChild) { | ||
if (isClosedByChild) { | ||
this._containerStack.pop(); | ||
_getTagDefinition(nodeOrName) { | ||
if (typeof nodeOrName === 'string') { | ||
return this.tagDefinitionResolver(nodeOrName); | ||
} | ||
else if (nodeOrName instanceof html.Element) { | ||
return this.tagDefinitionResolver(nodeOrName.name); | ||
} | ||
else if (nodeOrName instanceof html.Component && nodeOrName.tagName !== null) { | ||
return this.tagDefinitionResolver(nodeOrName.tagName); | ||
} | ||
else { | ||
return null; | ||
} | ||
} | ||
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._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._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`; | ||
this.errors.push( | ||
TreeError.create(fullName, endTagToken.sourceSpan, errMsg), | ||
); | ||
_pushContainer(node, isClosedByChild) { | ||
if (isClosedByChild) { | ||
this._containerStack.pop(); | ||
} | ||
this._addToParent(node); | ||
this._containerStack.push(node); | ||
} | ||
} | ||
/** | ||
* Closes the nearest element with the tag name `fullName` in the parse tree. | ||
* `endSourceSpan` is the span of the closing tag, or null if the element does | ||
* not have a closing tag (for example, this happens when an incomplete | ||
* opening tag is recovered). | ||
*/ | ||
_popContainer(expectedName, expectedType, endSourceSpan) { | ||
let unexpectedCloseTagDetected = false; | ||
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). | ||
node.endSourceSpan = endSourceSpan; | ||
node.sourceSpan.end = | ||
endSourceSpan !== null ? endSourceSpan.end : node.sourceSpan.end; | ||
this._containerStack.splice( | ||
stackIndex, | ||
this._containerStack.length - stackIndex, | ||
); | ||
return !unexpectedCloseTagDetected; | ||
} | ||
// 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 | ||
// stack so we can assign an `endSourceSpan` if there is a corresponding start tag for this | ||
// end tag in the stack. | ||
unexpectedCloseTagDetected = true; | ||
} | ||
_consumeElementEndTag(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, this._getClosestElementLikeParent()); | ||
; | ||
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._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`; | ||
this.errors.push(TreeError.create(fullName, endTagToken.sourceSpan, errMsg)); | ||
} | ||
} | ||
return false; | ||
} | ||
_consumeAttr(attrName) { | ||
const fullName = mergeNsAndName(attrName.parts[0], attrName.parts[1]); | ||
let attrEnd = attrName.sourceSpan.end; | ||
let startQuoteToken; | ||
// Consume any quote | ||
if (this._peek.type === 15 /* TokenType.ATTR_QUOTE */) { | ||
startQuoteToken = this._advance(); | ||
/** | ||
* Closes the nearest element with the tag name `fullName` in the parse tree. | ||
* `endSourceSpan` is the span of the closing tag, or null if the element does | ||
* not have a closing tag (for example, this happens when an incomplete | ||
* opening tag is recovered). | ||
*/ | ||
_popContainer(expectedName, expectedType, endSourceSpan) { | ||
let unexpectedCloseTagDetected = false; | ||
for (let stackIndex = this._containerStack.length - 1; stackIndex >= 0; stackIndex--) { | ||
const node = this._containerStack[stackIndex]; | ||
const nodeName = node instanceof html.Component ? node.fullName : node.name; | ||
if ( | ||
/* isForeignElement */ getNsPrefix(nodeName) ? | ||
nodeName === expectedName : | ||
(nodeName === expectedName || expectedName === null) && 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). | ||
node.endSourceSpan = endSourceSpan; | ||
node.sourceSpan.end = endSourceSpan !== null ? endSourceSpan.end : node.sourceSpan.end; | ||
this._containerStack.splice(stackIndex, this._containerStack.length - stackIndex); | ||
return !unexpectedCloseTagDetected; | ||
} | ||
// Blocks and most elements are not self closing. | ||
if (node instanceof html.Block || !this._getTagDefinition(node)?.closedByParent) { | ||
// Note that we encountered an unexpected close tag but continue processing the element | ||
// stack so we can assign an `endSourceSpan` if there is a corresponding start tag for this | ||
// end tag in the stack. | ||
unexpectedCloseTagDetected = true; | ||
} | ||
} | ||
return false; | ||
} | ||
// Consume the attribute value | ||
let value = ""; | ||
const valueTokens = []; | ||
let valueStartSpan = undefined; | ||
let valueEnd = undefined; | ||
// NOTE: We need to use a new variable `nextTokenType` here to hide the actual type of | ||
// `_peek.type` from TS. Otherwise TS will narrow the type of `_peek.type` preventing it from | ||
// being able to consider `ATTR_VALUE_INTERPOLATION` as an option. This is because TS is not | ||
// able to see that `_advance()` will actually mutate `_peek`. | ||
const nextTokenType = this._peek.type; | ||
if (nextTokenType === 16 /* TokenType.ATTR_VALUE_TEXT */) { | ||
valueStartSpan = this._peek.sourceSpan; | ||
valueEnd = this._peek.sourceSpan.end; | ||
while ( | ||
this._peek.type === 16 /* TokenType.ATTR_VALUE_TEXT */ || | ||
this._peek.type === 17 /* TokenType.ATTR_VALUE_INTERPOLATION */ || | ||
this._peek.type === 9 /* TokenType.ENCODED_ENTITY */ | ||
) { | ||
const valueToken = this._advance(); | ||
valueTokens.push(valueToken); | ||
if (valueToken.type === 17 /* TokenType.ATTR_VALUE_INTERPOLATION */) { | ||
// For backward compatibility we decode HTML entities that appear in interpolation | ||
// expressions. This is arguably a bug, but it could be a considerable breaking change to | ||
// fix it. It should be addressed in a larger project to refactor the entire parser/lexer | ||
// chain after View Engine has been removed. | ||
value += valueToken.parts | ||
.join("") | ||
.replace(/&([^;]+);/g, decodeEntity); | ||
} else if (valueToken.type === 9 /* TokenType.ENCODED_ENTITY */) { | ||
value += valueToken.parts[0]; | ||
} else { | ||
value += valueToken.parts.join(""); | ||
_consumeAttr(attrName) { | ||
const fullName = mergeNsAndName(attrName.parts[0], attrName.parts[1]); | ||
let attrEnd = attrName.sourceSpan.end; | ||
let startQuoteToken; | ||
// Consume any quote | ||
if (this._peek.type === 15 /* TokenType.ATTR_QUOTE */) { | ||
startQuoteToken = this._advance(); | ||
} | ||
valueEnd = attrEnd = valueToken.sourceSpan.end; | ||
} | ||
// Consume the attribute value | ||
let value = ''; | ||
const valueTokens = []; | ||
let valueStartSpan = undefined; | ||
let valueEnd = undefined; | ||
// NOTE: We need to use a new variable `nextTokenType` here to hide the actual type of | ||
// `_peek.type` from TS. Otherwise TS will narrow the type of `_peek.type` preventing it from | ||
// being able to consider `ATTR_VALUE_INTERPOLATION` as an option. This is because TS is not | ||
// able to see that `_advance()` will actually mutate `_peek`. | ||
const nextTokenType = this._peek.type; | ||
if (nextTokenType === 16 /* TokenType.ATTR_VALUE_TEXT */) { | ||
valueStartSpan = this._peek.sourceSpan; | ||
valueEnd = this._peek.sourceSpan.end; | ||
while (this._peek.type === 16 /* TokenType.ATTR_VALUE_TEXT */ || | ||
this._peek.type === 17 /* TokenType.ATTR_VALUE_INTERPOLATION */ || | ||
this._peek.type === 9 /* TokenType.ENCODED_ENTITY */) { | ||
const valueToken = this._advance(); | ||
valueTokens.push(valueToken); | ||
if (valueToken.type === 17 /* TokenType.ATTR_VALUE_INTERPOLATION */) { | ||
// For backward compatibility we decode HTML entities that appear in interpolation | ||
// expressions. This is arguably a bug, but it could be a considerable breaking change to | ||
// fix it. It should be addressed in a larger project to refactor the entire parser/lexer | ||
// chain after View Engine has been removed. | ||
value += valueToken.parts.join('').replace(/&([^;]+);/g, decodeEntity); | ||
} | ||
else if (valueToken.type === 9 /* TokenType.ENCODED_ENTITY */) { | ||
value += valueToken.parts[0]; | ||
} | ||
else { | ||
value += valueToken.parts.join(''); | ||
} | ||
valueEnd = attrEnd = valueToken.sourceSpan.end; | ||
} | ||
} | ||
// Consume any quote | ||
if (this._peek.type === 15 /* TokenType.ATTR_QUOTE */) { | ||
const quoteToken = this._advance(); | ||
valueEnd = attrEnd = quoteToken.sourceSpan.end; | ||
} | ||
const valueSpan = valueStartSpan && | ||
valueEnd && | ||
new ParseSourceSpan(startQuoteToken?.sourceSpan.start ?? valueStartSpan.start, valueEnd, startQuoteToken?.sourceSpan.fullStart ?? valueStartSpan.fullStart); | ||
return new html.Attribute(fullName, value, new ParseSourceSpan(attrName.sourceSpan.start, attrEnd, attrName.sourceSpan.fullStart), attrName.sourceSpan, valueSpan, valueTokens.length > 0 ? valueTokens : undefined, undefined); | ||
} | ||
// Consume any quote | ||
if (this._peek.type === 15 /* TokenType.ATTR_QUOTE */) { | ||
const quoteToken = this._advance(); | ||
valueEnd = attrEnd = quoteToken.sourceSpan.end; | ||
_consumeDirective(nameToken) { | ||
const attributes = []; | ||
let startSourceSpanEnd = nameToken.sourceSpan.end; | ||
let endSourceSpan = null; | ||
this._advance(); | ||
if (this._peek.type === 40 /* TokenType.DIRECTIVE_OPEN */) { | ||
// Capture the opening token in the start span. | ||
startSourceSpanEnd = this._peek.sourceSpan.end; | ||
this._advance(); | ||
// Cast here is necessary, because TS doesn't know that `_advance` changed `_peek`. | ||
while (this._peek.type === 14 /* TokenType.ATTR_NAME */) { | ||
attributes.push(this._consumeAttr(this._advance())); | ||
} | ||
if (this._peek.type === 41 /* TokenType.DIRECTIVE_CLOSE */) { | ||
endSourceSpan = this._peek.sourceSpan; | ||
this._advance(); | ||
} | ||
else { | ||
this.errors.push(TreeError.create(null, nameToken.sourceSpan, 'Unterminated directive definition')); | ||
} | ||
} | ||
const startSourceSpan = new ParseSourceSpan(nameToken.sourceSpan.start, startSourceSpanEnd, nameToken.sourceSpan.fullStart); | ||
const sourceSpan = new ParseSourceSpan(startSourceSpan.start, endSourceSpan === null ? nameToken.sourceSpan.end : endSourceSpan.end, startSourceSpan.fullStart); | ||
return new html.Directive(nameToken.parts[0], attributes, sourceSpan, startSourceSpan, endSourceSpan); | ||
} | ||
const valueSpan = | ||
valueStartSpan && | ||
valueEnd && | ||
new ParseSourceSpan( | ||
startQuoteToken?.sourceSpan.start ?? valueStartSpan.start, | ||
valueEnd, | ||
startQuoteToken?.sourceSpan.fullStart ?? valueStartSpan.fullStart, | ||
); | ||
return new html.Attribute( | ||
fullName, | ||
value, | ||
new ParseSourceSpan( | ||
attrName.sourceSpan.start, | ||
attrEnd, | ||
attrName.sourceSpan.fullStart, | ||
), | ||
attrName.sourceSpan, | ||
valueSpan, | ||
valueTokens.length > 0 ? valueTokens : undefined, | ||
undefined, | ||
); | ||
} | ||
_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), | ||
); | ||
_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, token.sourceSpan, startSpan); | ||
this._pushContainer(block, false); | ||
} | ||
if (this._peek.type === 26 /* TokenType.BLOCK_OPEN_END */) { | ||
this._advance(); | ||
_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.`)); | ||
} | ||
} | ||
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, | ||
token.sourceSpan, | ||
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, token.sourceSpan, 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.`)); | ||
} | ||
} | ||
_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), | ||
); | ||
_consumeLet(startToken) { | ||
const name = startToken.parts[0]; | ||
let valueToken; | ||
let endToken; | ||
if (this._peek.type !== 31 /* TokenType.LET_VALUE */) { | ||
this.errors.push(TreeError.create(startToken.parts[0], startToken.sourceSpan, `Invalid @let declaration "${name}". Declaration must have a value.`)); | ||
return; | ||
} | ||
else { | ||
valueToken = this._advance(); | ||
} | ||
// Type cast is necessary here since TS narrowed the type of `peek` above. | ||
if (this._peek.type !== 32 /* TokenType.LET_END */) { | ||
this.errors.push(TreeError.create(startToken.parts[0], startToken.sourceSpan, `Unterminated @let declaration "${name}". Declaration must be terminated with a semicolon.`)); | ||
return; | ||
} | ||
else { | ||
endToken = this._advance(); | ||
} | ||
const end = endToken.sourceSpan.fullStart; | ||
const span = new ParseSourceSpan(startToken.sourceSpan.start, end, startToken.sourceSpan.fullStart); | ||
// The start token usually captures the `@let`. Construct a name span by | ||
// offsetting the start by the length of any text before the name. | ||
const startOffset = startToken.sourceSpan.toString().lastIndexOf(name); | ||
const nameStart = startToken.sourceSpan.start.moveBy(startOffset); | ||
const nameSpan = new ParseSourceSpan(nameStart, startToken.sourceSpan.end); | ||
const node = new html.LetDeclaration(name, valueToken.parts[0], span, nameSpan, valueToken.sourceSpan); | ||
this._addToParent(node); | ||
} | ||
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, | ||
token.sourceSpan, | ||
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.`, | ||
), | ||
); | ||
} | ||
_consumeLet(startToken) { | ||
const name = startToken.parts[0]; | ||
let valueToken; | ||
let endToken; | ||
if (this._peek.type !== 31 /* TokenType.LET_VALUE */) { | ||
this.errors.push( | ||
TreeError.create( | ||
startToken.parts[0], | ||
startToken.sourceSpan, | ||
`Invalid @let declaration "${name}". Declaration must have a value.`, | ||
), | ||
); | ||
return; | ||
} else { | ||
valueToken = this._advance(); | ||
_consumeIncompleteLet(token) { | ||
// Incomplete `@let` declaration may end up with an empty name. | ||
const name = token.parts[0] ?? ''; | ||
const nameString = name ? ` "${name}"` : ''; | ||
// If there's at least a name, we can salvage an AST node that can be used for completions. | ||
if (name.length > 0) { | ||
const startOffset = token.sourceSpan.toString().lastIndexOf(name); | ||
const nameStart = token.sourceSpan.start.moveBy(startOffset); | ||
const nameSpan = new ParseSourceSpan(nameStart, token.sourceSpan.end); | ||
const valueSpan = new ParseSourceSpan(token.sourceSpan.start, token.sourceSpan.start.moveBy(0)); | ||
const node = new html.LetDeclaration(name, '', token.sourceSpan, nameSpan, valueSpan); | ||
this._addToParent(node); | ||
} | ||
this.errors.push(TreeError.create(token.parts[0], token.sourceSpan, `Incomplete @let declaration${nameString}. ` + | ||
`@let declarations must be written as \`@let <name> = <value>;\``)); | ||
} | ||
// Type cast is necessary here since TS narrowed the type of `peek` above. | ||
if (this._peek.type !== 32 /* TokenType.LET_END */) { | ||
this.errors.push( | ||
TreeError.create( | ||
startToken.parts[0], | ||
startToken.sourceSpan, | ||
`Unterminated @let declaration "${name}". Declaration must be terminated with a semicolon.`, | ||
), | ||
); | ||
return; | ||
} else { | ||
endToken = this._advance(); | ||
_getContainer() { | ||
return this._containerStack.length > 0 | ||
? this._containerStack[this._containerStack.length - 1] | ||
: null; | ||
} | ||
const end = endToken.sourceSpan.fullStart; | ||
const span = new ParseSourceSpan( | ||
startToken.sourceSpan.start, | ||
end, | ||
startToken.sourceSpan.fullStart, | ||
); | ||
// The start token usually captures the `@let`. Construct a name span by | ||
// offsetting the start by the length of any text before the name. | ||
const startOffset = startToken.sourceSpan.toString().lastIndexOf(name); | ||
const nameStart = startToken.sourceSpan.start.moveBy(startOffset); | ||
const nameSpan = new ParseSourceSpan(nameStart, startToken.sourceSpan.end); | ||
const node = new html.LetDeclaration( | ||
name, | ||
valueToken.parts[0], | ||
span, | ||
nameSpan, | ||
valueToken.sourceSpan, | ||
); | ||
this._addToParent(node); | ||
} | ||
_consumeIncompleteLet(token) { | ||
// Incomplete `@let` declaration may end up with an empty name. | ||
const name = token.parts[0] ?? ""; | ||
const nameString = name ? ` "${name}"` : ""; | ||
// If there's at least a name, we can salvage an AST node that can be used for completions. | ||
if (name.length > 0) { | ||
const startOffset = token.sourceSpan.toString().lastIndexOf(name); | ||
const nameStart = token.sourceSpan.start.moveBy(startOffset); | ||
const nameSpan = new ParseSourceSpan(nameStart, token.sourceSpan.end); | ||
const valueSpan = new ParseSourceSpan( | ||
token.sourceSpan.start, | ||
token.sourceSpan.start.moveBy(0), | ||
); | ||
const node = new html.LetDeclaration( | ||
name, | ||
"", | ||
token.sourceSpan, | ||
nameSpan, | ||
valueSpan, | ||
); | ||
this._addToParent(node); | ||
_getClosestElementLikeParent() { | ||
for (let i = this._containerStack.length - 1; i > -1; i--) { | ||
const current = this._containerStack[i]; | ||
if (current instanceof html.Element || current instanceof html.Component) { | ||
return current; | ||
} | ||
} | ||
return null; | ||
} | ||
this.errors.push( | ||
TreeError.create( | ||
token.parts[0], | ||
token.sourceSpan, | ||
`Incomplete @let declaration${nameString}. ` + | ||
`@let declarations must be written as \`@let <name> = <value>;\``, | ||
), | ||
); | ||
} | ||
_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]; | ||
} | ||
_addToParent(node) { | ||
const parent = this._getContainer(); | ||
if (parent === null) { | ||
this.rootNodes.push(node); | ||
} | ||
else { | ||
parent.children.push(node); | ||
} | ||
} | ||
return null; | ||
} | ||
_addToParent(node) { | ||
const parent = this._getContainer(); | ||
if (parent === null) { | ||
this.rootNodes.push(node); | ||
} else { | ||
parent.children.push(node); | ||
_getElementFullName(token, parent) { | ||
const prefix = this._getPrefix(token, parent); | ||
return mergeNsAndName(prefix, token.parts[1]); | ||
} | ||
} | ||
_getElementFullName(prefix, localName, parentElement) { | ||
if (prefix === "") { | ||
prefix = this.getTagDefinition(localName).implicitNamespacePrefix || ""; | ||
if (prefix === "" && parentElement != null) { | ||
const parentTagName = splitNsName(parentElement.name)[1]; | ||
const parentTagDefinition = this.getTagDefinition(parentTagName); | ||
if (!parentTagDefinition.preventNamespaceInheritance) { | ||
prefix = getNsPrefix(parentElement.name); | ||
_getComponentFullName(token, parent) { | ||
const componentName = token.parts[0]; | ||
const tagName = this._getComponentTagName(token, parent); | ||
if (tagName === null) { | ||
return componentName; | ||
} | ||
} | ||
return tagName.startsWith(':') ? componentName + tagName : `${componentName}:${tagName}`; | ||
} | ||
return mergeNsAndName(prefix, localName); | ||
} | ||
_getComponentTagName(token, parent) { | ||
const prefix = this._getPrefix(token, parent); | ||
const tagName = token.parts[2]; | ||
if (!prefix && !tagName) { | ||
return null; | ||
} | ||
else if (!prefix && tagName) { | ||
return tagName; | ||
} | ||
else { | ||
// TODO(crisbeto): re-evaluate this fallback. Maybe base it off the class name? | ||
return mergeNsAndName(prefix, tagName || 'ng-component'); | ||
} | ||
} | ||
_getPrefix(token, parent) { | ||
let prefix; | ||
let tagName; | ||
if (token.type === 34 /* TokenType.COMPONENT_OPEN_START */ || | ||
token.type === 38 /* TokenType.INCOMPLETE_COMPONENT_OPEN */ || | ||
token.type === 37 /* TokenType.COMPONENT_CLOSE */) { | ||
prefix = token.parts[1]; | ||
tagName = token.parts[2]; | ||
} | ||
else { | ||
prefix = token.parts[0]; | ||
tagName = token.parts[1]; | ||
} | ||
prefix = prefix || this._getTagDefinition(tagName)?.implicitNamespacePrefix || ''; | ||
if (!prefix && parent) { | ||
const parentName = parent instanceof html.Element ? parent.name : parent.tagName; | ||
if (parentName !== null) { | ||
const parentTagName = splitNsName(parentName)[1]; | ||
const parentTagDefinition = this._getTagDefinition(parentTagName); | ||
if (parentTagDefinition !== null && !parentTagDefinition.preventNamespaceInheritance) { | ||
prefix = getNsPrefix(parentName); | ||
} | ||
} | ||
} | ||
return prefix; | ||
} | ||
} | ||
function lastOnStack(stack, element) { | ||
return stack.length > 0 && stack[stack.length - 1] === element; | ||
return stack.length > 0 && stack[stack.length - 1] === element; | ||
} | ||
@@ -893,12 +714,12 @@ /** | ||
function decodeEntity(match, entity) { | ||
if (NAMED_ENTITIES[entity] !== undefined) { | ||
return NAMED_ENTITIES[entity] || match; | ||
} | ||
if (/^#x[a-f0-9]+$/i.test(entity)) { | ||
return String.fromCodePoint(parseInt(entity.slice(2), 16)); | ||
} | ||
if (/^#\d+$/.test(entity)) { | ||
return String.fromCodePoint(parseInt(entity.slice(1), 10)); | ||
} | ||
return match; | ||
if (NAMED_ENTITIES[entity] !== undefined) { | ||
return NAMED_ENTITIES[entity] || match; | ||
} | ||
if (/^#x[a-f0-9]+$/i.test(entity)) { | ||
return String.fromCodePoint(parseInt(entity.slice(2), 16)); | ||
} | ||
if (/^#\d+$/.test(entity)) { | ||
return String.fromCodePoint(parseInt(entity.slice(1), 10)); | ||
} | ||
return match; | ||
} |
@@ -44,5 +44,13 @@ /** | ||
INCOMPLETE_LET = 33, | ||
EOF = 34 | ||
COMPONENT_OPEN_START = 34, | ||
COMPONENT_OPEN_END = 35, | ||
COMPONENT_OPEN_END_VOID = 36, | ||
COMPONENT_CLOSE = 37, | ||
INCOMPLETE_COMPONENT_OPEN = 38, | ||
DIRECTIVE_NAME = 39, | ||
DIRECTIVE_OPEN = 40, | ||
DIRECTIVE_CLOSE = 41, | ||
EOF = 42 | ||
} | ||
export type Token = TagOpenStartToken | TagOpenEndToken | TagOpenEndVoidToken | TagCloseToken | IncompleteTagOpenToken | TextToken | InterpolationToken | EncodedEntityToken | CommentStartToken | CommentEndToken | CdataStartToken | CdataEndToken | AttributeNameToken | AttributeQuoteToken | AttributeValueTextToken | AttributeValueInterpolationToken | ExpansionFormStartToken | ExpansionCaseValueToken | ExpansionCaseExpressionStartToken | ExpansionCaseExpressionEndToken | ExpansionFormEndToken | EndOfFileToken | BlockParameterToken | BlockOpenStartToken | BlockOpenEndToken | BlockCloseToken | IncompleteBlockOpenToken | LetStartToken | LetValueToken | LetEndToken | IncompleteLetToken | DocTypeStartToken; | ||
export type Token = TagOpenStartToken | TagOpenEndToken | TagOpenEndVoidToken | TagCloseToken | IncompleteTagOpenToken | TextToken | InterpolationToken | EncodedEntityToken | CommentStartToken | CommentEndToken | CdataStartToken | CdataEndToken | AttributeNameToken | AttributeQuoteToken | AttributeValueTextToken | AttributeValueInterpolationToken | DocTypeStartToken | ExpansionFormStartToken | ExpansionCaseValueToken | ExpansionCaseExpressionStartToken | ExpansionCaseExpressionEndToken | ExpansionFormEndToken | EndOfFileToken | BlockParameterToken | BlockOpenStartToken | BlockOpenEndToken | BlockCloseToken | IncompleteBlockOpenToken | LetStartToken | LetValueToken | LetEndToken | IncompleteLetToken | ComponentOpenStartToken | ComponentOpenEndToken | ComponentOpenEndVoidToken | ComponentCloseToken | IncompleteComponentOpenToken | DirectiveNameToken | DirectiveOpenToken | DirectiveCloseToken; | ||
export type InterpolatedTextToken = TextToken | InterpolationToken | EncodedEntityToken; | ||
@@ -187,1 +195,33 @@ export type InterpolatedAttributeToken = AttributeValueTextToken | AttributeValueInterpolationToken | EncodedEntityToken; | ||
} | ||
export interface ComponentOpenStartToken extends TokenBase { | ||
type: TokenType.COMPONENT_OPEN_START; | ||
parts: [componentName: string, prefix: string, tagName: string]; | ||
} | ||
export interface ComponentOpenEndToken extends TokenBase { | ||
type: TokenType.COMPONENT_OPEN_END; | ||
parts: []; | ||
} | ||
export interface ComponentOpenEndVoidToken extends TokenBase { | ||
type: TokenType.COMPONENT_OPEN_END_VOID; | ||
parts: []; | ||
} | ||
export interface ComponentCloseToken extends TokenBase { | ||
type: TokenType.COMPONENT_CLOSE; | ||
parts: [componentName: string, prefix: string, tagName: string]; | ||
} | ||
export interface IncompleteComponentOpenToken extends TokenBase { | ||
type: TokenType.INCOMPLETE_COMPONENT_OPEN; | ||
parts: [componentName: string, prefix: string, tagName: string]; | ||
} | ||
export interface DirectiveNameToken extends TokenBase { | ||
type: TokenType.DIRECTIVE_NAME; | ||
parts: [name: string]; | ||
} | ||
export interface DirectiveOpenToken extends TokenBase { | ||
type: TokenType.DIRECTIVE_OPEN; | ||
parts: []; | ||
} | ||
export interface DirectiveCloseToken extends TokenBase { | ||
type: TokenType.DIRECTIVE_CLOSE; | ||
parts: []; | ||
} |
@@ -76,3 +76,3 @@ /** | ||
'[HTMLElement]^[Element]|accessKey,autocapitalize,!autofocus,contentEditable,dir,!draggable,enterKeyHint,!hidden,!inert,innerText,inputMode,lang,nonce,*abort,*animationend,*animationiteration,*animationstart,*auxclick,*beforexrselect,*blur,*cancel,*canplay,*canplaythrough,*change,*click,*close,*contextmenu,*copy,*cuechange,*cut,*dblclick,*drag,*dragend,*dragenter,*dragleave,*dragover,*dragstart,*drop,*durationchange,*emptied,*ended,*error,*focus,*formdata,*gotpointercapture,*input,*invalid,*keydown,*keypress,*keyup,*load,*loadeddata,*loadedmetadata,*loadstart,*lostpointercapture,*mousedown,*mouseenter,*mouseleave,*mousemove,*mouseout,*mouseover,*mouseup,*mousewheel,*paste,*pause,*play,*playing,*pointercancel,*pointerdown,*pointerenter,*pointerleave,*pointermove,*pointerout,*pointerover,*pointerrawupdate,*pointerup,*progress,*ratechange,*reset,*resize,*scroll,*securitypolicyviolation,*seeked,*seeking,*select,*selectionchange,*selectstart,*slotchange,*stalled,*submit,*suspend,*timeupdate,*toggle,*transitioncancel,*transitionend,*transitionrun,*transitionstart,*volumechange,*waiting,*webkitanimationend,*webkitanimationiteration,*webkitanimationstart,*webkittransitionend,*wheel,outerText,!spellcheck,%style,#tabIndex,title,!translate,virtualKeyboardPolicy', | ||
'abbr,address,article,aside,b,bdi,bdo,cite,content,code,dd,dfn,dt,em,figcaption,figure,footer,header,hgroup,i,kbd,main,mark,nav,noscript,rb,rp,rt,rtc,ruby,s,samp,section,small,strong,sub,sup,u,var,wbr^[HTMLElement]|accessKey,autocapitalize,!autofocus,contentEditable,dir,!draggable,enterKeyHint,!hidden,innerText,inputMode,lang,nonce,*abort,*animationend,*animationiteration,*animationstart,*auxclick,*beforexrselect,*blur,*cancel,*canplay,*canplaythrough,*change,*click,*close,*contextmenu,*copy,*cuechange,*cut,*dblclick,*drag,*dragend,*dragenter,*dragleave,*dragover,*dragstart,*drop,*durationchange,*emptied,*ended,*error,*focus,*formdata,*gotpointercapture,*input,*invalid,*keydown,*keypress,*keyup,*load,*loadeddata,*loadedmetadata,*loadstart,*lostpointercapture,*mousedown,*mouseenter,*mouseleave,*mousemove,*mouseout,*mouseover,*mouseup,*mousewheel,*paste,*pause,*play,*playing,*pointercancel,*pointerdown,*pointerenter,*pointerleave,*pointermove,*pointerout,*pointerover,*pointerrawupdate,*pointerup,*progress,*ratechange,*reset,*resize,*scroll,*securitypolicyviolation,*seeked,*seeking,*select,*selectionchange,*selectstart,*slotchange,*stalled,*submit,*suspend,*timeupdate,*toggle,*transitioncancel,*transitionend,*transitionrun,*transitionstart,*volumechange,*waiting,*webkitanimationend,*webkitanimationiteration,*webkitanimationstart,*webkittransitionend,*wheel,outerText,!spellcheck,%style,#tabIndex,title,!translate,virtualKeyboardPolicy', | ||
'abbr,address,article,aside,b,bdi,bdo,cite,content,code,dd,dfn,dt,em,figcaption,figure,footer,header,hgroup,i,kbd,main,mark,nav,noscript,rb,rp,rt,rtc,ruby,s,samp,search,section,small,strong,sub,sup,u,var,wbr^[HTMLElement]|accessKey,autocapitalize,!autofocus,contentEditable,dir,!draggable,enterKeyHint,!hidden,innerText,inputMode,lang,nonce,*abort,*animationend,*animationiteration,*animationstart,*auxclick,*beforexrselect,*blur,*cancel,*canplay,*canplaythrough,*change,*click,*close,*contextmenu,*copy,*cuechange,*cut,*dblclick,*drag,*dragend,*dragenter,*dragleave,*dragover,*dragstart,*drop,*durationchange,*emptied,*ended,*error,*focus,*formdata,*gotpointercapture,*input,*invalid,*keydown,*keypress,*keyup,*load,*loadeddata,*loadedmetadata,*loadstart,*lostpointercapture,*mousedown,*mouseenter,*mouseleave,*mousemove,*mouseout,*mouseover,*mouseup,*mousewheel,*paste,*pause,*play,*playing,*pointercancel,*pointerdown,*pointerenter,*pointerleave,*pointermove,*pointerout,*pointerover,*pointerrawupdate,*pointerup,*progress,*ratechange,*reset,*resize,*scroll,*securitypolicyviolation,*seeked,*seeking,*select,*selectionchange,*selectstart,*slotchange,*stalled,*submit,*suspend,*timeupdate,*toggle,*transitioncancel,*transitionend,*transitionrun,*transitionstart,*volumechange,*waiting,*webkitanimationend,*webkitanimationiteration,*webkitanimationstart,*webkittransitionend,*wheel,outerText,!spellcheck,%style,#tabIndex,title,!translate,virtualKeyboardPolicy', | ||
'media^[HTMLElement]|!autoplay,!controls,%controlsList,%crossOrigin,#currentTime,!defaultMuted,#defaultPlaybackRate,!disableRemotePlayback,!loop,!muted,*encrypted,*waitingforkey,#playbackRate,preload,!preservesPitch,src,%srcObject,#volume', | ||
@@ -139,2 +139,3 @@ ':svg:^[HTMLElement]|!autofocus,nonce,*abort,*animationend,*animationiteration,*animationstart,*auxclick,*beforexrselect,*blur,*cancel,*canplay,*canplaythrough,*change,*click,*close,*contextmenu,*copy,*cuechange,*cut,*dblclick,*drag,*dragend,*dragenter,*dragleave,*dragover,*dragstart,*drop,*durationchange,*emptied,*ended,*error,*focus,*formdata,*gotpointercapture,*input,*invalid,*keydown,*keypress,*keyup,*load,*loadeddata,*loadedmetadata,*loadstart,*lostpointercapture,*mousedown,*mouseenter,*mouseleave,*mousemove,*mouseout,*mouseover,*mouseup,*mousewheel,*paste,*pause,*play,*playing,*pointercancel,*pointerdown,*pointerenter,*pointerleave,*pointermove,*pointerout,*pointerover,*pointerrawupdate,*pointerup,*progress,*ratechange,*reset,*resize,*scroll,*securitypolicyviolation,*seeked,*seeking,*select,*selectionchange,*selectstart,*slotchange,*stalled,*submit,*suspend,*timeupdate,*toggle,*transitioncancel,*transitionend,*transitionrun,*transitionstart,*volumechange,*waiting,*webkitanimationend,*webkitanimationiteration,*webkitanimationstart,*webkittransitionend,*wheel,%style,#tabIndex', | ||
'select^[HTMLElement]|autocomplete,!disabled,#length,!multiple,name,!required,#selectedIndex,#size,value', | ||
'selectedcontent^[HTMLElement]|', | ||
'slot^[HTMLElement]|name', | ||
@@ -144,2 +145,3 @@ 'source^[HTMLElement]|#height,media,sizes,src,srcset,type,#width', | ||
'style^[HTMLElement]|!disabled,media,type', | ||
'search^[HTMLELement]|', | ||
'caption^[HTMLElement]|align', | ||
@@ -146,0 +148,0 @@ 'th,td^[HTMLElement]|abbr,align,axis,bgColor,ch,chOff,#colSpan,headers,height,!noWrap,#rowSpan,scope,vAlign,width', |
@@ -9,4 +9,4 @@ /** | ||
export declare function dashCaseToCamelCase(input: string): string; | ||
export declare function splitAtColon(input: string, defaultValues: string[]): string[]; | ||
export declare function splitAtPeriod(input: string, defaultValues: string[]): string[]; | ||
export declare function splitAtColon(input: string, defaultValues: (string | null)[]): (string | null)[]; | ||
export declare function splitAtPeriod(input: string, defaultValues: (string | null)[]): (string | null)[]; | ||
export declare function noUndefined<T>(val: T | undefined): T; | ||
@@ -13,0 +13,0 @@ export declare function error(msg: string): never; |
{ | ||
"name": "angular-html-parser", | ||
"version": "9.0.0", | ||
"version": "9.0.1", | ||
"description": "A HTML parser extracted from Angular with some modifications", | ||
@@ -5,0 +5,0 @@ "repository": "https://github.com/prettier/angular-html-parser/blob/HEAD/packages/angular-html-parser", |
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
316907
6.45%8141
1.13%