UNPKG

@hypothesis/frontend-shared

Version:

Shared components, styles and utilities for Hypothesis projects

145 lines (124 loc) 4.58 kB
import hljs from 'highlight.js/lib/core'; import hljsXMLLang from 'highlight.js/lib/languages/xml'; import hljsJavascriptLang from 'highlight.js/lib/languages/javascript'; import { Fragment } from 'preact'; /** * Escape `str` for use in a "-quoted string. * * @param {string} str */ function escapeQuotes(str) { return str.replace(/"/g, '\\"'); } function componentName(type) { var _type$displayName; const name = typeof type === 'string' ? type : (_type$displayName = type.displayName) !== null && _type$displayName !== void 0 ? _type$displayName : type.name; // Handle (display)name conflicts if there are two components with the same // name. e.g. if there are two components named `Foo`, the second of those // encountered will have a name of `Foo$1`. Strip the `$1` in this case. return name.replace(/\$[0-9]+$/, ''); } /** * Indent a multi-line string by `indent` spaces. * * @param {string} str * @param {number} indent */ function indentLines(str, indent) { const indentStr = ' '.repeat(indent); const lines = str.split('\n'); return lines.map(line => indentStr + line).join('\n'); } /** * Test if an element looks like a JSX element. * * @param {any} value * @return {value is import('preact').VNode<any>} */ function isJSXElement(value) { const elementType = value === null || value === void 0 ? void 0 : value.type; return typeof elementType === 'string' || typeof elementType === 'function'; } /** * Render a JSX expression as a code string. * * Currently this only supports serializing props with simple types (strings, * booleans, numbers). * * @example * jsxToString(<Widget expanded={true} label="Thing"/>) // returns `<Widget expanded label="Thing"/>` * * @param {import('preact').ComponentChildren} vnode * @return {string} */ export function jsxToString(vnode) { if (typeof vnode === 'string' || typeof vnode === 'number' || typeof vnode === 'bigint') { return vnode.toString(); } else if (typeof vnode === 'boolean') { return ''; } else if (vnode && 'type' in vnode) { // nb. The special `key` and `ref` props are not included in `vnode.props`. // `ref` is not serializable to a string and `key` is generally set dynamically // (eg. from an index or item ID) so it doesn't make sense to include it either. let propStr = Object.entries(vnode.props).map(([name, value]) => { if (name === 'children') { return ''; } // When a boolean property is present, render: // 'booleanPropName' => when true // 'booleanPropName={false}' => when false if (typeof value === 'boolean') { return value ? name : `${name}={false}`; } let valueStr; if (typeof value === 'string') { valueStr = `"${escapeQuotes(value)}"`; } else if (typeof value === 'function' && componentName(value)) { // Handle {import("preact").FunctionComponent<{}>} props valueStr = `{${componentName(value)}}`; } else if (isJSXElement(value)) { valueStr = `{${jsxToString(value)}}`; } else if (value) { // `toString` necessary for Symbols valueStr = `{${value.toString()}}`; } return `${name}=${valueStr}`; }).join(' ').trim(); if (propStr.length > 0) { propStr = ' ' + propStr; } const name = vnode.type === Fragment ? '' : componentName(vnode.type); const children = vnode.props.children; if (children) { let childrenStr = Array.isArray(children) ? children.map(jsxToString).join('\n') : jsxToString(children); childrenStr = indentLines(childrenStr, 2); return `<${name}${propStr}>\n${childrenStr}\n</${name}>`; } else { // No children - use a self-closing tag. return `<${name}${propStr} />`; } } else { return ''; } } /** * Render a JSX expression as syntax-highlighted HTML markup. * * For the syntax highlighting to be visible, a Highlight.js CSS stylesheet must be * loaded on the page. * * @param {import('preact').ComponentChildren} vnode - JSX expression to render. * See {@link jsxToString} * @return {string} */ export function jsxToHTML(vnode) { // JSX support in Highlight.js involves a combination of the JS and XML // languages, so we need to load both. if (!hljs.getLanguage('javascript')) { hljs.registerLanguage('javascript', hljsJavascriptLang); } if (!hljs.getLanguage('xml')) { hljs.registerLanguage('xml', hljsXMLLang); } const code = jsxToString(vnode); return hljs.highlightAuto(code).value; } //# sourceMappingURL=jsx-to-string.js.map