Repository: preactjs/preact-render-to-string
Pull request #310 (2023-07-31 10:09:29)

Fix incorrect namespaced attributes rendering
Resolves the remaining scenarios mentioned in #307 .

changeset-bot[bot]: ###  ⚠️  No Changeset found

Latest commit: 551423625dabfb1085ee7f0bde1f727283bcea5c

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. **If these changes should result in a version bump, you need to add a changeset.**

<details><summary>This PR includes no changesets</summary>

  When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

</details>

[Click here to learn what changesets are, and how to add one](https://github.com/changesets/changesets/blob/main/docs/adding-a-changeset.md).

[Click here if you're a maintainer who wants to add a changeset to this PR](https://github.com/preactjs/preact-render-to-string/new/xml-namespaces?filename=.changeset/healthy-bottles-scream.md&value=---%0A%22preact-render-to-string%22%3A%20patch%0A---%0A%0AFix%20incorrect%20namespaced%20attributes%20rendering%0A)



Patch:
diff --git a/.changeset/five-stingrays-return.md b/.changeset/five-stingrays-return.md
new file mode 100644
index 0000000..8f3db85
--- /dev/null
+++ b/.changeset/five-stingrays-return.md
@@ -0,0 +1,5 @@
+---
+'preact-render-to-string': patch
+---
+
+Fix casing of namespaced attribute names
diff --git a/src/index.js b/src/index.js
index d1e8b48..781ccc4 100644
--- a/src/index.js
+++ b/src/index.js
@@ -1,4 +1,4 @@
-import { encodeEntities, styleObjToCss, UNSAFE_NAME, XLINK } from './util.js';
+import { encodeEntities, styleObjToCss, UNSAFE_NAME } from './util.js';
 import { options, h, Fragment } from 'preact';
 import {
 	CHILDREN,
@@ -392,8 +392,8 @@ function _renderToString(vnode, context, isSvgMode, selectValue, parent) {
 				break;
 
 			default: {
-				if (isSvgMode && XLINK.test(name)) {
-					name = name.toLowerCase().replace(XLINK_REPLACE_REGEX, 'xlink:');
+				if (NAMESPACE_REPLACE_REGEX.test(name)) {
+					name = name.replace(NAMESPACE_REPLACE_REGEX, '$1:$2').toLowerCase();
 				} else if (UNSAFE_NAME.test(name)) {
 					continue;
 				} else if ((name[4] === '-' || name === 'draggable') && v != null) {
@@ -407,8 +407,6 @@ function _renderToString(vnode, context, isSvgMode, selectValue, parent) {
 							name === 'panose1'
 								? 'panose-1'
 								: name.replace(/([A-Z])/g, '-$1').toLowerCase();
-					} else if (XML_REPLACE_REGEX.test(name)) {
-						name = name.toLowerCase().replace(XML_REPLACE_REGEX, 'xml:');
 					}
 				} else if (HTML_LOWER_CASE.test(name)) {
 					name = name.toLowerCase();
@@ -458,8 +456,7 @@ function _renderToString(vnode, context, isSvgMode, selectValue, parent) {
 
 const HTML_LOWER_CASE = /^accessK|^auto[A-Z]|^ch|^col|cont|cross|dateT|encT|form[A-Z]|frame|hrefL|inputM|maxL|minL|noV|playsI|readO|rowS|spellC|src[A-Z]|tabI|item[A-Z]/;
 const SVG_CAMEL_CASE = /^ac|^ali|arabic|basel|cap|clipPath$|clipRule$|color|dominant|enable|fill|flood|font|glyph[^R]|horiz|image|letter|lighting|marker[^WUH]|overline|panose|pointe|paint|rendering|shape|stop|strikethrough|stroke|text[^L]|transform|underline|unicode|units|^v[^i]|^w|^xH/;
-const XML_REPLACE_REGEX = /^xml:?/;
-const XLINK_REPLACE_REGEX = /^xlink:?/;
+const NAMESPACE_REPLACE_REGEX = /^(xlink|xmlns|xml)(:|[A-Z])/;
 const SELF_CLOSING = new Set([
 	'area',
 	'base',


Test patch:
diff --git a/test/render.test.js b/test/render.test.js
index 4df2250..9d00a0c 100644
--- a/test/render.test.js
+++ b/test/render.test.js
@@ -333,7 +333,7 @@ describe('render', () => {
 			);
 
 			expect(rendered).to.equal(
-				`<svg><image xlink:href="#"></image><foreignObject><div xlinkHref="#"></div></foreignObject><g><image xlink:href="#"></image></g></svg>`
+				`<svg><image xlink:href="#"></image><foreignObject><div xlink:href="#"></div></foreignObject><g><image xlink:href="#"></image></g></svg>`
 			);
 		});
 	});
@@ -1855,6 +1855,21 @@ describe('render', () => {
 			}
 		});
 
+		it('should replace namespaces', () => {
+			let rendered = render(
+				<svg
+					xmlns="http://www.w3.org/2000/svg"
+					xmlnsXlink="http://www.w3.org/1999/xlink"
+				>
+					<script xlinkHref="cool-script.js" />
+				</svg>
+			);
+
+			expect(rendered).to.equal(
+				'<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><script xlink:href="cool-script.js"></script></svg>'
+			);
+		});
+
 		it('should have correct HTML casing', () => {
 			let htmlAttributes = {
 				accept: 'accept',