import { encodeEntities, indent, isLargeString, styleObjToCss, getChildren, createComponent, UNSAFE_NAME, VOID_ELEMENTS, NAMESPACE_REPLACE_REGEX, HTML_LOWER_CASE, SVG_CAMEL_CASE } from './util.js'; import { COMMIT, DIFF, DIFFED, RENDER, SKIP_EFFECTS } from './constants.js'; import { options, Fragment } from 'preact'; /** @typedef {import('preact').VNode} VNode */ // components without names, kept as a hash for later comparison to return consistent UnnamedComponentXX names. const UNNAMED = []; const EMPTY_ARR = []; /** * Render Preact JSX + Components to a pretty-printed HTML-like string. * @param {VNode} vnode JSX Element / VNode to render * @param {Object} [context={}] Initial root context object * @param {Object} [opts={}] Rendering options * @param {Boolean} [opts.shallow=false] Serialize nested Components (``) instead of rendering * @param {Boolean} [opts.xml=false] Use self-closing tags for elements without children * @param {Boolean} [opts.pretty=false] Add whitespace for readability * @param {RegExp|undefined} [opts.voidElements] RegeEx to define which element types are self-closing * @param {boolean} [_inner] * @returns {String} a pretty-printed HTML-like string */ export default function renderToStringPretty(vnode, context, opts, _inner) { // 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 // and avoid a lot of dirty checks and allocations by setting // `options._skipEffects` (`__s`) too. const previousSkipEffects = options[SKIP_EFFECTS]; options[SKIP_EFFECTS] = true; try { return _renderToStringPretty(vnode, context || {}, opts, _inner); } finally { // options._commit, we don't schedule any effects in this library right now, // so we can pass an empty queue to this hook. if (options[COMMIT]) options[COMMIT](vnode, EMPTY_ARR); options[SKIP_EFFECTS] = previousSkipEffects; EMPTY_ARR.length = 0; } } function _renderToStringPretty( vnode, context, opts, inner, isSvgMode, selectValue ) { if (vnode == null || typeof vnode === 'boolean') { return ''; } // #text nodes if (typeof vnode !== 'object') { if (typeof vnode === 'function') return ''; return encodeEntities(vnode + ''); } let pretty = opts.pretty, indentChar = pretty && typeof pretty === 'string' ? pretty : '\t'; if (Array.isArray(vnode)) { let rendered = ''; for (let i = 0; i < vnode.length; i++) { if (pretty && i > 0) rendered = rendered + '\n'; rendered = rendered + _renderToStringPretty( vnode[i], context, opts, inner, isSvgMode, selectValue ); } return rendered; } // VNodes have {constructor:undefined} to prevent JSON injection: if (vnode.constructor !== undefined) return ''; if (options[DIFF]) options[DIFF](vnode); let nodeName = vnode.type, props = vnode.props, isComponent = false; // components if (typeof nodeName === 'function') { isComponent = true; if (opts.shallow && (inner || opts.renderRootComponent === false)) { nodeName = getComponentName(nodeName); } else if (nodeName === Fragment) { const children = []; getChildren(children, vnode.props.children); return _renderToStringPretty( children, context, opts, opts.shallowHighOrder !== false, isSvgMode, selectValue ); } else { let rendered; let c = (vnode.__c = createComponent(vnode, context)); let renderHook = options[RENDER]; let cctx = context; let cxType = nodeName.contextType; if (cxType != null) { let provider = context[cxType.__c]; cctx = provider ? provider.props.value : cxType.__; } if ( !nodeName.prototype || typeof nodeName.prototype.render !== 'function' ) { // If a hook invokes setState() to invalidate the component during rendering, // re-render it up to 25 times to allow "settling" of memoized states. // Note: // This will need to be updated for Preact 11 to use internal.flags rather than component._dirty: // https://github.com/preactjs/preact/blob/d4ca6fdb19bc715e49fd144e69f7296b2f4daa40/src/diff/component.js#L35-L44 let count = 0; while (c.__d && count++ < 25) { c.__d = false; if (renderHook) renderHook(vnode); // stateless functional components rendered = nodeName.call(vnode.__c, props, cctx); } } else { // c = new nodeName(props, context); c = vnode.__c = new nodeName(props, cctx); c.__v = vnode; // turn off stateful re-rendering: c._dirty = c.__d = true; c.props = props; if (c.state == null) c.state = {}; if (c._nextState == null && c.__s == null) { c._nextState = c.__s = c.state; } c.context = cctx; if (nodeName.getDerivedStateFromProps) c.state = Object.assign( {}, c.state, nodeName.getDerivedStateFromProps(c.props, c.state) ); else if (c.componentWillMount) { c.componentWillMount(); // If the user called setState in cWM we need to flush pending, // state updates. This is the same behaviour in React. c.state = c._nextState !== c.state ? c._nextState : c.__s !== c.state ? c.__s : c.state; } if (renderHook) renderHook(vnode); rendered = c.render(c.props, c.state, c.context); } if (c.getChildContext) { context = Object.assign({}, context, c.getChildContext()); } const res = _renderToStringPretty( rendered, context, opts, opts.shallowHighOrder !== false, isSvgMode, selectValue ); if (options[DIFFED]) options[DIFFED](vnode); return res; } } // render JSX to HTML let s = '<' + nodeName, propChildren, html; if (props) { let attrs = Object.keys(props); // allow sorting lexicographically for more determinism (useful for tests, such as via preact-jsx-chai) if (opts && opts.sortAttributes === true) attrs.sort(); for (let i = 0; i < attrs.length; i++) { let name = attrs[i], v = props[name]; if (name === 'children') { propChildren = v; continue; } if (UNSAFE_NAME.test(name)) continue; if ( !(opts && opts.allAttributes) && (name === 'key' || name === 'ref' || name === '__self' || name === '__source') ) continue; if (name === 'defaultValue') { name = 'value'; } else if (name === 'defaultChecked') { name = 'checked'; } else if (name === 'defaultSelected') { name = 'selected'; } else if (name === 'className') { if (typeof props.class !== 'undefined') continue; name = 'class'; } else if (name === 'acceptCharset') { name = 'accept-charset'; } else if (name === 'httpEquiv') { name = 'http-equiv'; } else if (NAMESPACE_REPLACE_REGEX.test(name)) { name = name.replace(NAMESPACE_REPLACE_REGEX, '$1:$2').toLowerCase(); } else if (isSvgMode) { if (SVG_CAMEL_CASE.test(name)) { name = name === 'panose1' ? 'panose-1' : name.replace(/([A-Z])/g, '-$1').toLowerCase(); } } else if (HTML_LOWER_CASE.test(name)) { name = name.toLowerCase(); } if (name === 'htmlFor') { if (props.for) continue; name = 'for'; } if (name === 'style' && v && typeof v === 'object') { v = styleObjToCss(v); } // always use string values instead of booleans for aria attributes // also see https://github.com/preactjs/preact/pull/2347/files if (name[0] === 'a' && name['1'] === 'r' && typeof v === 'boolean') { v = String(v); } let hooked = opts.attributeHook && opts.attributeHook(name, v, context, opts, isComponent); if (hooked || hooked === '') { s = s + hooked; continue; } if (name === 'dangerouslySetInnerHTML') { html = v && v.__html; } else if (nodeName === 'textarea' && name === 'value') { // propChildren = v; } else if ((v || v === 0 || v === '') && typeof v !== 'function') { if (v === true || v === '') { v = name; // in non-xml mode, allow boolean attributes if (!opts || !opts.xml) { s = s + ' ' + name; continue; } } if (name === 'value') { if (nodeName === 'select') { selectValue = v; continue; } else if ( // If we're looking at an