Skip to content
Closed
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/funny-seahorses-refuse.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"preact-render-to-string": patch
---

Transform attribute names to proper casing
23 changes: 23 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@ const UNNAMED = [];

const VOID_ELEMENTS = /^(area|base|br|col|embed|hr|img|input|link|meta|param|source|track|wbr)$/;

const DASHED_ATTRS = /^(acceptC|httpE|(clip|color|fill|font|glyph|marker|stop|stroke|text|vert)[A-Z])/;
const CAMEL_ATTRS = /^(isP|viewB)/;
const COLON_ATTRS = /^(xlink|xml|xmlns)([A-Z])/;

const CAPITAL_REGEXP = /([A-Z])/g;

const UNSAFE_NAME = /[\s\n\\/='"\0<>]/;
Copy link

@SukkaW SukkaW Dec 12, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can they cover all the attributes? react-dom has an entire file for possibleStandardNames:

https://github.com/facebook/react/blob/ca106a02d1648f4f0048b07c6b88f69aac175d3c/packages/react-dom/src/shared/possibleStandardNames.js

If they can, at least add a unit test case to cover them all. cc @gpoitch


const noop = () => {};
Expand Down Expand Up @@ -281,6 +287,8 @@ function _renderToString(vnode, context, opts, inner, isSvgMode, selectValue) {
// <textarea value="a&b"> --> <textarea>a&amp;b</textarea>
propChildren = v;
} else if ((v || v === 0 || v === '') && typeof v !== 'function') {
name = transformAttributeName(name);

if (v === true || v === '') {
v = name;
// in non-xml mode, allow boolean attributes
Expand Down Expand Up @@ -426,6 +434,21 @@ function getFallbackComponentName(component) {
}
return name;
}

function transformAttributeName(name) {
if (CAMEL_ATTRS.test(name)) return name;

if (DASHED_ATTRS.test(name)) {
return name.replace(CAPITAL_REGEXP, '-$1').toLowerCase();
}

if (COLON_ATTRS.test(name)) {
return name.replace(CAPITAL_REGEXP, ':$1').toLowerCase();
}

return name.toLowerCase();
}

renderToString.shallowRender = shallowRender;

export default renderToString;
Expand Down
36 changes: 34 additions & 2 deletions test/render.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,38 @@ describe('render', () => {
expect(rendered).to.equal(expected);
});

it('should decamelize attributes', () => {
let rendered = render(<img srcSet="foo.png, foo2.png 2x" />),
expected = `<img srcset="foo.png, foo2.png 2x" />`;

expect(rendered).to.equal(expected);
});

it('should decamelize bool attributes', () => {
let rendered = render(
<link rel="preconnect" href="https://foo.com" crossOrigin />
),
expected = `<link rel="preconnect" href="https://foo.com" crossorigin />`;

expect(rendered).to.equal(expected);
});

it('should dasherize certain attributes', () => {
let rendered = render(<meta httpEquiv="refresh" />),
expected = `<meta http-equiv="refresh" />`;

expect(rendered).to.equal(expected);
});

it('should colonize/dasherize certain attributes & leave certain attributes camelized', () => {
let rendered = render(
<svg xmlSpace="preserve" viewBox="0 0 10 10" fillRule="nonzero" />
),
expected = `<svg xml:space="preserve" viewBox="0 0 10 10" fill-rule="nonzero"></svg>`;

expect(rendered).to.equal(expected);
});

it('should include boolean aria-* attributes', () => {
let rendered = render(<div aria-hidden aria-whatever={false} />),
expected = `<div aria-hidden="true" aria-whatever="false"></div>`;
Expand Down Expand Up @@ -265,7 +297,7 @@ describe('render', () => {

it('should render SVG elements', () => {
let rendered = render(
<svg>
<svg viewBox="0 0 100 100">
<image xlinkHref="#" />
<foreignObject>
<div xlinkHref="#" />
Expand All @@ -277,7 +309,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 viewBox="0 0 100 100"><image xlink:href="#"></image><foreignObject><div xlink:href="#"></div></foreignObject><g><image xlink:href="#"></image></g></svg>`
);
});
});
Expand Down