From 55cb2b877ec61d4b4a5bc2dbc6a21b4c20eaa101 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vytenis=20Urbonavi=C4=8Dius?= Date: Mon, 5 Oct 2026 11:28:50 +0300 Subject: [PATCH] fix: quote attribute values containing apostrophes correctly When a value is single-quoted (JSON-like values, quoteStyle Single, or quoteStyle Smart when the value contains `"`), a `'` in it isn't escaped, so the output can be malformed HTML. Such values are now double-quoted, with `"` escaped as ". Co-Authored-By: Claude Opus 5.5 --- src/index.ts | 14 ++++++++++++-- test/test-core.spec.ts | 30 ++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 2 deletions(-) diff --git a/src/index.ts b/src/index.ts index a0db0f7..907b2c9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -278,7 +278,7 @@ export function render(tree?: Node | Node[], options: Options = {}): string { function makeAttr(key: string, attrValue: string | number | boolean, quoteStyle = 1): string { if (quoteStyle === 1) { // Single Quote - return ` ${key}='${attrValue}'`; + return makeSingleQuotedAttr(key, attrValue); } if (quoteStyle === 2) { @@ -288,9 +288,19 @@ export function render(tree?: Node | Node[], options: Options = {}): string { // Smart Quote if (typeof attrValue === 'string' && attrValue.includes('"')) { - return ` ${key}='${attrValue}'`; + return makeSingleQuotedAttr(key, attrValue); } return ` ${key}="${attrValue}"`; } + + function makeSingleQuotedAttr(key: string, attrValue: string | number | boolean): string { + // A `'` can't appear inside single quotes, + // so such values are double-quoted with `"` escaped instead. + if (typeof attrValue === 'string' && attrValue.includes('\'')) { + return ` ${key}="${attrValue.replace(/"/g, '"')}"`; + } + + return ` ${key}='${attrValue}'`; + } } diff --git a/test/test-core.spec.ts b/test/test-core.spec.ts index c9e59e4..657b3e1 100644 --- a/test/test-core.spec.ts +++ b/test/test-core.spec.ts @@ -133,6 +133,18 @@ test('{Attrs} {String} (json)', t => { t.is(render(fixture), expected); }); +test('{Attrs} {String} (json with apostrophe)', t => { + const fixture = { + attrs: { + 'x-data': '{"a":"it\'s"}', + }, + }; + + const expected = '
'; + + t.is(render(fixture), expected); +}); + test('{Content} {String}', t => { const fixture = {content: 'Hello world!'}; const expected = '
Hello world!
'; @@ -546,6 +558,24 @@ test('{Options} {quoteStyle} 0 - smart quote', t => { t.is(render(fixture, options), expected); }); +test('{Options} {quoteStyle} 1 - single quote with apostrophe', t => { + const options = {quoteStyle: quoteStyleEnum.Single}; + + const fixture = {tag: 'img', attrs: {alt: 'it\'s'}}; + const expected = 'it\'s'; + + t.is(render(fixture, options), expected); +}); + +test('{Options} {quoteStyle} 0 - smart quote with apostrophe', t => { + const options = {replaceQuote: false, quoteStyle: quoteStyleEnum.Smart}; + + const fixture = {tag: 'img', attrs: {alt: '"it\'s"'}}; + const expected = '"it\'s"'; + + t.is(render(fixture, options), expected); +}); + test('{QuoteStyle} for width/height attrs in img', t => { const fixture = { tag: 'img',