puglite 1.0.0

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.
@@ -0,0 +1,769 @@
1
+ 'use strict';
2
+
3
+ var doctypes = require('doctypes');
4
+ var makeError = require('./error');
5
+ var buildRuntime = require('./runtime-build');
6
+ var runtime = require('./runtime');
7
+ var compileAttrs = require('./attrs');
8
+ var selfClosing = require('void-elements');
9
+ var constantinople = require('constantinople');
10
+ var stringify = require('js-stringify');
11
+ var addWith = require('with');
12
+
13
+ // This is used to prevent pretty printing inside certain tags
14
+ var WHITE_SPACE_SENSITIVE_TAGS = {
15
+ pre: true,
16
+ textarea: true,
17
+ };
18
+
19
+ var INTERNAL_VARIABLES = [
20
+ 'pug',
21
+ 'pug_interp',
22
+ 'pug_debug_filename',
23
+ 'pug_debug_line',
24
+ 'pug_debug_sources',
25
+ 'pug_html',
26
+ ];
27
+
28
+ module.exports = generateCode;
29
+ module.exports.CodeGenerator = Compiler;
30
+ function generateCode(ast, options) {
31
+ return new Compiler(ast, options).compile();
32
+ }
33
+
34
+ function isConstant(src) {
35
+ return constantinople(src, {pug: runtime, pug_interp: undefined});
36
+ }
37
+ function toConstant(src) {
38
+ return constantinople.toConstant(src, {pug: runtime, pug_interp: undefined});
39
+ }
40
+
41
+ function isIdentifier(name) {
42
+ return /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name);
43
+ }
44
+
45
+ /**
46
+ * Initialize `Compiler` with the given `node`.
47
+ *
48
+ * @param {Node} node
49
+ * @param {Object} options
50
+ * @api public
51
+ */
52
+
53
+ function Compiler(node, options) {
54
+ this.options = options = options || {};
55
+ this.node = node;
56
+ this.bufferedConcatenationCount = 0;
57
+ this.hasCompiledDoctype = false;
58
+ this.hasCompiledTag = false;
59
+ this.pp = options.pretty || false;
60
+ if (this.pp && typeof this.pp !== 'string') {
61
+ this.pp = ' ';
62
+ }
63
+ if (this.pp && !/^\s+$/.test(this.pp)) {
64
+ throw new Error(
65
+ 'The pretty parameter should either be a boolean or whitespace only string'
66
+ );
67
+ }
68
+ if (this.options.templateName && !isIdentifier(this.options.templateName)) {
69
+ throw new Error(
70
+ 'The templateName parameter must be a valid JavaScript identifier if specified.'
71
+ );
72
+ }
73
+ if (
74
+ this.doctype &&
75
+ (this.doctype.includes('<') || this.doctype.includes('>'))
76
+ ) {
77
+ throw new Error('Doctype can not contain "<" or ">"');
78
+ }
79
+ if (this.options.globals && !this.options.globals.every(isIdentifier)) {
80
+ throw new Error(
81
+ 'The globals option must be an array of valid JavaScript identifiers if specified.'
82
+ );
83
+ }
84
+
85
+ this.debug = false !== options.compileDebug;
86
+ this.indents = 0;
87
+ this.parentIndents = 0;
88
+ this.terse = false;
89
+ this.eachCount = 0;
90
+ if (options.doctype) this.setDoctype(options.doctype);
91
+ this.runtimeFunctionsUsed = [];
92
+ this.inlineRuntimeFunctions = options.inlineRuntimeFunctions || false;
93
+ if (this.debug && this.inlineRuntimeFunctions) {
94
+ this.runtimeFunctionsUsed.push('rethrow');
95
+ }
96
+ }
97
+
98
+ /**
99
+ * Compiler prototype.
100
+ */
101
+
102
+ Compiler.prototype = {
103
+ runtime: function(name) {
104
+ if (this.inlineRuntimeFunctions) {
105
+ this.runtimeFunctionsUsed.push(name);
106
+ return 'pug_' + name;
107
+ } else {
108
+ return 'pug.' + name;
109
+ }
110
+ },
111
+
112
+ error: function(message, code, node) {
113
+ var err = makeError(code, message, {
114
+ line: node.line,
115
+ column: node.column,
116
+ filename: node.filename,
117
+ });
118
+ throw err;
119
+ },
120
+
121
+ /**
122
+ * Compile parse tree to JavaScript.
123
+ *
124
+ * @api public
125
+ */
126
+
127
+ compile: function() {
128
+ this.buf = [];
129
+ if (this.pp) this.buf.push('var pug_indent = [];');
130
+ this.lastBufferedIdx = -1;
131
+ this.visit(this.node);
132
+ var js = this.buf.join('\n');
133
+ var globals = this.options.globals
134
+ ? this.options.globals.concat(INTERNAL_VARIABLES)
135
+ : INTERNAL_VARIABLES;
136
+ if (this.options.self) {
137
+ js = 'var self = locals || {};' + js;
138
+ } else {
139
+ js = addWith(
140
+ 'locals || {}',
141
+ js,
142
+ globals.concat(
143
+ this.runtimeFunctionsUsed.map(function(name) {
144
+ return 'pug_' + name;
145
+ })
146
+ )
147
+ );
148
+ }
149
+ if (this.debug) {
150
+ if (this.options.includeSources) {
151
+ js =
152
+ 'var pug_debug_sources = ' +
153
+ stringify(this.options.includeSources) +
154
+ ';\n' +
155
+ js;
156
+ }
157
+ js =
158
+ 'var pug_debug_filename, pug_debug_line;' +
159
+ 'try {' +
160
+ js +
161
+ '} catch (err) {' +
162
+ (this.inlineRuntimeFunctions ? 'pug_rethrow' : 'pug.rethrow') +
163
+ '(err, pug_debug_filename, pug_debug_line' +
164
+ (this.options.includeSources
165
+ ? ', pug_debug_sources[pug_debug_filename]'
166
+ : '') +
167
+ ');' +
168
+ '}';
169
+ }
170
+
171
+ return (
172
+ buildRuntime(this.runtimeFunctionsUsed) +
173
+ 'function ' +
174
+ (this.options.templateName || 'template') +
175
+ '(locals) {var pug_html = "", pug_interp;' +
176
+ js +
177
+ ';return pug_html;}'
178
+ );
179
+ },
180
+
181
+ /**
182
+ * Sets the default doctype `name`. Sets terse mode to `true` when
183
+ * html 5 is used, causing self-closing tags to end with ">" vs "/>",
184
+ * and boolean attributes are not mirrored.
185
+ *
186
+ * @param {string} name
187
+ * @api public
188
+ */
189
+
190
+ setDoctype: function(name) {
191
+ this.doctype = doctypes[name.toLowerCase()] || '<!DOCTYPE ' + name + '>';
192
+ this.terse = this.doctype.toLowerCase() == '<!doctype html>';
193
+ this.xml = 0 == this.doctype.indexOf('<?xml');
194
+ },
195
+
196
+ /**
197
+ * Buffer the given `str` exactly as is or with interpolation
198
+ *
199
+ * @param {String} str
200
+ * @param {Boolean} interpolate
201
+ * @api public
202
+ */
203
+
204
+ buffer: function(str) {
205
+ var self = this;
206
+
207
+ str = stringify(str);
208
+ str = str.substr(1, str.length - 2);
209
+
210
+ if (
211
+ this.lastBufferedIdx == this.buf.length &&
212
+ this.bufferedConcatenationCount < 100
213
+ ) {
214
+ if (this.lastBufferedType === 'code') {
215
+ this.lastBuffered += ' + "';
216
+ this.bufferedConcatenationCount++;
217
+ }
218
+ this.lastBufferedType = 'text';
219
+ this.lastBuffered += str;
220
+ this.buf[this.lastBufferedIdx - 1] =
221
+ 'pug_html = pug_html + ' +
222
+ this.bufferStartChar +
223
+ this.lastBuffered +
224
+ '";';
225
+ } else {
226
+ this.bufferedConcatenationCount = 0;
227
+ this.buf.push('pug_html = pug_html + "' + str + '";');
228
+ this.lastBufferedType = 'text';
229
+ this.bufferStartChar = '"';
230
+ this.lastBuffered = str;
231
+ this.lastBufferedIdx = this.buf.length;
232
+ }
233
+ },
234
+
235
+ /**
236
+ * Buffer the given `src` so it is evaluated at run time
237
+ *
238
+ * @param {String} src
239
+ * @api public
240
+ */
241
+
242
+ bufferExpression: function(src) {
243
+ if (isConstant(src)) {
244
+ return this.buffer(toConstant(src) + '');
245
+ }
246
+ if (
247
+ this.lastBufferedIdx == this.buf.length &&
248
+ this.bufferedConcatenationCount < 100
249
+ ) {
250
+ this.bufferedConcatenationCount++;
251
+ if (this.lastBufferedType === 'text') this.lastBuffered += '"';
252
+ this.lastBufferedType = 'code';
253
+ this.lastBuffered += ' + (' + src + ')';
254
+ this.buf[this.lastBufferedIdx - 1] =
255
+ 'pug_html = pug_html + (' +
256
+ this.bufferStartChar +
257
+ this.lastBuffered +
258
+ ');';
259
+ } else {
260
+ this.bufferedConcatenationCount = 0;
261
+ this.buf.push('pug_html = pug_html + (' + src + ');');
262
+ this.lastBufferedType = 'code';
263
+ this.bufferStartChar = '';
264
+ this.lastBuffered = '(' + src + ')';
265
+ this.lastBufferedIdx = this.buf.length;
266
+ }
267
+ },
268
+
269
+ /**
270
+ * Buffer an indent based on the current `indent`
271
+ * property and an additional `offset`.
272
+ *
273
+ * @param {Number} offset
274
+ * @param {Boolean} newline
275
+ * @api public
276
+ */
277
+
278
+ prettyIndent: function(offset, newline) {
279
+ offset = offset || 0;
280
+ newline = newline ? '\n' : '';
281
+ this.buffer(newline + Array(this.indents + offset).join(this.pp));
282
+ if (this.parentIndents)
283
+ this.buf.push('pug_html = pug_html + pug_indent.join("");');
284
+ },
285
+
286
+ /**
287
+ * Visit `node`.
288
+ *
289
+ * @param {Node} node
290
+ * @api public
291
+ */
292
+
293
+ visit: function(node, parent) {
294
+ var debug = this.debug;
295
+
296
+ if (!node) {
297
+ var msg;
298
+ if (parent) {
299
+ msg =
300
+ 'A child of ' +
301
+ parent.type +
302
+ ' (' +
303
+ (parent.filename || 'Pug') +
304
+ ':' +
305
+ parent.line +
306
+ ')';
307
+ } else {
308
+ msg = 'A top-level node';
309
+ }
310
+ msg += ' is ' + node + ', expected a Pug AST Node.';
311
+ throw new TypeError(msg);
312
+ }
313
+
314
+ if (debug && node.debug !== false && node.type !== 'Block') {
315
+ if (node.line) {
316
+ var js = ';pug_debug_line = ' + node.line;
317
+ if (node.filename)
318
+ js += ';pug_debug_filename = ' + stringify(node.filename);
319
+ this.buf.push(js + ';');
320
+ }
321
+ }
322
+
323
+ if (!this['visit' + node.type]) {
324
+ var msg;
325
+ if (parent) {
326
+ msg = 'A child of ' + parent.type;
327
+ } else {
328
+ msg = 'A top-level node';
329
+ }
330
+ msg +=
331
+ ' (' +
332
+ (node.filename || 'Pug') +
333
+ ':' +
334
+ node.line +
335
+ ')' +
336
+ ' is of type ' +
337
+ node.type +
338
+ ',' +
339
+ ' which is not supported by pug-code-gen.';
340
+ switch (node.type) {
341
+ case 'Filter':
342
+ msg += ' Filters are not supported in puglite.';
343
+ break;
344
+ case 'Extends':
345
+ case 'Include':
346
+ case 'NamedBlock':
347
+ case 'FileReference': // unlikely but for the sake of completeness
348
+ msg += ' Extends and includes are not supported in puglite.';
349
+ break;
350
+ }
351
+ throw new TypeError(msg);
352
+ }
353
+
354
+ this.visitNode(node);
355
+ },
356
+
357
+ /**
358
+ * Visit `node`.
359
+ *
360
+ * @param {Node} node
361
+ * @api public
362
+ */
363
+
364
+ visitNode: function(node) {
365
+ return this['visit' + node.type](node);
366
+ },
367
+
368
+ /**
369
+ * Visit case `node`.
370
+ *
371
+ * @param {Literal} node
372
+ * @api public
373
+ */
374
+
375
+ visitCase: function(node) {
376
+ throw new Error('Case statements are not supported in puglite. Use your framework for logic.');
377
+ },
378
+
379
+ /**
380
+ * Visit when `node`.
381
+ *
382
+ * @param {Literal} node
383
+ * @api public
384
+ */
385
+
386
+ visitWhen: function(node) {
387
+ throw new Error('Case/when statements are not supported in puglite. Use your framework for logic.');
388
+ },
389
+
390
+ /**
391
+ * Visit literal `node`.
392
+ *
393
+ * @param {Literal} node
394
+ * @api public
395
+ */
396
+
397
+ visitLiteral: function(node) {
398
+ this.buffer(node.str);
399
+ },
400
+
401
+ visitNamedBlock: function(block) {
402
+ return this.visitBlock(block);
403
+ },
404
+ /**
405
+ * Visit all nodes in `block`.
406
+ *
407
+ * @param {Block} block
408
+ * @api public
409
+ */
410
+
411
+ visitBlock: function(block) {
412
+ var escapePrettyMode = this.escapePrettyMode;
413
+ var pp = this.pp;
414
+
415
+ // Pretty print multi-line text
416
+ if (
417
+ pp &&
418
+ block.nodes.length > 1 &&
419
+ !escapePrettyMode &&
420
+ block.nodes[0].type === 'Text' &&
421
+ block.nodes[1].type === 'Text'
422
+ ) {
423
+ this.prettyIndent(1, true);
424
+ }
425
+ for (var i = 0; i < block.nodes.length; ++i) {
426
+ // Pretty print text
427
+ if (
428
+ pp &&
429
+ i > 0 &&
430
+ !escapePrettyMode &&
431
+ block.nodes[i].type === 'Text' &&
432
+ block.nodes[i - 1].type === 'Text' &&
433
+ /\n$/.test(block.nodes[i - 1].val)
434
+ ) {
435
+ this.prettyIndent(1, false);
436
+ }
437
+ this.visit(block.nodes[i], block);
438
+ }
439
+ },
440
+
441
+ /**
442
+ * Visit a mixin's `block` keyword.
443
+ *
444
+ * @param {MixinBlock} block
445
+ * @api public
446
+ */
447
+
448
+ visitMixinBlock: function(block) {
449
+ throw new Error('Mixins are not supported in puglite. Please remove mixin usage.');
450
+ },
451
+
452
+ /**
453
+ * Visit `doctype`. Sets terse mode to `true` when html 5
454
+ * is used, causing self-closing tags to end with ">" vs "/>",
455
+ * and boolean attributes are not mirrored.
456
+ *
457
+ * @param {Doctype} doctype
458
+ * @api public
459
+ */
460
+
461
+ visitDoctype: function(doctype) {
462
+ if (doctype && (doctype.val || !this.doctype)) {
463
+ this.setDoctype(doctype.val || 'html');
464
+ }
465
+
466
+ if (this.doctype) this.buffer(this.doctype);
467
+ this.hasCompiledDoctype = true;
468
+ },
469
+
470
+ /**
471
+ * Visit `mixin`, generating a function that
472
+ * may be called within the template.
473
+ *
474
+ * @param {Mixin} mixin
475
+ * @api public
476
+ */
477
+
478
+ visitMixin: function(mixin) {
479
+ throw new Error('Mixins are not supported in puglite. Please remove mixin usage.');
480
+ },
481
+
482
+ /**
483
+ * Visit `tag` buffering tag markup, generating
484
+ * attributes, visiting the `tag`'s code and block.
485
+ *
486
+ * @param {Tag} tag
487
+ * @param {boolean} interpolated
488
+ * @api public
489
+ */
490
+
491
+ visitTag: function(tag, interpolated) {
492
+ this.indents++;
493
+ var name = tag.name,
494
+ pp = this.pp,
495
+ self = this;
496
+
497
+ function bufferName() {
498
+ if (interpolated) self.bufferExpression(tag.expr);
499
+ else self.buffer(name);
500
+ }
501
+
502
+ if (WHITE_SPACE_SENSITIVE_TAGS[tag.name] === true)
503
+ this.escapePrettyMode = true;
504
+
505
+ if (!this.hasCompiledTag) {
506
+ if (!this.hasCompiledDoctype && 'html' == name) {
507
+ this.visitDoctype();
508
+ }
509
+ this.hasCompiledTag = true;
510
+ }
511
+
512
+ // pretty print
513
+ if (pp && !tag.isInline) this.prettyIndent(0, true);
514
+ if (tag.selfClosing || (!this.xml && selfClosing[tag.name])) {
515
+ this.buffer('<');
516
+ bufferName();
517
+ this.visitAttributes(
518
+ tag.attrs,
519
+ this.attributeBlocks(tag.attributeBlocks)
520
+ );
521
+ if (this.terse && !tag.selfClosing) {
522
+ this.buffer('>');
523
+ } else {
524
+ this.buffer('/>');
525
+ }
526
+ // if it is non-empty throw an error
527
+ if (
528
+ tag.code ||
529
+ (tag.block &&
530
+ !(tag.block.type === 'Block' && tag.block.nodes.length === 0) &&
531
+ tag.block.nodes.some(function(tag) {
532
+ return tag.type !== 'Text' || !/^\s*$/.test(tag.val);
533
+ }))
534
+ ) {
535
+ this.error(
536
+ name +
537
+ ' is a self closing element: <' +
538
+ name +
539
+ '/> but contains nested content.',
540
+ 'SELF_CLOSING_CONTENT',
541
+ tag
542
+ );
543
+ }
544
+ } else {
545
+ // Optimize attributes buffering
546
+ this.buffer('<');
547
+ bufferName();
548
+ this.visitAttributes(
549
+ tag.attrs,
550
+ this.attributeBlocks(tag.attributeBlocks)
551
+ );
552
+ this.buffer('>');
553
+ if (tag.code) this.visitCode(tag.code);
554
+ this.visit(tag.block, tag);
555
+
556
+ // pretty print
557
+ if (
558
+ pp &&
559
+ !tag.isInline &&
560
+ WHITE_SPACE_SENSITIVE_TAGS[tag.name] !== true &&
561
+ !tagCanInline(tag)
562
+ )
563
+ this.prettyIndent(0, true);
564
+
565
+ this.buffer('</');
566
+ bufferName();
567
+ this.buffer('>');
568
+ }
569
+
570
+ if (WHITE_SPACE_SENSITIVE_TAGS[tag.name] === true)
571
+ this.escapePrettyMode = false;
572
+
573
+ this.indents--;
574
+ },
575
+
576
+ /**
577
+ * Visit InterpolatedTag.
578
+ *
579
+ * @param {InterpolatedTag} tag
580
+ * @api public
581
+ */
582
+
583
+ visitInterpolatedTag: function(tag) {
584
+ throw new Error('Interpolation (#{var}) is not supported in puglite. Use your framework for data binding.');
585
+ },
586
+
587
+ /**
588
+ * Visit `text` node.
589
+ *
590
+ * @param {Text} text
591
+ * @api public
592
+ */
593
+
594
+ visitText: function(text) {
595
+ this.buffer(text.val);
596
+ },
597
+
598
+ /**
599
+ * Visit a `comment`, only buffering when the buffer flag is set.
600
+ *
601
+ * @param {Comment} comment
602
+ * @api public
603
+ */
604
+
605
+ visitComment: function(comment) {
606
+ if (!comment.buffer) return;
607
+ if (this.pp) this.prettyIndent(1, true);
608
+ this.buffer('<!--' + comment.val + '-->');
609
+ },
610
+
611
+ /**
612
+ * Visit a `YieldBlock`.
613
+ *
614
+ * This is necessary since we allow compiling a file with `yield`.
615
+ *
616
+ * @param {YieldBlock} block
617
+ * @api public
618
+ */
619
+
620
+ visitYieldBlock: function(block) {},
621
+
622
+ /**
623
+ * Visit a `BlockComment`.
624
+ *
625
+ * @param {Comment} comment
626
+ * @api public
627
+ */
628
+
629
+ visitBlockComment: function(comment) {
630
+ if (!comment.buffer) return;
631
+ if (this.pp) this.prettyIndent(1, true);
632
+ this.buffer('<!--' + (comment.val || ''));
633
+ this.visit(comment.block, comment);
634
+ if (this.pp) this.prettyIndent(1, true);
635
+ this.buffer('-->');
636
+ },
637
+
638
+ /**
639
+ * Visit `code`, respecting buffer / escape flags.
640
+ * If the code is followed by a block, wrap it in
641
+ * a self-calling function.
642
+ *
643
+ * @param {Code} code
644
+ * @api public
645
+ */
646
+
647
+ visitCode: function(code) {
648
+ throw new Error('Code blocks (= and -) are not supported in puglite. Use your framework for logic and data binding.');
649
+ },
650
+
651
+ /**
652
+ * Visit `Conditional`.
653
+ *
654
+ * @param {Conditional} cond
655
+ * @api public
656
+ */
657
+
658
+ visitConditional: function(cond) {
659
+ throw new Error('Conditionals (if/else/unless) are not supported in puglite. Use your framework for logic.');
660
+ },
661
+
662
+ /**
663
+ * Visit `While`.
664
+ *
665
+ * @param {While} loop
666
+ * @api public
667
+ */
668
+
669
+ visitWhile: function(loop) {
670
+ throw new Error('While loops are not supported in puglite. Use your framework for logic.');
671
+ },
672
+
673
+ /**
674
+ * Visit `each` block.
675
+ *
676
+ * @param {Each} each
677
+ * @api public
678
+ */
679
+
680
+ visitEach: function(each) {
681
+ throw new Error('Each loops are not supported in puglite. Use your framework for iteration.');
682
+ },
683
+
684
+ visitEachOf: function(each) {
685
+ throw new Error('Each loops are not supported in puglite. Use your framework for iteration.');
686
+ },
687
+
688
+ /**
689
+ * Visit `attrs`.
690
+ *
691
+ * @param {Array} attrs
692
+ * @api public
693
+ */
694
+
695
+ visitAttributes: function(attrs, attributeBlocks) {
696
+ if (attributeBlocks.length) {
697
+ if (attrs.length) {
698
+ var val = this.attrs(attrs);
699
+ attributeBlocks.unshift(val);
700
+ }
701
+ if (attributeBlocks.length > 1) {
702
+ this.bufferExpression(
703
+ this.runtime('attrs') +
704
+ '(' +
705
+ this.runtime('merge') +
706
+ '([' +
707
+ attributeBlocks.join(',') +
708
+ ']), ' +
709
+ stringify(this.terse) +
710
+ ')'
711
+ );
712
+ } else {
713
+ this.bufferExpression(
714
+ this.runtime('attrs') +
715
+ '(' +
716
+ attributeBlocks[0] +
717
+ ', ' +
718
+ stringify(this.terse) +
719
+ ')'
720
+ );
721
+ }
722
+ } else if (attrs.length) {
723
+ this.attrs(attrs, true);
724
+ }
725
+ },
726
+
727
+ /**
728
+ * Compile attributes.
729
+ */
730
+
731
+ attrs: function(attrs, buffer) {
732
+ var res = compileAttrs(attrs, {
733
+ terse: this.terse,
734
+ format: buffer ? 'html' : 'object',
735
+ runtime: this.runtime.bind(this),
736
+ });
737
+ if (buffer) {
738
+ this.bufferExpression(res);
739
+ }
740
+ return res;
741
+ },
742
+
743
+ /**
744
+ * Compile attribute blocks.
745
+ */
746
+
747
+ attributeBlocks: function(attributeBlocks) {
748
+ return (
749
+ attributeBlocks &&
750
+ attributeBlocks.slice().map(function(attrBlock) {
751
+ return attrBlock.val;
752
+ })
753
+ );
754
+ },
755
+ };
756
+
757
+ function tagCanInline(tag) {
758
+ function isInline(node) {
759
+ // Recurse if the node is a block
760
+ if (node.type === 'Block') return node.nodes.every(isInline);
761
+ // When there is a YieldBlock here, it is an indication that the file is
762
+ // expected to be included but is not. If this is the case, the block
763
+ // must be empty.
764
+ if (node.type === 'YieldBlock') return true;
765
+ return (node.type === 'Text' && !/\n/.test(node.val)) || node.isInline;
766
+ }
767
+
768
+ return tag.block.nodes.every(isInline);
769
+ }