From 79a9c25563a87db38da47a241e193aeeb3035ca8 Mon Sep 17 00:00:00 2001 From: Garth Poitras <411908+gpoitch@users.noreply.github.com> Date: Wed, 26 Jul 2023 10:45:26 -0400 Subject: [PATCH 1/2] Attribute hook render option --- src/index.d.ts | 28 ++++++++++++++++++++++++---- src/index.js | 7 +++++-- test/render.test.js | 43 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 72 insertions(+), 6 deletions(-) diff --git a/src/index.d.ts b/src/index.d.ts index 113cbcb7..728d13dd 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -1,7 +1,27 @@ import { VNode } from 'preact'; -export default function renderToString(vnode: VNode, context?: any): string; +interface RenderOptions { + attrHook?: (name: string) => string; +} -export function render(vnode: VNode, context?: any): string; -export function renderToString(vnode: VNode, context?: any): string; -export function renderToStaticMarkup(vnode: VNode, context?: any): string; +export default function renderToString( + vnode: VNode, + context?: any, + renderOpts?: RenderOptions +): string; + +export function render( + vnode: VNode, + context?: any, + renderOpts?: RenderOptions +): string; +export function renderToString( + vnode: VNode, + context?: any, + renderOpts?: RenderOptions +): string; +export function renderToStaticMarkup( + vnode: VNode, + context?: any, + renderOpts?: RenderOptions +): string; diff --git a/src/index.js b/src/index.js index 45bb9bfc..4ce64d24 100644 --- a/src/index.js +++ b/src/index.js @@ -21,7 +21,7 @@ const isArray = Array.isArray; const assign = Object.assign; // Global state for the current render pass -let beforeDiff, afterDiff, renderHook, ummountHook; +let beforeDiff, afterDiff, renderHook, ummountHook, attrHook; /** * Render Preact JSX + Components to an HTML string. @@ -29,7 +29,7 @@ let beforeDiff, afterDiff, renderHook, ummountHook; * @param {Object} [context={}] Initial root context object * @returns {string} serialized HTML */ -export function renderToString(vnode, context) { +export function renderToString(vnode, context, renderOpts) { // Performance optimization: `renderToString` is synchronous and we // therefore don't execute any effects. To do that we pass an empty // array to `options._commit` (`__c`). But we can go one step further @@ -43,6 +43,7 @@ export function renderToString(vnode, context) { afterDiff = options[DIFFED]; renderHook = options[RENDER]; ummountHook = options.unmount; + attrHook = renderOpts?.attrHook; const parent = h(Fragment, null); parent[CHILDREN] = [vnode]; @@ -399,6 +400,8 @@ function _renderToString(vnode, context, isSvgMode, selectValue, parent) { } } + if (attrHook) name = attrHook(name); + // write this attribute to the buffer if (v != null && v !== false && typeof v !== 'function') { if (v === true || v === '') { diff --git a/test/render.test.js b/test/render.test.js index bd264b6b..12946197 100644 --- a/test/render.test.js +++ b/test/render.test.js @@ -1598,4 +1598,47 @@ describe('render', () => { }); }); }); + + describe('Render Options', () => { + describe('Attribute Hook', () => { + it('Transforms attributes with custom attrHook option', () => { + function attrHook(name) { + 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; + 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(); + } + + const content = ( + + + + + + + + + + +
+ + + + + ); + + const expected = + '
'; + + const rendered = render(content, {}, { attrHook }); + expect(rendered).to.equal(expected); + }); + }); + }); }); From ed1913ec4c6e272ed985bd4e1e045af46157e1be Mon Sep 17 00:00:00 2001 From: Garth Poitras <411908+gpoitch@users.noreply.github.com> Date: Wed, 26 Jul 2023 17:02:46 -0400 Subject: [PATCH 2/2] support pretty mode --- jsx.d.ts | 1 + src/index.d.ts | 18 +++++------- src/index.js | 8 ++--- src/jsx.d.ts | 2 ++ src/jsx.js | 6 ++-- src/pretty.js | 8 +++-- test/pretty.test.js | 42 ++++++++++++++++++++++++++- test/render.test.js | 71 ++++++++++++++++++++++----------------------- 8 files changed, 100 insertions(+), 56 deletions(-) diff --git a/jsx.d.ts b/jsx.d.ts index 90042308..25131e32 100644 --- a/jsx.d.ts +++ b/jsx.d.ts @@ -8,6 +8,7 @@ interface Options { functions?: boolean; functionNames?: boolean; skipFalseAttributes?: boolean; + attributeHook?: (name: string) => string; } export default function renderToStringPretty( diff --git a/src/index.d.ts b/src/index.d.ts index 728d13dd..7a65cd7b 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -1,27 +1,25 @@ import { VNode } from 'preact'; -interface RenderOptions { - attrHook?: (name: string) => string; +interface Options { + attributeHook?: (name: string) => string; } export default function renderToString( vnode: VNode, context?: any, - renderOpts?: RenderOptions + options?: Options ): string; -export function render( - vnode: VNode, - context?: any, - renderOpts?: RenderOptions -): string; +export function render(vnode: VNode, context?: any, options?: Options): string; + export function renderToString( vnode: VNode, context?: any, - renderOpts?: RenderOptions + options?: Options ): string; + export function renderToStaticMarkup( vnode: VNode, context?: any, - renderOpts?: RenderOptions + options?: Options ): string; diff --git a/src/index.js b/src/index.js index 4ce64d24..99ef1662 100644 --- a/src/index.js +++ b/src/index.js @@ -21,7 +21,7 @@ const isArray = Array.isArray; const assign = Object.assign; // Global state for the current render pass -let beforeDiff, afterDiff, renderHook, ummountHook, attrHook; +let beforeDiff, afterDiff, renderHook, ummountHook, attributeHook; /** * Render Preact JSX + Components to an HTML string. @@ -29,7 +29,7 @@ let beforeDiff, afterDiff, renderHook, ummountHook, attrHook; * @param {Object} [context={}] Initial root context object * @returns {string} serialized HTML */ -export function renderToString(vnode, context, renderOpts) { +export function renderToString(vnode, context, opts) { // Performance optimization: `renderToString` is synchronous and we // therefore don't execute any effects. To do that we pass an empty // array to `options._commit` (`__c`). But we can go one step further @@ -43,7 +43,7 @@ export function renderToString(vnode, context, renderOpts) { afterDiff = options[DIFFED]; renderHook = options[RENDER]; ummountHook = options.unmount; - attrHook = renderOpts?.attrHook; + attributeHook = opts && opts.attributeHook; const parent = h(Fragment, null); parent[CHILDREN] = [vnode]; @@ -400,7 +400,7 @@ function _renderToString(vnode, context, isSvgMode, selectValue, parent) { } } - if (attrHook) name = attrHook(name); + if (attributeHook) name = attributeHook(name); // write this attribute to the buffer if (v != null && v !== false && typeof v !== 'function') { diff --git a/src/jsx.d.ts b/src/jsx.d.ts index 752f3e55..9b79cd4e 100644 --- a/src/jsx.d.ts +++ b/src/jsx.d.ts @@ -8,6 +8,7 @@ interface Options { functions?: boolean; functionNames?: boolean; skipFalseAttributes?: boolean; + attributeHook?: (name: string) => string; } export default function renderToStringPretty( @@ -15,6 +16,7 @@ export default function renderToStringPretty( context?: any, options?: Options ): string; + export function render(vnode: VNode, context?: any, options?: Options): string; export function shallowRender( diff --git a/src/jsx.js b/src/jsx.js index 6007311a..c16e259b 100644 --- a/src/jsx.js +++ b/src/jsx.js @@ -25,7 +25,7 @@ let prettyFormatOpts = { plugins: [preactPlugin] }; -function attributeHook(name, value, context, opts, isComponent) { +function jsxAttributeHook(name, value, context, opts, isComponent) { let type = typeof value; // Use render-to-string's built-in handling for these properties @@ -60,7 +60,7 @@ function attributeHook(name, value, context, opts, isComponent) { } let defaultOpts = { - attributeHook, + jsxAttributeHook, jsx: true, xml: false, functions: true, @@ -83,7 +83,7 @@ let defaultOpts = { */ export default function renderToStringPretty(vnode, context, options) { const opts = Object.assign({}, defaultOpts, options || {}); - if (!opts.jsx) opts.attributeHook = null; + if (!opts.jsx || opts.attributeHook) opts.jsxAttributeHook = null; return renderToString(vnode, context, opts); } export { renderToStringPretty as render }; diff --git a/src/pretty.js b/src/pretty.js index 4e37d92c..7c0c00aa 100644 --- a/src/pretty.js +++ b/src/pretty.js @@ -209,6 +209,8 @@ function _renderToStringPretty( propChildren, html; + const attributeHook = opts && opts.attributeHook; + if (props) { let attrs = Object.keys(props); @@ -263,8 +265,8 @@ function _renderToStringPretty( } let hooked = - opts.attributeHook && - opts.attributeHook(name, v, context, opts, isComponent); + opts.jsxAttributeHook && + opts.jsxAttributeHook(name, v, context, opts, isComponent); if (hooked || hooked === '') { s = s + hooked; continue; @@ -280,6 +282,7 @@ function _renderToStringPretty( v = name; // in non-xml mode, allow boolean attributes if (!opts || !opts.xml) { + if (attributeHook) name = attributeHook(name); s = s + ' ' + name; continue; } @@ -299,6 +302,7 @@ function _renderToStringPretty( s = s + ` selected`; } } + if (attributeHook) name = attributeHook(name); s = s + ` ${name}="${encodeEntities(v + '')}"`; } } diff --git a/test/pretty.test.js b/test/pretty.test.js index 7d6ecd1a..792e0f94 100644 --- a/test/pretty.test.js +++ b/test/pretty.test.js @@ -5,7 +5,7 @@ import { expect } from 'chai'; import { dedent } from './utils.js'; describe('pretty', () => { - let prettyRender = (jsx) => render(jsx, {}, { pretty: true }); + let prettyRender = (jsx, opts) => render(jsx, {}, { pretty: true, ...opts }); it('should render no whitespace by default', () => { let rendered = basicRender( @@ -196,4 +196,44 @@ describe('pretty', () => { it('should not render function children', () => { expect(prettyRender(
{() => {}}
)).to.equal('
'); }); + + it('transforms attributes with custom attributeHook option', () => { + function attributeHook(name) { + 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; + 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(); + } + + const content = ( + + + + + + + + + + + +
+ + + + + ); + + const expected = + '\n\t\n\t\t\n\t\t\n\t\t\n\t\t\n\t\n\t\n\t\t\n\t\t\n\t\t\t\n\t\t\t\t
\n\t\t\t
\n\t\t
\n\t\n'; + + const rendered = prettyRender(content, { attributeHook }); + expect(rendered).to.equal(expected); + }); }); diff --git a/test/render.test.js b/test/render.test.js index 12946197..b8ad6578 100644 --- a/test/render.test.js +++ b/test/render.test.js @@ -1600,45 +1600,44 @@ describe('render', () => { }); describe('Render Options', () => { - describe('Attribute Hook', () => { - it('Transforms attributes with custom attrHook option', () => { - function attrHook(name) { - 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; - 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(); - } + it('transforms attributes with custom attributeHook option', () => { + function attributeHook(name) { + 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; + 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(); + } - const content = ( - - - - - - - - - - -
- - - - - ); + const content = ( + + + + + + + + + + + +
+ + + + + ); - const expected = - '
'; + const expected = + '
'; - const rendered = render(content, {}, { attrHook }); - expect(rendered).to.equal(expected); - }); + const rendered = render(content, {}, { attributeHook }); + expect(rendered).to.equal(expected); }); }); });