From b433f3cd878814d6d141be359e5dafa9959b5f9e Mon Sep 17 00:00:00 2001 From: Bao Nguyen <39545125+giaBaoJS@users.noreply.github.com> Date: Tue, 25 Aug 2026 19:22:56 +0700 Subject: [PATCH] fix: preserve line feed character references in attribute values (#18691) Fixes #15555 Pass on whether we're in an attribute to not replace the charcode with the whitespace charcode in that case --- .changeset/tricky-donkeys-clap.md | 5 ++ .../src/compiler/phases/1-parse/utils/html.js | 15 ++++-- .../convert-entities-line-feed/input.svelte | 1 + .../convert-entities-line-feed/output.json | 53 +++++++++++++++++++ .../html-entity-line-feed/Component.svelte | 5 ++ .../samples/html-entity-line-feed/_config.js | 15 ++++++ .../samples/html-entity-line-feed/main.svelte | 9 ++++ 7 files changed, 98 insertions(+), 5 deletions(-) create mode 100644 .changeset/tricky-donkeys-clap.md create mode 100644 packages/svelte/tests/parser-legacy/samples/convert-entities-line-feed/input.svelte create mode 100644 packages/svelte/tests/parser-legacy/samples/convert-entities-line-feed/output.json create mode 100644 packages/svelte/tests/runtime-runes/samples/html-entity-line-feed/Component.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/html-entity-line-feed/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/html-entity-line-feed/main.svelte diff --git a/.changeset/tricky-donkeys-clap.md b/.changeset/tricky-donkeys-clap.md new file mode 100644 index 0000000000..b363b4ff57 --- /dev/null +++ b/.changeset/tricky-donkeys-clap.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: preserve line feed character references in attribute values diff --git a/packages/svelte/src/compiler/phases/1-parse/utils/html.js b/packages/svelte/src/compiler/phases/1-parse/utils/html.js index 1d660298c7..ccb2005054 100644 --- a/packages/svelte/src/compiler/phases/1-parse/utils/html.js +++ b/packages/svelte/src/compiler/phases/1-parse/utils/html.js @@ -60,7 +60,7 @@ export function decode_character_references(html, is_attribute_value) { return match; } - return String.fromCodePoint(validate_code(code)); + return String.fromCodePoint(validate_code(code, is_attribute_value)); } ); } @@ -75,10 +75,15 @@ const NUL = 0; // Also see: https://en.wikipedia.org/wiki/Plane_(Unicode) // Also see: https://html.spec.whatwg.org/multipage/parsing.html#preprocessing-the-input-stream -/** @param {number} code */ -function validate_code(code) { - // line feed becomes generic whitespace - if (code === 10) { +/** + * @param {number} code + * @param {boolean} is_attribute_value + */ +function validate_code(code, is_attribute_value) { + // line feed becomes generic whitespace, since it is collapsed along with the + // surrounding whitespace anyway. In an attribute value it is significant, so it + // is left alone there + if (code === 10 && !is_attribute_value) { return 32; } diff --git a/packages/svelte/tests/parser-legacy/samples/convert-entities-line-feed/input.svelte b/packages/svelte/tests/parser-legacy/samples/convert-entities-line-feed/input.svelte new file mode 100644 index 0000000000..3ce2c605d6 --- /dev/null +++ b/packages/svelte/tests/parser-legacy/samples/convert-entities-line-feed/input.svelte @@ -0,0 +1 @@ +

A B

diff --git a/packages/svelte/tests/parser-legacy/samples/convert-entities-line-feed/output.json b/packages/svelte/tests/parser-legacy/samples/convert-entities-line-feed/output.json new file mode 100644 index 0000000000..5d1127ccdc --- /dev/null +++ b/packages/svelte/tests/parser-legacy/samples/convert-entities-line-feed/output.json @@ -0,0 +1,53 @@ +{ + "html": { + "type": "Fragment", + "start": 0, + "end": 32, + "children": [ + { + "type": "Element", + "start": 0, + "end": 32, + "name": "p", + "attributes": [ + { + "type": "Attribute", + "start": 3, + "end": 19, + "name": "title", + "name_loc": { + "start": { + "line": 1, + "column": 3, + "character": 3 + }, + "end": { + "line": 1, + "column": 8, + "character": 8 + } + }, + "value": [ + { + "start": 10, + "end": 18, + "type": "Text", + "raw": "A B", + "data": "A\nB" + } + ] + } + ], + "children": [ + { + "type": "Text", + "start": 20, + "end": 28, + "raw": "A B", + "data": "A B" + } + ] + } + ] + } +} diff --git a/packages/svelte/tests/runtime-runes/samples/html-entity-line-feed/Component.svelte b/packages/svelte/tests/runtime-runes/samples/html-entity-line-feed/Component.svelte new file mode 100644 index 0000000000..85b608c739 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/html-entity-line-feed/Component.svelte @@ -0,0 +1,5 @@ + + +{text} diff --git a/packages/svelte/tests/runtime-runes/samples/html-entity-line-feed/_config.js b/packages/svelte/tests/runtime-runes/samples/html-entity-line-feed/_config.js new file mode 100644 index 0000000000..f6befd7703 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/html-entity-line-feed/_config.js @@ -0,0 +1,15 @@ +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + // a line feed entity in an attribute value stays a line feed... + assert.equal(target.querySelector('#attr')?.getAttribute('title'), 'A\nB'); + assert.equal(target.querySelector('#attr-decimal')?.getAttribute('title'), 'A\nB'); + + // ...including when the attribute is a prop passed to a component + assert.equal(target.querySelector('#prop')?.textContent, 'A\nB'); + + // other entities in attribute values still decode + assert.equal(target.querySelector('#attr-other-entity')?.getAttribute('title'), '©'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/html-entity-line-feed/main.svelte b/packages/svelte/tests/runtime-runes/samples/html-entity-line-feed/main.svelte new file mode 100644 index 0000000000..d7f5684351 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/html-entity-line-feed/main.svelte @@ -0,0 +1,9 @@ + + + + + + +