]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
refactor(compiler-vapor): skip unnecessary attribute quoting in templates (#13673)
authormary <148872143+mary-ext@users.noreply.github.com>
Sun, 4 Jan 2026 07:00:36 +0000 (14:00 +0700)
committerGitHub <noreply@github.com>
Sun, 4 Jan 2026 07:00:36 +0000 (15:00 +0800)
Co-authored-by: daiwei <daiwei521@126.com>
packages/compiler-vapor/__tests__/__snapshots__/compile.spec.ts.snap
packages/compiler-vapor/__tests__/compile.spec.ts
packages/compiler-vapor/__tests__/transforms/__snapshots__/templateTransformAssetUrl.spec.ts.snap
packages/compiler-vapor/__tests__/transforms/__snapshots__/templateTransformSrcset.spec.ts.snap
packages/compiler-vapor/__tests__/transforms/__snapshots__/transformElement.spec.ts.snap
packages/compiler-vapor/__tests__/transforms/__snapshots__/vBind.spec.ts.snap
packages/compiler-vapor/__tests__/transforms/__snapshots__/vModel.spec.ts.snap
packages/compiler-vapor/__tests__/transforms/templateTransformAssetUrl.spec.ts
packages/compiler-vapor/__tests__/transforms/transformElement.spec.ts
packages/compiler-vapor/src/transforms/transformElement.ts
packages/runtime-vapor/__tests__/dom/template.spec.ts

index 467cdbdff0ca845a58d01378a462dc4f5df1e294..61463860d213d3b4e23cff45f634cda18696bca2 100644 (file)
@@ -140,7 +140,7 @@ export function render(_ctx) {
 
 exports[`compile > directives > v-pre > basic 1`] = `
 "import { template as _template } from 'vue';
-const t0 = _template("<div :id=\\"foo\\"><Comp></Comp>{{ bar }}", true)
+const t0 = _template("<div :id=foo><Comp></Comp>{{ 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("<div :id=\\"foo\\"><Comp></Comp>{{ bar }}")
+const t0 = _template("<div :id=foo><Comp></Comp>{{ bar }}")
 const t1 = _template("<div> ")
 
 export function render(_ctx, $props, $emit, $attrs, $slots) {
index 6af36a7875c741e3a5da43edb7b7698ad1135b20..85cc0b5a7da5e4e704cbede989f90d76020e4e88 100644 (file)
@@ -67,7 +67,7 @@ describe('compile', () => {
 
         expect(code).toMatchSnapshot()
         expect(code).contains(
-          JSON.stringify('<div :id="foo"><Comp></Comp>{{ bar }}'),
+          JSON.stringify('<div :id=foo><Comp></Comp>{{ bar }}'),
         )
         expect(code).not.contains('effect')
       })
index b48877ebf7a700b4223bc6e48d5b9025e6063520..766937617f2bd10b302f53968510e9c82b3ca4e4 100644 (file)
@@ -2,7 +2,7 @@
 
 exports[`compiler sfc: transform asset url > should allow for full base URLs, with paths 1`] = `
 "import { template as _template } from 'vue';
-const t0 = _template("<img src=\\"http://localhost:3000/src/logo.png\\">", true)
+const t0 = _template("<img src=http://localhost:3000/src/logo.png>", true)
 
 export function render(_ctx) {
   const n0 = t0()
@@ -12,7 +12,7 @@ export function render(_ctx) {
 
 exports[`compiler sfc: transform asset url > should allow for full base URLs, without paths 1`] = `
 "import { template as _template } from 'vue';
-const t0 = _template("<img src=\\"http://localhost:3000/logo.png\\">", true)
+const t0 = _template("<img src=http://localhost:3000/logo.png>", true)
 
 export function render(_ctx) {
   const n0 = t0()
@@ -22,7 +22,7 @@ export function render(_ctx) {
 
 exports[`compiler sfc: transform asset url > should allow for full base URLs, without port 1`] = `
 "import { template as _template } from 'vue';
-const t0 = _template("<img src=\\"http://localhost/logo.png\\">", true)
+const t0 = _template("<img src=http://localhost/logo.png>", true)
 
 export function render(_ctx) {
   const n0 = t0()
@@ -32,7 +32,7 @@ export function render(_ctx) {
 
 exports[`compiler sfc: transform asset url > should allow for full base URLs, without protocol 1`] = `
 "import { template as _template } from 'vue';
-const t0 = _template("<img src=\\"//localhost/logo.png\\">", true)
+const t0 = _template("<img src=//localhost/logo.png>", true)
 
 export function render(_ctx) {
   const n0 = t0()
@@ -54,7 +54,7 @@ export function render(_ctx) {
 
 exports[`compiler sfc: transform asset url > support uri is empty 1`] = `
 "import { template as _template } from 'vue';
-const t0 = _template("<use href=\\"\\">", true, 1)
+const t0 = _template("<use href>", true, 1)
 
 export function render(_ctx) {
   const n0 = t0()
@@ -69,10 +69,10 @@ import _imports_1 from 'fixtures/logo.png';
 import _imports_2 from '/fixtures/logo.png';
 const t0 = _template("<img src=\\"" + _imports_0 + "\\">")
 const t1 = _template("<img src=\\"" + _imports_1 + "\\">")
-const t2 = _template("<img src=\\"http://example.com/fixtures/logo.png\\">")
-const t3 = _template("<img src=\\"//example.com/fixtures/logo.png\\">")
+const t2 = _template("<img src=http://example.com/fixtures/logo.png>")
+const t3 = _template("<img src=//example.com/fixtures/logo.png>")
 const t4 = _template("<img src=\\"" + _imports_2 + "\\">")
-const t5 = _template("<img src=\\"data:image/png;base64,i\\">")
+const t5 = _template("<img src=data:image/png;base64,i>")
 
 export function render(_ctx) {
   const n0 = t0()
@@ -90,7 +90,7 @@ exports[`compiler sfc: transform asset url > transform with stringify 1`] = `
 "import { template as _template } from 'vue';
 import _imports_0 from './bar.png';
 import _imports_1 from '/bar.png';
-const t0 = _template("<div><img src=\\"" + _imports_0 + "\\"><img src=\\"" + _imports_1 + "\\"><img src=\\"https://foo.bar/baz.png\\"><img src=\\"//foo.bar/baz.png\\"><img src=\\"" + _imports_0 + "\\">", true)
+const t0 = _template("<div><img src=\\"" + _imports_0 + "\\"><img src=\\"" + _imports_1 + "\\"><img src=https://foo.bar/baz.png><img src=//foo.bar/baz.png><img src=\\"" + _imports_0 + "\\">", true)
 
 export function render(_ctx) {
   const n0 = t0()
@@ -102,7 +102,7 @@ exports[`compiler sfc: transform asset url > with explicit base 1`] = `
 "import { template as _template } from 'vue';
 import _imports_0 from 'bar.png';
 import _imports_1 from '@theme/bar.png';
-const t0 = _template("<img src=\\"/foo/bar.png\\">")
+const t0 = _template("<img src=/foo/bar.png>")
 const t1 = _template("<img src=\\"" + _imports_0 + "\\">")
 const t2 = _template("<img src=\\"" + _imports_1 + "\\">")
 
@@ -121,8 +121,8 @@ import _imports_0 from './bar.png';
 import _imports_1 from '/bar.png';
 const t0 = _template("<img src=\\"" + _imports_0 + "\\">")
 const t1 = _template("<img src=\\"" + _imports_1 + "\\">")
-const t2 = _template("<img src=\\"https://foo.bar/baz.png\\">")
-const t3 = _template("<img src=\\"//foo.bar/baz.png\\">")
+const t2 = _template("<img src=https://foo.bar/baz.png>")
+const t3 = _template("<img src=//foo.bar/baz.png>")
 
 export function render(_ctx) {
   const n0 = t0()
index f6dc18a73cc6568a2be01ed7de4c998736450710..d87283d30ec1d1a10e27ef805486ed94dea13b72 100644 (file)
@@ -18,17 +18,17 @@ exports[`compiler sfc: transform srcset > transform srcset 1`] = `
 "import { template as _template } from 'vue';
 import _imports_0 from './logo.png';
 import _imports_1 from '/logo.png';
-const t0 = _template("<img src=\\"" + _imports_0 + "\\" srcset>")
-const t1 = _template("<img src=\\"" + _imports_0 + "\\" srcset=\\"" + _imports_0 + "\\">")
-const t2 = _template("<img src=\\"" + _imports_0 + "\\" srcset=\\"" + _imports_0 + ' 2x' + "\\">")
-const t3 = _template("<img src=\\"" + _imports_0 + "\\" srcset=\\"" + _imports_0 + ', ' + _imports_0 + ' 2x' + "\\">")
-const t4 = _template("<img src=\\"" + _imports_0 + "\\" srcset=\\"" + _imports_0 + ' 2x, ' + _imports_0 + "\\">")
-const t5 = _template("<img src=\\"" + _imports_0 + "\\" srcset=\\"" + _imports_0 + ' 2x, ' + _imports_0 + ' 3x' + "\\">")
-const t6 = _template("<img src=\\"" + _imports_0 + "\\" srcset=\\"" + _imports_0 + ', ' + _imports_0 + ' 2x, ' + _imports_0 + ' 3x' + "\\">")
-const t7 = _template("<img src=\\"" + _imports_1 + "\\" srcset=\\"" + _imports_1 + ', ' + _imports_1 + ' 2x' + "\\">")
-const t8 = _template("<img src=\\"https://example.com/logo.png\\" srcset=\\"https://example.com/logo.png, https://example.com/logo.png 2x\\">")
-const t9 = _template("<img src=\\"" + _imports_1 + "\\" srcset=\\"" + _imports_1 + ', ' + _imports_0 + ' 2x' + "\\">")
-const t10 = _template("<img src=\\"data:image/png;base64,i\\" srcset=\\"data:image/png;base64,i 1x, data:image/png;base64,i 2x\\">")
+const t0 = _template("<img src=\\"" + _imports_0 + "\\"srcset>")
+const t1 = _template("<img src=\\"" + _imports_0 + "\\"srcset=\\"" + _imports_0 + "\\">")
+const t2 = _template("<img src=\\"" + _imports_0 + "\\"srcset=\\"" + _imports_0 + ' 2x' + "\\">")
+const t3 = _template("<img src=\\"" + _imports_0 + "\\"srcset=\\"" + _imports_0 + ', ' + _imports_0 + ' 2x' + "\\">")
+const t4 = _template("<img src=\\"" + _imports_0 + "\\"srcset=\\"" + _imports_0 + ' 2x, ' + _imports_0 + "\\">")
+const t5 = _template("<img src=\\"" + _imports_0 + "\\"srcset=\\"" + _imports_0 + ' 2x, ' + _imports_0 + ' 3x' + "\\">")
+const t6 = _template("<img src=\\"" + _imports_0 + "\\"srcset=\\"" + _imports_0 + ', ' + _imports_0 + ' 2x, ' + _imports_0 + ' 3x' + "\\">")
+const t7 = _template("<img src=\\"" + _imports_1 + "\\"srcset=\\"" + _imports_1 + ', ' + _imports_1 + ' 2x' + "\\">")
+const t8 = _template("<img src=https://example.com/logo.png srcset=\\"https://example.com/logo.png, https://example.com/logo.png 2x\\">")
+const t9 = _template("<img src=\\"" + _imports_1 + "\\"srcset=\\"" + _imports_1 + ', ' + _imports_0 + ' 2x' + "\\">")
+const t10 = _template("<img src=data:image/png;base64,i srcset=\\"data:image/png;base64,i 1x, data:image/png;base64,i 2x\\">")
 
 export function render(_ctx) {
   const n0 = t0()
@@ -51,17 +51,17 @@ exports[`compiler sfc: transform srcset > transform srcset w/ base 1`] = `
 "import { template as _template } from 'vue';
 import _imports_0 from '/logo.png';
 import _imports_1 from '/foo/logo.png';
-const t0 = _template("<img src=\\"/foo/logo.png\\" srcset>")
-const t1 = _template("<img src=\\"/foo/logo.png\\" srcset=\\"/foo/logo.png\\">")
-const t2 = _template("<img src=\\"/foo/logo.png\\" srcset=\\"/foo/logo.png 2x\\">")
-const t3 = _template("<img src=\\"/foo/logo.png\\" srcset=\\"/foo/logo.png, /foo/logo.png 2x\\">")
-const t4 = _template("<img src=\\"/foo/logo.png\\" srcset=\\"/foo/logo.png 2x, /foo/logo.png\\">")
-const t5 = _template("<img src=\\"/foo/logo.png\\" srcset=\\"/foo/logo.png 2x, /foo/logo.png 3x\\">")
-const t6 = _template("<img src=\\"/foo/logo.png\\" srcset=\\"/foo/logo.png, /foo/logo.png 2x, /foo/logo.png 3x\\">")
-const t7 = _template("<img src=\\"" + _imports_0 + "\\" srcset=\\"" + _imports_0 + ', ' + _imports_0 + ' 2x' + "\\">")
-const t8 = _template("<img src=\\"https://example.com/logo.png\\" srcset=\\"https://example.com/logo.png, https://example.com/logo.png 2x\\">")
-const t9 = _template("<img src=\\"" + _imports_0 + "\\" srcset=\\"" + _imports_0 + ', ' + _imports_1 + ' 2x' + "\\">")
-const t10 = _template("<img src=\\"data:image/png;base64,i\\" srcset=\\"data:image/png;base64,i 1x, data:image/png;base64,i 2x\\">")
+const t0 = _template("<img src=/foo/logo.png srcset>")
+const t1 = _template("<img src=/foo/logo.png srcset=/foo/logo.png>")
+const t2 = _template("<img src=/foo/logo.png srcset=\\"/foo/logo.png 2x\\">")
+const t3 = _template("<img src=/foo/logo.png srcset=\\"/foo/logo.png, /foo/logo.png 2x\\">")
+const t4 = _template("<img src=/foo/logo.png srcset=\\"/foo/logo.png 2x, /foo/logo.png\\">")
+const t5 = _template("<img src=/foo/logo.png srcset=\\"/foo/logo.png 2x, /foo/logo.png 3x\\">")
+const t6 = _template("<img src=/foo/logo.png srcset=\\"/foo/logo.png, /foo/logo.png 2x, /foo/logo.png 3x\\">")
+const t7 = _template("<img src=\\"" + _imports_0 + "\\"srcset=\\"" + _imports_0 + ', ' + _imports_0 + ' 2x' + "\\">")
+const t8 = _template("<img src=https://example.com/logo.png srcset=\\"https://example.com/logo.png, https://example.com/logo.png 2x\\">")
+const t9 = _template("<img src=\\"" + _imports_0 + "\\"srcset=\\"" + _imports_0 + ', ' + _imports_1 + ' 2x' + "\\">")
+const t10 = _template("<img src=data:image/png;base64,i srcset=\\"data:image/png;base64,i 1x, data:image/png;base64,i 2x\\">")
 
 export function render(_ctx) {
   const n0 = t0()
@@ -84,17 +84,17 @@ exports[`compiler sfc: transform srcset > transform srcset w/ includeAbsolute: t
 "import { template as _template } from 'vue';
 import _imports_0 from './logo.png';
 import _imports_1 from '/logo.png';
-const t0 = _template("<img src=\\"" + _imports_0 + "\\" srcset>")
-const t1 = _template("<img src=\\"" + _imports_0 + "\\" srcset=\\"" + _imports_0 + "\\">")
-const t2 = _template("<img src=\\"" + _imports_0 + "\\" srcset=\\"" + _imports_0 + ' 2x' + "\\">")
-const t3 = _template("<img src=\\"" + _imports_0 + "\\" srcset=\\"" + _imports_0 + ', ' + _imports_0 + ' 2x' + "\\">")
-const t4 = _template("<img src=\\"" + _imports_0 + "\\" srcset=\\"" + _imports_0 + ' 2x, ' + _imports_0 + "\\">")
-const t5 = _template("<img src=\\"" + _imports_0 + "\\" srcset=\\"" + _imports_0 + ' 2x, ' + _imports_0 + ' 3x' + "\\">")
-const t6 = _template("<img src=\\"" + _imports_0 + "\\" srcset=\\"" + _imports_0 + ', ' + _imports_0 + ' 2x, ' + _imports_0 + ' 3x' + "\\">")
-const t7 = _template("<img src=\\"" + _imports_1 + "\\" srcset=\\"" + _imports_1 + ', ' + _imports_1 + ' 2x' + "\\">")
-const t8 = _template("<img src=\\"https://example.com/logo.png\\" srcset=\\"https://example.com/logo.png, https://example.com/logo.png 2x\\">")
-const t9 = _template("<img src=\\"" + _imports_1 + "\\" srcset=\\"" + _imports_1 + ', ' + _imports_0 + ' 2x' + "\\">")
-const t10 = _template("<img src=\\"data:image/png;base64,i\\" srcset=\\"data:image/png;base64,i 1x, data:image/png;base64,i 2x\\">")
+const t0 = _template("<img src=\\"" + _imports_0 + "\\"srcset>")
+const t1 = _template("<img src=\\"" + _imports_0 + "\\"srcset=\\"" + _imports_0 + "\\">")
+const t2 = _template("<img src=\\"" + _imports_0 + "\\"srcset=\\"" + _imports_0 + ' 2x' + "\\">")
+const t3 = _template("<img src=\\"" + _imports_0 + "\\"srcset=\\"" + _imports_0 + ', ' + _imports_0 + ' 2x' + "\\">")
+const t4 = _template("<img src=\\"" + _imports_0 + "\\"srcset=\\"" + _imports_0 + ' 2x, ' + _imports_0 + "\\">")
+const t5 = _template("<img src=\\"" + _imports_0 + "\\"srcset=\\"" + _imports_0 + ' 2x, ' + _imports_0 + ' 3x' + "\\">")
+const t6 = _template("<img src=\\"" + _imports_0 + "\\"srcset=\\"" + _imports_0 + ', ' + _imports_0 + ' 2x, ' + _imports_0 + ' 3x' + "\\">")
+const t7 = _template("<img src=\\"" + _imports_1 + "\\"srcset=\\"" + _imports_1 + ', ' + _imports_1 + ' 2x' + "\\">")
+const t8 = _template("<img src=https://example.com/logo.png srcset=\\"https://example.com/logo.png, https://example.com/logo.png 2x\\">")
+const t9 = _template("<img src=\\"" + _imports_1 + "\\"srcset=\\"" + _imports_1 + ', ' + _imports_0 + ' 2x' + "\\">")
+const t10 = _template("<img src=data:image/png;base64,i srcset=\\"data:image/png;base64,i 1x, data:image/png;base64,i 2x\\">")
 
 export function render(_ctx) {
   const n0 = t0()
@@ -117,7 +117,7 @@ exports[`compiler sfc: transform srcset > transform srcset w/ stringify 1`] = `
 "import { template as _template } from 'vue';
 import _imports_0 from './logo.png';
 import _imports_1 from '/logo.png';
-const t0 = _template("<div><img src=\\"" + _imports_0 + "\\" srcset><img src=\\"" + _imports_0 + "\\" srcset=\\"" + _imports_0 + "\\"><img src=\\"" + _imports_0 + "\\" srcset=\\"" + _imports_0 + ' 2x' + "\\"><img src=\\"" + _imports_0 + "\\" srcset=\\"" + _imports_0 + ' 2x' + "\\"><img src=\\"" + _imports_0 + "\\" srcset=\\"" + _imports_0 + ', ' + _imports_0 + ' 2x' + "\\"><img src=\\"" + _imports_0 + "\\" srcset=\\"" + _imports_0 + ' 2x, ' + _imports_0 + "\\"><img src=\\"" + _imports_0 + "\\" srcset=\\"" + _imports_0 + ' 2x, ' + _imports_0 + ' 3x' + "\\"><img src=\\"" + _imports_0 + "\\" srcset=\\"" + _imports_0 + ', ' + _imports_0 + ' 2x, ' + _imports_0 + ' 3x' + "\\"><img src=\\"" + _imports_1 + "\\" srcset=\\"" + _imports_1 + ', ' + _imports_1 + ' 2x' + "\\"><img src=\\"https://example.com/logo.png\\" srcset=\\"https://example.com/logo.png, https://example.com/logo.png 2x\\"><img src=\\"" + _imports_1 + "\\" srcset=\\"" + _imports_1 + ', ' + _imports_0 + ' 2x' + "\\"><img src=\\"data:image/png;base64,i\\" srcset=\\"data:image/png;base64,i 1x, data:image/png;base64,i 2x\\">", true)
+const t0 = _template("<div><img src=\\"" + _imports_0 + "\\"srcset><img src=\\"" + _imports_0 + "\\"srcset=\\"" + _imports_0 + "\\"><img src=\\"" + _imports_0 + "\\"srcset=\\"" + _imports_0 + ' 2x' + "\\"><img src=\\"" + _imports_0 + "\\"srcset=\\"" + _imports_0 + ' 2x' + "\\"><img src=\\"" + _imports_0 + "\\"srcset=\\"" + _imports_0 + ', ' + _imports_0 + ' 2x' + "\\"><img src=\\"" + _imports_0 + "\\"srcset=\\"" + _imports_0 + ' 2x, ' + _imports_0 + "\\"><img src=\\"" + _imports_0 + "\\"srcset=\\"" + _imports_0 + ' 2x, ' + _imports_0 + ' 3x' + "\\"><img src=\\"" + _imports_0 + "\\"srcset=\\"" + _imports_0 + ', ' + _imports_0 + ' 2x, ' + _imports_0 + ' 3x' + "\\"><img src=\\"" + _imports_1 + "\\"srcset=\\"" + _imports_1 + ', ' + _imports_1 + ' 2x' + "\\"><img src=https://example.com/logo.png srcset=\\"https://example.com/logo.png, https://example.com/logo.png 2x\\"><img src=\\"" + _imports_1 + "\\"srcset=\\"" + _imports_1 + ', ' + _imports_0 + ' 2x' + "\\"><img src=data:image/png;base64,i srcset=\\"data:image/png;base64,i 1x, data:image/png;base64,i 2x\\">", true)
 
 export function render(_ctx) {
   const n0 = t0()
index 6eea04512f5a55a3e09c754dda2591ae1a96eef6..a80f5d88d9ad12a4278381742c4c3aa101bcd30e 100644 (file)
@@ -22,7 +22,7 @@ export function render(_ctx) {
 
 exports[`compiler: element transform > checkbox with static indeterminate 1`] = `
 "import { setProp as _setProp, template as _template } from 'vue';
-const t0 = _template("<input type=\\"checkbox\\">", true)
+const t0 = _template("<input type=checkbox>", true)
 
 export function render(_ctx) {
   const n0 = t0()
@@ -483,7 +483,7 @@ export function render(_ctx) {
 
 exports[`compiler: element transform > props + child 1`] = `
 "import { template as _template } from 'vue';
-const t0 = _template("<div id=\\"foo\\"><span>", true)
+const t0 = _template("<div id=foo><span>", true)
 
 export function render(_ctx) {
   const n0 = t0()
@@ -493,7 +493,7 @@ export function render(_ctx) {
 
 exports[`compiler: element transform > props + children 1`] = `
 "import { template as _template } from 'vue';
-const t0 = _template("<div id=\\"foo\\"><span><b></b></span><main><b></b><div><div><span></span><span>", true)
+const t0 = _template("<div id=foo><span><b></b></span><main><b></b><div><div><span></span><span>", true)
 
 export function render(_ctx) {
   const n0 = t0()
@@ -556,9 +556,99 @@ export function render(_ctx) {
 }"
 `;
 
-exports[`compiler: element transform > static props 1`] = `
+exports[`compiler: element transform > static props quoting > escapes double quotes in value 1`] = `
 "import { template as _template } from 'vue';
-const t0 = _template("<div id=\\"foo\\" class=\\"bar\\">", true)
+const t0 = _template("<div title=\\"say &quot;hello&quot;\\">", true)
+
+export function render(_ctx) {
+  const n0 = t0()
+  return n0
+}"
+`;
+
+exports[`compiler: element transform > static props quoting > mixed quoting with boolean attribute 1`] = `
+"import { template as _template } from 'vue';
+const t0 = _template("<div title=\\"has whitespace\\"inert data-targets=\\"foo>bar\\">", true)
+
+export function render(_ctx) {
+  const n0 = t0()
+  return n0
+}"
+`;
+
+exports[`compiler: element transform > static props quoting > quoted when value contains < 1`] = `
+"import { template as _template } from 'vue';
+const t0 = _template("<div data-expr=\\"a<b\\">", true)
+
+export function render(_ctx) {
+  const n0 = t0()
+  return n0
+}"
+`;
+
+exports[`compiler: element transform > static props quoting > quoted when value contains = 1`] = `
+"import { template as _template } from 'vue';
+const t0 = _template("<div data-expr=\\"a=b\\">", true)
+
+export function render(_ctx) {
+  const n0 = t0()
+  return n0
+}"
+`;
+
+exports[`compiler: element transform > static props quoting > quoted when value contains > 1`] = `
+"import { template as _template } from 'vue';
+const t0 = _template("<div data-expr=\\"a>b\\">", true)
+
+export function render(_ctx) {
+  const n0 = t0()
+  return n0
+}"
+`;
+
+exports[`compiler: element transform > static props quoting > quoted when value contains backtick 1`] = `
+"import { template as _template } from 'vue';
+const t0 = _template("<div title=\\"foo\`bar\\">", true)
+
+export function render(_ctx) {
+  const n0 = t0()
+  return n0
+}"
+`;
+
+exports[`compiler: element transform > static props quoting > quoted when value contains single quote 1`] = `
+"import { template as _template } from 'vue';
+const t0 = _template("<div title=\\"it's\\">", true)
+
+export function render(_ctx) {
+  const n0 = t0()
+  return n0
+}"
+`;
+
+exports[`compiler: element transform > static props quoting > quoted when value contains whitespace 1`] = `
+"import { template as _template } from 'vue';
+const t0 = _template("<div title=\\"has whitespace\\">", true)
+
+export function render(_ctx) {
+  const n0 = t0()
+  return n0
+}"
+`;
+
+exports[`compiler: element transform > static props quoting > space omitted after quoted attribute 1`] = `
+"import { template as _template } from 'vue';
+const t0 = _template("<div title=\\"has whitespace\\"alt=\\"&quot;contains quotes&quot;\\"data-targets=\\"foo>bar\\">", true)
+
+export function render(_ctx) {
+  const n0 = t0()
+  return n0
+}"
+`;
+
+exports[`compiler: element transform > static props quoting > unquoted when value has no special chars 1`] = `
+"import { template as _template } from 'vue';
+const t0 = _template("<div id=foo class=bar>", true)
 
 export function render(_ctx) {
   const n0 = t0()
@@ -568,7 +658,7 @@ export function render(_ctx) {
 
 exports[`compiler: element transform > svg 1`] = `
 "import { template as _template } from 'vue';
-const t0 = _template("<svg><circle r=\\"40\\">", true, 1)
+const t0 = _template("<svg><circle r=40>", true, 1)
 
 export function render(_ctx) {
   const n0 = t0()
index df90ca2e64890bc9b7d6bda490568648a56c87e0..39e26e3230cfc1c4820e489a182a64fc5fa94144 100644 (file)
@@ -786,7 +786,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("<div e=\\"2\\" f=\\"foo1\\" g=\\"1\\" h=\\"1\\">", true)
+const t0 = _template("<div e=2 f=foo1 g=1 h=1>", true)
 
 export function render(_ctx, $props, $emit, $attrs, $slots) {
   const n0 = t0()
index dad14ded140c24f18bc8bab575753051dd1e9e8d..a6f0a2abfa56f1393567343a557f0d2f93f7b145 100644 (file)
@@ -156,7 +156,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("<input type=\\"checkbox\\">", true)
+const t0 = _template("<input type=checkbox>", true)
 
 export function render(_ctx) {
   const n0 = t0()
@@ -178,7 +178,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("<input type=\\"radio\\">", true)
+const t0 = _template("<input type=radio>", true)
 
 export function render(_ctx) {
   const n0 = t0()
@@ -189,7 +189,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("<input type=\\"text\\">", true)
+const t0 = _template("<input type=text>", true)
 
 export function render(_ctx) {
   const n0 = t0()
index 225d18aded20d4bf9e1fb97d5ea2a8d72ff6045c..80d4660b240c26b1a97c119bef74929d1298100d 100644 (file)
@@ -98,7 +98,7 @@ describe('compiler sfc: transform asset url', () => {
       </svg>`,
     )
     // should not remove it
-    expect(code).toMatch(`xlink:href=\\"#myCircle\\"`) // compiled to template string, not object, so remove quotes
+    expect(code).toMatch(`xlink:href=#myCircle`) // compiled to template string, not object, so remove quotes
   })
 
   test('should allow for full base URLs, with paths', () => {
index a1cf589c61d910324a8549df67ecaf1710d76800..dede6cb5264d1b96debafdbfdd14fc8566df98a2 100644 (file)
@@ -573,18 +573,6 @@ describe('compiler: element transform', () => {
     })
   })
 
-  test('static props', () => {
-    const { code, ir } = compileWithElementTransform(
-      `<div id="foo" class="bar" />`,
-    )
-
-    const template = '<div id="foo" class="bar">'
-    expect(code).toMatchSnapshot()
-    expect(code).contains(JSON.stringify(template))
-    expect([...ir.template.keys()]).toMatchObject([template])
-    expect(ir.block.effect).lengthOf(0)
-  })
-
   test('checkbox with static indeterminate', () => {
     const { code } = compileWithElementTransform(
       `<input type="checkbox" indeterminate/>`,
@@ -599,7 +587,7 @@ describe('compiler: element transform', () => {
       `<div id="foo"><span/></div>`,
     )
 
-    const template = '<div id="foo"><span>'
+    const template = '<div id=foo><span>'
     expect(code).toMatchSnapshot()
     expect(code).contains(JSON.stringify(template))
     expect([...ir.template.keys()]).toMatchObject([template])
@@ -612,7 +600,7 @@ describe('compiler: element transform', () => {
     )
 
     const template =
-      '<div id="foo"><span><b></b></span><main><b></b><div><div><span></span><span>'
+      '<div id=foo><span><b></b></span><main><b></b><div><div><span></span><span>'
     expect(code).toMatchSnapshot()
     expect(code).contains(JSON.stringify(template))
     expect([...ir.template.keys()]).toMatchObject([template])
@@ -1112,9 +1100,10 @@ describe('compiler: element transform', () => {
     const t = `<svg><circle r="40"></circle></svg>`
     const { code, ir } = compileWithElementTransform(t)
     expect(code).toMatchSnapshot()
-    expect(code).contains('_template("<svg><circle r=\\"40\\">", true, 1)')
-    expect([...ir.template.keys()]).toMatchObject(['<svg><circle r="40">'])
-    expect(ir.template.get('<svg><circle r="40">')).toBe(1)
+    const expectedTemplate = '<svg><circle r=40>'
+    expect(code).contains(`_template("${expectedTemplate}", true, 1)`)
+    expect([...ir.template.keys()]).toMatchObject([expectedTemplate])
+    expect(ir.template.get(expectedTemplate)).toBe(1)
   })
 
   test('MathML', () => {
@@ -1125,4 +1114,116 @@ describe('compiler: element transform', () => {
     expect([...ir.template.keys()]).toMatchObject(['<math><mrow><mi>x'])
     expect(ir.template.get('<math><mrow><mi>x')).toBe(2)
   })
+
+  describe('static props quoting', () => {
+    test('unquoted when value has no special chars', () => {
+      const { code, ir } = compileWithElementTransform(
+        `<div id="foo" class="bar" />`,
+      )
+
+      const template = '<div id=foo class=bar>'
+      expect(code).toMatchSnapshot()
+      expect(code).contains(JSON.stringify(template))
+      expect([...ir.template.keys()]).toMatchObject([template])
+    })
+
+    test('quoted when value contains whitespace', () => {
+      const { code, ir } = compileWithElementTransform(
+        `<div title="has whitespace" />`,
+      )
+
+      const template = '<div title="has whitespace">'
+      expect(code).toMatchSnapshot()
+      expect(code).contains(JSON.stringify(template))
+      expect([...ir.template.keys()]).toMatchObject([template])
+    })
+
+    test('quoted when value contains >', () => {
+      const { code, ir } = compileWithElementTransform(
+        `<div data-expr="a>b" />`,
+      )
+
+      const template = '<div data-expr="a>b">'
+      expect(code).toMatchSnapshot()
+      expect(code).contains(JSON.stringify(template))
+      expect([...ir.template.keys()]).toMatchObject([template])
+    })
+
+    test('quoted when value contains <', () => {
+      const { code, ir } = compileWithElementTransform(
+        `<div data-expr="a<b" />`,
+      )
+
+      const template = '<div data-expr="a<b">'
+      expect(code).toMatchSnapshot()
+      expect(code).contains(JSON.stringify(template))
+      expect([...ir.template.keys()]).toMatchObject([template])
+    })
+
+    test('quoted when value contains =', () => {
+      const { code, ir } = compileWithElementTransform(
+        `<div data-expr="a=b" />`,
+      )
+
+      const template = '<div data-expr="a=b">'
+      expect(code).toMatchSnapshot()
+      expect(code).contains(JSON.stringify(template))
+      expect([...ir.template.keys()]).toMatchObject([template])
+    })
+
+    test('quoted when value contains single quote', () => {
+      const { code, ir } = compileWithElementTransform(`<div title="it's" />`)
+
+      const template = `<div title="it's">`
+      expect(code).toMatchSnapshot()
+      expect(code).contains(JSON.stringify(template))
+      expect([...ir.template.keys()]).toMatchObject([template])
+    })
+
+    test('quoted when value contains backtick', () => {
+      const { code, ir } = compileWithElementTransform(
+        '<div title="foo`bar" />',
+      )
+
+      const template = '<div title="foo`bar">'
+      expect(code).toMatchSnapshot()
+      expect(code).contains(JSON.stringify(template))
+      expect([...ir.template.keys()]).toMatchObject([template])
+    })
+
+    test('escapes double quotes in value', () => {
+      const { code, ir } = compileWithElementTransform(
+        `<div title='say "hello"' />`,
+      )
+
+      const template = '<div title="say &quot;hello&quot;">'
+      expect(code).toMatchSnapshot()
+      expect(code).contains(JSON.stringify(template))
+      expect([...ir.template.keys()]).toMatchObject([template])
+    })
+
+    test('mixed quoting with boolean attribute', () => {
+      const { code, ir } = compileWithElementTransform(
+        `<div title="has whitespace" inert data-targets="foo>bar" />`,
+      )
+
+      const template =
+        '<div title="has whitespace"inert data-targets="foo>bar">'
+      expect(code).toMatchSnapshot()
+      expect(code).contains(JSON.stringify(template))
+      expect([...ir.template.keys()]).toMatchObject([template])
+    })
+
+    test('space omitted after quoted attribute', () => {
+      const { code, ir } = compileWithElementTransform(
+        `<div title="has whitespace" alt='"contains quotes"' data-targets="foo>bar" />`,
+      )
+
+      const template =
+        '<div title="has whitespace"alt="&quot;contains quotes&quot;"data-targets="foo>bar">'
+      expect(code).toMatchSnapshot()
+      expect(code).contains(JSON.stringify(template))
+      expect([...ir.template.keys()]).toMatchObject([template])
+    })
+  })
 })
index bb20787e67cd6b96da0ca0f29df09d46802af3f6..81ae8295a3057ae9767e4d8184cfa19c318591b5 100644 (file)
@@ -283,6 +283,11 @@ function resolveSetupReference(name: string, context: TransformContext) {
 // keys cannot be a part of the template and need to be set dynamically
 const dynamicKeys = ['indeterminate']
 
+// The attribute value can remain unquoted if it doesn't contain ASCII whitespace
+// or any of " ' ` = < or >.
+// https://html.spec.whatwg.org/multipage/introduction.html#intro-early-example
+const NEEDS_QUOTES_RE = /[\s"'`=<>]/
+
 function transformNativeElement(
   node: PlainElementNode,
   propsResult: PropsResult,
@@ -313,6 +318,9 @@ function transformNativeElement(
       getEffectIndex,
     )
   } else {
+    // tracks if previous attribute was quoted, allowing space omission
+    // e.g. `class="foo"id="bar"` is valid, `class=foo id=bar` needs space
+    let prevWasQuoted = false
     for (const prop of propsResult[1]) {
       const { key, values } = prop
       // handling asset imports
@@ -321,18 +329,28 @@ function transformNativeElement(
           values[0].content.includes(imported.exp.content),
         )
       ) {
+        if (!prevWasQuoted) template += ` `
         // add start and end markers to the import expression, so it can be replaced
         // with string concatenation in the generator, see genTemplates
-        template += ` ${key.content}="${IMPORT_EXP_START}${values[0].content}${IMPORT_EXP_END}"`
+        template += `${key.content}="${IMPORT_EXP_START}${values[0].content}${IMPORT_EXP_END}"`
+        prevWasQuoted = true
       } else if (
         key.isStatic &&
         values.length === 1 &&
         (values[0].isStatic || values[0].content === "''") &&
         !dynamicKeys.includes(key.content)
       ) {
-        template += ` ${key.content}`
-        if (values[0].content)
-          template += `="${values[0].content === "''" ? '' : values[0].content}"`
+        if (!prevWasQuoted) template += ` `
+        const value = values[0].content === "''" ? '' : values[0].content
+        template += key.content
+
+        if (value) {
+          template += (prevWasQuoted = NEEDS_QUOTES_RE.test(value))
+            ? `="${value.replace(/"/g, '&quot;')}"`
+            : `=${value}`
+        } else {
+          prevWasQuoted = false
+        }
       } else {
         dynamicProps.push(key.content)
         context.registerEffect(
index 792c55242b16102d858c784ad29c1be619c08226..7c5ab3d2dda2998dbe4dfeade374fe31e2c10cb1 100644 (file)
@@ -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('<div id=foo class=bar alt=`<="foo></div>')
+      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('<div id="foo>bar"class="has whitespace"></div>')
+      const root = t() as HTMLElement
+
+      expect(root.attributes).toHaveLength(2)
+      expect(root.getAttribute('id')).toBe('foo>bar')
+      expect(root.getAttribute('class')).toBe('has whitespace')
+    }
+  })
 })