fix(compiler-vapor): keep the space after a quoted attribute value
Project: vuejs/core | Language: TypeScript | Concepts: serialization, compiler-internals, backwards-compatibility | Difficulty: mid
Link: https://github.com/vuejs/core/pull/15259
The problem
When compiling templates to static HTML strings, the compiler was omitting the space between attributes if the previous attribute's value was quoted. For example, <div class="a b" title="c"> would compile to <div class="a b"title=c>, which is invalid HTML (missing whitespace between attributes). Browsers recover from this, but stricter tools or re-parsers may fail.
Why it's non-trivial
A naive fix might just always add spaces, but the original code tried to optimize by omitting spaces in certain cases, based on whether the previous attribute was quoted. However, this led to invalid HTML output. The fix must ensure that attribute separation always produces valid HTML, regardless of quoting.
The approach
The core fix is in packages/compiler-vapor/src/transforms/transformElement.ts. The old logic tried to track whether the previous attribute was quoted, and only added a space if it wasn't:
- // 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
const appendTemplateProp = (
key: string,
value: string = '',
generated: boolean = false,
) => {
- if (!prevWasQuoted) template += ` `
- template += key
+ template += ` ${key}`
Annotation:
- The
prevWasQuotedvariable and its logic have been removed. - Now, a space is always added before every attribute key, ensuring proper separation.
Handling attribute values:
if (value) {
const escapedValue = generated
? escapeGeneratedAttrValue(value)
: value.replace(/"/g, '"')
- template += (prevWasQuoted = NEEDS_QUOTES_RE.test(value))
+ template += NEEDS_QUOTES_RE.test(value)
? `="${escapedValue}"`
: `=${escapedValue}`
- } else {
- prevWasQuoted = false
}
Annotation:
- The logic for updating
prevWasQuotedis removed. - Quoting logic for attribute values remains, but it no longer affects spacing between attributes.
Special handling for import expressions:
- 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}"`
- prevWasQuoted = true
+ template += ` ${key.content}="${IMPORT_EXP_START}${values[0].content}${IMPORT_EXP_END}"`
Annotation:
- The space is now always added before the attribute, not conditionally.
Tests were updated to reflect the new, always-correct output. For example, before:
-const t0 = _template("<div title=\"has whitespace\"alt=\""contains quotes"\"data-targets=\"foo>bar\">", 3)
+const t0 = _template("<div title=\"has whitespace\" alt=\""contains quotes"\" data-targets=\"foo>bar\">", 3)
Annotation:
- The generated template string now always includes spaces between attributes.
From the review thread
No notable review discussion on this PR.
Takeaways
- When serializing HTML, always emit a space between attributes, regardless of quoting; omitting it can produce invalid HTML.
- Optimizations that depend on subtle HTML parsing rules can easily introduce bugs—prefer clarity and spec compliance over micro-optimizations.
- Automated tests should cover edge cases of serialization, especially when output is consumed by other parsers or tools.