Skip to content

Commit

Permalink
support pretty mode
Browse files Browse the repository at this point in the history
  • Loading branch information
gpoitch committed Jul 26, 2023
1 parent 79a9c25 commit ed1913e
Show file tree
Hide file tree
Showing 8 changed files with 100 additions and 56 deletions.
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
18 changes: 8 additions & 10 deletions src/index.d.ts
Original file line number Diff line number Diff line change
@@ -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;
8 changes: 4 additions & 4 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, attrHook;
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, 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
Expand All @@ -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];
Expand Down Expand Up @@ -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') {
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);
});
});
71 changes: 35 additions & 36 deletions test/render.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = (
<html>
<head>
<meta charSet="utf=8" />
<meta httpEquiv="refresh" />
<link rel="preconnect" href="https://foo.com" crossOrigin />
</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 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/></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 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, {}, { attrHook });
expect(rendered).to.equal(expected);
});
const rendered = render(content, {}, { attributeHook });
expect(rendered).to.equal(expected);
});
});
});

0 comments on commit ed1913e

Please sign in to comment.