UNPKG

7.16 kBJavaScriptView Raw
1import { encodeEntities, indent, isLargeString, styleObjToCss, assign, getChildren } from './util';
2import { ENABLE_PRETTY } from '../env';
3import { options, Fragment } from 'preact';
4
5const SHALLOW = { shallow: true };
6
7// components without names, kept as a hash for later comparison to return consistent UnnamedComponentXX names.
8const UNNAMED = [];
9
10const VOID_ELEMENTS = /^(area|base|br|col|embed|hr|img|input|link|meta|param|source|track|wbr)$/;
11
12
13/** Render Preact JSX + Components to an HTML string.
14 * @name render
15 * @function
16 * @param {VNode} vnode JSX VNode to render.
17 * @param {Object} [context={}] Optionally pass an initial context object through the render path.
18 * @param {Object} [options={}] Rendering options
19 * @param {Boolean} [options.shallow=false] If `true`, renders nested Components as HTML elements (`<Foo a="b" />`).
20 * @param {Boolean} [options.xml=false] If `true`, uses self-closing tags for elements without children.
21 * @param {Boolean} [options.pretty=false] If `true`, adds whitespace for readability
22 */
23renderToString.render = renderToString;
24
25
26/** Only render elements, leaving Components inline as `<ComponentName ... />`.
27 * This method is just a convenience alias for `render(vnode, context, { shallow:true })`
28 * @name shallow
29 * @function
30 * @param {VNode} vnode JSX VNode to render.
31 * @param {Object} [context={}] Optionally pass an initial context object through the render path.
32 */
33let shallowRender = (vnode, context) => renderToString(vnode, context, SHALLOW);
34
35
36/** The default export is an alias of `render()`. */
37function renderToString(vnode, context, opts, inner, isSvgMode) {
38 if (vnode==null || typeof vnode==='boolean') {
39 return '';
40 }
41
42 let nodeName = vnode.type,
43 props = vnode.props,
44 isComponent = false;
45 context = context || {};
46 opts = opts || {};
47
48 let pretty = ENABLE_PRETTY && opts.pretty,
49 indentChar = pretty && typeof pretty==='string' ? pretty : '\t';
50
51 // #text nodes
52 if (typeof vnode!=='object' && !nodeName) {
53 return encodeEntities(vnode);
54 }
55
56 // components
57 if (typeof nodeName==='function') {
58 isComponent = true;
59 if (opts.shallow && (inner || opts.renderRootComponent===false)) {
60 nodeName = getComponentName(nodeName);
61 }
62 else if (nodeName===Fragment) {
63 let rendered = '';
64 let children = [];
65 getChildren(children, vnode.props.children);
66
67 for (let i = 0; i < children.length; i++) {
68 rendered += renderToString(children[i], context, opts, opts.shallowHighOrder!==false, isSvgMode);
69 }
70 return rendered;
71 }
72 else {
73 let rendered;
74
75 let c = vnode.__c = { __v: vnode, context, props: vnode.props };
76 if (options.render) options.render(vnode);
77
78 if (!nodeName.prototype || typeof nodeName.prototype.render!=='function') {
79 // stateless functional components
80 rendered = nodeName.call(vnode.__c, props, context);
81 }
82 else {
83 // class-based components
84 // c = new nodeName(props, context);
85 c = vnode.__c = new nodeName(props, context);
86 c.__v = vnode;
87 // turn off stateful re-rendering:
88 c._dirty = c.__d = true;
89 c.props = props;
90 c.context = context;
91 if (nodeName.getDerivedStateFromProps) c.state = assign(assign({}, c.state), nodeName.getDerivedStateFromProps(c.props, c.state));
92 else if (c.componentWillMount) c.componentWillMount();
93 rendered = c.render(c.props, c.state, c.context);
94 }
95
96 if (c.getChildContext) {
97 context = assign(assign({}, context), c.getChildContext());
98 }
99
100 return renderToString(rendered, context, opts, opts.shallowHighOrder!==false);
101 }
102 }
103
104 // render JSX to HTML
105 let s = '', html;
106
107 if (props) {
108 let attrs = Object.keys(props);
109
110 // allow sorting lexicographically for more determinism (useful for tests, such as via preact-jsx-chai)
111 if (opts && opts.sortAttributes===true) attrs.sort();
112
113 for (let i=0; i<attrs.length; i++) {
114 let name = attrs[i],
115 v = props[name];
116 if (name==='children') continue;
117
118 if (name.match(/[\s\n\\/='"\0<>]/)) continue;
119
120 if (!(opts && opts.allAttributes) && (name==='key' || name==='ref')) continue;
121
122 if (name==='className') {
123 if (props.class) continue;
124 name = 'class';
125 }
126 else if (isSvgMode && name.match(/^xlink:?./)) {
127 name = name.toLowerCase().replace(/^xlink:?/, 'xlink:');
128 }
129
130 if (name==='style' && v && typeof v==='object') {
131 v = styleObjToCss(v);
132 }
133
134 let hooked = opts.attributeHook && opts.attributeHook(name, v, context, opts, isComponent);
135 if (hooked || hooked==='') {
136 s += hooked;
137 continue;
138 }
139
140 if (name==='dangerouslySetInnerHTML') {
141 html = v && v.__html;
142 }
143 else if ((v || v===0 || v==='') && typeof v!=='function') {
144 if (v===true || v==='') {
145 v = name;
146 // in non-xml mode, allow boolean attributes
147 if (!opts || !opts.xml) {
148 s += ' ' + name;
149 continue;
150 }
151 }
152 s += ` ${name}="${encodeEntities(v)}"`;
153 }
154 }
155 }
156
157 // account for >1 multiline attribute
158 if (pretty) {
159 let sub = s.replace(/^\n\s*/, ' ');
160 if (sub!==s && !~sub.indexOf('\n')) s = sub;
161 else if (pretty && ~s.indexOf('\n')) s += '\n';
162 }
163
164 s = `<${nodeName}${s}>`;
165 if (String(nodeName).match(/[\s\n\\/='"\0<>]/)) throw s;
166
167 let isVoid = String(nodeName).match(VOID_ELEMENTS);
168 if (isVoid) s = s.replace(/>$/, ' />');
169
170 let pieces = [];
171
172 let children;
173 if (html) {
174 // if multiline, indent.
175 if (pretty && isLargeString(html)) {
176 html = '\n' + indentChar + indent(html, indentChar);
177 }
178 s += html;
179 }
180 else if (props && getChildren(children = [], props.children).length) {
181 let hasLarge = pretty && ~s.indexOf('\n');
182 for (let i=0; i<children.length; i++) {
183 let child = children[i];
184 if (child!=null && child!==false) {
185 let childSvgMode = nodeName==='svg' ? true : nodeName==='foreignObject' ? false : isSvgMode,
186 ret = renderToString(child, context, opts, true, childSvgMode);
187 if (pretty && !hasLarge && isLargeString(ret)) hasLarge = true;
188 if (ret) pieces.push(ret);
189 }
190 }
191 if (pretty && hasLarge) {
192 for (let i=pieces.length; i--; ) {
193 pieces[i] = '\n' + indentChar + indent(pieces[i], indentChar);
194 }
195 }
196 }
197
198 if (pieces.length) {
199 s += pieces.join('');
200 }
201 else if (opts && opts.xml) {
202 return s.substring(0, s.length-1) + ' />';
203 }
204
205 if (!isVoid) {
206 if (pretty && ~s.indexOf('\n')) s += '\n';
207 s += `</${nodeName}>`;
208 }
209
210 return s;
211}
212
213function getComponentName(component) {
214 return component.displayName || component!==Function && component.name || getFallbackComponentName(component);
215}
216
217function getFallbackComponentName(component) {
218 let str = Function.prototype.toString.call(component),
219 name = (str.match(/^\s*function\s+([^( ]+)/) || '')[1];
220 if (!name) {
221 // search for an existing indexed name for the given component:
222 let index = -1;
223 for (let i=UNNAMED.length; i--; ) {
224 if (UNNAMED[i]===component) {
225 index = i;
226 break;
227 }
228 }
229 // not found, create a new indexed name:
230 if (index<0) {
231 index = UNNAMED.push(component) - 1;
232 }
233 name = `UnnamedComponent${index}`;
234 }
235 return name;
236}
237renderToString.shallowRender = shallowRender;
238
239export default renderToString;
240
241export {
242 renderToString as render,
243 renderToString,
244 shallowRender
245};