From 083c0847c755998cecb705d4f7017886dbb16f87 Mon Sep 17 00:00:00 2001 From: Mary Date: Mon, 21 Jul 2025 14:42:07 +0700 Subject: [PATCH 1/5] refactor(compiler-vapor): skip unnecessary attribute quoting in templates --- .../__tests__/__snapshots__/compile.spec.ts.snap | 4 ++-- packages/compiler-vapor/__tests__/compile.spec.ts | 2 +- .../__snapshots__/transformElement.spec.ts.snap | 4 ++-- .../transforms/__snapshots__/vBind.spec.ts.snap | 2 +- .../transforms/__snapshots__/vModel.spec.ts.snap | 6 +++--- .../__tests__/transforms/transformElement.spec.ts | 7 ++++--- .../src/transforms/transformElement.ts | 14 +++++++++++++- 7 files changed, 26 insertions(+), 13 deletions(-) diff --git a/packages/compiler-vapor/__tests__/__snapshots__/compile.spec.ts.snap b/packages/compiler-vapor/__tests__/__snapshots__/compile.spec.ts.snap index b10a98d32cb..c692095d2f8 100644 --- a/packages/compiler-vapor/__tests__/__snapshots__/compile.spec.ts.snap +++ b/packages/compiler-vapor/__tests__/__snapshots__/compile.spec.ts.snap @@ -140,7 +140,7 @@ export function render(_ctx) { exports[`compile > directives > v-pre > basic 1`] = ` "import { template as _template } from 'vue'; -const t0 = _template("
{{ bar }}
", true) +const t0 = _template("
{{ bar }}
", true) export function render(_ctx, $props, $emit, $attrs, $slots) { const n0 = t0() @@ -150,7 +150,7 @@ export function render(_ctx, $props, $emit, $attrs, $slots) { exports[`compile > directives > v-pre > should not affect siblings after it 1`] = ` "import { resolveComponent as _resolveComponent, setInsertionState as _setInsertionState, createComponentWithFallback as _createComponentWithFallback, child as _child, setProp as _setProp, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, template as _template } from 'vue'; -const t0 = _template("
{{ bar }}
") +const t0 = _template("
{{ bar }}
") const t1 = _template("
") export function render(_ctx, $props, $emit, $attrs, $slots) { diff --git a/packages/compiler-vapor/__tests__/compile.spec.ts b/packages/compiler-vapor/__tests__/compile.spec.ts index 178021d13dd..f134bd23bf6 100644 --- a/packages/compiler-vapor/__tests__/compile.spec.ts +++ b/packages/compiler-vapor/__tests__/compile.spec.ts @@ -71,7 +71,7 @@ describe('compile', () => { expect(code).toMatchSnapshot() expect(code).contains( - JSON.stringify('
{{ bar }}
'), + JSON.stringify('
{{ bar }}
'), ) expect(code).not.contains('effect') }) diff --git a/packages/compiler-vapor/__tests__/transforms/__snapshots__/transformElement.spec.ts.snap b/packages/compiler-vapor/__tests__/transforms/__snapshots__/transformElement.spec.ts.snap index 7aa56aa9c2f..e3bd75cab71 100644 --- a/packages/compiler-vapor/__tests__/transforms/__snapshots__/transformElement.spec.ts.snap +++ b/packages/compiler-vapor/__tests__/transforms/__snapshots__/transformElement.spec.ts.snap @@ -354,7 +354,7 @@ export function render(_ctx) { exports[`compiler: element transform > props + children 1`] = ` "import { template as _template } from 'vue'; -const t0 = _template("
", true) +const t0 = _template("
", true) export function render(_ctx) { const n0 = t0() @@ -399,7 +399,7 @@ export function render(_ctx) { exports[`compiler: element transform > static props 1`] = ` "import { template as _template } from 'vue'; -const t0 = _template("
", true) +const t0 = _template("
bar\\">
", true) export function render(_ctx) { const n0 = t0() diff --git a/packages/compiler-vapor/__tests__/transforms/__snapshots__/vBind.spec.ts.snap b/packages/compiler-vapor/__tests__/transforms/__snapshots__/vBind.spec.ts.snap index 4ea0db55fe5..60fdfd89435 100644 --- a/packages/compiler-vapor/__tests__/transforms/__snapshots__/vBind.spec.ts.snap +++ b/packages/compiler-vapor/__tests__/transforms/__snapshots__/vBind.spec.ts.snap @@ -633,7 +633,7 @@ export function render(_ctx) { exports[`compiler v-bind > with constant value 1`] = ` "import { setProp as _setProp, template as _template } from 'vue'; -const t0 = _template("
", true) +const t0 = _template("
", true) export function render(_ctx, $props, $emit, $attrs, $slots) { const n0 = t0() diff --git a/packages/compiler-vapor/__tests__/transforms/__snapshots__/vModel.spec.ts.snap b/packages/compiler-vapor/__tests__/transforms/__snapshots__/vModel.spec.ts.snap index 5ef064974c0..40199b451f8 100644 --- a/packages/compiler-vapor/__tests__/transforms/__snapshots__/vModel.spec.ts.snap +++ b/packages/compiler-vapor/__tests__/transforms/__snapshots__/vModel.spec.ts.snap @@ -116,7 +116,7 @@ export function render(_ctx) { exports[`compiler: vModel transform > should support input (checkbox) 1`] = ` "import { applyCheckboxModel as _applyCheckboxModel, template as _template } from 'vue'; -const t0 = _template("", true) +const t0 = _template("", true) export function render(_ctx) { const n0 = t0() @@ -138,7 +138,7 @@ export function render(_ctx) { exports[`compiler: vModel transform > should support input (radio) 1`] = ` "import { applyRadioModel as _applyRadioModel, template as _template } from 'vue'; -const t0 = _template("", true) +const t0 = _template("", true) export function render(_ctx) { const n0 = t0() @@ -149,7 +149,7 @@ export function render(_ctx) { exports[`compiler: vModel transform > should support input (text) 1`] = ` "import { applyTextModel as _applyTextModel, template as _template } from 'vue'; -const t0 = _template("", true) +const t0 = _template("", true) export function render(_ctx) { const n0 = t0() diff --git a/packages/compiler-vapor/__tests__/transforms/transformElement.spec.ts b/packages/compiler-vapor/__tests__/transforms/transformElement.spec.ts index a693db4ad39..7985de7d79e 100644 --- a/packages/compiler-vapor/__tests__/transforms/transformElement.spec.ts +++ b/packages/compiler-vapor/__tests__/transforms/transformElement.spec.ts @@ -573,10 +573,11 @@ describe('compiler: element transform', () => { test('static props', () => { const { code, ir } = compileWithElementTransform( - `
`, + `
`, ) - const template = '
' + const template = + '
' expect(code).toMatchSnapshot() expect(code).contains(JSON.stringify(template)) expect(ir.template).toMatchObject([template]) @@ -588,7 +589,7 @@ describe('compiler: element transform', () => { `
`, ) - const template = '
' + const template = '
' expect(code).toMatchSnapshot() expect(code).contains(JSON.stringify(template)) expect(ir.template).toMatchObject([template]) diff --git a/packages/compiler-vapor/src/transforms/transformElement.ts b/packages/compiler-vapor/src/transforms/transformElement.ts index 05153e729af..5fd66550d4a 100644 --- a/packages/compiler-vapor/src/transforms/transformElement.ts +++ b/packages/compiler-vapor/src/transforms/transformElement.ts @@ -224,8 +224,20 @@ function transformNativeElement( for (const prop of propsResult[1]) { const { key, values } = prop if (key.isStatic && values.length === 1 && values[0].isStatic) { + const value = values[0].content + template += ` ${key.content}` - if (values[0].content) template += `="${values[0].content}"` + + if (value) { + // https://html.spec.whatwg.org/multipage/introduction.html#intro-early-example + const needsQuoting = /[\s>]|^["'=]/.test(value) + + if (needsQuoting) { + template += `="${value}"` + } else { + template += `=${value}` + } + } } else { dynamicProps.push(key.content) context.registerEffect( From 2fe9c1cc067d1f039d780c7f468bd2c2db899ac7 Mon Sep 17 00:00:00 2001 From: Mary Date: Mon, 21 Jul 2025 14:43:45 +0700 Subject: [PATCH 2/5] refactor(compiler-vapor): skip unnecessary whitespace between attributes --- .../transforms/__snapshots__/transformElement.spec.ts.snap | 2 +- .../__tests__/transforms/transformElement.spec.ts | 2 +- packages/compiler-vapor/src/transforms/transformElement.ts | 7 +++++-- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/compiler-vapor/__tests__/transforms/__snapshots__/transformElement.spec.ts.snap b/packages/compiler-vapor/__tests__/transforms/__snapshots__/transformElement.spec.ts.snap index e3bd75cab71..ee1311b262f 100644 --- a/packages/compiler-vapor/__tests__/transforms/__snapshots__/transformElement.spec.ts.snap +++ b/packages/compiler-vapor/__tests__/transforms/__snapshots__/transformElement.spec.ts.snap @@ -399,7 +399,7 @@ export function render(_ctx) { exports[`compiler: element transform > static props 1`] = ` "import { template as _template } from 'vue'; -const t0 = _template("
bar\\">
", true) +const t0 = _template("
bar\\">
", true) export function render(_ctx) { const n0 = t0() diff --git a/packages/compiler-vapor/__tests__/transforms/transformElement.spec.ts b/packages/compiler-vapor/__tests__/transforms/transformElement.spec.ts index 7985de7d79e..46ea0f29893 100644 --- a/packages/compiler-vapor/__tests__/transforms/transformElement.spec.ts +++ b/packages/compiler-vapor/__tests__/transforms/transformElement.spec.ts @@ -577,7 +577,7 @@ describe('compiler: element transform', () => { ) const template = - '
' + '
' expect(code).toMatchSnapshot() expect(code).contains(JSON.stringify(template)) expect(ir.template).toMatchObject([template]) diff --git a/packages/compiler-vapor/src/transforms/transformElement.ts b/packages/compiler-vapor/src/transforms/transformElement.ts index 5fd66550d4a..106bc85b72c 100644 --- a/packages/compiler-vapor/src/transforms/transformElement.ts +++ b/packages/compiler-vapor/src/transforms/transformElement.ts @@ -221,16 +221,19 @@ function transformNativeElement( getEffectIndex, ) } else { + let needsQuoting = false + for (const prop of propsResult[1]) { const { key, values } = prop if (key.isStatic && values.length === 1 && values[0].isStatic) { const value = values[0].content - template += ` ${key.content}` + if (!needsQuoting) template += ` ` + template += key.content if (value) { // https://html.spec.whatwg.org/multipage/introduction.html#intro-early-example - const needsQuoting = /[\s>]|^["'=]/.test(value) + needsQuoting = /[\s>]|^["'=]/.test(value) if (needsQuoting) { template += `="${value}"` From 0b5b4ca43868a732d37b015315b708e2827afce1 Mon Sep 17 00:00:00 2001 From: Mary Date: Mon, 21 Jul 2025 14:55:51 +0700 Subject: [PATCH 3/5] fix(compiler-vapor): unset needsQuoting if there is no value --- .../__tests__/transforms/transformElement.spec.ts | 4 ++-- packages/compiler-vapor/src/transforms/transformElement.ts | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/compiler-vapor/__tests__/transforms/transformElement.spec.ts b/packages/compiler-vapor/__tests__/transforms/transformElement.spec.ts index 46ea0f29893..a44166000e7 100644 --- a/packages/compiler-vapor/__tests__/transforms/transformElement.spec.ts +++ b/packages/compiler-vapor/__tests__/transforms/transformElement.spec.ts @@ -573,11 +573,11 @@ describe('compiler: element transform', () => { test('static props', () => { const { code, ir } = compileWithElementTransform( - `
`, + `
`, ) const template = - '
' + '
' expect(code).toMatchSnapshot() expect(code).contains(JSON.stringify(template)) expect(ir.template).toMatchObject([template]) diff --git a/packages/compiler-vapor/src/transforms/transformElement.ts b/packages/compiler-vapor/src/transforms/transformElement.ts index 106bc85b72c..6b024a5f8ee 100644 --- a/packages/compiler-vapor/src/transforms/transformElement.ts +++ b/packages/compiler-vapor/src/transforms/transformElement.ts @@ -240,6 +240,8 @@ function transformNativeElement( } else { template += `=${value}` } + } else { + needsQuoting = false } } else { dynamicProps.push(key.content) From 3e90f4e2955e06f01de8ed7d0e1daa1eb3fce946 Mon Sep 17 00:00:00 2001 From: Mary Date: Mon, 21 Jul 2025 15:18:46 +0700 Subject: [PATCH 4/5] fix(compiler-vapor): encode quotes back --- .../transformElement.spec.ts.snap | 32 ++++++++++++++- .../transforms/transformElement.spec.ts | 41 ++++++++++++++++++- .../src/transforms/transformElement.ts | 4 +- 3 files changed, 73 insertions(+), 4 deletions(-) diff --git a/packages/compiler-vapor/__tests__/transforms/__snapshots__/transformElement.spec.ts.snap b/packages/compiler-vapor/__tests__/transforms/__snapshots__/transformElement.spec.ts.snap index ee1311b262f..ddeda802316 100644 --- a/packages/compiler-vapor/__tests__/transforms/__snapshots__/transformElement.spec.ts.snap +++ b/packages/compiler-vapor/__tests__/transforms/__snapshots__/transformElement.spec.ts.snap @@ -399,7 +399,37 @@ export function render(_ctx) { exports[`compiler: element transform > static props 1`] = ` "import { template as _template } from 'vue'; -const t0 = _template("
bar\\">
", true) +const t0 = _template("
", true) + +export function render(_ctx) { + const n0 = t0() + return n0 +}" +`; + +exports[`compiler: element transform > static props mixed quoting 1`] = ` +"import { template as _template } from 'vue'; +const t0 = _template("
bar\\">
", true) + +export function render(_ctx) { + const n0 = t0() + return n0 +}" +`; + +exports[`compiler: element transform > static props quoted 1`] = ` +"import { template as _template } from 'vue'; +const t0 = _template("
bar\\">
", true) + +export function render(_ctx) { + const n0 = t0() + return n0 +}" +`; + +exports[`compiler: element transform > static props unquoted 1`] = ` +"import { template as _template } from 'vue'; +const t0 = _template("
", true) export function render(_ctx) { const n0 = t0() diff --git a/packages/compiler-vapor/__tests__/transforms/transformElement.spec.ts b/packages/compiler-vapor/__tests__/transforms/transformElement.spec.ts index a44166000e7..44413e8088d 100644 --- a/packages/compiler-vapor/__tests__/transforms/transformElement.spec.ts +++ b/packages/compiler-vapor/__tests__/transforms/transformElement.spec.ts @@ -573,11 +573,48 @@ describe('compiler: element transform', () => { test('static props', () => { const { code, ir } = compileWithElementTransform( - `
`, + `
`, + ) + + const template = '
' + expect(code).toMatchSnapshot() + expect(code).contains(JSON.stringify(template)) + expect(ir.template).toMatchObject([template]) + expect(ir.block.effect).lengthOf(0) + }) + + test('static props unquoted', () => { + const { code, ir } = compileWithElementTransform( + `
`, + ) + + const template = '
' + expect(code).toMatchSnapshot() + expect(code).contains(JSON.stringify(template)) + expect(ir.template).toMatchObject([template]) + expect(ir.block.effect).lengthOf(0) + }) + + test('static props quoted', () => { + const { code, ir } = compileWithElementTransform( + `
`, + ) + + const template = + '
' + expect(code).toMatchSnapshot() + expect(code).contains(JSON.stringify(template)) + expect(ir.template).toMatchObject([template]) + expect(ir.block.effect).lengthOf(0) + }) + + test('static props mixed quoting', () => { + const { code, ir } = compileWithElementTransform( + `
`, ) const template = - '
' + '
' expect(code).toMatchSnapshot() expect(code).contains(JSON.stringify(template)) expect(ir.template).toMatchObject([template]) diff --git a/packages/compiler-vapor/src/transforms/transformElement.ts b/packages/compiler-vapor/src/transforms/transformElement.ts index 6b024a5f8ee..8cc71a82136 100644 --- a/packages/compiler-vapor/src/transforms/transformElement.ts +++ b/packages/compiler-vapor/src/transforms/transformElement.ts @@ -236,7 +236,9 @@ function transformNativeElement( needsQuoting = /[\s>]|^["'=]/.test(value) if (needsQuoting) { - template += `="${value}"` + const encoded = value.replace(/"/g, '"') + + template += `="${encoded}"` } else { template += `=${value}` } From ba9cd1c7460b03b0f71c7cfc7f9ee32b29a7bd74 Mon Sep 17 00:00:00 2001 From: Mary Date: Fri, 25 Jul 2025 14:02:58 +0700 Subject: [PATCH 5/5] chore(compiler-vapor): add runtime test for attribute parsing behavior --- .../__tests__/dom/template.spec.ts | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/packages/runtime-vapor/__tests__/dom/template.spec.ts b/packages/runtime-vapor/__tests__/dom/template.spec.ts index 85de30987a5..b10d7abfd9f 100644 --- a/packages/runtime-vapor/__tests__/dom/template.spec.ts +++ b/packages/runtime-vapor/__tests__/dom/template.spec.ts @@ -40,4 +40,25 @@ describe('api: template', () => { expect(nthChild(root, 2)).toBe(root.childNodes[2]) expect(next(b)).toBe(root.childNodes[2]) }) + + test('attribute quote omission', () => { + { + const t = template('
') + const root = t() as HTMLElement + + expect(root.attributes).toHaveLength(3) + expect(root.getAttribute('id')).toBe('foo') + expect(root.getAttribute('class')).toBe('bar') + expect(root.getAttribute('alt')).toBe('`<="foo') + } + + { + const t = template('
') + const root = t() as HTMLElement + + expect(root.attributes).toHaveLength(2) + expect(root.getAttribute('id')).toBe('foo>bar') + expect(root.getAttribute('class')).toBe('has whitespace') + } + }) })