feat(compiler): basic transform implementation

This commit is contained in:
Evan You 2019-09-17 19:08:47 -04:00
parent a5c1b3283d
commit bbb57c26a2
11 changed files with 1889 additions and 1500 deletions

File diff suppressed because it is too large Load Diff

View File

@ -8,20 +8,23 @@ export const enum Namespaces {
} }
export const enum NodeTypes { export const enum NodeTypes {
ROOT,
ELEMENT,
TEXT, TEXT,
COMMENT, COMMENT,
ELEMENT,
ATTRIBUTE,
EXPRESSION, EXPRESSION,
ATTRIBUTE,
DIRECTIVE, DIRECTIVE,
ROOT IF,
IF_BRANCH,
FOR
} }
export const enum ElementTypes { export const enum ElementTypes {
ELEMENT, ELEMENT,
COMPONENT, COMPONENT,
SLOT, // slot SLOT,
TEMPLATE // template, component TEMPLATE
} }
export interface Node { export interface Node {
@ -29,9 +32,18 @@ export interface Node {
loc: SourceLocation loc: SourceLocation
} }
export type ParentNode = RootNode | ElementNode | IfBranchNode | ForNode
export type ChildNode =
| ElementNode
| ExpressionNode
| TextNode
| CommentNode
| IfNode
| ForNode
export interface RootNode extends Node { export interface RootNode extends Node {
type: NodeTypes.ROOT type: NodeTypes.ROOT
children: Array<ElementNode | ExpressionNode | TextNode | CommentNode> children: ChildNode[]
} }
export interface ElementNode extends Node { export interface ElementNode extends Node {
@ -40,8 +52,9 @@ export interface ElementNode extends Node {
tag: string tag: string
tagType: ElementTypes tagType: ElementTypes
isSelfClosing: boolean isSelfClosing: boolean
props: Array<AttributeNode | DirectiveNode> attrs: AttributeNode[]
children: Array<ElementNode | ExpressionNode | TextNode | CommentNode> directives: DirectiveNode[]
children: ChildNode[]
} }
export interface TextNode extends Node { export interface TextNode extends Node {
@ -75,8 +88,28 @@ export interface ExpressionNode extends Node {
isStatic: boolean isStatic: boolean
} }
export interface IfNode extends Node {
type: NodeTypes.IF
branches: IfBranchNode[]
}
export interface IfBranchNode extends Node {
type: NodeTypes.IF_BRANCH
condition: ExpressionNode | undefined // else
children: ChildNode[]
}
export interface ForNode extends Node {
type: NodeTypes.FOR
source: ExpressionNode
valueAlias: ExpressionNode
keyAlias: ExpressionNode
objectIndexAlias: ExpressionNode
children: ChildNode[]
}
export interface Position { export interface Position {
offset: number // from start of file offset: number // from start of file (in SFCs)
line: number line: number
column: number column: number
} }

View File

@ -1 +1,56 @@
// TODO import { createDirectiveTransform } from '../transform'
import {
NodeTypes,
ElementTypes,
ElementNode,
DirectiveNode,
IfBranchNode
} from '../ast'
import { createCompilerError, ErrorCodes } from '../errors'
export const transformIf = createDirectiveTransform(
/^(if|else|else-if)$/,
(node, dir, context) => {
if (dir.name === 'if') {
context.replaceNode({
type: NodeTypes.IF,
loc: node.loc,
branches: [createIfBranch(node, dir)]
})
} else {
// locate the adjacent v-if
const siblings = context.parent.children
let i = context.childIndex
while (i--) {
const sibling = siblings[i]
if (sibling.type === NodeTypes.COMMENT) {
continue
}
if (sibling.type === NodeTypes.IF) {
// move the node to the if node's branches
context.removeNode()
sibling.branches.push(createIfBranch(node, dir))
} else {
context.onError(
createCompilerError(
dir.name === 'else'
? ErrorCodes.X_ELSE_NO_ADJACENT_IF
: ErrorCodes.X_ELSE_IF_NO_ADJACENT_IF,
node.loc.start
)
)
}
break
}
}
}
)
function createIfBranch(node: ElementNode, dir: DirectiveNode): IfBranchNode {
return {
type: NodeTypes.IF_BRANCH,
loc: node.loc,
condition: dir.name === 'else' ? undefined : dir.exp,
children: node.tagType === ElementTypes.TEMPLATE ? node.children : [node]
}
}

View File

@ -1,89 +0,0 @@
export const enum ParserErrorTypes {
ABRUPT_CLOSING_OF_EMPTY_COMMENT,
ABSENCE_OF_DIGITS_IN_NUMERIC_CHARACTER_REFERENCE,
CDATA_IN_HTML_CONTENT,
CHARACTER_REFERENCE_OUTSIDE_UNICODE_RANGE,
CONTROL_CHARACTER_REFERENCE,
DUPLICATE_ATTRIBUTE,
END_TAG_WITH_ATTRIBUTES,
END_TAG_WITH_TRAILING_SOLIDUS,
EOF_BEFORE_TAG_NAME,
EOF_IN_CDATA,
EOF_IN_COMMENT,
EOF_IN_SCRIPT_HTML_COMMENT_LIKE_TEXT,
EOF_IN_TAG,
INCORRECTLY_CLOSED_COMMENT,
INCORRECTLY_OPENED_COMMENT,
INVALID_FIRST_CHARACTER_OF_TAG_NAME,
MISSING_ATTRIBUTE_VALUE,
MISSING_END_TAG_NAME,
MISSING_SEMICOLON_AFTER_CHARACTER_REFERENCE,
MISSING_WHITESPACE_BETWEEN_ATTRIBUTES,
NESTED_COMMENT,
NONCHARACTER_CHARACTER_REFERENCE,
NULL_CHARACTER_REFERENCE,
SURROGATE_CHARACTER_REFERENCE,
UNEXPECTED_CHARACTER_IN_ATTRIBUTE_NAME,
UNEXPECTED_CHARACTER_IN_UNQUOTED_ATTRIBUTE_VALUE,
UNEXPECTED_EQUALS_SIGN_BEFORE_ATTRIBUTE_NAME,
UNEXPECTED_NULL_CHARACTER,
UNEXPECTED_QUESTION_MARK_INSTEAD_OF_TAG_NAME,
UNEXPECTED_SOLIDUS_IN_TAG,
UNKNOWN_NAMED_CHARACTER_REFERENCE,
X_INVALID_END_TAG,
X_MISSING_END_TAG,
X_MISSING_INTERPOLATION_END,
X_MISSING_DYNAMIC_DIRECTIVE_ARGUMENT_END
}
export const errorMessages: { [code: number]: string } = {
[ParserErrorTypes.ABRUPT_CLOSING_OF_EMPTY_COMMENT]: 'Illegal comment.',
[ParserErrorTypes.ABSENCE_OF_DIGITS_IN_NUMERIC_CHARACTER_REFERENCE]:
'Illegal numeric character reference: invalid character.',
[ParserErrorTypes.CDATA_IN_HTML_CONTENT]:
'CDATA section is allowed only in XML context.',
[ParserErrorTypes.CHARACTER_REFERENCE_OUTSIDE_UNICODE_RANGE]:
'Illegal numeric character reference: too big.',
[ParserErrorTypes.CONTROL_CHARACTER_REFERENCE]:
'Illegal numeric character reference: control character.',
[ParserErrorTypes.DUPLICATE_ATTRIBUTE]: 'Duplicate attribute.',
[ParserErrorTypes.END_TAG_WITH_ATTRIBUTES]: 'End tag cannot have attributes.',
[ParserErrorTypes.END_TAG_WITH_TRAILING_SOLIDUS]: "Illegal '/' in tags.",
[ParserErrorTypes.EOF_BEFORE_TAG_NAME]: 'Unexpected EOF in tag.',
[ParserErrorTypes.EOF_IN_CDATA]: 'Unexpected EOF in CDATA section.',
[ParserErrorTypes.EOF_IN_COMMENT]: 'Unexpected EOF in comment.',
[ParserErrorTypes.EOF_IN_SCRIPT_HTML_COMMENT_LIKE_TEXT]:
'Unexpected EOF in script.',
[ParserErrorTypes.EOF_IN_TAG]: 'Unexpected EOF in tag.',
[ParserErrorTypes.INCORRECTLY_CLOSED_COMMENT]: 'Incorrectly closed comment.',
[ParserErrorTypes.INCORRECTLY_OPENED_COMMENT]: 'Incorrectly opened comment.',
[ParserErrorTypes.INVALID_FIRST_CHARACTER_OF_TAG_NAME]:
"Illegal tag name. Use '&lt;' to print '<'.",
[ParserErrorTypes.MISSING_ATTRIBUTE_VALUE]: 'Attribute value was expected.',
[ParserErrorTypes.MISSING_END_TAG_NAME]: 'End tag name was expected.',
[ParserErrorTypes.MISSING_SEMICOLON_AFTER_CHARACTER_REFERENCE]:
'Semicolon was expected.',
[ParserErrorTypes.MISSING_WHITESPACE_BETWEEN_ATTRIBUTES]:
'Whitespace was expected.',
[ParserErrorTypes.NESTED_COMMENT]: "Unexpected '<!--' in comment.",
[ParserErrorTypes.NONCHARACTER_CHARACTER_REFERENCE]:
'Illegal numeric character reference: non character.',
[ParserErrorTypes.NULL_CHARACTER_REFERENCE]:
'Illegal numeric character reference: null character.',
[ParserErrorTypes.SURROGATE_CHARACTER_REFERENCE]:
'Illegal numeric character reference: non-pair surrogate.',
[ParserErrorTypes.UNEXPECTED_CHARACTER_IN_ATTRIBUTE_NAME]:
'Attribute name cannot contain U+0022 ("), U+0027 (\'), and U+003C (<).',
[ParserErrorTypes.UNEXPECTED_CHARACTER_IN_UNQUOTED_ATTRIBUTE_VALUE]:
'Unquoted attribute value cannot contain U+0022 ("), U+0027 (\'), U+003C (<), U+003D (=), and U+0060 (`).',
[ParserErrorTypes.UNEXPECTED_EQUALS_SIGN_BEFORE_ATTRIBUTE_NAME]:
"Attribute name cannot start with '='.",
[ParserErrorTypes.UNEXPECTED_QUESTION_MARK_INSTEAD_OF_TAG_NAME]:
"'<?' is allowed only in XML context.",
[ParserErrorTypes.UNEXPECTED_SOLIDUS_IN_TAG]: "Illegal '/' in tags.",
[ParserErrorTypes.UNKNOWN_NAMED_CHARACTER_REFERENCE]: 'Unknown entity name.',
[ParserErrorTypes.X_INVALID_END_TAG]: 'Invalid end tag.',
[ParserErrorTypes.X_MISSING_END_TAG]: 'End tag was not found.',
[ParserErrorTypes.X_MISSING_INTERPOLATION_END]:
'Interpolation end sign was not found.'
}

View File

@ -0,0 +1,120 @@
import { Position } from './ast'
export interface CompilerError extends SyntaxError {
code: ErrorCodes
loc: Position
}
export function createCompilerError(
code: ErrorCodes,
loc: Position
): CompilerError {
const error = new SyntaxError(
`${__DEV__ || !__BROWSER__ ? errorMessages[code] : code} (${loc.line}:${
loc.column
})`
) as CompilerError
error.code = code
error.loc = loc
return error
}
export const enum ErrorCodes {
// parse errors
ABRUPT_CLOSING_OF_EMPTY_COMMENT,
ABSENCE_OF_DIGITS_IN_NUMERIC_CHARACTER_REFERENCE,
CDATA_IN_HTML_CONTENT,
CHARACTER_REFERENCE_OUTSIDE_UNICODE_RANGE,
CONTROL_CHARACTER_REFERENCE,
DUPLICATE_ATTRIBUTE,
END_TAG_WITH_ATTRIBUTES,
END_TAG_WITH_TRAILING_SOLIDUS,
EOF_BEFORE_TAG_NAME,
EOF_IN_CDATA,
EOF_IN_COMMENT,
EOF_IN_SCRIPT_HTML_COMMENT_LIKE_TEXT,
EOF_IN_TAG,
INCORRECTLY_CLOSED_COMMENT,
INCORRECTLY_OPENED_COMMENT,
INVALID_FIRST_CHARACTER_OF_TAG_NAME,
MISSING_ATTRIBUTE_VALUE,
MISSING_END_TAG_NAME,
MISSING_SEMICOLON_AFTER_CHARACTER_REFERENCE,
MISSING_WHITESPACE_BETWEEN_ATTRIBUTES,
NESTED_COMMENT,
NONCHARACTER_CHARACTER_REFERENCE,
NULL_CHARACTER_REFERENCE,
SURROGATE_CHARACTER_REFERENCE,
UNEXPECTED_CHARACTER_IN_ATTRIBUTE_NAME,
UNEXPECTED_CHARACTER_IN_UNQUOTED_ATTRIBUTE_VALUE,
UNEXPECTED_EQUALS_SIGN_BEFORE_ATTRIBUTE_NAME,
UNEXPECTED_NULL_CHARACTER,
UNEXPECTED_QUESTION_MARK_INSTEAD_OF_TAG_NAME,
UNEXPECTED_SOLIDUS_IN_TAG,
UNKNOWN_NAMED_CHARACTER_REFERENCE,
X_INVALID_END_TAG,
X_MISSING_END_TAG,
X_MISSING_INTERPOLATION_END,
X_MISSING_DYNAMIC_DIRECTIVE_ARGUMENT_END,
// transform errors
X_ELSE_IF_NO_ADJACENT_IF,
X_ELSE_NO_ADJACENT_IF
}
export const errorMessages: { [code: number]: string } = {
// parse errors
[ErrorCodes.ABRUPT_CLOSING_OF_EMPTY_COMMENT]: 'Illegal comment.',
[ErrorCodes.ABSENCE_OF_DIGITS_IN_NUMERIC_CHARACTER_REFERENCE]:
'Illegal numeric character reference: invalid character.',
[ErrorCodes.CDATA_IN_HTML_CONTENT]:
'CDATA section is allowed only in XML context.',
[ErrorCodes.CHARACTER_REFERENCE_OUTSIDE_UNICODE_RANGE]:
'Illegal numeric character reference: too big.',
[ErrorCodes.CONTROL_CHARACTER_REFERENCE]:
'Illegal numeric character reference: control character.',
[ErrorCodes.DUPLICATE_ATTRIBUTE]: 'Duplicate attribute.',
[ErrorCodes.END_TAG_WITH_ATTRIBUTES]: 'End tag cannot have attributes.',
[ErrorCodes.END_TAG_WITH_TRAILING_SOLIDUS]: "Illegal '/' in tags.",
[ErrorCodes.EOF_BEFORE_TAG_NAME]: 'Unexpected EOF in tag.',
[ErrorCodes.EOF_IN_CDATA]: 'Unexpected EOF in CDATA section.',
[ErrorCodes.EOF_IN_COMMENT]: 'Unexpected EOF in comment.',
[ErrorCodes.EOF_IN_SCRIPT_HTML_COMMENT_LIKE_TEXT]:
'Unexpected EOF in script.',
[ErrorCodes.EOF_IN_TAG]: 'Unexpected EOF in tag.',
[ErrorCodes.INCORRECTLY_CLOSED_COMMENT]: 'Incorrectly closed comment.',
[ErrorCodes.INCORRECTLY_OPENED_COMMENT]: 'Incorrectly opened comment.',
[ErrorCodes.INVALID_FIRST_CHARACTER_OF_TAG_NAME]:
"Illegal tag name. Use '&lt;' to print '<'.",
[ErrorCodes.MISSING_ATTRIBUTE_VALUE]: 'Attribute value was expected.',
[ErrorCodes.MISSING_END_TAG_NAME]: 'End tag name was expected.',
[ErrorCodes.MISSING_SEMICOLON_AFTER_CHARACTER_REFERENCE]:
'Semicolon was expected.',
[ErrorCodes.MISSING_WHITESPACE_BETWEEN_ATTRIBUTES]:
'Whitespace was expected.',
[ErrorCodes.NESTED_COMMENT]: "Unexpected '<!--' in comment.",
[ErrorCodes.NONCHARACTER_CHARACTER_REFERENCE]:
'Illegal numeric character reference: non character.',
[ErrorCodes.NULL_CHARACTER_REFERENCE]:
'Illegal numeric character reference: null character.',
[ErrorCodes.SURROGATE_CHARACTER_REFERENCE]:
'Illegal numeric character reference: non-pair surrogate.',
[ErrorCodes.UNEXPECTED_CHARACTER_IN_ATTRIBUTE_NAME]:
'Attribute name cannot contain U+0022 ("), U+0027 (\'), and U+003C (<).',
[ErrorCodes.UNEXPECTED_CHARACTER_IN_UNQUOTED_ATTRIBUTE_VALUE]:
'Unquoted attribute value cannot contain U+0022 ("), U+0027 (\'), U+003C (<), U+003D (=), and U+0060 (`).',
[ErrorCodes.UNEXPECTED_EQUALS_SIGN_BEFORE_ATTRIBUTE_NAME]:
"Attribute name cannot start with '='.",
[ErrorCodes.UNEXPECTED_QUESTION_MARK_INSTEAD_OF_TAG_NAME]:
"'<?' is allowed only in XML context.",
[ErrorCodes.UNEXPECTED_SOLIDUS_IN_TAG]: "Illegal '/' in tags.",
[ErrorCodes.UNKNOWN_NAMED_CHARACTER_REFERENCE]: 'Unknown entity name.',
[ErrorCodes.X_INVALID_END_TAG]: 'Invalid end tag.',
[ErrorCodes.X_MISSING_END_TAG]: 'End tag was not found.',
[ErrorCodes.X_MISSING_INTERPOLATION_END]:
'Interpolation end sign was not found.',
// transform errors
[ErrorCodes.X_ELSE_IF_NO_ADJACENT_IF]: `v-else-if has no adjacent v-if`,
[ErrorCodes.X_ELSE_NO_ADJACENT_IF]: `v-else has no adjacent v-if`
}

View File

@ -1,3 +1,6 @@
export { parse, ParserOptions, TextModes } from './parser' export { parse, ParserOptions, TextModes } from './parse'
export { ParserErrorTypes } from './errorTypes' export { transform, Transform, TransformContext } from './transform'
export { ErrorCodes } from './errors'
export * from './ast' export * from './ast'
export { transformIf } from './directives/vIf'

View File

@ -1,4 +1,4 @@
import { ParserErrorTypes, errorMessages } from './errorTypes' import { ErrorCodes, CompilerError, createCompilerError } from './errors'
import { import {
Namespace, Namespace,
Namespaces, Namespaces,
@ -12,7 +12,8 @@ import {
Position, Position,
RootNode, RootNode,
SourceLocation, SourceLocation,
TextNode TextNode,
ChildNode
} from './ast' } from './ast'
export interface ParserOptions { export interface ParserOptions {
@ -26,7 +27,7 @@ export interface ParserOptions {
// The full set is https://html.spec.whatwg.org/multipage/named-characters.html#named-character-references // The full set is https://html.spec.whatwg.org/multipage/named-characters.html#named-character-references
namedCharacterReferences?: { [name: string]: string | undefined } namedCharacterReferences?: { [name: string]: string | undefined }
onError?: (type: ParserErrorTypes, loc: Position) => void onError?: (error: CompilerError) => void
} }
export const defaultParserOptions: Required<ParserOptions> = { export const defaultParserOptions: Required<ParserOptions> = {
@ -42,14 +43,7 @@ export const defaultParserOptions: Required<ParserOptions> = {
'apos;': "'", 'apos;': "'",
'quot;': '"' 'quot;': '"'
}, },
onError(code: ParserErrorTypes, loc: Position): void { onError(error: CompilerError): void {
const error: any = new SyntaxError(
`${__DEV__ || !__BROWSER__ ? errorMessages[code] : code} (${loc.line}:${
loc.column
})`
)
error.code = code
error.loc = loc
throw error throw error
} }
} }
@ -106,10 +100,10 @@ function parseChildren(
context: ParserContext, context: ParserContext,
mode: TextModes, mode: TextModes,
ancestors: ElementNode[] ancestors: ElementNode[]
): RootNode['children'] { ): ChildNode[] {
const parent = last(ancestors) const parent = last(ancestors)
const ns = parent ? parent.ns : Namespaces.HTML const ns = parent ? parent.ns : Namespaces.HTML
const nodes: RootNode['children'] = [] const nodes: ChildNode[] = []
while (!isEnd(context, mode, ancestors)) { while (!isEnd(context, mode, ancestors)) {
__DEV__ && assert(context.source.length > 0) __DEV__ && assert(context.source.length > 0)
@ -122,7 +116,7 @@ function parseChildren(
} else if (mode === TextModes.DATA && s[0] === '<') { } else if (mode === TextModes.DATA && s[0] === '<') {
// https://html.spec.whatwg.org/multipage/parsing.html#tag-open-state // https://html.spec.whatwg.org/multipage/parsing.html#tag-open-state
if (s.length === 1) { if (s.length === 1) {
emitError(context, ParserErrorTypes.EOF_BEFORE_TAG_NAME, 1) emitError(context, ErrorCodes.EOF_BEFORE_TAG_NAME, 1)
} else if (s[1] === '!') { } else if (s[1] === '!') {
// https://html.spec.whatwg.org/multipage/parsing.html#markup-declaration-open-state // https://html.spec.whatwg.org/multipage/parsing.html#markup-declaration-open-state
if (startsWith(s, '<!--')) { if (startsWith(s, '<!--')) {
@ -134,31 +128,27 @@ function parseChildren(
if (ns !== Namespaces.HTML) { if (ns !== Namespaces.HTML) {
node = parseCDATA(context, ancestors) node = parseCDATA(context, ancestors)
} else { } else {
emitError(context, ParserErrorTypes.CDATA_IN_HTML_CONTENT) emitError(context, ErrorCodes.CDATA_IN_HTML_CONTENT)
node = parseBogusComment(context) node = parseBogusComment(context)
} }
} else { } else {
emitError(context, ParserErrorTypes.INCORRECTLY_OPENED_COMMENT) emitError(context, ErrorCodes.INCORRECTLY_OPENED_COMMENT)
node = parseBogusComment(context) node = parseBogusComment(context)
} }
} else if (s[1] === '/') { } else if (s[1] === '/') {
// https://html.spec.whatwg.org/multipage/parsing.html#end-tag-open-state // https://html.spec.whatwg.org/multipage/parsing.html#end-tag-open-state
if (s.length === 2) { if (s.length === 2) {
emitError(context, ParserErrorTypes.EOF_BEFORE_TAG_NAME, 2) emitError(context, ErrorCodes.EOF_BEFORE_TAG_NAME, 2)
} else if (s[2] === '>') { } else if (s[2] === '>') {
emitError(context, ParserErrorTypes.MISSING_END_TAG_NAME, 2) emitError(context, ErrorCodes.MISSING_END_TAG_NAME, 2)
advanceBy(context, 3) advanceBy(context, 3)
continue continue
} else if (/[a-z]/i.test(s[2])) { } else if (/[a-z]/i.test(s[2])) {
emitError(context, ParserErrorTypes.X_INVALID_END_TAG) emitError(context, ErrorCodes.X_INVALID_END_TAG)
parseTag(context, TagType.End, parent) parseTag(context, TagType.End, parent)
continue continue
} else { } else {
emitError( emitError(context, ErrorCodes.INVALID_FIRST_CHARACTER_OF_TAG_NAME, 2)
context,
ParserErrorTypes.INVALID_FIRST_CHARACTER_OF_TAG_NAME,
2
)
node = parseBogusComment(context) node = parseBogusComment(context)
} }
} else if (/[a-z]/i.test(s[1])) { } else if (/[a-z]/i.test(s[1])) {
@ -166,16 +156,12 @@ function parseChildren(
} else if (s[1] === '?') { } else if (s[1] === '?') {
emitError( emitError(
context, context,
ParserErrorTypes.UNEXPECTED_QUESTION_MARK_INSTEAD_OF_TAG_NAME, ErrorCodes.UNEXPECTED_QUESTION_MARK_INSTEAD_OF_TAG_NAME,
1 1
) )
node = parseBogusComment(context) node = parseBogusComment(context)
} else { } else {
emitError( emitError(context, ErrorCodes.INVALID_FIRST_CHARACTER_OF_TAG_NAME, 1)
context,
ParserErrorTypes.INVALID_FIRST_CHARACTER_OF_TAG_NAME,
1
)
} }
} }
if (!node) { if (!node) {
@ -183,7 +169,9 @@ function parseChildren(
} }
if (Array.isArray(node)) { if (Array.isArray(node)) {
node.forEach(pushNode.bind(null, context, nodes)) for (let i = 0; i < node.length; i++) {
pushNode(context, nodes, node[i])
}
} else { } else {
pushNode(context, nodes, node) pushNode(context, nodes, node)
} }
@ -194,8 +182,8 @@ function parseChildren(
function pushNode( function pushNode(
context: ParserContext, context: ParserContext,
nodes: RootNode['children'], nodes: ChildNode[],
node: RootNode['children'][0] node: ChildNode
): void { ): void {
if (context.ignoreSpaces && node.type === NodeTypes.TEXT && node.isEmpty) { if (context.ignoreSpaces && node.type === NodeTypes.TEXT && node.isEmpty) {
return return
@ -222,7 +210,7 @@ function pushNode(
function parseCDATA( function parseCDATA(
context: ParserContext, context: ParserContext,
ancestors: ElementNode[] ancestors: ElementNode[]
): RootNode['children'] { ): ChildNode[] {
__DEV__ && __DEV__ &&
assert(last(ancestors) == null || last(ancestors)!.ns !== Namespaces.HTML) assert(last(ancestors) == null || last(ancestors)!.ns !== Namespaces.HTML)
__DEV__ && assert(startsWith(context.source, '<![CDATA[')) __DEV__ && assert(startsWith(context.source, '<![CDATA['))
@ -230,7 +218,7 @@ function parseCDATA(
advanceBy(context, 9) advanceBy(context, 9)
const nodes = parseChildren(context, TextModes.CDATA, ancestors) const nodes = parseChildren(context, TextModes.CDATA, ancestors)
if (context.source.length === 0) { if (context.source.length === 0) {
emitError(context, ParserErrorTypes.EOF_IN_CDATA) emitError(context, ErrorCodes.EOF_IN_CDATA)
} else { } else {
__DEV__ && assert(startsWith(context.source, ']]>')) __DEV__ && assert(startsWith(context.source, ']]>'))
advanceBy(context, 3) advanceBy(context, 3)
@ -250,13 +238,13 @@ function parseComment(context: ParserContext): CommentNode {
if (!match) { if (!match) {
content = context.source.slice(4) content = context.source.slice(4)
advanceBy(context, context.source.length) advanceBy(context, context.source.length)
emitError(context, ParserErrorTypes.EOF_IN_COMMENT) emitError(context, ErrorCodes.EOF_IN_COMMENT)
} else { } else {
if (match.index <= 3) { if (match.index <= 3) {
emitError(context, ParserErrorTypes.ABRUPT_CLOSING_OF_EMPTY_COMMENT) emitError(context, ErrorCodes.ABRUPT_CLOSING_OF_EMPTY_COMMENT)
} }
if (match[1]) { if (match[1]) {
emitError(context, ParserErrorTypes.INCORRECTLY_CLOSED_COMMENT) emitError(context, ErrorCodes.INCORRECTLY_CLOSED_COMMENT)
} }
content = context.source.slice(4, match.index) content = context.source.slice(4, match.index)
@ -267,7 +255,7 @@ function parseComment(context: ParserContext): CommentNode {
while ((nestedIndex = s.indexOf('<!--', prevIndex)) !== -1) { while ((nestedIndex = s.indexOf('<!--', prevIndex)) !== -1) {
advanceBy(context, nestedIndex - prevIndex + 1) advanceBy(context, nestedIndex - prevIndex + 1)
if (nestedIndex + 4 < s.length) { if (nestedIndex + 4 < s.length) {
emitError(context, ParserErrorTypes.NESTED_COMMENT) emitError(context, ErrorCodes.NESTED_COMMENT)
} }
prevIndex = nestedIndex + 1 prevIndex = nestedIndex + 1
} }
@ -333,14 +321,11 @@ function parseElement(
if (startsWithEndTagOpen(context.source, element.tag)) { if (startsWithEndTagOpen(context.source, element.tag)) {
parseTag(context, TagType.End, parent) parseTag(context, TagType.End, parent)
} else { } else {
emitError(context, ParserErrorTypes.X_MISSING_END_TAG) emitError(context, ErrorCodes.X_MISSING_END_TAG)
if (context.source.length === 0 && element.tag.toLowerCase() === 'script') { if (context.source.length === 0 && element.tag.toLowerCase() === 'script') {
const first = children[0] const first = children[0]
if (first && startsWith(first.loc.source, '<!--')) { if (first && startsWith(first.loc.source, '<!--')) {
emitError( emitError(context, ErrorCodes.EOF_IN_SCRIPT_HTML_COMMENT_LIKE_TEXT)
context,
ParserErrorTypes.EOF_IN_SCRIPT_HTML_COMMENT_LIKE_TEXT
)
} }
} }
} }
@ -372,7 +357,8 @@ function parseTag(
const start = getCursor(context) const start = getCursor(context)
const match = /^<\/?([a-z][^\t\r\n\f />]*)/i.exec(context.source)! const match = /^<\/?([a-z][^\t\r\n\f />]*)/i.exec(context.source)!
const tag = match[1] const tag = match[1]
const props = [] const attrs = []
const directives = []
const ns = context.getNamespace(tag, parent) const ns = context.getNamespace(tag, parent)
advanceBy(context, match[0].length) advanceBy(context, match[0].length)
@ -386,22 +372,26 @@ function parseTag(
!startsWith(context.source, '/>') !startsWith(context.source, '/>')
) { ) {
if (startsWith(context.source, '/')) { if (startsWith(context.source, '/')) {
emitError(context, ParserErrorTypes.UNEXPECTED_SOLIDUS_IN_TAG) emitError(context, ErrorCodes.UNEXPECTED_SOLIDUS_IN_TAG)
advanceBy(context, 1) advanceBy(context, 1)
advanceSpaces(context) advanceSpaces(context)
continue continue
} }
if (type === TagType.End) { if (type === TagType.End) {
emitError(context, ParserErrorTypes.END_TAG_WITH_ATTRIBUTES) emitError(context, ErrorCodes.END_TAG_WITH_ATTRIBUTES)
} }
const attr = parseAttribute(context, attributeNames) const attr = parseAttribute(context, attributeNames)
if (type === TagType.Start) { if (type === TagType.Start) {
props.push(attr) if (attr.type === NodeTypes.DIRECTIVE) {
directives.push(attr)
} else {
attrs.push(attr)
}
} }
if (/^[^\t\r\n\f />]/.test(context.source)) { if (/^[^\t\r\n\f />]/.test(context.source)) {
emitError(context, ParserErrorTypes.MISSING_WHITESPACE_BETWEEN_ATTRIBUTES) emitError(context, ErrorCodes.MISSING_WHITESPACE_BETWEEN_ATTRIBUTES)
} }
advanceSpaces(context) advanceSpaces(context)
} }
@ -409,11 +399,11 @@ function parseTag(
// Tag close. // Tag close.
let isSelfClosing = false let isSelfClosing = false
if (context.source.length === 0) { if (context.source.length === 0) {
emitError(context, ParserErrorTypes.EOF_IN_TAG) emitError(context, ErrorCodes.EOF_IN_TAG)
} else { } else {
isSelfClosing = startsWith(context.source, '/>') isSelfClosing = startsWith(context.source, '/>')
if (type === TagType.End && isSelfClosing) { if (type === TagType.End && isSelfClosing) {
emitError(context, ParserErrorTypes.END_TAG_WITH_TRAILING_SOLIDUS) emitError(context, ErrorCodes.END_TAG_WITH_TRAILING_SOLIDUS)
} }
advanceBy(context, isSelfClosing ? 2 : 1) advanceBy(context, isSelfClosing ? 2 : 1)
} }
@ -429,7 +419,8 @@ function parseTag(
ns, ns,
tag, tag,
tagType, tagType,
props, attrs,
directives,
isSelfClosing, isSelfClosing,
children: [], children: [],
loc: getSelection(context, start) loc: getSelection(context, start)
@ -448,15 +439,12 @@ function parseAttribute(
const name = match[0] const name = match[0]
if (nameSet.has(name)) { if (nameSet.has(name)) {
emitError(context, ParserErrorTypes.DUPLICATE_ATTRIBUTE) emitError(context, ErrorCodes.DUPLICATE_ATTRIBUTE)
} }
nameSet.add(name) nameSet.add(name)
if (name[0] === '=') { if (name[0] === '=') {
emitError( emitError(context, ErrorCodes.UNEXPECTED_EQUALS_SIGN_BEFORE_ATTRIBUTE_NAME)
context,
ParserErrorTypes.UNEXPECTED_EQUALS_SIGN_BEFORE_ATTRIBUTE_NAME
)
} }
{ {
const pattern = /["'<]/g const pattern = /["'<]/g
@ -464,7 +452,7 @@ function parseAttribute(
while ((m = pattern.exec(name)) !== null) { while ((m = pattern.exec(name)) !== null) {
emitError( emitError(
context, context,
ParserErrorTypes.UNEXPECTED_CHARACTER_IN_ATTRIBUTE_NAME, ErrorCodes.UNEXPECTED_CHARACTER_IN_ATTRIBUTE_NAME,
m.index m.index
) )
} }
@ -480,7 +468,7 @@ function parseAttribute(
advanceSpaces(context) advanceSpaces(context)
value = parseAttributeValue(context) value = parseAttributeValue(context)
if (!value) { if (!value) {
emitError(context, ParserErrorTypes.MISSING_ATTRIBUTE_VALUE) emitError(context, ErrorCodes.MISSING_ATTRIBUTE_VALUE)
} }
} }
const loc = getSelection(context, start) const loc = getSelection(context, start)
@ -508,7 +496,7 @@ function parseAttribute(
if (!content.endsWith(']')) { if (!content.endsWith(']')) {
emitError( emitError(
context, context,
ParserErrorTypes.X_MISSING_DYNAMIC_DIRECTIVE_ARGUMENT_END ErrorCodes.X_MISSING_DYNAMIC_DIRECTIVE_ARGUMENT_END
) )
} }
@ -590,7 +578,7 @@ function parseAttributeValue(
while ((m = unexpectedChars.exec(match[0])) !== null) { while ((m = unexpectedChars.exec(match[0])) !== null) {
emitError( emitError(
context, context,
ParserErrorTypes.UNEXPECTED_CHARACTER_IN_UNQUOTED_ATTRIBUTE_VALUE, ErrorCodes.UNEXPECTED_CHARACTER_IN_UNQUOTED_ATTRIBUTE_VALUE,
m.index m.index
) )
} }
@ -609,7 +597,7 @@ function parseInterpolation(
const closeIndex = context.source.indexOf(close, open.length) const closeIndex = context.source.indexOf(close, open.length)
if (closeIndex === -1) { if (closeIndex === -1) {
emitError(context, ParserErrorTypes.X_MISSING_INTERPOLATION_END) emitError(context, ErrorCodes.X_MISSING_INTERPOLATION_END)
return undefined return undefined
} }
@ -712,12 +700,12 @@ function parseTextData(
if (!semi) { if (!semi) {
emitError( emitError(
context, context,
ParserErrorTypes.MISSING_SEMICOLON_AFTER_CHARACTER_REFERENCE ErrorCodes.MISSING_SEMICOLON_AFTER_CHARACTER_REFERENCE
) )
} }
} }
} else { } else {
emitError(context, ParserErrorTypes.UNKNOWN_NAMED_CHARACTER_REFERENCE) emitError(context, ErrorCodes.UNKNOWN_NAMED_CHARACTER_REFERENCE)
text += '&' text += '&'
text += name text += name
advanceBy(context, 1 + name.length) advanceBy(context, 1 + name.length)
@ -735,33 +723,33 @@ function parseTextData(
text += head[0] text += head[0]
emitError( emitError(
context, context,
ParserErrorTypes.ABSENCE_OF_DIGITS_IN_NUMERIC_CHARACTER_REFERENCE ErrorCodes.ABSENCE_OF_DIGITS_IN_NUMERIC_CHARACTER_REFERENCE
) )
advanceBy(context, head[0].length) advanceBy(context, head[0].length)
} else { } else {
// https://html.spec.whatwg.org/multipage/parsing.html#numeric-character-reference-end-state // https://html.spec.whatwg.org/multipage/parsing.html#numeric-character-reference-end-state
let cp = Number.parseInt(body[1], hex ? 16 : 10) let cp = Number.parseInt(body[1], hex ? 16 : 10)
if (cp === 0) { if (cp === 0) {
emitError(context, ParserErrorTypes.NULL_CHARACTER_REFERENCE) emitError(context, ErrorCodes.NULL_CHARACTER_REFERENCE)
cp = 0xfffd cp = 0xfffd
} else if (cp > 0x10ffff) { } else if (cp > 0x10ffff) {
emitError( emitError(
context, context,
ParserErrorTypes.CHARACTER_REFERENCE_OUTSIDE_UNICODE_RANGE ErrorCodes.CHARACTER_REFERENCE_OUTSIDE_UNICODE_RANGE
) )
cp = 0xfffd cp = 0xfffd
} else if (cp >= 0xd800 && cp <= 0xdfff) { } else if (cp >= 0xd800 && cp <= 0xdfff) {
emitError(context, ParserErrorTypes.SURROGATE_CHARACTER_REFERENCE) emitError(context, ErrorCodes.SURROGATE_CHARACTER_REFERENCE)
cp = 0xfffd cp = 0xfffd
} else if ((cp >= 0xfdd0 && cp <= 0xfdef) || (cp & 0xfffe) === 0xfffe) { } else if ((cp >= 0xfdd0 && cp <= 0xfdef) || (cp & 0xfffe) === 0xfffe) {
emitError(context, ParserErrorTypes.NONCHARACTER_CHARACTER_REFERENCE) emitError(context, ErrorCodes.NONCHARACTER_CHARACTER_REFERENCE)
} else if ( } else if (
(cp >= 0x01 && cp <= 0x08) || (cp >= 0x01 && cp <= 0x08) ||
cp === 0x0b || cp === 0x0b ||
(cp >= 0x0d && cp <= 0x1f) || (cp >= 0x0d && cp <= 0x1f) ||
(cp >= 0x7f && cp <= 0x9f) (cp >= 0x7f && cp <= 0x9f)
) { ) {
emitError(context, ParserErrorTypes.CONTROL_CHARACTER_REFERENCE) emitError(context, ErrorCodes.CONTROL_CHARACTER_REFERENCE)
cp = CCR_REPLACEMENTS[cp] || cp cp = CCR_REPLACEMENTS[cp] || cp
} }
text += String.fromCodePoint(cp) text += String.fromCodePoint(cp)
@ -769,7 +757,7 @@ function parseTextData(
if (!body![0].endsWith(';')) { if (!body![0].endsWith(';')) {
emitError( emitError(
context, context,
ParserErrorTypes.MISSING_SEMICOLON_AFTER_CHARACTER_REFERENCE ErrorCodes.MISSING_SEMICOLON_AFTER_CHARACTER_REFERENCE
) )
} }
} }
@ -854,7 +842,7 @@ function getNewPosition(
function emitError( function emitError(
context: ParserContext, context: ParserContext,
type: ParserErrorTypes, code: ErrorCodes,
offset?: number offset?: number
): void { ): void {
const loc = getCursor(context) const loc = getCursor(context)
@ -862,7 +850,7 @@ function emitError(
loc.offset += offset loc.offset += offset
loc.column += offset loc.column += offset
} }
context.onError(type, loc) context.onError(createCompilerError(code, loc))
} }
function isEnd( function isEnd(

View File

@ -1 +1,143 @@
// TODO import {
RootNode,
NodeTypes,
ParentNode,
ChildNode,
ElementNode,
DirectiveNode
} from './ast'
import { isString } from '@vue/shared'
import { CompilerError } from './errors'
export type Transform = (node: ChildNode, context: TransformContext) => void
export type DirectiveTransform = (
node: ElementNode,
dir: DirectiveNode,
context: TransformContext
) => false | void
export interface TransformOptions {
transforms: Transform[]
onError?: (error: CompilerError) => void
}
export interface TransformContext extends Required<TransformOptions> {
parent: ParentNode
ancestors: ParentNode[]
childIndex: number
replaceNode(node: ChildNode): void
removeNode(): void
nodeRemoved: boolean
}
export function transform(root: RootNode, options: TransformOptions) {
const context = createTransformContext(root, options)
traverseChildren(root, context, context.ancestors)
}
function createTransformContext(
root: RootNode,
options: TransformOptions
): TransformContext {
const context: TransformContext = {
onError(error: CompilerError) {
throw error
},
...options,
parent: root,
ancestors: [root],
childIndex: 0,
replaceNode(node) {
context.parent.children[context.childIndex] = node
},
removeNode() {
context.parent.children.splice(context.childIndex, 1)
context.nodeRemoved = true
},
nodeRemoved: false
}
return context
}
function traverseChildren(
parent: ParentNode,
context: TransformContext,
ancestors: ParentNode[]
) {
ancestors = ancestors.concat(parent)
for (let i = 0; i < parent.children.length; i++) {
context.parent = parent
context.ancestors = ancestors
context.childIndex = i
traverseNode(parent.children[i], context, ancestors)
if (context.nodeRemoved) {
i--
}
}
}
function traverseNode(
node: ChildNode,
context: TransformContext,
ancestors: ParentNode[]
) {
// apply transform plugins
const transforms = context.transforms
for (let i = 0; i < transforms.length; i++) {
const transform = transforms[i]
context.nodeRemoved = false
transform(node, context)
if (context.nodeRemoved) {
return
} else {
// node may have been replaced
node = context.parent.children[context.childIndex]
}
}
// further traverse downwards
switch (node.type) {
case NodeTypes.IF:
for (let i = 0; i < node.branches.length; i++) {
traverseChildren(node.branches[i], context, ancestors)
}
break
case NodeTypes.FOR:
case NodeTypes.ELEMENT:
traverseChildren(node, context, ancestors)
break
}
}
const identity = <T>(_: T): T => _
export function createDirectiveTransform(
name: string | RegExp,
fn: DirectiveTransform
): Transform {
const matches = isString(name)
? (n: string) => n === name
: (n: string) => name.test(n)
return (node, context) => {
if (node.type === NodeTypes.ELEMENT) {
const dirs = node.directives
let didRemove = false
for (let i = 0; i < dirs.length; i++) {
if (matches(dirs[i].name)) {
const res = fn(node, dirs[i], context)
// Directives are removed after transformation by default. A transform
// returning false means the directive should not be removed.
if (res !== false) {
;(dirs as any)[i] = undefined
didRemove = true
}
}
}
if (didRemove) {
node.directives = dirs.filter(identity)
}
}
}
}

View File

@ -3,7 +3,7 @@ import {
NodeTypes, NodeTypes,
ElementNode, ElementNode,
TextNode, TextNode,
ParserErrorTypes, ErrorCodes,
ExpressionNode, ExpressionNode,
ElementTypes ElementTypes
} from '@vue/compiler-core' } from '@vue/compiler-core'
@ -134,7 +134,8 @@ describe('DOM parser', () => {
ns: DOMNamespaces.HTML, ns: DOMNamespaces.HTML,
tag: 'img', tag: 'img',
tagType: ElementTypes.ELEMENT, tagType: ElementTypes.ELEMENT,
props: [], attrs: [],
directives: [],
isSelfClosing: false, isSelfClosing: false,
children: [], children: [],
loc: { loc: {
@ -150,9 +151,9 @@ describe('DOM parser', () => {
'<textarea>hello</textarea</textarea0></texTArea a="<>">', '<textarea>hello</textarea</textarea0></texTArea a="<>">',
{ {
...parserOptions, ...parserOptions,
onError: type => { onError: err => {
if (type !== ParserErrorTypes.END_TAG_WITH_ATTRIBUTES) { if (err.code !== ErrorCodes.END_TAG_WITH_ATTRIBUTES) {
throw new Error(String(type)) throw err
} }
} }
} }

View File

@ -1,5 +1,4 @@
import { import {
NodeTypes,
TextModes, TextModes,
ParserOptions, ParserOptions,
ElementNode, ElementNode,
@ -23,9 +22,8 @@ export const parserOptionsMinimal: ParserOptions = {
return DOMNamespaces.SVG return DOMNamespaces.SVG
} }
if ( if (
parent.props.some( parent.attrs.some(
a => a =>
a.type === NodeTypes.ATTRIBUTE &&
a.name === 'encoding' && a.name === 'encoding' &&
a.value != null && a.value != null &&
(a.value.content === 'text/html' || (a.value.content === 'text/html' ||