UNPKG

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