diff --git a/.changeset/fix-html-lower-case.md b/.changeset/fix-html-lower-case.md
new file mode 100644
index 00000000..56fc6e61
--- /dev/null
+++ b/.changeset/fix-html-lower-case.md
@@ -0,0 +1,5 @@
+---
+"preact-render-to-string": patch
+---
+
+Replace `HTML_LOWER_CASE` regex with an explicit `Set` to prevent false matches on custom element properties (e.g. `channelId` being incorrectly lowercased to `channelid`)
diff --git a/src/index.js b/src/index.js
index f1bf5b93..39b267c2 100644
--- a/src/index.js
+++ b/src/index.js
@@ -300,6 +300,8 @@ function _renderToString(
let type = vnode.type,
props = vnode.props;
+
+ const isCustomElement = type.includes("-");
// Invoke rendering on Components
if (typeof type == 'function') {
@@ -666,7 +668,7 @@ function _renderToString(
? 'panose-1'
: name.replace(/([A-Z])/g, '-$1').toLowerCase();
}
- } else if (HTML_LOWER_CASE.test(name)) {
+ } else if (!isCustomElement && HTML_LOWER_CASE.has(name)) {
name = name.toLowerCase();
}
}
diff --git a/src/lib/util.js b/src/lib/util.js
index 4be4e192..f4c9e817 100644
--- a/src/lib/util.js
+++ b/src/lib/util.js
@@ -1,10 +1,54 @@
import { DIRTY, BITS } from './constants';
export const VOID_ELEMENTS = /^(?:area|base|br|col|embed|hr|img|input|link|meta|param|source|track|wbr)$/;
+
// oxlint-disable-next-line no-control-regex
export const UNSAFE_NAME = /[\s\n\\/='"\0<>]/;
export const NAMESPACE_REPLACE_REGEX = /^(xlink|xmlns|xml)([A-Z])/;
-export const HTML_LOWER_CASE = /^(?:accessK|auto[A-Z]|cell|ch|col|cont|cross|dateT|encT|form[A-Z]|frame|hrefL|inputM|maxL|minL|noV|playsI|popoverT|readO|rowS|src[A-Z]|tabI|useM|item[A-Z])/;
+export const HTML_LOWER_CASE = new Set([
+ 'accessKey',
+ 'autoCapitalize',
+ 'autoComplete',
+ 'autoCorrect',
+ 'autoFocus',
+ 'autoPlay',
+ 'cellPadding',
+ 'cellSpacing',
+ 'charSet',
+ 'colSpan',
+ 'contentEditable',
+ 'contextMenu',
+ 'controlsList',
+ 'crossOrigin',
+ 'dateTime',
+ 'encType',
+ 'formAction',
+ 'formEncType',
+ 'formMethod',
+ 'formNoValidate',
+ 'formTarget',
+ 'frameBorder',
+ 'hrefLang',
+ 'inputMode',
+ 'maxLength',
+ 'minLength',
+ 'noValidate',
+ 'playsInline',
+ 'popoverTarget',
+ 'popoverTargetAction',
+ 'readOnly',
+ 'rowSpan',
+ 'srcDoc',
+ 'srcLang',
+ 'srcSet',
+ 'tabIndex',
+ 'useMap',
+ 'itemScope',
+ 'itemType',
+ 'itemID',
+ 'itemRef',
+ 'itemProp'
+]);
export 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/;
// Boolean DOM properties that translate to enumerated ('true'/'false') attributes
diff --git a/src/pretty.js b/src/pretty.js
index fe630744..f9c62e9c 100644
--- a/src/pretty.js
+++ b/src/pretty.js
@@ -129,6 +129,8 @@ function _renderToStringPretty(
props = vnode.props,
isComponent = false;
+ const isCustomElement = nodeName.includes("-");
+
// components
if (typeof nodeName === 'function') {
isComponent = true;
@@ -299,7 +301,7 @@ function _renderToStringPretty(
? 'panose-1'
: name.replace(/([A-Z])/g, '-$1').toLowerCase();
}
- } else if (HTML_LOWER_CASE.test(name)) {
+ } else if (!isCustomElement && HTML_LOWER_CASE.has(name)) {
name = name.toLowerCase();
}