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(); }