preact-render-to-string 5.2.0 → 5.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/index.js CHANGED
@@ -1,28 +1,30 @@
1
1
  import {
2
2
  encodeEntities,
3
- indent,
4
- isLargeString,
5
3
  styleObjToCss,
6
- assign,
7
- getChildren
4
+ getContext,
5
+ createComponent,
6
+ UNSAFE_NAME,
7
+ XLINK,
8
+ VOID_ELEMENTS
8
9
  } from './util';
9
10
  import { options, Fragment } from 'preact';
11
+ import { _renderToStringPretty } from './pretty';
12
+ import {
13
+ COMMIT,
14
+ COMPONENT,
15
+ DIFF,
16
+ DIFFED,
17
+ DIRTY,
18
+ NEXT_STATE,
19
+ RENDER,
20
+ SKIP_EFFECTS,
21
+ VNODE
22
+ } from './constants';
10
23
 
11
24
  /** @typedef {import('preact').VNode} VNode */
12
25
 
13
26
  const SHALLOW = { shallow: true };
14
27
 
15
- // components without names, kept as a hash for later comparison to return consistent UnnamedComponentXX names.
16
- const UNNAMED = [];
17
-
18
- const VOID_ELEMENTS = /^(area|base|br|col|embed|hr|img|input|link|meta|param|source|track|wbr)$/;
19
-
20
- const UNSAFE_NAME = /[\s\n\\/='"\0<>]/;
21
-
22
- function markAsDirty() {
23
- this.__d = true;
24
- }
25
-
26
28
  /** Render Preact JSX + Components to an HTML string.
27
29
  * @name render
28
30
  * @function
@@ -48,408 +50,310 @@ let shallowRender = (vnode, context) => renderToString(vnode, context, SHALLOW);
48
50
  const EMPTY_ARR = [];
49
51
  function renderToString(vnode, context, opts) {
50
52
  context = context || {};
51
- opts = opts || {};
52
53
 
53
54
  // Performance optimization: `renderToString` is synchronous and we
54
55
  // therefore don't execute any effects. To do that we pass an empty
55
56
  // array to `options._commit` (`__c`). But we can go one step further
56
57
  // and avoid a lot of dirty checks and allocations by setting
57
58
  // `options._skipEffects` (`__s`) too.
58
- const previousSkipEffects = options.__s;
59
- options.__s = true;
60
-
61
- const res = _renderToString(vnode, context, opts);
59
+ const previousSkipEffects = options[SKIP_EFFECTS];
60
+ options[SKIP_EFFECTS] = true;
61
+
62
+ let res;
63
+ if (
64
+ opts &&
65
+ (opts.pretty ||
66
+ opts.voidElements ||
67
+ opts.sortAttributes ||
68
+ opts.shallow ||
69
+ opts.allAttributes ||
70
+ opts.xml ||
71
+ opts.attributeHook)
72
+ ) {
73
+ res = _renderToStringPretty(vnode, context, opts);
74
+ } else {
75
+ res = _renderToString(vnode, context, false, undefined);
76
+ }
62
77
 
63
78
  // options._commit, we don't schedule any effects in this library right now,
64
79
  // so we can pass an empty queue to this hook.
65
- if (options.__c) options.__c(vnode, EMPTY_ARR);
80
+ if (options[COMMIT]) options[COMMIT](vnode, EMPTY_ARR);
81
+ options[SKIP_EFFECTS] = previousSkipEffects;
66
82
  EMPTY_ARR.length = 0;
67
- options.__s = previousSkipEffects;
68
83
  return res;
69
84
  }
70
85
 
71
- /** The default export is an alias of `render()`. */
72
- function _renderToString(vnode, context, opts, inner, isSvgMode, selectValue) {
73
- if (vnode == null || typeof vnode === 'boolean') {
74
- return '';
86
+ function renderFunctionComponent(vnode, context) {
87
+ let rendered,
88
+ c = createComponent(vnode, context),
89
+ cctx = getContext(vnode.type, context);
90
+
91
+ vnode[COMPONENT] = c;
92
+
93
+ // If a hook invokes setState() to invalidate the component during rendering,
94
+ // re-render it up to 25 times to allow "settling" of memoized states.
95
+ // Note:
96
+ // This will need to be updated for Preact 11 to use internal.flags rather than component._dirty:
97
+ // https://github.com/preactjs/preact/blob/d4ca6fdb19bc715e49fd144e69f7296b2f4daa40/src/diff/component.js#L35-L44
98
+ let renderHook = options[RENDER];
99
+ let count = 0;
100
+ while (c[DIRTY] && count++ < 25) {
101
+ c[DIRTY] = false;
102
+
103
+ if (renderHook) renderHook(vnode);
104
+
105
+ // stateless functional components
106
+ rendered = vnode.type.call(c, vnode.props, cctx);
75
107
  }
76
108
 
77
- // #text nodes
78
- if (typeof vnode !== 'object') {
79
- return encodeEntities(vnode);
109
+ return rendered;
110
+ }
111
+
112
+ function renderClassComponent(vnode, context) {
113
+ let nodeName = vnode.type,
114
+ cctx = getContext(nodeName, context);
115
+
116
+ // c = new nodeName(props, context);
117
+ let c = new nodeName(vnode.props, cctx);
118
+ vnode[COMPONENT] = c;
119
+ c[VNODE] = vnode;
120
+ // turn off stateful re-rendering:
121
+ c[DIRTY] = true;
122
+ c.props = vnode.props;
123
+ if (c.state == null) c.state = {};
124
+
125
+ if (c[NEXT_STATE] == null) {
126
+ c[NEXT_STATE] = c.state;
80
127
  }
81
128
 
82
- let pretty = opts.pretty,
83
- indentChar = pretty && typeof pretty === 'string' ? pretty : '\t';
129
+ c.context = cctx;
130
+ if (nodeName.getDerivedStateFromProps) {
131
+ c.state = assign(
132
+ {},
133
+ c.state,
134
+ nodeName.getDerivedStateFromProps(c.props, c.state)
135
+ );
136
+ } else if (c.componentWillMount) {
137
+ c.componentWillMount();
138
+
139
+ // If the user called setState in cWM we need to flush pending,
140
+ // state updates. This is the same behaviour in React.
141
+ c.state = c[NEXT_STATE] !== c.state ? c[NEXT_STATE] : c.state;
142
+ }
84
143
 
85
- if (Array.isArray(vnode)) {
86
- let rendered = '';
87
- for (let i = 0; i < vnode.length; i++) {
88
- if (pretty && i > 0) rendered += '\n';
89
- rendered += _renderToString(
90
- vnode[i],
91
- context,
92
- opts,
93
- inner,
94
- isSvgMode,
95
- selectValue
96
- );
97
- }
98
- return rendered;
144
+ let renderHook = options[RENDER];
145
+ if (renderHook) renderHook(vnode);
146
+
147
+ return c.render(c.props, c.state, c.context);
148
+ }
149
+
150
+ function normalizePropName(name, isSvgMode) {
151
+ if (name === 'className') {
152
+ return 'class';
153
+ } else if (name === 'htmlFor') {
154
+ return 'for';
155
+ } else if (name === 'defaultValue') {
156
+ return 'value';
157
+ } else if (name === 'defaultChecked') {
158
+ return 'checked';
159
+ } else if (name === 'defaultSelected') {
160
+ return 'selected';
161
+ } else if (isSvgMode && XLINK.test(name)) {
162
+ return name.toLowerCase().replace(/^xlink:?/, 'xlink:');
99
163
  }
100
164
 
101
- let nodeName = vnode.type,
102
- props = vnode.props,
103
- isComponent = false;
104
-
105
- // components
106
- if (typeof nodeName === 'function') {
107
- isComponent = true;
108
- if (opts.shallow && (inner || opts.renderRootComponent === false)) {
109
- nodeName = getComponentName(nodeName);
110
- } else if (nodeName === Fragment) {
111
- const children = [];
112
- getChildren(children, vnode.props.children);
113
- return _renderToString(
114
- children,
115
- context,
116
- opts,
117
- opts.shallowHighOrder !== false,
118
- isSvgMode,
119
- selectValue
120
- );
121
- } else {
122
- let rendered;
165
+ return name;
166
+ }
123
167
 
124
- let c = (vnode.__c = {
125
- __v: vnode,
126
- context,
127
- props: vnode.props,
128
- // silently drop state updates
129
- setState: markAsDirty,
130
- forceUpdate: markAsDirty,
131
- __d: true,
132
- // hooks
133
- __h: []
134
- });
168
+ function normalizePropValue(name, v) {
169
+ if (name === 'style' && v != null && typeof v === 'object') {
170
+ return styleObjToCss(v);
171
+ } else if (name[0] === 'a' && name[1] === 'r' && typeof v === 'boolean') {
172
+ // always use string values instead of booleans for aria attributes
173
+ // also see https://github.com/preactjs/preact/pull/2347/files
174
+ return String(v);
175
+ }
135
176
 
136
- // options._diff
137
- if (options.__b) options.__b(vnode);
177
+ return v;
178
+ }
138
179
 
139
- // options._render
140
- let renderHook = options.__r;
180
+ const isArray = Array.isArray;
181
+ const assign = Object.assign;
141
182
 
142
- if (
143
- !nodeName.prototype ||
144
- typeof nodeName.prototype.render !== 'function'
145
- ) {
146
- // Necessary for createContext api. Setting this property will pass
147
- // the context value as `this.context` just for this component.
148
- let cxType = nodeName.contextType;
149
- let provider = cxType && context[cxType.__c];
150
- let cctx =
151
- cxType != null
152
- ? provider
153
- ? provider.props.value
154
- : cxType.__
155
- : context;
156
-
157
- // If a hook invokes setState() to invalidate the component during rendering,
158
- // re-render it up to 25 times to allow "settling" of memoized states.
159
- // Note:
160
- // This will need to be updated for Preact 11 to use internal.flags rather than component._dirty:
161
- // https://github.com/preactjs/preact/blob/d4ca6fdb19bc715e49fd144e69f7296b2f4daa40/src/diff/component.js#L35-L44
162
- let count = 0;
163
- while (c.__d && count++ < 25) {
164
- c.__d = false;
165
-
166
- if (renderHook) renderHook(vnode);
167
-
168
- // stateless functional components
169
- rendered = nodeName.call(vnode.__c, props, cctx);
170
- }
171
- } else {
172
- // class-based components
173
- let cxType = nodeName.contextType;
174
- let provider = cxType && context[cxType.__c];
175
- let cctx =
176
- cxType != null
177
- ? provider
178
- ? provider.props.value
179
- : cxType.__
180
- : context;
181
-
182
- // c = new nodeName(props, context);
183
- c = vnode.__c = new nodeName(props, cctx);
184
- c.__v = vnode;
185
- // turn off stateful re-rendering:
186
- c._dirty = c.__d = true;
187
- c.props = props;
188
- if (c.state == null) c.state = {};
189
-
190
- if (c._nextState == null && c.__s == null) {
191
- c._nextState = c.__s = c.state;
192
- }
183
+ /** The default export is an alias of `render()`. */
184
+ function _renderToString(vnode, context, isSvgMode, selectValue) {
185
+ // Ignore non-rendered VNodes/values
186
+ if (vnode == null || vnode === true || vnode === false || vnode === '') {
187
+ return '';
188
+ }
193
189
 
194
- c.context = cctx;
195
- if (nodeName.getDerivedStateFromProps)
196
- c.state = assign(
197
- assign({}, c.state),
198
- nodeName.getDerivedStateFromProps(c.props, c.state)
199
- );
200
- else if (c.componentWillMount) {
201
- c.componentWillMount();
202
-
203
- // If the user called setState in cWM we need to flush pending,
204
- // state updates. This is the same behaviour in React.
205
- c.state =
206
- c._nextState !== c.state
207
- ? c._nextState
208
- : c.__s !== c.state
209
- ? c.__s
210
- : c.state;
211
- }
190
+ // Text VNodes: escape as HTML
191
+ if (typeof vnode !== 'object') {
192
+ return encodeEntities(vnode);
193
+ }
212
194
 
213
- if (renderHook) renderHook(vnode);
195
+ // Recurse into children / Arrays
196
+ if (isArray(vnode)) {
197
+ let rendered = '';
198
+ for (let i = 0; i < vnode.length; i++) {
199
+ rendered =
200
+ rendered + _renderToString(vnode[i], context, isSvgMode, selectValue);
201
+ }
202
+ return rendered;
203
+ }
214
204
 
215
- rendered = c.render(c.props, c.state, c.context);
216
- }
205
+ if (options[DIFF]) options[DIFF](vnode);
217
206
 
218
- if (c.getChildContext) {
219
- context = assign(assign({}, context), c.getChildContext());
220
- }
207
+ let type = vnode.type,
208
+ props = vnode.props;
221
209
 
222
- if (options.diffed) options.diffed(vnode);
210
+ // Invoke rendering on Components
211
+ const isComponent = typeof type === 'function';
212
+ if (isComponent) {
213
+ if (type === Fragment) {
223
214
  return _renderToString(
224
- rendered,
215
+ vnode.props.children,
225
216
  context,
226
- opts,
227
- opts.shallowHighOrder !== false,
228
217
  isSvgMode,
229
218
  selectValue
230
219
  );
231
220
  }
232
- }
233
221
 
234
- // render JSX to HTML
235
- let s = '<' + nodeName,
236
- propChildren,
237
- html;
222
+ let rendered;
223
+ if (type.prototype && typeof type.prototype.render === 'function') {
224
+ rendered = renderClassComponent(vnode, context);
225
+ } else {
226
+ rendered = renderFunctionComponent(vnode, context);
227
+ }
238
228
 
239
- if (props) {
240
- let attrs = Object.keys(props);
229
+ let component = vnode[COMPONENT];
230
+ if (component.getChildContext) {
231
+ context = assign({}, context, component.getChildContext());
232
+ }
241
233
 
242
- // allow sorting lexicographically for more determinism (useful for tests, such as via preact-jsx-chai)
243
- if (opts && opts.sortAttributes === true) attrs.sort();
234
+ // Recurse into children before invoking the after-diff hook
235
+ const str = _renderToString(rendered, context, isSvgMode, selectValue);
236
+ if (options[DIFFED]) options[DIFFED](vnode);
237
+ return str;
238
+ }
244
239
 
245
- for (let i = 0; i < attrs.length; i++) {
246
- let name = attrs[i],
247
- v = props[name];
248
- if (name === 'children') {
249
- propChildren = v;
250
- continue;
251
- }
240
+ // Serialize Element VNodes to HTML
241
+ let s = '<',
242
+ children,
243
+ html;
252
244
 
253
- if (UNSAFE_NAME.test(name)) continue;
245
+ s = s + type;
246
+
247
+ if (props) {
248
+ children = props.children;
249
+ for (let name in props) {
250
+ let v = props[name];
254
251
 
255
252
  if (
256
- !(opts && opts.allAttributes) &&
257
- (name === 'key' ||
258
- name === 'ref' ||
259
- name === '__self' ||
260
- name === '__source')
261
- )
253
+ name === 'key' ||
254
+ name === 'ref' ||
255
+ name === '__self' ||
256
+ name === '__source' ||
257
+ name === 'children' ||
258
+ (name === 'className' && 'class' in props) ||
259
+ (name === 'htmlFor' && 'for' in props)
260
+ ) {
262
261
  continue;
263
-
264
- if (name === 'defaultValue') {
265
- name = 'value';
266
- } else if (name === 'className') {
267
- if (typeof props.class !== 'undefined') continue;
268
- name = 'class';
269
- } else if (isSvgMode && name.match(/^xlink:?./)) {
270
- name = name.toLowerCase().replace(/^xlink:?/, 'xlink:');
271
- }
272
-
273
- if (name === 'htmlFor') {
274
- if (props.for) continue;
275
- name = 'for';
276
262
  }
277
263
 
278
- if (name === 'style' && v && typeof v === 'object') {
279
- v = styleObjToCss(v);
280
- }
264
+ if (UNSAFE_NAME.test(name)) continue;
281
265
 
282
- // always use string values instead of booleans for aria attributes
283
- // also see https://github.com/preactjs/preact/pull/2347/files
284
- if (name[0] === 'a' && name['1'] === 'r' && typeof v === 'boolean') {
285
- v = String(v);
286
- }
287
-
288
- let hooked =
289
- opts.attributeHook &&
290
- opts.attributeHook(name, v, context, opts, isComponent);
291
- if (hooked || hooked === '') {
292
- s += hooked;
293
- continue;
294
- }
266
+ name = normalizePropName(name, isSvgMode);
267
+ v = normalizePropValue(name, v);
295
268
 
296
269
  if (name === 'dangerouslySetInnerHTML') {
297
270
  html = v && v.__html;
298
- } else if (nodeName === 'textarea' && name === 'value') {
271
+ } else if (type === 'textarea' && name === 'value') {
299
272
  // <textarea value="a&b"> --> <textarea>a&amp;b</textarea>
300
- propChildren = v;
273
+ children = v;
301
274
  } else if ((v || v === 0 || v === '') && typeof v !== 'function') {
302
275
  if (v === true || v === '') {
303
276
  v = name;
304
- // in non-xml mode, allow boolean attributes
305
- if (!opts || !opts.xml) {
306
- s += ' ' + name;
307
- continue;
308
- }
277
+ s = s + ' ' + name;
278
+ continue;
309
279
  }
310
280
 
311
281
  if (name === 'value') {
312
- if (nodeName === 'select') {
282
+ if (type === 'select') {
313
283
  selectValue = v;
314
284
  continue;
315
285
  } else if (
316
286
  // If we're looking at an <option> and it's the currently selected one
317
- nodeName === 'option' &&
287
+ type === 'option' &&
318
288
  selectValue == v &&
319
289
  // and the <option> doesn't already have a selected attribute on it
320
- typeof props.selected === 'undefined'
290
+ !('selected' in props)
321
291
  ) {
322
- s += ` selected`;
292
+ s = s + ' selected';
323
293
  }
324
294
  }
325
- s += ` ${name}="${encodeEntities(v)}"`;
295
+ s = s + ' ' + name + '="' + encodeEntities(v) + '"';
326
296
  }
327
297
  }
328
298
  }
329
299
 
330
- // account for >1 multiline attribute
331
- if (pretty) {
332
- let sub = s.replace(/\n\s*/, ' ');
333
- if (sub !== s && !~sub.indexOf('\n')) s = sub;
334
- else if (pretty && ~s.indexOf('\n')) s += '\n';
335
- }
336
-
337
- s += '>';
300
+ let startElement = s;
301
+ s = s + '>';
338
302
 
339
- if (UNSAFE_NAME.test(nodeName))
340
- throw new Error(`${nodeName} is not a valid HTML tag name in ${s}`);
303
+ if (UNSAFE_NAME.test(type)) {
304
+ throw new Error(`${type} is not a valid HTML tag name in ${s}`);
305
+ }
341
306
 
342
- let isVoid =
343
- VOID_ELEMENTS.test(nodeName) ||
344
- (opts.voidElements && opts.voidElements.test(nodeName));
345
- let pieces = [];
307
+ let pieces = '';
308
+ let hasChildren = false;
346
309
 
347
- let children;
348
310
  if (html) {
349
- // if multiline, indent.
350
- if (pretty && isLargeString(html)) {
351
- html = '\n' + indentChar + indent(html, indentChar);
352
- }
353
- s += html;
354
- } else if (
355
- propChildren != null &&
356
- getChildren((children = []), propChildren).length
357
- ) {
358
- let hasLarge = pretty && ~s.indexOf('\n');
359
- let lastWasText = false;
360
-
311
+ pieces = pieces + html;
312
+ hasChildren = true;
313
+ } else if (typeof children === 'string') {
314
+ pieces = pieces + encodeEntities(children);
315
+ hasChildren = true;
316
+ } else if (isArray(children)) {
361
317
  for (let i = 0; i < children.length; i++) {
362
318
  let child = children[i];
363
319
 
364
320
  if (child != null && child !== false) {
365
321
  let childSvgMode =
366
- nodeName === 'svg'
367
- ? true
368
- : nodeName === 'foreignObject'
369
- ? false
370
- : isSvgMode,
371
- ret = _renderToString(
372
- child,
373
- context,
374
- opts,
375
- true,
376
- childSvgMode,
377
- selectValue
378
- );
379
-
380
- if (pretty && !hasLarge && isLargeString(ret)) hasLarge = true;
322
+ type === 'svg' || (type !== 'foreignObject' && isSvgMode);
323
+ let ret = _renderToString(child, context, childSvgMode, selectValue);
381
324
 
382
325
  // Skip if we received an empty string
383
326
  if (ret) {
384
- if (pretty) {
385
- let isText = ret.length > 0 && ret[0] != '<';
386
-
387
- // We merge adjacent text nodes, otherwise each piece would be printed
388
- // on a new line.
389
- if (lastWasText && isText) {
390
- pieces[pieces.length - 1] += ret;
391
- } else {
392
- pieces.push(ret);
393
- }
394
-
395
- lastWasText = isText;
396
- } else {
397
- pieces.push(ret);
398
- }
327
+ pieces = pieces + ret;
328
+ hasChildren = true;
399
329
  }
400
330
  }
401
331
  }
402
- if (pretty && hasLarge) {
403
- for (let i = pieces.length; i--; ) {
404
- pieces[i] = '\n' + indentChar + indent(pieces[i], indentChar);
405
- }
332
+ } else if (children != null && children !== false && children !== true) {
333
+ let childSvgMode =
334
+ type === 'svg' || (type !== 'foreignObject' && isSvgMode);
335
+ let ret = _renderToString(children, context, childSvgMode, selectValue);
336
+
337
+ // Skip if we received an empty string
338
+ if (ret) {
339
+ pieces = pieces + ret;
340
+ hasChildren = true;
406
341
  }
407
342
  }
408
343
 
409
- if (pieces.length || html) {
410
- s += pieces.join('');
411
- } else if (opts && opts.xml) {
412
- return s.substring(0, s.length - 1) + ' />';
413
- }
344
+ if (options[DIFFED]) options[DIFFED](vnode);
414
345
 
415
- if (isVoid && !children && !html) {
416
- s = s.replace(/>$/, ' />');
417
- } else {
418
- if (pretty && ~s.indexOf('\n')) s += '\n';
419
- s += `</${nodeName}>`;
346
+ if (hasChildren) {
347
+ s = s + pieces;
348
+ } else if (VOID_ELEMENTS.test(type)) {
349
+ return startElement + ' />';
420
350
  }
421
351
 
422
- return s;
352
+ return s + '</' + type + '>';
423
353
  }
424
354
 
425
- function getComponentName(component) {
426
- return (
427
- component.displayName ||
428
- (component !== Function && component.name) ||
429
- getFallbackComponentName(component)
430
- );
431
- }
355
+ /** The default export is an alias of `render()`. */
432
356
 
433
- function getFallbackComponentName(component) {
434
- let str = Function.prototype.toString.call(component),
435
- name = (str.match(/^\s*function\s+([^( ]+)/) || '')[1];
436
- if (!name) {
437
- // search for an existing indexed name for the given component:
438
- let index = -1;
439
- for (let i = UNNAMED.length; i--; ) {
440
- if (UNNAMED[i] === component) {
441
- index = i;
442
- break;
443
- }
444
- }
445
- // not found, create a new indexed name:
446
- if (index < 0) {
447
- index = UNNAMED.push(component) - 1;
448
- }
449
- name = `UnnamedComponent${index}`;
450
- }
451
- return name;
452
- }
453
357
  renderToString.shallowRender = shallowRender;
454
358
 
455
359
  export default renderToString;
package/src/jsx.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import './polyfills';
2
2
  import renderToString from './index';
3
- import { indent, encodeEntities, assign } from './util';
3
+ import { indent, encodeEntities } from './util';
4
4
  import prettyFormat from 'pretty-format';
5
5
 
6
6
  // we have to patch in Array support, Possible issue in npm.im/pretty-format
@@ -68,7 +68,7 @@ let defaultOpts = {
68
68
  };
69
69
 
70
70
  function renderToJsxString(vnode, context, opts, inner) {
71
- opts = assign(assign({}, defaultOpts), opts || {});
71
+ opts = Object.assign({}, defaultOpts, opts || {});
72
72
  return renderToString(vnode, context, opts, inner);
73
73
  }
74
74