Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Transform attribute names to proper casing #199

Closed
wants to merge 10 commits into from
Closed
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
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 9 additions & 5 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import { encodeEntities, styleObjToCss, UNSAFE_NAME, XLINK } from './util';
import {
encodeEntities,
styleObjToCss,
transformAttributeName,
UNSAFE_NAME
} from './util';
import { options, h, Fragment } from 'preact';
import {
CHILDREN,
Expand Down Expand Up @@ -307,15 +312,15 @@ function _renderToString(vnode, context, isSvgMode, selectValue, parent) {
break;

default: {
if (isSvgMode && XLINK.test(name)) {
name = name.toLowerCase().replace(XLINK_REPLACE_REGEX, 'xlink:');
} else if (UNSAFE_NAME.test(name)) {
if (UNSAFE_NAME.test(name)) {
continue;
} else if ((name[4] === '-' || name === 'draggable') && v != null) {
// serialize boolean aria-xyz or draggable attribute values as strings
// `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 {
name = transformAttributeName(name);
}
}
}
Expand Down Expand Up @@ -360,7 +365,6 @@ function _renderToString(vnode, context, isSvgMode, selectValue, parent) {
return s + '>' + html + '</' + type + '>';
}

const XLINK_REPLACE_REGEX = /^xlink:?/;
const SELF_CLOSING = new Set([
'area',
'base',
Expand Down
5 changes: 3 additions & 2 deletions src/pretty.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
styleObjToCss,
getChildren,
createComponent,
transformAttributeName,
UNSAFE_NAME,
XLINK,
VOID_ELEMENTS
Expand Down Expand Up @@ -133,7 +134,6 @@ function _renderToStringPretty(
!nodeName.prototype ||
typeof nodeName.prototype.render !== 'function'
) {

// If a hook invokes setState() to invalidate the component during rendering,
// re-render it up to 25 times to allow "settling" of memoized states.
// Note:
Expand All @@ -149,7 +149,6 @@ function _renderToStringPretty(
rendered = nodeName.call(vnode.__c, props, cctx);
}
} else {

// c = new nodeName(props, context);
c = vnode.__c = new nodeName(props, cctx);
c.__v = vnode;
Expand Down Expand Up @@ -278,6 +277,8 @@ function _renderToStringPretty(
// <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
20 changes: 20 additions & 0 deletions src/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -148,3 +148,23 @@ export function createComponent(vnode, context) {
__h: []
};
}

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;

export 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();
}
36 changes: 34 additions & 2 deletions test/render.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,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 @@ -321,7 +353,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 @@ -333,7 +365,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