From e9017d19d84da5f21e98352dfa7d13cc2ea69c2b Mon Sep 17 00:00:00 2001 From: CathLee <447932704@qq.com> Date: Sun, 21 Apr 2024 23:04:04 +0800 Subject: [PATCH 1/4] feat: Add 'isShouldSelfClosing' tag to identify whether it is the last element --- packages/compiler-core/src/ast.ts | 1 + packages/compiler-core/src/parser.ts | 5 ++- packages/compiler-core/src/tokenizer.ts | 45 ++++++++++++++++++- .../src/transforms/transformElement.ts | 14 +++++- 4 files changed, 60 insertions(+), 5 deletions(-) diff --git a/packages/compiler-core/src/ast.ts b/packages/compiler-core/src/ast.ts index 720d43cb3..2748645ca 100644 --- a/packages/compiler-core/src/ast.ts +++ b/packages/compiler-core/src/ast.ts @@ -70,6 +70,7 @@ export enum ElementTypes { export interface Node { type: NodeTypes loc: SourceLocation + isShouldSelfClosing?: boolean } // The node's range. The `start` is inclusive and `end` is exclusive. diff --git a/packages/compiler-core/src/parser.ts b/packages/compiler-core/src/parser.ts index da8861b92..8fe484ad8 100644 --- a/packages/compiler-core/src/parser.ts +++ b/packages/compiler-core/src/parser.ts @@ -151,7 +151,7 @@ const tokenizer = new Tokenizer(stack, { endOpenTag(end) }, - onclosetag(start, end) { + onclosetag(start, end, isLastElement) { const name = getSlice(start, end) if (!currentOptions.isVoidTag(name)) { let found = false @@ -159,6 +159,9 @@ const tokenizer = new Tokenizer(stack, { const e = stack[i] if (e.tag.toLowerCase() === name.toLowerCase()) { found = true + if (isLastElement) { + e.isShouldSelfClosing = true + } if (i > 0) { emitError(ErrorCodes.X_MISSING_END_TAG, stack[0].loc.start.offset) } diff --git a/packages/compiler-core/src/tokenizer.ts b/packages/compiler-core/src/tokenizer.ts index 561a84b5f..cb3cf394d 100644 --- a/packages/compiler-core/src/tokenizer.ts +++ b/packages/compiler-core/src/tokenizer.ts @@ -186,7 +186,7 @@ export interface Callbacks { onopentagname(start: number, endIndex: number): void onopentagend(endIndex: number): void onselfclosingtag(endIndex: number): void - onclosetag(start: number, endIndex: number): void + onclosetag(start: number, endIndex: number, isLastElement: boolean): void onattribdata(start: number, endIndex: number): void onattribentity(char: string, start: number, end: number): void @@ -246,6 +246,8 @@ export default class Tokenizer { public inVPre = false /** Record newline positions for fast line / column calculation */ private newlines: number[] = [] + // Record current stage + private currentStage = '' private readonly entityDecoder?: EntityDecoder @@ -311,6 +313,7 @@ export default class Tokenizer { if (c === CharCodes.Lt) { if (this.index > this.sectionStart) { this.cbs.ontext(this.sectionStart, this.index) + this.currentStage = 'stateText' } this.state = State.BeforeTagName this.sectionStart = this.index @@ -608,8 +611,13 @@ export default class Tokenizer { } private stateInClosingTagName(c: number): void { if (c === CharCodes.Gt || isWhitespace(c)) { - this.cbs.onclosetag(this.sectionStart, this.index) + this.cbs.onclosetag( + this.sectionStart, + this.index, + this.currentStage === 'stateInTagName', + ) this.sectionStart = -1 + this.currentStage = 'InClosingTagName' this.state = State.AfterClosingTagName this.stateAfterClosingTagName(c) } @@ -619,6 +627,7 @@ export default class Tokenizer { if (c === CharCodes.Gt) { this.state = State.Text this.sectionStart = this.index + 1 + this.currentStage = 'stateAfterClosingTagName' } } private stateBeforeAttrName(c: number): void { @@ -927,78 +936,97 @@ export default class Tokenizer { switch (this.state) { case State.Text: { this.stateText(c) + break } case State.InterpolationOpen: { this.stateInterpolationOpen(c) + this.currentStage = 'stateInterpolationOpen' break } case State.Interpolation: { this.stateInterpolation(c) + this.currentStage = 'stateInterpolation' break } case State.InterpolationClose: { this.stateInterpolationClose(c) + this.currentStage = 'stateInterpolationClose' break } case State.SpecialStartSequence: { this.stateSpecialStartSequence(c) + this.currentStage = 'stateSpecialStartSequence' break } case State.InRCDATA: { this.stateInRCDATA(c) + this.currentStage = 'stateInRCDATA' break } case State.CDATASequence: { this.stateCDATASequence(c) + this.currentStage = 'stateCDATASequence' break } case State.InAttrValueDq: { this.stateInAttrValueDoubleQuotes(c) + this.currentStage = 'stateInAttrValueDoubleQuotes' break } case State.InAttrName: { this.stateInAttrName(c) + this.currentStage = 'stateInAttrName' break } case State.InDirName: { this.stateInDirName(c) + this.currentStage = 'stateInDirName' break } case State.InDirArg: { this.stateInDirArg(c) + this.currentStage = 'stateInDirArg' break } case State.InDirDynamicArg: { this.stateInDynamicDirArg(c) + this.currentStage = 'stateInDynamicDirArg' break } case State.InDirModifier: { this.stateInDirModifier(c) + this.currentStage = 'stateInDirModifier' break } case State.InCommentLike: { this.stateInCommentLike(c) + this.currentStage = 'stateInCommentLike' break } case State.InSpecialComment: { this.stateInSpecialComment(c) + this.currentStage = 'stateInSpecialComment' break } case State.BeforeAttrName: { this.stateBeforeAttrName(c) + this.currentStage = 'stateBeforeAttrName' break } case State.InTagName: { this.stateInTagName(c) + this.currentStage = 'stateInTagName' break } case State.InSFCRootTagName: { this.stateInSFCRootTagName(c) + this.currentStage = 'stateInSFCRootTagName' break } case State.InClosingTagName: { this.stateInClosingTagName(c) + break } case State.BeforeTagName: { @@ -1007,14 +1035,17 @@ export default class Tokenizer { } case State.AfterAttrName: { this.stateAfterAttrName(c) + this.currentStage = 'stateAfterAttrName' break } case State.InAttrValueSq: { this.stateInAttrValueSingleQuotes(c) + this.currentStage = 'stateInAttrValueSingleQuotes' break } case State.BeforeAttrValue: { this.stateBeforeAttrValue(c) + this.currentStage = 'stateBeforeAttrValue' break } case State.BeforeClosingTagName: { @@ -1023,42 +1054,52 @@ export default class Tokenizer { } case State.AfterClosingTagName: { this.stateAfterClosingTagName(c) + break } case State.BeforeSpecialS: { this.stateBeforeSpecialS(c) + this.currentStage = 'stateBeforeSpecialS' break } case State.BeforeSpecialT: { this.stateBeforeSpecialT(c) + this.currentStage = 'stateBeforeSpecialT' break } case State.InAttrValueNq: { this.stateInAttrValueNoQuotes(c) + this.currentStage = 'stateInAttrValueNoQuotes' break } case State.InSelfClosingTag: { this.stateInSelfClosingTag(c) + this.currentStage = 'stateInSelfClosingTag' break } case State.InDeclaration: { this.stateInDeclaration(c) + this.currentStage = 'stateInDeclaration' break } case State.BeforeDeclaration: { this.stateBeforeDeclaration(c) + this.currentStage = 'stateBeforeDeclaration' break } case State.BeforeComment: { this.stateBeforeComment(c) + this.currentStage = 'stateBeforeComment' break } case State.InProcessingInstruction: { this.stateInProcessingInstruction(c) + this.currentStage = 'stateInProcessingInstruction' break } case State.InEntity: { this.stateInEntity() + this.currentStage = 'stateInEntity' break } } diff --git a/packages/compiler-vapor/src/transforms/transformElement.ts b/packages/compiler-vapor/src/transforms/transformElement.ts index 7c51bb6a5..8a5139b53 100644 --- a/packages/compiler-vapor/src/transforms/transformElement.ts +++ b/packages/compiler-vapor/src/transforms/transformElement.ts @@ -159,11 +159,21 @@ function transformNativeElement( } } } + const { node } = context + + if (node.isShouldSelfClosing) { + template += context.childrenTemplate.join('') + } else { + template += `>` + context.childrenTemplate.join('') + } - template += `>` + context.childrenTemplate.join('') // TODO remove unnecessary close tag, e.g. if it's the last element of the template if (!isVoidTag(tag)) { - template += `</${tag}>` + if (node.isShouldSelfClosing) { + template += ` />` + } else { + template += `</${tag}>` + } } if ( From 1bec10221571ddf911ad6e3a3232fb387a809228 Mon Sep 17 00:00:00 2001 From: CathLee <447932704@qq.com> Date: Sun, 21 Apr 2024 23:06:57 +0800 Subject: [PATCH 2/4] test:The end tag abbreviation test --- .../__snapshots__/parse.spec.ts.snap | 2 ++ .../compiler-core/__tests__/parse.spec.ts | 1 + .../__snapshots__/compile.spec.ts.snap | 30 +++++++++++++++++++ .../__tests__/abbreviation.spec.ts | 19 +++++++++++- .../compiler-vapor/__tests__/compile.spec.ts | 17 +++++++++++ 5 files changed, 68 insertions(+), 1 deletion(-) diff --git a/packages/compiler-core/__tests__/__snapshots__/parse.spec.ts.snap b/packages/compiler-core/__tests__/__snapshots__/parse.spec.ts.snap index 678548e35..7f765fd68 100644 --- a/packages/compiler-core/__tests__/__snapshots__/parse.spec.ts.snap +++ b/packages/compiler-core/__tests__/__snapshots__/parse.spec.ts.snap @@ -2399,6 +2399,7 @@ exports[`compiler: parse > Errors > MISSING_END_TAG_NAME > <template></></templa { "children": [], "codegenNode": undefined, + "isShouldSelfClosing": true, "loc": { "end": { "column": 25, @@ -4549,6 +4550,7 @@ exports[`compiler: parse > Errors > X_MISSING_END_TAG > <template><div></templat }, ], "codegenNode": undefined, + "isShouldSelfClosing": true, "loc": { "end": { "column": 27, diff --git a/packages/compiler-core/__tests__/parse.spec.ts b/packages/compiler-core/__tests__/parse.spec.ts index 22fb209cf..dc6fe7a0a 100644 --- a/packages/compiler-core/__tests__/parse.spec.ts +++ b/packages/compiler-core/__tests__/parse.spec.ts @@ -476,6 +476,7 @@ describe('compiler: parse', () => { type: NodeTypes.ELEMENT, ns: Namespaces.HTML, tag: 'div', + isShouldSelfClosing: true, tagType: ElementTypes.ELEMENT, codegenNode: undefined, props: [], diff --git a/packages/compiler-vapor/__tests__/__snapshots__/compile.spec.ts.snap b/packages/compiler-vapor/__tests__/__snapshots__/compile.spec.ts.snap index f70e66d95..f6a884586 100644 --- a/packages/compiler-vapor/__tests__/__snapshots__/compile.spec.ts.snap +++ b/packages/compiler-vapor/__tests__/__snapshots__/compile.spec.ts.snap @@ -11,6 +11,16 @@ export function render(_ctx) { }" `; +exports[`compile > close tag 1`] = ` +"import { template as _template } from 'vue/vapor'; +const t0 = _template("<div><span><div /></span></div>") + +export function render(_ctx) { + const n0 = t0() + return n0 +}" +`; + exports[`compile > custom directive > basic 1`] = ` "import { resolveDirective as _resolveDirective, withDirectives as _withDirectives, template as _template } from 'vue/vapor'; const t0 = _template("<div></div>") @@ -206,3 +216,23 @@ export function render(_ctx) { return n0 }" `; + +exports[`compile > two close tag 1`] = ` +"import { template as _template } from 'vue/vapor'; +const t0 = _template("<div><span /><span /></div>") + +export function render(_ctx) { + const n0 = t0() + return n0 +}" +`; + +exports[`compile > two close tag with text 1`] = ` +"import { template as _template } from 'vue/vapor'; +const t0 = _template("<div><span>ddd</span><span /></div>") + +export function render(_ctx) { + const n0 = t0() + return n0 +}" +`; diff --git a/packages/compiler-vapor/__tests__/abbreviation.spec.ts b/packages/compiler-vapor/__tests__/abbreviation.spec.ts index 2b52bd48e..e7d3c3224 100644 --- a/packages/compiler-vapor/__tests__/abbreviation.spec.ts +++ b/packages/compiler-vapor/__tests__/abbreviation.spec.ts @@ -1,15 +1,32 @@ +/* + * @Date: 2024-04-21 15:29:37 + * @Description: + */ /** * @vitest-environment jsdom */ - +import { + compile as _compile, + transformChildren, + transformElement, + transformText, +} from '../src' +import { makeCompile } from './transforms/_utils' const parser = new DOMParser() function parseHTML(html: string) { return parser.parseFromString(html, 'text/html').body.innerHTML } +const compileWithElementTransform = makeCompile({ + nodeTransforms: [transformElement, transformChildren, transformText], +}) + function checkAbbr(template: string, abbrevation: string, expected: string) { // TODO do some optimzations to make sure template === abbrevation + const { ir } = compileWithElementTransform(template) + const templateOfIr = ir.template + abbrevation = templateOfIr.reduce((cur, next) => cur + next) expect(parseHTML(abbrevation)).toBe(expected) } diff --git a/packages/compiler-vapor/__tests__/compile.spec.ts b/packages/compiler-vapor/__tests__/compile.spec.ts index b406d9e95..71afc2eae 100644 --- a/packages/compiler-vapor/__tests__/compile.spec.ts +++ b/packages/compiler-vapor/__tests__/compile.spec.ts @@ -22,6 +22,23 @@ describe('compile', () => { expect(code).matchSnapshot() }) + test('close tag', () => { + const code = compile(`<div><span><div></div></span></div>`) + expect(code).matchSnapshot() + expect(code).contains(JSON.stringify('<div><span><div /></span></div>')) + }) + test('two close tag ', () => { + const code = compile(`<div><span></span><span></span></div>`) + expect(code).matchSnapshot() + expect(code).contains(JSON.stringify('<div><span /><span /></div>')) + }) + + test('two close tag with text', () => { + const code = compile(`<div><span>ddd</span><span></span></div>`) + expect(code).matchSnapshot() + expect(code).contains(JSON.stringify('<div><span>ddd</span><span /></div>')) + }) + test('dynamic root', () => { const code = compile(`{{ 1 }}{{ 2 }}`) expect(code).matchSnapshot() From 2e6cf240568eceecb21c2eea615979e6b776832d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=89=E5=92=B2=E6=99=BA=E5=AD=90=20Kevin=20Deng?= <sxzz@sxzz.moe> Date: Mon, 22 Apr 2024 02:32:31 +0800 Subject: [PATCH 3/4] test: compare abbr --- packages/compiler-vapor/__tests__/abbreviation.spec.ts | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/packages/compiler-vapor/__tests__/abbreviation.spec.ts b/packages/compiler-vapor/__tests__/abbreviation.spec.ts index e7d3c3224..4b14527b4 100644 --- a/packages/compiler-vapor/__tests__/abbreviation.spec.ts +++ b/packages/compiler-vapor/__tests__/abbreviation.spec.ts @@ -1,7 +1,3 @@ -/* - * @Date: 2024-04-21 15:29:37 - * @Description: - */ /** * @vitest-environment jsdom */ @@ -23,10 +19,8 @@ const compileWithElementTransform = makeCompile({ }) function checkAbbr(template: string, abbrevation: string, expected: string) { - // TODO do some optimzations to make sure template === abbrevation const { ir } = compileWithElementTransform(template) - const templateOfIr = ir.template - abbrevation = templateOfIr.reduce((cur, next) => cur + next) + expect(ir.template.reduce((cur, next) => cur + next)).toBe(abbrevation) expect(parseHTML(abbrevation)).toBe(expected) } From 80ec2c6e223c9ecaf4ea6410eb7c72dc47f0e8ec Mon Sep 17 00:00:00 2001 From: CathLee <447932704@qq.com> Date: Mon, 29 Apr 2024 15:23:02 +0800 Subject: [PATCH 4/4] test:change template into abbreviation --- .../__tests__/abbreviation.spec.ts | 54 ++++++++++++++++++- 1 file changed, 52 insertions(+), 2 deletions(-) diff --git a/packages/compiler-vapor/__tests__/abbreviation.spec.ts b/packages/compiler-vapor/__tests__/abbreviation.spec.ts index 4b14527b4..3a9c9e9a3 100644 --- a/packages/compiler-vapor/__tests__/abbreviation.spec.ts +++ b/packages/compiler-vapor/__tests__/abbreviation.spec.ts @@ -17,10 +17,50 @@ function parseHTML(html: string) { const compileWithElementTransform = makeCompile({ nodeTransforms: [transformElement, transformChildren, transformText], }) +const splitHTMLTags = (htmlString: string) => { + // change `<div>hello</div>` to `['<div>','hello','</div>']` + const tagPattern = + /<\/?[\w-]+(?:\s+[\w-]+(?:\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|[^>\s]+))?)*\s*\/?>|[^<>]+/g + return htmlString.match(tagPattern) +} -function checkAbbr(template: string, abbrevation: string, expected: string) { +const checkBeforeAbbr = (template: string) => { const { ir } = compileWithElementTransform(template) - expect(ir.template.reduce((cur, next) => cur + next)).toBe(abbrevation) + let templateToAbbreviation = '' + const getRes = (irTemplate: string) => { + let childIndex = 0 + const res = splitHTMLTags(irTemplate) + const loop = (node: any) => { + if (!node) { + return + } + node.forEach((ele: any) => { + if (ele.children && ele.children.length !== 0) { + childIndex++ + loop(ele.children) + } + }) + } + // get the node's children index,then filter the close tags + loop(ir.node.children) + let abbr: string[] = res?.slice(0, res.length - childIndex) ?? [] + const pre = abbr[abbr?.length - 2] + const last = abbr[abbr?.length - 1] + // if the last two elements has the same `tag` type + if (childIndex && last === `</${pre.replace(/<\/?(\w+).*>/, '$1')}>`) { + abbr = abbr?.slice(0, -1) + } + templateToAbbreviation += abbr?.join('') + } + ir.template.forEach(irNode => { + getRes(irNode) + }) + return templateToAbbreviation +} + +function checkAbbr(template: string, abbrevation: string, expected: string) { + const tempToAbbr = checkBeforeAbbr(template) + expect(tempToAbbr).toBe(abbrevation) expect(parseHTML(abbrevation)).toBe(expected) } @@ -41,6 +81,11 @@ test('template abbreviation', () => { '<div><hr><div>', '<div><hr><div></div></div>', ) + checkAbbr( + '<div><hr/><span/></div>', + '<div><hr><span>', + '<div><hr><span></span></div>', + ) checkAbbr( '<div><div/><hr/></div>', '<div><div></div><hr>', @@ -48,4 +93,9 @@ test('template abbreviation', () => { ) checkAbbr('<span/>hello', '<span></span>hello', '<span></span>hello') + checkAbbr( + '<span/>hello<div/>', + '<span></span>hello<div></div>', + '<span></span>hello<div></div>', + ) })