diff --git a/packages/compiler-core/__tests__/parse.spec.ts b/packages/compiler-core/__tests__/parse.spec.ts index 4df608b6..0d3fab31 100644 --- a/packages/compiler-core/__tests__/parse.spec.ts +++ b/packages/compiler-core/__tests__/parse.spec.ts @@ -1278,6 +1278,88 @@ describe('compiler: parse', () => { }) }) + test('v-pre', () => { + const ast = parse( + `
{{ bar }}
\n` + + `
{{ bar }}
` + ) + + const divWithPre = ast.children[0] as ElementNode + expect(divWithPre.props).toMatchObject([ + { + type: NodeTypes.ATTRIBUTE, + name: `:id`, + value: { + type: NodeTypes.TEXT, + content: `foo` + }, + loc: { + source: `:id="foo"`, + start: { + line: 1, + column: 12 + }, + end: { + line: 1, + column: 21 + } + } + } + ]) + expect(divWithPre.children[0]).toMatchObject({ + type: NodeTypes.ELEMENT, + tagType: ElementTypes.ELEMENT, + tag: `Comp` + }) + expect(divWithPre.children[1]).toMatchObject({ + type: NodeTypes.TEXT, + content: `{{ bar }}` + }) + + // should not affect siblings after it + const divWithoutPre = ast.children[1] as ElementNode + expect(divWithoutPre.props).toMatchObject([ + { + type: NodeTypes.DIRECTIVE, + name: `bind`, + arg: { + type: NodeTypes.SIMPLE_EXPRESSION, + isStatic: true, + content: `id` + }, + exp: { + type: NodeTypes.SIMPLE_EXPRESSION, + isStatic: false, + content: `foo` + }, + loc: { + source: `:id="foo"`, + start: { + line: 2, + column: 6 + }, + end: { + line: 2, + column: 15 + } + } + } + ]) + expect(divWithoutPre.children[0]).toMatchObject({ + type: NodeTypes.ELEMENT, + tagType: ElementTypes.COMPONENT, + tag: `Comp` + }) + expect(divWithoutPre.children[1]).toMatchObject({ + type: NodeTypes.INTERPOLATION, + content: { + type: NodeTypes.SIMPLE_EXPRESSION, + content: `bar`, + isStatic: false + } + }) + }) + test('end tags are case-insensitive.', () => { const ast = parse('
hello
after') const element = ast.children[0] as ElementNode diff --git a/packages/compiler-core/src/parse.ts b/packages/compiler-core/src/parse.ts index cbb382c3..019eddb4 100644 --- a/packages/compiler-core/src/parse.ts +++ b/packages/compiler-core/src/parse.ts @@ -26,6 +26,7 @@ import { TemplateChildNode, InterpolationNode } from './ast' +import { extend } from '@vue/shared' export interface ParserOptions { isVoidTag?: (tag: string) => boolean // e.g. img, br, hr @@ -74,6 +75,7 @@ interface ParserContext { line: number column: number maxCRNameLength: number + inPre: boolean } export function parse(content: string, options: ParserOptions = {}): RootNode { @@ -109,7 +111,8 @@ function createParserContext( maxCRNameLength: Object.keys( options.namedCharacterReferences || defaultParserOptions.namedCharacterReferences - ).reduce((max, name) => Math.max(max, name.length), 0) + ).reduce((max, name) => Math.max(max, name.length), 0), + inPre: false } } @@ -127,7 +130,7 @@ function parseChildren( const s = context.source let node: TemplateChildNode | TemplateChildNode[] | undefined = undefined - if (startsWith(s, context.options.delimiters[0])) { + if (!context.inPre && startsWith(s, context.options.delimiters[0])) { // '{{' node = parseInterpolation(context, mode) } else if (mode === TextModes.DATA && s[0] === '<') { @@ -325,8 +328,10 @@ function parseElement( __DEV__ && assert(/^<[a-z]/i.test(context.source)) // Start tag. + const wasInPre = context.inPre const parent = last(ancestors) const element = parseTag(context, TagType.Start, parent) + const isPreBoundary = context.inPre && !wasInPre if (element.isSelfClosing || context.options.isVoidTag(element.tag)) { return element @@ -354,6 +359,10 @@ function parseElement( } element.loc = getSelection(context, element.loc.start) + + if (isPreBoundary) { + context.inPre = false + } return element } @@ -380,18 +389,68 @@ function parseTag( const start = getCursor(context) const match = /^<\/?([a-z][^\t\r\n\f />]*)/i.exec(context.source)! const tag = match[1] - const props = [] const ns = context.options.getNamespace(tag, parent) - let tagType = ElementTypes.ELEMENT - if (tag === 'slot') tagType = ElementTypes.SLOT - else if (tag === 'template') tagType = ElementTypes.TEMPLATE - else if (/[A-Z-]/.test(tag)) tagType = ElementTypes.COMPONENT - advanceBy(context, match[0].length) advanceSpaces(context) + // save current state in case we need to re-parse attributes with v-pre + const cursor = getCursor(context) + const currentSource = context.source + // Attributes. + let props = parseAttributes(context, type) + + // check v-pre + if ( + !context.inPre && + props.some(p => p.type === NodeTypes.DIRECTIVE && p.name === 'pre') + ) { + context.inPre = true + // reset context + extend(context, cursor) + context.source = currentSource + // re-parse attrs and filter out v-pre itself + props = parseAttributes(context, type).filter(p => p.name !== 'v-pre') + } + + // Tag close. + let isSelfClosing = false + if (context.source.length === 0) { + emitError(context, ErrorCodes.EOF_IN_TAG) + } else { + isSelfClosing = startsWith(context.source, '/>') + if (type === TagType.End && isSelfClosing) { + emitError(context, ErrorCodes.END_TAG_WITH_TRAILING_SOLIDUS) + } + advanceBy(context, isSelfClosing ? 2 : 1) + } + + let tagType = ElementTypes.ELEMENT + if (!context.inPre) { + if (tag === 'slot') tagType = ElementTypes.SLOT + else if (tag === 'template') tagType = ElementTypes.TEMPLATE + else if (/[A-Z-]/.test(tag)) tagType = ElementTypes.COMPONENT + } + + return { + type: NodeTypes.ELEMENT, + ns, + tag, + tagType, + props, + isSelfClosing, + children: [], + loc: getSelection(context, start), + codegenNode: undefined // to be created during transform phase + } +} + +function parseAttributes( + context: ParserContext, + type: TagType +): (AttributeNode | DirectiveNode)[] { + const props = [] const attributeNames = new Set() while ( context.source.length > 0 && @@ -418,30 +477,7 @@ function parseTag( } advanceSpaces(context) } - - // Tag close. - let isSelfClosing = false - if (context.source.length === 0) { - emitError(context, ErrorCodes.EOF_IN_TAG) - } else { - isSelfClosing = startsWith(context.source, '/>') - if (type === TagType.End && isSelfClosing) { - emitError(context, ErrorCodes.END_TAG_WITH_TRAILING_SOLIDUS) - } - advanceBy(context, isSelfClosing ? 2 : 1) - } - - return { - type: NodeTypes.ELEMENT, - ns, - tag, - tagType, - props, - isSelfClosing, - children: [], - loc: getSelection(context, start), - codegenNode: undefined // to be created during transform phase - } + return props } function parseAttribute( @@ -497,7 +533,7 @@ function parseAttribute( } const loc = getSelection(context, start) - if (/^(v-|:|@|#)/.test(name)) { + if (!context.inPre && /^(v-|:|@|#)/.test(name)) { const match = /(?:^v-([a-z0-9-]+))?(?:(?::|^@|^#)([^\.]+))?(.+)?$/i.exec( name )!