diff --git a/.changeset/fifty-chefs-mate.md b/.changeset/fifty-chefs-mate.md new file mode 100644 index 00000000..76ff2323 --- /dev/null +++ b/.changeset/fifty-chefs-mate.md @@ -0,0 +1,5 @@ +--- +'preact-render-to-string': patch +--- + +Fix incorrect casing of HTML attributes and SVG attributes diff --git a/src/index.js b/src/index.js index 45bb9bfc..d1e8b48e 100644 --- a/src/index.js +++ b/src/index.js @@ -384,6 +384,12 @@ function _renderToString(vnode, context, isSvgMode, selectValue, parent) { v = styleObjToCss(v); } break; + case 'acceptCharset': + name = 'accept-charset'; + break; + case 'httpEquiv': + name = 'http-equiv'; + break; default: { if (isSvgMode && XLINK.test(name)) { @@ -395,6 +401,17 @@ function _renderToString(vnode, context, isSvgMode, selectValue, parent) { // `draggable` is an enumerated attribute and not Boolean. A value of `true` or `false` is mandatory // https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/draggable v += ''; + } else if (isSvgMode) { + if (SVG_CAMEL_CASE.test(name)) { + name = + 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(); } } } @@ -439,6 +456,9 @@ function _renderToString(vnode, context, isSvgMode, selectValue, parent) { return s + '>' + html + ''; } +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 SELF_CLOSING = new Set([ 'area', diff --git a/test/render.test.js b/test/render.test.js index bd264b6b..4df2250c 100644 --- a/test/render.test.js +++ b/test/render.test.js @@ -1598,4 +1598,441 @@ describe('render', () => { }); }); }); + + describe('Attribute casing', () => { + it('should have correct SVG casing', () => { + const svgAttributes = { + accentHeight: 'accent-height', + accumulate: 'accumulate', + additive: 'additive', + alignmentBaseline: 'alignment-baseline', + allowReorder: 'allowReorder', // unsure + alphabetic: 'alphabetic', + amplitude: 'amplitude', + arabicForm: 'arabic-form', + ascent: 'ascent', + attributeName: 'attributeName', + attributeType: 'attributeType', + autoReverse: 'autoReverse', // unsure + azimuth: 'azimuth', + baseFrequency: 'baseFrequency', + baselineShift: 'baseline-shift', + baseProfile: 'baseProfile', + bbox: 'bbox', + begin: 'begin', + bias: 'bias', + by: 'by', + calcMode: 'calcMode', + capHeight: 'cap-height', + class: 'class', + clip: 'clip', + clipPathUnits: 'clipPathUnits', + clipPath: 'clip-path', + clipRule: 'clip-rule', + color: 'color', + colorInterpolation: 'color-interpolation', + colorInterpolationFilters: 'color-interpolation-filters', + colorProfile: 'color-profile', + colorRendering: 'color-rendering', + contentScriptType: 'contentScriptType', + contentStyleType: 'contentStyleType', + crossorigin: 'crossorigin', + cursor: 'cursor', + cx: 'cx', + cy: 'cy', + d: 'd', + decelerate: 'decelerate', + descent: 'descent', + diffuseConstant: 'diffuseConstant', + direction: 'direction', + display: 'display', + divisor: 'divisor', + dominantBaseline: 'dominant-baseline', + dur: 'dur', + dx: 'dx', + dy: 'dy', + edgeMode: 'edgeMode', + elevation: 'elevation', + enableBackground: 'enable-background', + end: 'end', + exponent: 'exponent', + fill: 'fill', + fillOpacity: 'fill-opacity', + fillRule: 'fill-rule', + filter: 'filter', + filterRes: 'filterRes', + filterUnits: 'filterUnits', + floodColor: 'flood-color', + floodOpacity: 'flood-opacity', + fontFamily: 'font-family', + fontSize: 'font-size', + fontSizeAdjust: 'font-size-adjust', + fontStretch: 'font-stretch', + fontStyle: 'font-style', + fontVariant: 'font-variant', + fontWeight: 'font-weight', + format: 'format', + from: 'from', + fx: 'fx', + fy: 'fy', + g1: 'g1', + g2: 'g2', + glyphName: 'glyph-name', + glyphOrientationHorizontal: 'glyph-orientation-horizontal', + glyphOrientationVertical: 'glyph-orientation-vertical', + glyphRef: 'glyphRef', + gradientTransform: 'gradientTransform', + gradientUnits: 'gradientUnits', + hanging: 'hanging', + horizAdvX: 'horiz-adv-x', + horizOriginX: 'horiz-origin-x', + ideographic: 'ideographic', + imageRendering: 'image-rendering', + in: 'in', + in2: 'in2', + intercept: 'intercept', + k: 'k', + k1: 'k1', + k2: 'k2', + k3: 'k3', + k4: 'k4', + kernelMatrix: 'kernelMatrix', + kernelUnitLength: 'kernelUnitLength', + kerning: 'kerning', + keyPoints: 'keyPoints', + keySplines: 'keySplines', + keyTimes: 'keyTimes', + lengthAdjust: 'lengthAdjust', + letterSpacing: 'letter-spacing', + lightingColor: 'lighting-color', + limitingConeAngle: 'limitingConeAngle', + local: 'local', + markerEnd: 'marker-end', + markerMid: 'marker-mid', + markerStart: 'marker-start', + markerHeight: 'markerHeight', + markerUnits: 'markerUnits', + markerWidth: 'markerWidth', + mask: 'mask', + maskContentUnits: 'maskContentUnits', + maskUnits: 'maskUnits', + mathematical: 'mathematical', + numOctaves: 'numOctaves', + offset: 'offset', + opacity: 'opacity', + operator: 'operator', + order: 'order', + orient: 'orient', + orientation: 'orientation', + origin: 'origin', + overflow: 'overflow', + overlinePosition: 'overline-position', + overlineThickness: 'overline-thickness', + panose1: 'panose-1', + paintOrder: 'paint-order', + pathLength: 'pathLength', + patternContentUnits: 'patternContentUnits', + patternTransform: 'patternTransform', + patternUnits: 'patternUnits', + pointerEvents: 'pointer-events', + points: 'points', + pointsAtX: 'pointsAtX', + pointsAtY: 'pointsAtY', + pointsAtZ: 'pointsAtZ', + preserveAlpha: 'preserveAlpha', + preserveAspectRatio: 'preserveAspectRatio', + primitiveUnits: 'primitiveUnits', + r: 'r', + radius: 'radius', + refX: 'refX', + refY: 'refY', + rel: 'rel', + renderingIntent: 'rendering-intent', + repeatCount: 'repeatCount', + repeatDur: 'repeatDur', + requiredExtensions: 'requiredExtensions', + requiredFeatures: 'requiredFeatures', + restart: 'restart', + result: 'result', + rotate: 'rotate', + rx: 'rx', + ry: 'ry', + scale: 'scale', + seed: 'seed', + shapeRendering: 'shape-rendering', + slope: 'slope', + spacing: 'spacing', + specularConstant: 'specularConstant', + specularExponent: 'specularExponent', + speed: 'speed', + spreadMethod: 'spreadMethod', + startOffset: 'startOffset', + stdDeviation: 'stdDeviation', + stemh: 'stemh', + stemv: 'stemv', + stitchTiles: 'stitchTiles', + stopColor: 'stop-color', + stopOpacity: 'stop-opacity', + strikethroughPosition: 'strikethrough-position', + strikethroughThickness: 'strikethrough-thickness', + string: 'string', + stroke: 'stroke', + strokeDasharray: 'stroke-dasharray', + strokeDashoffset: 'stroke-dashoffset', + strokeLinecap: 'stroke-linecap', + strokeLinejoin: 'stroke-linejoin', + strokeMiterlimit: 'stroke-miterlimit', + strokeOpacity: 'stroke-opacity', + strokeWidth: 'stroke-width', + surfaceScale: 'surfaceScale', + systemLanguage: 'systemLanguage', + tableValues: 'tableValues', + targetX: 'targetX', + targetY: 'targetY', + textAnchor: 'text-anchor', + textDecoration: 'text-decoration', + textRendering: 'text-rendering', + textLength: 'textLength', + to: 'to', + transform: 'transform', + transformOrigin: 'transform-origin', + u1: 'u1', + u2: 'u2', + underlinePosition: 'underline-position', + underlineThickness: 'underline-thickness', + unicode: 'unicode', + unicodeBidi: 'unicode-bidi', + unicodeRange: 'unicode-range', + unitsPerEm: 'units-per-em', + vAlphabetic: 'v-alphabetic', + vHanging: 'v-hanging', + vIdeographic: 'v-ideographic', + vMathematical: 'v-mathematical', + values: 'values', + vectorEffect: 'vector-effect', + version: 'version', + vertAdvY: 'vert-adv-y', + vertOriginX: 'vert-origin-x', + vertOriginY: 'vert-origin-y', + viewBox: 'viewBox', + viewTarget: 'viewTarget', + visibility: 'visibility', + widths: 'widths', + wordSpacing: 'word-spacing', + writingMode: 'writing-mode', + x: 'x', + xHeight: 'x-height', + x1: 'x1', + x2: 'x2', + xChannelSelector: 'xChannelSelector', + xlinkActuate: 'xlink:actuate', + xlinkArcrole: 'xlink:arcrole', + xlinkHref: 'xlink:href', + xlinkRole: 'xlink:role', + xlinkShow: 'xlink:show', + xlinkTitle: 'xlink:title', + xlinkType: 'xlink:type', + xmlBase: 'xml:base', + xmlLang: 'xml:lang', + xmlSpace: 'xml:space', + y: 'y', + y1: 'y1', + y2: 'y2', + yChannelSelector: 'yChannelSelector', + z: 'z', + zoomAndPan: 'zoomAndPan' + }; + + for (let name in svgAttributes) { + let value = svgAttributes[name]; + + let rendered = render( + + + + ); + expect(rendered).to.equal(``); + } + }); + + it('should have correct HTML casing', () => { + let htmlAttributes = { + accept: 'accept', + acceptCharset: 'accept-charset', + accessKey: 'accesskey', + action: 'action', + allow: 'allow', + // allowFullScreen: '', // unsure? + // allowTransparency: '', // unsure? + alt: 'alt', + as: 'as', + async: 'async', + autocomplete: 'autocomplete', + autoComplete: 'autocomplete', + autocorrect: 'autocorrect', + autoCorrect: 'autocorrect', + autofocus: 'autofocus', + autoFocus: 'autofocus', + autoPlay: 'autoplay', + capture: 'capture', + cellPadding: 'cellPadding', + cellSpacing: 'cellSpacing', + charSet: 'charset', + challenge: 'challenge', + checked: 'checked', + cite: 'cite', + class: 'class', + className: 'class', + cols: 'cols', + colSpan: 'colspan', + content: 'content', + contentEditable: 'contenteditable', + contextMenu: 'contextmenu', + controls: 'controls', + coords: 'coords', + crossOrigin: 'crossorigin', + data: 'data', + dateTime: 'datetime', + default: 'default', + defaultChecked: 'checked', + defaultValue: 'value', + defer: 'defer', + dir: 'dir', + disabled: 'disabled', + download: 'download', + decoding: 'decoding', + draggable: 'draggable', + encType: 'enctype', + enterkeyhint: 'enterkeyhint', + for: 'for', + form: 'form', + formAction: 'formaction', + formEncType: 'formenctype', + formMethod: 'formmethod', + formNoValidate: 'formnovalidate', + formTarget: 'formtarget', + frameBorder: 'frameborder', + headers: 'headers', + height: 'height', + hidden: 'hidden', + high: 'high', + href: 'href', + hrefLang: 'hreflang', + htmlFor: 'for', + httpEquiv: 'http-equiv', + icon: 'icon', + id: 'id', + indeterminate: 'indeterminate', + inputMode: 'inputmode', + integrity: 'integrity', + is: 'is', + kind: 'kind', + label: 'label', + lang: 'lang', + list: 'list', + loading: 'loading', + loop: 'loop', + low: 'low', + manifest: 'manifest', + max: 'max', + maxLength: 'maxlength', + media: 'media', + method: 'method', + min: 'min', + minLength: 'minlength', + multiple: 'multiple', + muted: 'muted', + name: 'name', + nomodule: 'nomodule', + nonce: 'nonce', + noValidate: 'novalidate', + open: 'open', + optimum: 'optimum', + part: 'part', + pattern: 'pattern', + ping: 'ping', + placeholder: 'placeholder', + playsInline: 'playsinline', + poster: 'poster', + preload: 'preload', + readonly: 'readonly', + readOnly: 'readonly', + referrerpolicy: 'referrerpolicy', + rel: 'rel', + required: 'required', + reversed: 'reversed', + role: 'role', + rows: 'rows', + rowSpan: 'rowspan', + sandbox: 'sandbox', + scope: 'scope', + scoped: 'scoped', + scrolling: 'scrolling', + seamless: 'seamless', + selected: 'selected', + shape: 'shape', + size: 'size', + sizes: 'sizes', + slot: 'slot', + span: 'span', + spellcheck: 'spellcheck', + spellCheck: 'spellcheck', + src: 'src', + srcset: 'srcset', + srcDoc: 'srcdoc', + srcLang: 'srclang', + srcSet: 'srcset', + start: 'start', + step: 'step', + style: 'style', + summary: 'summary', + tabIndex: 'tabindex', + target: 'target', + title: 'title', + type: 'type', + useMap: 'useMap', + value: 'value', + volume: 'volume', + width: 'width', + wmode: 'wmode', + wrap: 'wrap', + + // Non-standard Attributes + autocapitalize: 'autocapitalize', + autoCapitalize: 'autocapitalize', + results: 'results', + translate: 'translate', + + // RDFa Attributes + about: 'about', + datatype: 'datatype', + inlist: 'inlist', + prefix: 'prefix', + property: 'property', + resource: 'resource', + typeof: 'typeof', + vocab: 'vocab', + + // Microdata Attributes + itemProp: 'itemprop', + itemScope: 'itemscope', + itemType: 'itemtype', + itemID: 'itemid', + itemRef: 'itemref' + }; + + for (let name in htmlAttributes) { + let value = htmlAttributes[name]; + + if (name === 'checked') { + let rendered = render(); + expect(rendered).to.equal(``); + continue; + } else { + let rendered = render(
); + expect(rendered).to.equal(`
`); + } + } + }); + }); });