From 859dff5ed0bbcfa6c6c5f35159f66e5502d71834 Mon Sep 17 00:00:00 2001 From: gpoitch <garth22@gmail.com> Date: Fri, 30 Jul 2021 09:32:45 -0400 Subject: [PATCH 1/7] Decamelize html attributes --- src/index.js | 11 +++++++++-- test/jsx.test.js | 14 ++++++++++++-- test/render.test.js | 4 ++-- 3 files changed, 23 insertions(+), 6 deletions(-) diff --git a/src/index.js b/src/index.js index e506d054..75b4ef78 100644 --- a/src/index.js +++ b/src/index.js @@ -17,6 +17,9 @@ const UNNAMED = []; const VOID_ELEMENTS = /^(area|base|br|col|embed|hr|img|input|link|meta|param|source|track|wbr)$/; +const DASHED_ATTRS = /^(acceptC|httpE)/; +const CAMEL_ATTRS = /^(viewB)/; + const UNSAFE_NAME = /[\s\n\\/='"\0<>]/; const noop = () => {}; @@ -250,9 +253,13 @@ function _renderToString(vnode, context, opts, inner, isSvgMode, selectValue) { name = 'class'; } else if (isSvgMode && name.match(/^xlink:?./)) { name = name.toLowerCase().replace(/^xlink:?/, 'xlink:'); + } else if (DASHED_ATTRS.test(name)) { + name = name.replace(/([A-Z])/g, (l) => '-' + l.toLowerCase()); + } else if (!CAMEL_ATTRS.test(name)) { + name = name.toLowerCase(); } - if (name === 'htmlFor') { + if (name === 'htmlfor') { if (props.for) continue; name = 'for'; } @@ -275,7 +282,7 @@ function _renderToString(vnode, context, opts, inner, isSvgMode, selectValue) { continue; } - if (name === 'dangerouslySetInnerHTML') { + if (name === 'dangerouslysetinnerhtml') { html = v && v.__html; } else if (nodeName === 'textarea' && name === 'value') { // <textarea value="a&b"> --> <textarea>a&b</textarea> diff --git a/test/jsx.test.js b/test/jsx.test.js index 733ebaf4..ec221f8b 100644 --- a/test/jsx.test.js +++ b/test/jsx.test.js @@ -74,6 +74,16 @@ describe('jsx', () => { `); }); + it('should decamelize attributes', () => { + expect(renderJsx(<img srcSet="foo.png, foo2.png 2x" />)).to.equal( + `<img srcset="foo.png, foo2.png 2x" />` + ); + }); + + it('should dasherize certain attributes', () => { + expect(renderJsx(<meta httpEquiv="" />)).to.equal(`<meta http-equiv="" />`); + }); + it('should skip null and undefined attributes', () => { expect(renderJsx(<a b={null}>bar</a>)).to.equal(`<a>bar</a>`); @@ -151,11 +161,11 @@ describe('jsx', () => { it('should skip function names if functionNames=false', () => { expect( renderJsx(<div onClick={() => {}} />, { functionNames: false }) - ).to.equal('<div onClick={Function}></div>'); + ).to.equal('<div onclick={Function}></div>'); expect( renderJsx(<div onClick={function foo() {}} />, { functionNames: false }) - ).to.equal('<div onClick={Function}></div>'); + ).to.equal('<div onclick={Function}></div>'); }); it('should render self-closing elements', () => { diff --git a/test/render.test.js b/test/render.test.js index 9327d496..aeae9f58 100644 --- a/test/render.test.js +++ b/test/render.test.js @@ -265,7 +265,7 @@ describe('render', () => { it('should render SVG elements', () => { let rendered = render( - <svg> + <svg viewBox="0 0 100 100"> <image xlinkHref="#" /> <foreignObject> <div xlinkHref="#" /> @@ -277,7 +277,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 xlinkhref="#"></div></foreignObject><g><image xlink:href="#"></image></g></svg>` ); }); }); From 14b0ca62460f369799b429b060bd3d870ec1cc7b Mon Sep 17 00:00:00 2001 From: gpoitch <garth22@gmail.com> Date: Mon, 2 Aug 2021 16:56:15 -0400 Subject: [PATCH 2/7] Apply later so it leaves jsx mode untouched --- src/index.js | 16 ++++++++++------ test/jsx.test.js | 14 ++------------ test/render.test.js | 14 ++++++++++++++ 3 files changed, 26 insertions(+), 18 deletions(-) diff --git a/src/index.js b/src/index.js index 75b4ef78..4059359e 100644 --- a/src/index.js +++ b/src/index.js @@ -253,13 +253,9 @@ function _renderToString(vnode, context, opts, inner, isSvgMode, selectValue) { name = 'class'; } else if (isSvgMode && name.match(/^xlink:?./)) { name = name.toLowerCase().replace(/^xlink:?/, 'xlink:'); - } else if (DASHED_ATTRS.test(name)) { - name = name.replace(/([A-Z])/g, (l) => '-' + l.toLowerCase()); - } else if (!CAMEL_ATTRS.test(name)) { - name = name.toLowerCase(); } - if (name === 'htmlfor') { + if (name === 'htmlFor') { if (props.for) continue; name = 'for'; } @@ -282,7 +278,7 @@ function _renderToString(vnode, context, opts, inner, isSvgMode, selectValue) { continue; } - if (name === 'dangerouslysetinnerhtml') { + if (name === 'dangerouslySetInnerHTML') { html = v && v.__html; } else if (nodeName === 'textarea' && name === 'value') { // <textarea value="a&b"> --> <textarea>a&b</textarea> @@ -305,6 +301,14 @@ function _renderToString(vnode, context, opts, inner, isSvgMode, selectValue) { s += ` selected`; } } + + // Convert attribute names to proper html casing + if (DASHED_ATTRS.test(name)) { + name = name.replace(/([A-Z])/g, (l) => '-' + l.toLowerCase()); + } else if (!CAMEL_ATTRS.test(name)) { + name = name.toLowerCase(); + } + s += ` ${name}="${encodeEntities(v)}"`; } } diff --git a/test/jsx.test.js b/test/jsx.test.js index ec221f8b..733ebaf4 100644 --- a/test/jsx.test.js +++ b/test/jsx.test.js @@ -74,16 +74,6 @@ describe('jsx', () => { `); }); - it('should decamelize attributes', () => { - expect(renderJsx(<img srcSet="foo.png, foo2.png 2x" />)).to.equal( - `<img srcset="foo.png, foo2.png 2x" />` - ); - }); - - it('should dasherize certain attributes', () => { - expect(renderJsx(<meta httpEquiv="" />)).to.equal(`<meta http-equiv="" />`); - }); - it('should skip null and undefined attributes', () => { expect(renderJsx(<a b={null}>bar</a>)).to.equal(`<a>bar</a>`); @@ -161,11 +151,11 @@ describe('jsx', () => { it('should skip function names if functionNames=false', () => { expect( renderJsx(<div onClick={() => {}} />, { functionNames: false }) - ).to.equal('<div onclick={Function}></div>'); + ).to.equal('<div onClick={Function}></div>'); expect( renderJsx(<div onClick={function foo() {}} />, { functionNames: false }) - ).to.equal('<div onclick={Function}></div>'); + ).to.equal('<div onClick={Function}></div>'); }); it('should render self-closing elements', () => { diff --git a/test/render.test.js b/test/render.test.js index aeae9f58..f7ae6781 100644 --- a/test/render.test.js +++ b/test/render.test.js @@ -89,6 +89,20 @@ 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 dasherize certain attributes', () => { + let rendered = render(<meta httpEquiv="refresh" />), + expected = `<meta http-equiv="refresh" />`; + + 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>`; From 3455e633cd240c49c81187de1a8d1752c32e00c1 Mon Sep 17 00:00:00 2001 From: gpoitch <garth22@gmail.com> Date: Wed, 29 Sep 2021 10:19:26 -0400 Subject: [PATCH 3/7] Support colon attrs --- src/index.js | 9 +++++++-- test/render.test.js | 9 ++++++++- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/index.js b/src/index.js index 4059359e..4a499061 100644 --- a/src/index.js +++ b/src/index.js @@ -18,7 +18,10 @@ const UNNAMED = []; const VOID_ELEMENTS = /^(area|base|br|col|embed|hr|img|input|link|meta|param|source|track|wbr)$/; const DASHED_ATTRS = /^(acceptC|httpE)/; -const CAMEL_ATTRS = /^(viewB)/; +const CAMEL_ATTRS = /^(viewB|isP)/; +const COLON_ATTRS = /^(xmlS|xlinkH)/; +const transformAttr = (attr, separator) => + attr.replace(/([A-Z])/g, (w) => separator + w.toLowerCase()); const UNSAFE_NAME = /[\s\n\\/='"\0<>]/; @@ -304,7 +307,9 @@ function _renderToString(vnode, context, opts, inner, isSvgMode, selectValue) { // Convert attribute names to proper html casing if (DASHED_ATTRS.test(name)) { - name = name.replace(/([A-Z])/g, (l) => '-' + l.toLowerCase()); + name = transformAttr(name, '-'); + } else if (COLON_ATTRS.test(name)) { + name = transformAttr(name, ':'); } else if (!CAMEL_ATTRS.test(name)) { name = name.toLowerCase(); } diff --git a/test/render.test.js b/test/render.test.js index f7ae6781..e2a20b2f 100644 --- a/test/render.test.js +++ b/test/render.test.js @@ -103,6 +103,13 @@ describe('render', () => { expect(rendered).to.equal(expected); }); + it('should colonize certain attributes & leave certain attributes camelized', () => { + let rendered = render(<svg xmlSpace="preserve" viewBox="0 0 10 10" />), + expected = `<svg xml:space="preserve" viewBox="0 0 10 10"></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>`; @@ -291,7 +298,7 @@ describe('render', () => { ); expect(rendered).to.equal( - `<svg viewBox="0 0 100 100"><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>` ); }); }); From 2c5657cd14bf4b0639a37f932326c0854b9055a5 Mon Sep 17 00:00:00 2001 From: gpoitch <garth22@gmail.com> Date: Wed, 29 Sep 2021 11:00:37 -0400 Subject: [PATCH 4/7] Support bool attrs --- src/index.js | 28 +++++++++++++++++----------- test/render.test.js | 9 +++++++++ 2 files changed, 26 insertions(+), 11 deletions(-) diff --git a/src/index.js b/src/index.js index 4a499061..c2509ff3 100644 --- a/src/index.js +++ b/src/index.js @@ -20,8 +20,8 @@ const VOID_ELEMENTS = /^(area|base|br|col|embed|hr|img|input|link|meta|param|sou const DASHED_ATTRS = /^(acceptC|httpE)/; const CAMEL_ATTRS = /^(viewB|isP)/; const COLON_ATTRS = /^(xmlS|xlinkH)/; -const transformAttr = (attr, separator) => - attr.replace(/([A-Z])/g, (w) => separator + w.toLowerCase()); + +const CAPITAL_REGEXP = /([A-Z])/g; const UNSAFE_NAME = /[\s\n\\/='"\0<>]/; @@ -287,6 +287,8 @@ function _renderToString(vnode, context, opts, inner, isSvgMode, selectValue) { // <textarea value="a&b"> --> <textarea>a&b</textarea> propChildren = v; } else if ((v || v === 0 || v === '') && typeof v !== 'function') { + name = getAttributeNameInHtmlCase(name); + if (v === true || v === '') { v = name; // in non-xml mode, allow boolean attributes @@ -305,15 +307,6 @@ function _renderToString(vnode, context, opts, inner, isSvgMode, selectValue) { } } - // Convert attribute names to proper html casing - if (DASHED_ATTRS.test(name)) { - name = transformAttr(name, '-'); - } else if (COLON_ATTRS.test(name)) { - name = transformAttr(name, ':'); - } else if (!CAMEL_ATTRS.test(name)) { - name = name.toLowerCase(); - } - s += ` ${name}="${encodeEntities(v)}"`; } } @@ -442,6 +435,19 @@ function getFallbackComponentName(component) { } return name; } + +function getAttributeNameInHtmlCase(name) { + if (CAMEL_ATTRS.test(name)) return name; + + if (DASHED_ATTRS.test(name)) + return name.replace(CAPITAL_REGEXP, (w) => '-' + w.toLowerCase()); + + if (COLON_ATTRS.test(name)) + return name.replace(CAPITAL_REGEXP, (w) => ':' + w.toLowerCase()); + + return name.toLowerCase(); +} + renderToString.shallowRender = shallowRender; export default renderToString; diff --git a/test/render.test.js b/test/render.test.js index e2a20b2f..3c1c11bd 100644 --- a/test/render.test.js +++ b/test/render.test.js @@ -96,6 +96,15 @@ describe('render', () => { 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" />`; From 0ff6093216c2216523908bb4d883bef86c42e424 Mon Sep 17 00:00:00 2001 From: gpoitch <garth22@gmail.com> Date: Fri, 22 Oct 2021 10:56:18 -0400 Subject: [PATCH 5/7] Add more svg & rss attrs --- src/index.js | 6 +++--- test/render.test.js | 8 +++++--- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/index.js b/src/index.js index c2509ff3..0bd6a9c5 100644 --- a/src/index.js +++ b/src/index.js @@ -17,9 +17,9 @@ const UNNAMED = []; const VOID_ELEMENTS = /^(area|base|br|col|embed|hr|img|input|link|meta|param|source|track|wbr)$/; -const DASHED_ATTRS = /^(acceptC|httpE)/; -const CAMEL_ATTRS = /^(viewB|isP)/; -const COLON_ATTRS = /^(xmlS|xlinkH)/; +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; diff --git a/test/render.test.js b/test/render.test.js index 3c1c11bd..d847556d 100644 --- a/test/render.test.js +++ b/test/render.test.js @@ -112,9 +112,11 @@ describe('render', () => { expect(rendered).to.equal(expected); }); - it('should colonize certain attributes & leave certain attributes camelized', () => { - let rendered = render(<svg xmlSpace="preserve" viewBox="0 0 10 10" />), - expected = `<svg xml:space="preserve" viewBox="0 0 10 10"></svg>`; + 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); }); From 697cb20082769902efcf21ab479dd01fcb77fc8f Mon Sep 17 00:00:00 2001 From: gpoitch <garth22@gmail.com> Date: Fri, 12 Nov 2021 10:30:21 -0500 Subject: [PATCH 6/7] Review pass 1 --- src/index.js | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/index.js b/src/index.js index 0bd6a9c5..442dff59 100644 --- a/src/index.js +++ b/src/index.js @@ -19,7 +19,7 @@ const VOID_ELEMENTS = /^(area|base|br|col|embed|hr|img|input|link|meta|param|sou 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 COLON_ATTRS = /^(xlink|xml|xmlns)([A-Z])/; const CAPITAL_REGEXP = /([A-Z])/g; @@ -287,7 +287,7 @@ function _renderToString(vnode, context, opts, inner, isSvgMode, selectValue) { // <textarea value="a&b"> --> <textarea>a&b</textarea> propChildren = v; } else if ((v || v === 0 || v === '') && typeof v !== 'function') { - name = getAttributeNameInHtmlCase(name); + name = transformAttributeName(name); if (v === true || v === '') { v = name; @@ -306,7 +306,6 @@ function _renderToString(vnode, context, opts, inner, isSvgMode, selectValue) { s += ` selected`; } } - s += ` ${name}="${encodeEntities(v)}"`; } } @@ -436,14 +435,16 @@ function getFallbackComponentName(component) { return name; } -function getAttributeNameInHtmlCase(name) { +function transformAttributeName(name) { if (CAMEL_ATTRS.test(name)) return name; - if (DASHED_ATTRS.test(name)) - return name.replace(CAPITAL_REGEXP, (w) => '-' + w.toLowerCase()); + if (DASHED_ATTRS.test(name)) { + return name.replace(CAPITAL_REGEXP, '-$1').toLowerCase(); + } - if (COLON_ATTRS.test(name)) - return name.replace(CAPITAL_REGEXP, (w) => ':' + w.toLowerCase()); + if (COLON_ATTRS.test(name)) { + return name.replace(CAPITAL_REGEXP, ':$1').toLowerCase(); + } return name.toLowerCase(); } From b566b917300b242890f40df33bd58fa33f1c51c2 Mon Sep 17 00:00:00 2001 From: Jovi De Croock <decroockjovi@gmail.com> Date: Mon, 15 Nov 2021 18:21:03 +0100 Subject: [PATCH 7/7] add changeset --- .changeset/funny-seahorses-refuse.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/funny-seahorses-refuse.md diff --git a/.changeset/funny-seahorses-refuse.md b/.changeset/funny-seahorses-refuse.md new file mode 100644 index 00000000..4a7b5b38 --- /dev/null +++ b/.changeset/funny-seahorses-refuse.md @@ -0,0 +1,5 @@ +--- +"preact-render-to-string": patch +--- + +Transform attribute names to proper casing