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

Attribute hook render option #307

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions jsx.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ interface Options {
functions?: boolean;
functionNames?: boolean;
skipFalseAttributes?: boolean;
attributeHook?: (name: string) => string;
}

export default function renderToStringPretty(
Expand Down
26 changes: 22 additions & 4 deletions src/index.d.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,25 @@
import { VNode } from 'preact';

export default function renderToString(vnode: VNode, context?: any): string;
interface Options {
attributeHook?: (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,
options?: Options
): string;

export function render(vnode: VNode, context?: any, options?: Options): string;

export function renderToString(
vnode: VNode,
context?: any,
options?: Options
): string;

export function renderToStaticMarkup(
vnode: VNode,
context?: any,
options?: Options
): string;
7 changes: 5 additions & 2 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,15 @@ 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, attributeHook;

/**
* Render Preact JSX + Components to an HTML string.
* @param {VNode} vnode JSX Element / VNode to render
* @param {Object} [context={}] Initial root context object
* @returns {string} serialized HTML
*/
export function renderToString(vnode, context) {
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
Expand All @@ -43,6 +43,7 @@ export function renderToString(vnode, context) {
afterDiff = options[DIFFED];
renderHook = options[RENDER];
ummountHook = options.unmount;
attributeHook = opts && opts.attributeHook;

const parent = h(Fragment, null);
parent[CHILDREN] = [vnode];
Expand Down Expand Up @@ -399,6 +400,8 @@ function _renderToString(vnode, context, isSvgMode, selectValue, parent) {
}
}

if (attributeHook) name = attributeHook(name);

// write this attribute to the buffer
if (v != null && v !== false && typeof v !== 'function') {
if (v === true || v === '') {
Expand Down
2 changes: 2 additions & 0 deletions src/jsx.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,15 @@ interface Options {
functions?: boolean;
functionNames?: boolean;
skipFalseAttributes?: boolean;
attributeHook?: (name: string) => string;
}

export default function renderToStringPretty(
vnode: VNode,
context?: any,
options?: Options
): string;

export function render(vnode: VNode, context?: any, options?: Options): string;

export function shallowRender(
Expand Down
6 changes: 3 additions & 3 deletions src/jsx.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -60,7 +60,7 @@ function attributeHook(name, value, context, opts, isComponent) {
}

let defaultOpts = {
attributeHook,
jsxAttributeHook,
jsx: true,
xml: false,
functions: true,
Expand All @@ -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 };
Expand Down
8 changes: 6 additions & 2 deletions src/pretty.js
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,8 @@ function _renderToStringPretty(
propChildren,
html;

const attributeHook = opts && opts.attributeHook;

if (props) {
let attrs = Object.keys(props);

Expand Down Expand Up @@ -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;
Expand All @@ -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;
}
Expand All @@ -299,6 +302,7 @@ function _renderToStringPretty(
s = s + ` selected`;
}
}
if (attributeHook) name = attributeHook(name);
s = s + ` ${name}="${encodeEntities(v + '')}"`;
}
}
Expand Down
42 changes: 41 additions & 1 deletion test/pretty.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -196,4 +196,44 @@ describe('pretty', () => {
it('should not render function children', () => {
expect(prettyRender(<div>{() => {}}</div>)).to.equal('<div></div>');
});

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 = (
<html>
<head>
<meta charSet="utf=8" />
<meta httpEquiv="refresh" />
<link rel="preconnect" href="https://foo.com" crossOrigin />
<link rel="preconnect" href="https://bar.com" crossOrigin={false} />
</head>
<body>
<img srcSet="foo.png, foo2.png 2x" />
<svg xmlSpace="preserve" viewBox="0 0 10 10" fillRule="nonzero">
<foreignObject>
<div xlinkHref="#" />
</foreignObject>
</svg>
</body>
</html>
);

const expected =
'<html>\n\t<head>\n\t\t<meta charset="utf=8" />\n\t\t<meta http-equiv="refresh" />\n\t\t<link rel="preconnect" href="https://foo.com" crossorigin />\n\t\t<link rel="preconnect" href="https://bar.com" />\n\t</head>\n\t<body>\n\t\t<img srcset="foo.png, foo2.png 2x" />\n\t\t<svg xml:space="preserve" viewBox="0 0 10 10" fill-rule="nonzero">\n\t\t\t<foreignObject>\n\t\t\t\t<div xlink:href="#"></div>\n\t\t\t</foreignObject>\n\t\t</svg>\n\t</body>\n</html>';

const rendered = prettyRender(content, { attributeHook });
expect(rendered).to.equal(expected);
});
});
42 changes: 42 additions & 0 deletions test/render.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1598,4 +1598,46 @@ describe('render', () => {
});
});
});

describe('Render Options', () => {
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 = (
<html>
<head>
<meta charSet="utf=8" />
<meta httpEquiv="refresh" />
<link rel="preconnect" href="https://foo.com" crossOrigin />
<link rel="preconnect" href="https://bar.com" crossOrigin={false} />
</head>
<body>
<img srcSet="foo.png, foo2.png 2x" />
<svg xmlSpace="preserve" viewBox="0 0 10 10" fillRule="nonzero">
<foreignObject>
<div xlinkHref="#" />
</foreignObject>
</svg>
</body>
</html>
);

const expected =
'<html><head><meta charset="utf=8"/><meta http-equiv="refresh"/><link rel="preconnect" href="https://foo.com" crossorigin/><link rel="preconnect" href="https://bar.com"/></head><body><img srcset="foo.png, foo2.png 2x"/><svg xml:space="preserve" viewBox="0 0 10 10" fill-rule="nonzero"><foreignObject><div xlink:href="#"></div></foreignObject></svg></body></html>';

const rendered = render(content, {}, { attributeHook });
expect(rendered).to.equal(expected);
});
});
});