preact-render-to-string 5.1.21 → 5.2.2

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
@@ -3,7 +3,6 @@ import {
3
3
  indent,
4
4
  isLargeString,
5
5
  styleObjToCss,
6
- assign,
7
6
  getChildren
8
7
  } from './util';
9
8
  import { options, Fragment } from 'preact';
@@ -18,8 +17,11 @@ const UNNAMED = [];
18
17
  const VOID_ELEMENTS = /^(area|base|br|col|embed|hr|img|input|link|meta|param|source|track|wbr)$/;
19
18
 
20
19
  const UNSAFE_NAME = /[\s\n\\/='"\0<>]/;
20
+ const XLINK = /^xlink:?./;
21
21
 
22
- const noop = () => {};
22
+ function markAsDirty() {
23
+ this.__d = true;
24
+ }
23
25
 
24
26
  /** Render Preact JSX + Components to an HTML string.
25
27
  * @name render
@@ -56,7 +58,12 @@ function renderToString(vnode, context, opts) {
56
58
  const previousSkipEffects = options.__s;
57
59
  options.__s = true;
58
60
 
59
- const res = _renderToString(vnode, context, opts);
61
+ let res;
62
+ if (opts.pretty || opts.sortAttributes) {
63
+ res = _renderToStringPretty(vnode, context, opts);
64
+ } else {
65
+ res = _renderToString(vnode, context, opts);
66
+ }
60
67
 
61
68
  // options._commit, we don't schedule any effects in this library right now,
62
69
  // so we can pass an empty queue to this hook.
@@ -66,6 +73,32 @@ function renderToString(vnode, context, opts) {
66
73
  return res;
67
74
  }
68
75
 
76
+ function createComponent(vnode, context) {
77
+ return {
78
+ __v: vnode,
79
+ context,
80
+ props: vnode.props,
81
+ // silently drop state updates
82
+ setState: markAsDirty,
83
+ forceUpdate: markAsDirty,
84
+ __d: true,
85
+ // hooks
86
+ __h: []
87
+ };
88
+ }
89
+
90
+ // Necessary for createContext api. Setting this property will pass
91
+ // the context value as `this.context` just for this component.
92
+ function getContext(nodeName, context) {
93
+ let cxType = nodeName.contextType;
94
+ let provider = cxType && context[cxType.__c];
95
+ return cxType != null
96
+ ? provider
97
+ ? provider.props.value
98
+ : cxType.__
99
+ : context;
100
+ }
101
+
69
102
  /** The default export is an alias of `render()`. */
70
103
  function _renderToString(vnode, context, opts, inner, isSvgMode, selectValue) {
71
104
  if (vnode == null || typeof vnode === 'boolean') {
@@ -77,21 +110,309 @@ function _renderToString(vnode, context, opts, inner, isSvgMode, selectValue) {
77
110
  return encodeEntities(vnode);
78
111
  }
79
112
 
80
- let pretty = opts.pretty,
81
- indentChar = pretty && typeof pretty === 'string' ? pretty : '\t';
82
-
83
113
  if (Array.isArray(vnode)) {
84
- let rendered = '';
114
+ const rendered = [];
85
115
  for (let i = 0; i < vnode.length; i++) {
86
- if (pretty && i > 0) rendered += '\n';
87
- rendered += _renderToString(
88
- vnode[i],
116
+ rendered.push(
117
+ _renderToString(vnode[i], context, opts, inner, isSvgMode, selectValue)
118
+ );
119
+ }
120
+ return rendered.join('');
121
+ }
122
+
123
+ let nodeName = vnode.type,
124
+ props = vnode.props,
125
+ isComponent = false;
126
+
127
+ // components
128
+ if (typeof nodeName === 'function') {
129
+ isComponent = true;
130
+ if (opts.shallow && (inner || opts.renderRootComponent === false)) {
131
+ nodeName = getComponentName(nodeName);
132
+ } else if (nodeName === Fragment) {
133
+ const children = [];
134
+ getChildren(children, vnode.props.children);
135
+ return _renderToString(
136
+ children,
89
137
  context,
90
138
  opts,
91
- inner,
139
+ opts.shallowHighOrder !== false,
92
140
  isSvgMode,
93
141
  selectValue
94
142
  );
143
+ } else {
144
+ let rendered;
145
+
146
+ let c = (vnode.__c = createComponent(vnode, context));
147
+
148
+ // options._diff
149
+ if (options.__b) options.__b(vnode);
150
+
151
+ // options._render
152
+ let renderHook = options.__r;
153
+
154
+ if (
155
+ !nodeName.prototype ||
156
+ typeof nodeName.prototype.render !== 'function'
157
+ ) {
158
+ let cctx = getContext(nodeName, context);
159
+
160
+ // If a hook invokes setState() to invalidate the component during rendering,
161
+ // re-render it up to 25 times to allow "settling" of memoized states.
162
+ // Note:
163
+ // This will need to be updated for Preact 11 to use internal.flags rather than component._dirty:
164
+ // https://github.com/preactjs/preact/blob/d4ca6fdb19bc715e49fd144e69f7296b2f4daa40/src/diff/component.js#L35-L44
165
+ let count = 0;
166
+ while (c.__d && count++ < 25) {
167
+ c.__d = false;
168
+
169
+ if (renderHook) renderHook(vnode);
170
+
171
+ // stateless functional components
172
+ rendered = nodeName.call(vnode.__c, props, cctx);
173
+ }
174
+ } else {
175
+ let cctx = getContext(nodeName, context);
176
+
177
+ // c = new nodeName(props, context);
178
+ c = vnode.__c = new nodeName(props, cctx);
179
+ c.__v = vnode;
180
+ // turn off stateful re-rendering:
181
+ c._dirty = c.__d = true;
182
+ c.props = props;
183
+ if (c.state == null) c.state = {};
184
+
185
+ if (c._nextState == null && c.__s == null) {
186
+ c._nextState = c.__s = c.state;
187
+ }
188
+
189
+ c.context = cctx;
190
+ if (nodeName.getDerivedStateFromProps)
191
+ c.state = Object.assign(
192
+ {},
193
+ c.state,
194
+ nodeName.getDerivedStateFromProps(c.props, c.state)
195
+ );
196
+ else if (c.componentWillMount) {
197
+ c.componentWillMount();
198
+
199
+ // If the user called setState in cWM we need to flush pending,
200
+ // state updates. This is the same behaviour in React.
201
+ c.state =
202
+ c._nextState !== c.state
203
+ ? c._nextState
204
+ : c.__s !== c.state
205
+ ? c.__s
206
+ : c.state;
207
+ }
208
+
209
+ if (renderHook) renderHook(vnode);
210
+
211
+ rendered = c.render(c.props, c.state, c.context);
212
+ }
213
+
214
+ if (c.getChildContext) {
215
+ context = Object.assign({}, context, c.getChildContext());
216
+ }
217
+
218
+ if (options.diffed) options.diffed(vnode);
219
+ return _renderToString(
220
+ rendered,
221
+ context,
222
+ opts,
223
+ opts.shallowHighOrder !== false,
224
+ isSvgMode,
225
+ selectValue
226
+ );
227
+ }
228
+ }
229
+
230
+ // render JSX to HTML
231
+ let s = `<${nodeName}`,
232
+ propChildren,
233
+ html;
234
+
235
+ if (props) {
236
+ for (let name in props) {
237
+ let v = props[name];
238
+ if (name === 'children') {
239
+ propChildren = v;
240
+ continue;
241
+ }
242
+
243
+ if (UNSAFE_NAME.test(name)) continue;
244
+
245
+ if (
246
+ !(opts && opts.allAttributes) &&
247
+ (name === 'key' ||
248
+ name === 'ref' ||
249
+ name === '__self' ||
250
+ name === '__source')
251
+ )
252
+ continue;
253
+
254
+ if (name === 'defaultValue') {
255
+ name = 'value';
256
+ } else if (name === 'defaultChecked') {
257
+ name = 'checked';
258
+ } else if (name === 'defaultSelected') {
259
+ name = 'selected';
260
+ } else if (name === 'className') {
261
+ if (typeof props.class !== 'undefined') continue;
262
+ name = 'class';
263
+ } else if (isSvgMode && XLINK.test(name)) {
264
+ name = name.toLowerCase().replace(/^xlink:?/, 'xlink:');
265
+ }
266
+
267
+ if (name === 'htmlFor') {
268
+ if (props.for) continue;
269
+ name = 'for';
270
+ }
271
+
272
+ if (name === 'style' && v && typeof v === 'object') {
273
+ v = styleObjToCss(v);
274
+ }
275
+
276
+ // always use string values instead of booleans for aria attributes
277
+ // also see https://github.com/preactjs/preact/pull/2347/files
278
+ if (name[0] === 'a' && name['1'] === 'r' && typeof v === 'boolean') {
279
+ v = String(v);
280
+ }
281
+
282
+ let hooked =
283
+ opts.attributeHook &&
284
+ opts.attributeHook(name, v, context, opts, isComponent);
285
+ if (hooked || hooked === '') {
286
+ s = s + hooked;
287
+ continue;
288
+ }
289
+
290
+ if (name === 'dangerouslySetInnerHTML') {
291
+ html = v && v.__html;
292
+ } else if (nodeName === 'textarea' && name === 'value') {
293
+ // <textarea value="a&b"> --> <textarea>a&amp;b</textarea>
294
+ propChildren = v;
295
+ } else if ((v || v === 0 || v === '') && typeof v !== 'function') {
296
+ if (v === true || v === '') {
297
+ v = name;
298
+ // in non-xml mode, allow boolean attributes
299
+ if (!opts || !opts.xml) {
300
+ s = s + ' ' + name;
301
+ continue;
302
+ }
303
+ }
304
+
305
+ if (name === 'value') {
306
+ if (nodeName === 'select') {
307
+ selectValue = v;
308
+ continue;
309
+ } else if (
310
+ // If we're looking at an <option> and it's the currently selected one
311
+ nodeName === 'option' &&
312
+ selectValue == v &&
313
+ // and the <option> doesn't already have a selected attribute on it
314
+ typeof props.selected === 'undefined'
315
+ ) {
316
+ s = `${s} selected`;
317
+ }
318
+ }
319
+ s = s + ` ${name}="${encodeEntities(v)}"`;
320
+ }
321
+ }
322
+ }
323
+
324
+ s = s + '>';
325
+
326
+ if (UNSAFE_NAME.test(nodeName))
327
+ throw new Error(`${nodeName} is not a valid HTML tag name in ${s}`);
328
+
329
+ let isVoid =
330
+ VOID_ELEMENTS.test(nodeName) ||
331
+ (opts.voidElements && opts.voidElements.test(nodeName));
332
+ let pieces = '';
333
+
334
+ let children;
335
+ if (html) {
336
+ s = s + html;
337
+ } else if (
338
+ propChildren != null &&
339
+ getChildren((children = []), propChildren).length
340
+ ) {
341
+ for (let i = 0; i < children.length; i++) {
342
+ let child = children[i];
343
+
344
+ if (child != null && child !== false) {
345
+ let childSvgMode =
346
+ nodeName === 'svg'
347
+ ? true
348
+ : nodeName === 'foreignObject'
349
+ ? false
350
+ : isSvgMode,
351
+ ret = _renderToString(
352
+ child,
353
+ context,
354
+ opts,
355
+ true,
356
+ childSvgMode,
357
+ selectValue
358
+ );
359
+
360
+ // Skip if we received an empty string
361
+ if (ret) {
362
+ pieces = pieces + ret;
363
+ }
364
+ }
365
+ }
366
+ }
367
+
368
+ if (pieces.length || html) {
369
+ s = s + pieces;
370
+ } else if (opts && opts.xml) {
371
+ return s.substring(0, s.length - 1) + ' />';
372
+ }
373
+
374
+ if (isVoid && !children && !html) {
375
+ return s.replace(/>$/, ' />');
376
+ }
377
+
378
+ return `${s}</${nodeName}>`;
379
+ }
380
+
381
+ /** The default export is an alias of `render()`. */
382
+ function _renderToStringPretty(
383
+ vnode,
384
+ context,
385
+ opts,
386
+ inner,
387
+ isSvgMode,
388
+ selectValue
389
+ ) {
390
+ if (vnode == null || typeof vnode === 'boolean') {
391
+ return '';
392
+ }
393
+
394
+ // #text nodes
395
+ if (typeof vnode !== 'object') {
396
+ return encodeEntities(vnode);
397
+ }
398
+
399
+ let pretty = opts.pretty,
400
+ indentChar = pretty && typeof pretty === 'string' ? pretty : '\t';
401
+
402
+ if (Array.isArray(vnode)) {
403
+ let rendered = '';
404
+ for (let i = 0; i < vnode.length; i++) {
405
+ if (pretty && i > 0) rendered = rendered + '\n';
406
+ rendered =
407
+ rendered +
408
+ _renderToStringPretty(
409
+ vnode[i],
410
+ context,
411
+ opts,
412
+ inner,
413
+ isSvgMode,
414
+ selectValue
415
+ );
95
416
  }
96
417
  return rendered;
97
418
  }
@@ -108,7 +429,7 @@ function _renderToString(vnode, context, opts, inner, isSvgMode, selectValue) {
108
429
  } else if (nodeName === Fragment) {
109
430
  const children = [];
110
431
  getChildren(children, vnode.props.children);
111
- return _renderToString(
432
+ return _renderToStringPretty(
112
433
  children,
113
434
  context,
114
435
  opts,
@@ -119,50 +440,36 @@ function _renderToString(vnode, context, opts, inner, isSvgMode, selectValue) {
119
440
  } else {
120
441
  let rendered;
121
442
 
122
- let c = (vnode.__c = {
123
- __v: vnode,
124
- context,
125
- props: vnode.props,
126
- // silently drop state updates
127
- setState: noop,
128
- forceUpdate: noop,
129
- // hooks
130
- __h: []
131
- });
443
+ let c = (vnode.__c = createComponent(vnode, context));
132
444
 
133
445
  // options._diff
134
446
  if (options.__b) options.__b(vnode);
135
447
 
136
448
  // options._render
137
- if (options.__r) options.__r(vnode);
449
+ let renderHook = options.__r;
138
450
 
139
451
  if (
140
452
  !nodeName.prototype ||
141
453
  typeof nodeName.prototype.render !== 'function'
142
454
  ) {
143
- // Necessary for createContext api. Setting this property will pass
144
- // the context value as `this.context` just for this component.
145
- let cxType = nodeName.contextType;
146
- let provider = cxType && context[cxType.__c];
147
- let cctx =
148
- cxType != null
149
- ? provider
150
- ? provider.props.value
151
- : cxType.__
152
- : context;
153
-
154
- // stateless functional components
155
- rendered = nodeName.call(vnode.__c, props, cctx);
455
+ let cctx = getContext(nodeName, context);
456
+
457
+ // If a hook invokes setState() to invalidate the component during rendering,
458
+ // re-render it up to 25 times to allow "settling" of memoized states.
459
+ // Note:
460
+ // This will need to be updated for Preact 11 to use internal.flags rather than component._dirty:
461
+ // https://github.com/preactjs/preact/blob/d4ca6fdb19bc715e49fd144e69f7296b2f4daa40/src/diff/component.js#L35-L44
462
+ let count = 0;
463
+ while (c.__d && count++ < 25) {
464
+ c.__d = false;
465
+
466
+ if (renderHook) renderHook(vnode);
467
+
468
+ // stateless functional components
469
+ rendered = nodeName.call(vnode.__c, props, cctx);
470
+ }
156
471
  } else {
157
- // class-based components
158
- let cxType = nodeName.contextType;
159
- let provider = cxType && context[cxType.__c];
160
- let cctx =
161
- cxType != null
162
- ? provider
163
- ? provider.props.value
164
- : cxType.__
165
- : context;
472
+ let cctx = getContext(nodeName, context);
166
473
 
167
474
  // c = new nodeName(props, context);
168
475
  c = vnode.__c = new nodeName(props, cctx);
@@ -178,8 +485,9 @@ function _renderToString(vnode, context, opts, inner, isSvgMode, selectValue) {
178
485
 
179
486
  c.context = cctx;
180
487
  if (nodeName.getDerivedStateFromProps)
181
- c.state = assign(
182
- assign({}, c.state),
488
+ c.state = Object.assign(
489
+ {},
490
+ c.state,
183
491
  nodeName.getDerivedStateFromProps(c.props, c.state)
184
492
  );
185
493
  else if (c.componentWillMount) {
@@ -195,15 +503,17 @@ function _renderToString(vnode, context, opts, inner, isSvgMode, selectValue) {
195
503
  : c.state;
196
504
  }
197
505
 
506
+ if (renderHook) renderHook(vnode);
507
+
198
508
  rendered = c.render(c.props, c.state, c.context);
199
509
  }
200
510
 
201
511
  if (c.getChildContext) {
202
- context = assign(assign({}, context), c.getChildContext());
512
+ context = Object.assign({}, context, c.getChildContext());
203
513
  }
204
514
 
205
515
  if (options.diffed) options.diffed(vnode);
206
- return _renderToString(
516
+ return _renderToStringPretty(
207
517
  rendered,
208
518
  context,
209
519
  opts,
@@ -246,10 +556,14 @@ function _renderToString(vnode, context, opts, inner, isSvgMode, selectValue) {
246
556
 
247
557
  if (name === 'defaultValue') {
248
558
  name = 'value';
559
+ } else if (name === 'defaultChecked') {
560
+ name = 'checked';
561
+ } else if (name === 'defaultSelected') {
562
+ name = 'selected';
249
563
  } else if (name === 'className') {
250
564
  if (typeof props.class !== 'undefined') continue;
251
565
  name = 'class';
252
- } else if (isSvgMode && name.match(/^xlink:?./)) {
566
+ } else if (isSvgMode && XLINK.test(name)) {
253
567
  name = name.toLowerCase().replace(/^xlink:?/, 'xlink:');
254
568
  }
255
569
 
@@ -272,7 +586,7 @@ function _renderToString(vnode, context, opts, inner, isSvgMode, selectValue) {
272
586
  opts.attributeHook &&
273
587
  opts.attributeHook(name, v, context, opts, isComponent);
274
588
  if (hooked || hooked === '') {
275
- s += hooked;
589
+ s = s + hooked;
276
590
  continue;
277
591
  }
278
592
 
@@ -286,7 +600,7 @@ function _renderToString(vnode, context, opts, inner, isSvgMode, selectValue) {
286
600
  v = name;
287
601
  // in non-xml mode, allow boolean attributes
288
602
  if (!opts || !opts.xml) {
289
- s += ' ' + name;
603
+ s = s + ' ' + name;
290
604
  continue;
291
605
  }
292
606
  }
@@ -302,10 +616,10 @@ function _renderToString(vnode, context, opts, inner, isSvgMode, selectValue) {
302
616
  // and the <option> doesn't already have a selected attribute on it
303
617
  typeof props.selected === 'undefined'
304
618
  ) {
305
- s += ` selected`;
619
+ s = s + ` selected`;
306
620
  }
307
621
  }
308
- s += ` ${name}="${encodeEntities(v)}"`;
622
+ s = s + ` ${name}="${encodeEntities(v)}"`;
309
623
  }
310
624
  }
311
625
  }
@@ -314,10 +628,10 @@ function _renderToString(vnode, context, opts, inner, isSvgMode, selectValue) {
314
628
  if (pretty) {
315
629
  let sub = s.replace(/\n\s*/, ' ');
316
630
  if (sub !== s && !~sub.indexOf('\n')) s = sub;
317
- else if (pretty && ~s.indexOf('\n')) s += '\n';
631
+ else if (pretty && ~s.indexOf('\n')) s = s + '\n';
318
632
  }
319
633
 
320
- s += '>';
634
+ s = s + '>';
321
635
 
322
636
  if (UNSAFE_NAME.test(nodeName))
323
637
  throw new Error(`${nodeName} is not a valid HTML tag name in ${s}`);
@@ -333,7 +647,7 @@ function _renderToString(vnode, context, opts, inner, isSvgMode, selectValue) {
333
647
  if (pretty && isLargeString(html)) {
334
648
  html = '\n' + indentChar + indent(html, indentChar);
335
649
  }
336
- s += html;
650
+ s = s + html;
337
651
  } else if (
338
652
  propChildren != null &&
339
653
  getChildren((children = []), propChildren).length
@@ -351,7 +665,7 @@ function _renderToString(vnode, context, opts, inner, isSvgMode, selectValue) {
351
665
  : nodeName === 'foreignObject'
352
666
  ? false
353
667
  : isSvgMode,
354
- ret = _renderToString(
668
+ ret = _renderToStringPretty(
355
669
  child,
356
670
  context,
357
671
  opts,
@@ -390,7 +704,7 @@ function _renderToString(vnode, context, opts, inner, isSvgMode, selectValue) {
390
704
  }
391
705
 
392
706
  if (pieces.length || html) {
393
- s += pieces.join('');
707
+ s = s + pieces.join('');
394
708
  } else if (opts && opts.xml) {
395
709
  return s.substring(0, s.length - 1) + ' />';
396
710
  }
@@ -398,8 +712,8 @@ function _renderToString(vnode, context, opts, inner, isSvgMode, selectValue) {
398
712
  if (isVoid && !children && !html) {
399
713
  s = s.replace(/>$/, ' />');
400
714
  } else {
401
- if (pretty && ~s.indexOf('\n')) s += '\n';
402
- s += `</${nodeName}>`;
715
+ if (pretty && ~s.indexOf('\n')) s = s + '\n';
716
+ s = s + `</${nodeName}>`;
403
717
  }
404
718
 
405
719
  return s;
package/src/util.js CHANGED
@@ -3,16 +3,34 @@ export const IS_NON_DIMENSIONAL = /acit|ex(?:s|g|n|p|$)|rph|grid|ows|mnc|ntw|ine
3
3
 
4
4
  const ENCODED_ENTITIES = /[&<>"]/;
5
5
 
6
- export function encodeEntities(input) {
7
- const s = String(input);
8
- if (!ENCODED_ENTITIES.test(s)) {
9
- return s;
6
+ export function encodeEntities(str) {
7
+ // Ensure we're always parsing and returning a string:
8
+ str += '';
9
+
10
+ // Skip all work for strings with no entities needing encoding:
11
+ if (ENCODED_ENTITIES.test(str) === false) return str;
12
+
13
+ let last = 0,
14
+ i = 0,
15
+ out = '',
16
+ ch = '';
17
+
18
+ // Seek forward in str until the next entity char:
19
+ for (; i<str.length; i++) {
20
+ switch (str.charCodeAt(i)) {
21
+ case 60: ch = '&lt;'; break;
22
+ case 62: ch = '&gt;'; break;
23
+ case 34: ch = '&quot;'; break;
24
+ case 38: ch = '&amp;'; break;
25
+ default: continue;
26
+ }
27
+ // Append skipped/buffered characters and the encoded entity:
28
+ if (i > last) out += str.slice(last, i);
29
+ out += ch;
30
+ // Start the next seek/buffer after the entity's offset:
31
+ last = i + 1;
10
32
  }
11
- return s
12
- .replace(/&/g, '&amp;')
13
- .replace(/</g, '&lt;')
14
- .replace(/>/g, '&gt;')
15
- .replace(/"/g, '&quot;');
33
+ return out + str.slice(last, i);
16
34
  }
17
35
 
18
36
  export let indent = (s, char) =>
@@ -57,8 +75,7 @@ export function styleObjToCss(s) {
57
75
  * @private
58
76
  */
59
77
  export function assign(obj, props) {
60
- for (let i in props) obj[i] = props[i];
61
- return obj;
78
+ return Object.assign(obj, props);
62
79
  }
63
80
 
64
81
  /**