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>',
+  )
 })