UNPKG

@hypothesis/frontend-shared

Version:

Shared components, styles and utilities for Hypothesis projects

172 lines (164 loc) 5.98 kB
import hljs from 'highlight.js/lib/core'; import hljsTypeScriptLang from 'highlight.js/lib/languages/typescript'; import hljsXMLLang from 'highlight.js/lib/languages/xml'; import { Fragment } from 'preact'; /** * Escape `str` for use in a "-quoted string. */ function escapeQuotes(str) { return str.replace(/"/g, '\\"'); } /** * Format a component's name for display */ function componentName(type) { var _type$displayName; let 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. name = name.replace(/\$[0-9]+$/, ''); // Remove trailing underscore from component names. This allows the pattern // library to create a convenience wrapper component around a given component // being documented, and have it appear that the documented component is being // used directly in rendered source content. name = name.replace(/\B_$/, ''); return name; } /** * Indent a multi-line string by `indent` spaces. */ 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. */ function isJSXElement(value) { const elementType = value === null || value === void 0 ? void 0 : value.type; return typeof elementType === 'string' || typeof elementType === 'function'; } /** * Should the prop with `name` be ignored from output code strings? * * The special `children` prop, as well as any prop name beginning with an * underscore, should be ignored. */ function isExcludedProp(name) { return name.match(/^_/) || name === 'children'; } /** * Render a JSX expression as a code string. * * A trailing underscore in component names will be omitted, allowing wrapper * components to be represented as the component they wrap. Props with a * leading underscore will be ignored. * * @example * jsxToString(<Widget_ count={0} error={false} open={true} label="Thing" onClick={() => go()} _hello={"hello"} />) * // returns '<Widget count={0}error={false} open label="Thing" onClick={onClick} />' */ 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 (isExcludedProp(name)) { return ''; } if (isJSXElement(value)) { return `${name}={${jsxToString(value)}}`; } let valueStr = ''; switch (typeof value) { case 'boolean': // When a boolean property is present, render: // 'booleanPropName' => when true // 'booleanPropName={false}' => when false valueStr = value ? name : `${name}={false}`; break; case 'string': valueStr = `${name}="${escapeQuotes(value)}"`; break; case 'bigint': case 'number': case 'undefined': valueStr = `${name}={${value}}`; break; case 'function': // This also handles function components valueStr = componentName(value) ? `${name}={${componentName(value)}}` : `${name}={${value.toString()}}`; break; case 'object': // Use the prop name instead of [Object object]; it's more helpful if (value) { valueStr = `${name}={${name}}`; } else { // null is an object valueStr = `${name}={${value}}`; } break; default: if (value) { // `toString` necessary for Symbols valueStr = `${name}={${value.toString()}}`; } break; } return 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 code snippet as syntax-highlighted HTML markup. * * For the syntax highlighting to be visible, a Highlight.js CSS stylesheet must be * loaded on the page. * * The content returned by this function is sanitized and safe to use as * `dangerouslySetInnerHTML` prop. */ export function highlightCode(code) { // JSX support in Highlight.js involves a combination of the TS and XML // languages, so we need to load both. if (!hljs.getLanguage('typescript')) { hljs.registerLanguage('typescript', hljsTypeScriptLang); } if (!hljs.getLanguage('xml')) { hljs.registerLanguage('xml', hljsXMLLang); } return hljs.highlightAuto(code).value; } /** * Render a JSX expression as syntax-highlighted HTML markup. * * The content returned by this function is sanitized and safe to use as * `dangerouslySetInnerHTML` prop. */ export function jsxToHTML(vnode) { const code = jsxToString(vnode); return highlightCode(code); } //# sourceMappingURL=jsx-to-string.js.map