wikiparser-node 0.7.0-b → 0.7.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.
Files changed (84) hide show
  1. package/README.md +39 -0
  2. package/config/default.json +832 -0
  3. package/config/llwiki.json +630 -0
  4. package/config/moegirl.json +728 -0
  5. package/config/zhwiki.json +1269 -0
  6. package/index.js +321 -0
  7. package/lib/element.js +611 -0
  8. package/lib/node.js +772 -0
  9. package/lib/ranges.js +130 -0
  10. package/lib/text.js +215 -0
  11. package/lib/title.js +80 -0
  12. package/mixin/attributeParent.js +117 -0
  13. package/mixin/fixedToken.js +40 -0
  14. package/mixin/hidden.js +21 -0
  15. package/mixin/singleLine.js +31 -0
  16. package/mixin/sol.js +65 -0
  17. package/package.json +11 -9
  18. package/parser/brackets.js +120 -0
  19. package/parser/commentAndExt.js +62 -0
  20. package/parser/converter.js +46 -0
  21. package/parser/externalLinks.js +33 -0
  22. package/parser/hrAndDoubleUnderscore.js +38 -0
  23. package/parser/html.js +42 -0
  24. package/parser/links.js +94 -0
  25. package/parser/list.js +59 -0
  26. package/parser/magicLinks.js +41 -0
  27. package/parser/quotes.js +64 -0
  28. package/parser/selector.js +177 -0
  29. package/parser/table.js +114 -0
  30. package/src/arg.js +203 -0
  31. package/src/atom/hidden.js +13 -0
  32. package/src/atom/index.js +43 -0
  33. package/src/attribute.js +420 -0
  34. package/src/attributes.js +452 -0
  35. package/src/charinsert.js +97 -0
  36. package/src/converter.js +176 -0
  37. package/src/converterFlags.js +284 -0
  38. package/src/converterRule.js +258 -0
  39. package/src/extLink.js +179 -0
  40. package/src/gallery.js +151 -0
  41. package/src/hasNowiki/index.js +44 -0
  42. package/src/hasNowiki/pre.js +40 -0
  43. package/src/heading.js +134 -0
  44. package/src/html.js +248 -0
  45. package/src/imageParameter.js +277 -0
  46. package/src/imagemap.js +199 -0
  47. package/src/imagemapLink.js +41 -0
  48. package/src/index.js +913 -0
  49. package/src/link/category.js +49 -0
  50. package/src/link/file.js +282 -0
  51. package/src/link/galleryImage.js +120 -0
  52. package/src/link/index.js +383 -0
  53. package/src/magicLink.js +149 -0
  54. package/src/nested/choose.js +24 -0
  55. package/src/nested/combobox.js +23 -0
  56. package/src/nested/index.js +96 -0
  57. package/src/nested/references.js +23 -0
  58. package/src/nowiki/comment.js +71 -0
  59. package/src/nowiki/dd.js +59 -0
  60. package/src/nowiki/doubleUnderscore.js +56 -0
  61. package/src/nowiki/hr.js +41 -0
  62. package/src/nowiki/index.js +56 -0
  63. package/src/nowiki/list.js +16 -0
  64. package/src/nowiki/noinclude.js +28 -0
  65. package/src/nowiki/quote.js +69 -0
  66. package/src/onlyinclude.js +64 -0
  67. package/src/paramTag/index.js +89 -0
  68. package/src/paramTag/inputbox.js +44 -0
  69. package/src/parameter.js +239 -0
  70. package/src/syntax.js +91 -0
  71. package/src/table/index.js +984 -0
  72. package/src/table/td.js +339 -0
  73. package/src/table/tr.js +319 -0
  74. package/src/tagPair/ext.js +138 -0
  75. package/src/tagPair/include.js +60 -0
  76. package/src/tagPair/index.js +126 -0
  77. package/src/transclude.js +824 -0
  78. package/tool/index.js +1202 -0
  79. package/util/base.js +17 -0
  80. package/util/debug.js +73 -0
  81. package/util/diff.js +76 -0
  82. package/util/lint.js +54 -0
  83. package/util/string.js +107 -0
  84. package/bundle/bundle.min.js +0 -40
package/lib/element.js ADDED
@@ -0,0 +1,611 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs'),
4
+ path = require('path'),
5
+ {toCase, noWrap, print} = require('../util/string'),
6
+ {nth} = require('./ranges'),
7
+ parseSelector = require('../parser/selector'),
8
+ Parser = require('..'),
9
+ AstNode = require('./node'),
10
+ AstText = require('./text');
11
+
12
+ /**
13
+ * 检测:lang()伪选择器
14
+ * @param {AstElement & {attributes: Records<string, string|true>}} node 节点
15
+ * @param {RegExp} regex 语言正则
16
+ */
17
+ const matchesLang = ({attributes}, regex) => {
18
+ const /** @type {string} */ lang = attributes?.lang;
19
+ return typeof lang === 'string' && regex.test(lang);
20
+ };
21
+
22
+ /** 类似HTMLElement */
23
+ class AstElement extends AstNode {
24
+ /** @type {string} */ name;
25
+
26
+ /**
27
+ * 检查是否符合某条属性规则
28
+ * @param {string} key 属性键
29
+ * @param {string|undefined} equal 属性规则运算符,`equal`存在时`val`和`i`也一定存在
30
+ * @param {string|undefined} val 属性值
31
+ * @param {string|undefined} i 是否对大小写不敏感
32
+ * @throws `RangeError` 复杂属性
33
+ */
34
+ #matchesAttr = (key, equal, val, i) => {
35
+ if (!equal) {
36
+ return this.hasAttribute(key);
37
+ } else if (!this.hasAttribute(key)) {
38
+ return equal === '!=';
39
+ }
40
+ val = toCase(val, i);
41
+ let thisVal = this.getAttribute(key);
42
+ if (thisVal instanceof RegExp) {
43
+ thisVal = thisVal.source;
44
+ }
45
+ if (equal === '~=') {
46
+ const thisVals = typeof thisVal === 'string' ? thisVal.split(/\s/u) : thisVal;
47
+ return Boolean(thisVals?.[Symbol.iterator]) && [...thisVals].some(v => toCase(v, i) === val);
48
+ } else if (typeof thisVal !== 'string') {
49
+ throw new RangeError(`复杂属性 ${key} 不能用于选择器!`);
50
+ }
51
+ thisVal = toCase(thisVal, i);
52
+ switch (equal) {
53
+ case '|=':
54
+ return thisVal === val || thisVal.startsWith(`${val}-`);
55
+ case '^=':
56
+ return thisVal.startsWith(val);
57
+ case '$=':
58
+ return thisVal.endsWith(val);
59
+ case '*=':
60
+ return thisVal.includes(val);
61
+ case '!=':
62
+ return thisVal !== val;
63
+ default: // `=`
64
+ return thisVal === val;
65
+ }
66
+ };
67
+
68
+ /** 子节点总数 */
69
+ get length() {
70
+ return this.childNodes.length;
71
+ }
72
+
73
+ /**
74
+ * 全部非文本子节点
75
+ * @complexity `n`
76
+ */
77
+ get children() {
78
+ const /** @type {this[]} */ children = this.childNodes.filter(({type}) => type !== 'text');
79
+ return children;
80
+ }
81
+
82
+ /**
83
+ * 首位非文本子节点
84
+ * @returns {this}
85
+ */
86
+ get firstElementChild() {
87
+ return this.childNodes.find(({type}) => type !== 'text');
88
+ }
89
+
90
+ /**
91
+ * 末位非文本子节点
92
+ * @complexity `n`
93
+ */
94
+ get lastElementChild() {
95
+ return this.children.at(-1);
96
+ }
97
+
98
+ /**
99
+ * 非文本子节点总数
100
+ * @complexity `n`
101
+ */
102
+ get childElementCount() {
103
+ return this.children.length;
104
+ }
105
+
106
+ /** 父节点 */
107
+ get parentElement() {
108
+ return this.parentNode;
109
+ }
110
+
111
+ /**
112
+ * AstElement.prototype.text()的getter写法
113
+ * @complexity `n`
114
+ */
115
+ get outerText() {
116
+ return this.text();
117
+ }
118
+
119
+ /**
120
+ * 不可见
121
+ */
122
+ get hidden() {
123
+ return this.text() === '';
124
+ }
125
+
126
+ /**
127
+ * 后一个可见的兄弟节点
128
+ * @complexity `n`
129
+ */
130
+ get nextVisibleSibling() {
131
+ let {nextSibling} = this;
132
+ while (nextSibling?.text() === '') {
133
+ ({nextSibling} = nextSibling);
134
+ }
135
+ return nextSibling;
136
+ }
137
+
138
+ /**
139
+ * 前一个可见的兄弟节点
140
+ * @complexity `n`
141
+ */
142
+ get previousVisibleSibling() {
143
+ let {previousSibling} = this;
144
+ while (previousSibling?.text() === '') {
145
+ ({previousSibling} = previousSibling);
146
+ }
147
+ return previousSibling;
148
+ }
149
+
150
+ /** 内部高度 */
151
+ get clientHeight() {
152
+ const {innerText} = this;
153
+ return typeof innerText === 'string' ? innerText.split('\n').length : undefined;
154
+ }
155
+
156
+ /** 内部宽度 */
157
+ get clientWidth() {
158
+ const {innerText} = this;
159
+ return typeof innerText === 'string' ? innerText.split('\n').at(-1).length : undefined;
160
+ }
161
+
162
+ constructor() {
163
+ super();
164
+ this.seal('name');
165
+ }
166
+
167
+ /**
168
+ * 销毁
169
+ * @complexity `n`
170
+ * @param {boolean} deep 是否从根节点开始销毁
171
+ * @throws `Error` 不能销毁子节点
172
+ */
173
+ destroy(deep) {
174
+ if (this.parentNode && !deep) {
175
+ throw new Error('不能销毁子节点!');
176
+ }
177
+ this.parentNode?.destroy(deep);
178
+ for (const child of this.childNodes) {
179
+ child.setAttribute('parentNode');
180
+ }
181
+ Object.setPrototypeOf(this, null);
182
+ }
183
+
184
+ /**
185
+ * @override
186
+ * @template {string} T
187
+ * @param {T} key 属性键
188
+ * @returns {TokenAttribute<T>}
189
+ */
190
+ getAttribute(key) {
191
+ return key === 'matchesAttr' ? this.#matchesAttr : super.getAttribute(key);
192
+ }
193
+
194
+ /** 是否受保护。保护条件来自Token,这里仅提前用于:required和:optional伪选择器。 */
195
+ #isProtected() {
196
+ const /** @type {{parentNode: AstElement & {constructor: {fixed: boolean}}}} */ {parentNode} = this;
197
+ if (!parentNode) {
198
+ return undefined;
199
+ }
200
+ const {childNodes, constructor: {fixed}} = parentNode,
201
+ protectedIndices = parentNode.getAttribute('protectedChildren')?.applyTo(childNodes);
202
+ return fixed || protectedIndices?.includes(childNodes.indexOf(this));
203
+ }
204
+
205
+ /**
206
+ * 检查是否符合解析后的选择器,不含节点关系
207
+ * @this {AstElement & {link: string, constructor: {fixed: boolean}}}
208
+ * @param {SelectorArray} step 解析后的选择器
209
+ * @throws `SyntaxError` 未定义的伪选择器
210
+ */
211
+ #matches(step) {
212
+ const {parentNode, type, name, childNodes, link, constructor: {fixed, name: tokenName}} = this,
213
+ children = parentNode?.children,
214
+ childrenOfType = children?.filter(({type: t}) => t === type),
215
+ siblingsCount = children?.length ?? 1,
216
+ siblingsCountOfType = childrenOfType?.length ?? 1,
217
+ index = (children?.indexOf(this) ?? 0) + 1,
218
+ indexOfType = (childrenOfType?.indexOf(this) ?? 0) + 1,
219
+ lastIndex = siblingsCount - index + 1,
220
+ lastIndexOfType = siblingsCountOfType - indexOfType + 1;
221
+ return step.every(selector => {
222
+ if (typeof selector === 'string') {
223
+ switch (selector) { // 情形1:简单伪选择器、type和name
224
+ case '*':
225
+ return true;
226
+ case ':root':
227
+ return !parentNode;
228
+ case ':first-child':
229
+ return index === 1;
230
+ case ':first-of-type':
231
+ return indexOfType === 1;
232
+ case ':last-child':
233
+ return lastIndex === 1;
234
+ case ':last-of-type':
235
+ return lastIndexOfType === 1;
236
+ case ':only-child':
237
+ return siblingsCount === 1;
238
+ case ':only-of-type':
239
+ return siblingsCountOfType === 1;
240
+ case ':empty':
241
+ return !childNodes.some(child => child instanceof AstElement || String(child));
242
+ case ':parent':
243
+ return childNodes.some(child => child instanceof AstElement || String(child));
244
+ case ':header':
245
+ return type === 'heading';
246
+ case ':hidden':
247
+ return this.text() === '';
248
+ case ':visible':
249
+ return this.text() !== '';
250
+ case ':only-whitespace':
251
+ return this.text().trim() === '';
252
+ case ':any-link':
253
+ return type === 'link' || type === 'free-ext-link' || type === 'ext-link'
254
+ || (type === 'file' || type === 'gallery-image' && link);
255
+ case ':local-link':
256
+ return (type === 'link' || type === 'file' || type === 'gallery-image')
257
+ && link?.[0] === '#';
258
+ case ':read-only':
259
+ return fixed;
260
+ case ':read-write':
261
+ return !fixed;
262
+ case ':invalid':
263
+ return type === 'table-inter' || tokenName === 'HiddenToken';
264
+ case ':required':
265
+ return this.#isProtected() === true;
266
+ case ':optional':
267
+ return this.#isProtected() === false;
268
+ default: {
269
+ const [t, n] = selector.split('#');
270
+ return (!t || t === type || Boolean(Parser.typeAliases[type]?.includes(t)))
271
+ && (!n || n === name);
272
+ }
273
+ }
274
+ } else if (selector.length === 4) { // 情形2:属性选择器
275
+ return this.getAttribute('matchesAttr')(...selector);
276
+ }
277
+ const [s, pseudo] = selector; // 情形3:复杂伪选择器
278
+ switch (pseudo) {
279
+ case 'is':
280
+ return this.matches(s);
281
+ case 'not':
282
+ return !this.matches(s);
283
+ case 'nth-child':
284
+ return nth(s, index);
285
+ case 'nth-of-type':
286
+ return nth(s, indexOfType);
287
+ case 'nth-last-child':
288
+ return nth(s, lastIndex);
289
+ case 'nth-last-of-type':
290
+ return nth(s, lastIndexOfType);
291
+ case 'contains':
292
+ return this.text().includes(s);
293
+ case 'has':
294
+ return Boolean(this.querySelector(s));
295
+ case 'lang': {
296
+ const regex = new RegExp(`^${s}(?:-|$)`, 'u');
297
+ return matchesLang(this, regex)
298
+ || this.getAncestors().some(ancestor => matchesLang(ancestor, regex));
299
+ }
300
+ default:
301
+ throw new SyntaxError(`未定义的伪选择器!${pseudo}`);
302
+ }
303
+ });
304
+ }
305
+
306
+ /**
307
+ * 检查是否符合选择器
308
+ * @param {string|SelectorArray[]} selector
309
+ * @returns {boolean}
310
+ * @complexity `n`
311
+ */
312
+ matches(selector) {
313
+ if (selector === undefined) {
314
+ return true;
315
+ } else if (typeof selector === 'string') {
316
+ const stack = parseSelector(selector),
317
+ /** @type {Set<string>} */
318
+ pseudos = new Set(stack.flat(2).filter(step => typeof step === 'string' && step[0] === ':'));
319
+ if (pseudos.size > 0) {
320
+ Parser.warn('检测到伪选择器,请确认是否需要将":"转义成"\\:"。', pseudos);
321
+ }
322
+ return Parser.run(() => stack.some(condition => this.matches(condition)));
323
+ } else if (!Parser.running) {
324
+ this.typeError('matches', 'String');
325
+ }
326
+ selector = structuredClone(selector);
327
+ const step = selector.pop();
328
+ if (this.#matches(step)) {
329
+ const {parentNode, previousElementSibling} = this;
330
+ switch (selector.at(-1)?.relation) {
331
+ case undefined:
332
+ return true;
333
+ case '>':
334
+ return parentNode?.matches(selector);
335
+ case '+':
336
+ return previousElementSibling?.matches(selector);
337
+ case '~': {
338
+ if (!parentNode) {
339
+ return false;
340
+ }
341
+ const {children} = parentNode,
342
+ i = children.indexOf(this);
343
+ return children.slice(0, i).some(child => child.matches(selector));
344
+ }
345
+ default: // ' '
346
+ return this.getAncestors().some(ancestor => ancestor.matches(selector));
347
+ }
348
+ }
349
+ return false;
350
+ }
351
+
352
+ /**
353
+ * 符合选择器的第一个后代节点
354
+ * @param {string} selector
355
+ * @returns {this|undefined}
356
+ * @complexity `n`
357
+ */
358
+ querySelector(selector) {
359
+ for (const child of this.children) {
360
+ if (child.matches(selector)) {
361
+ return child;
362
+ }
363
+ const descendant = child.querySelector(selector);
364
+ if (descendant) {
365
+ return descendant;
366
+ }
367
+ }
368
+ return undefined;
369
+ }
370
+
371
+ /**
372
+ * 符合选择器的所有后代节点
373
+ * @param {string} selector
374
+ * @complexity `n`
375
+ */
376
+ querySelectorAll(selector) {
377
+ const /** @type {this[]} */ descendants = [];
378
+ for (const child of this.children) {
379
+ if (child.matches(selector)) {
380
+ descendants.push(child);
381
+ }
382
+ descendants.push(...child.querySelectorAll(selector));
383
+ }
384
+ return descendants;
385
+ }
386
+
387
+ /**
388
+ * 类选择器
389
+ * @param {string} className 类名之一
390
+ */
391
+ getElementsByClassName(className) {
392
+ return typeof className === 'string'
393
+ ? this.querySelectorAll(`[className~="${className.replace(/(?<!\\)"/gu, '\\"')}"]`)
394
+ : this.typeError('getElementsByClassName', 'String');
395
+ }
396
+
397
+ /**
398
+ * 标签名选择器
399
+ * @param {string} name 标签名
400
+ */
401
+ getElementsByTagName(name) {
402
+ if (typeof name === 'string') {
403
+ name = name.replace(/(?<!\\)"/gu, '\\"');
404
+ return this.querySelectorAll(`ext[name="${name}"], html[name="${name}"]`);
405
+ }
406
+ return this.typeError('getElementsByTagName', 'String');
407
+ }
408
+
409
+ /**
410
+ * 获取某一行的wikitext
411
+ * @param {number} n 行号
412
+ */
413
+ getLine(n) {
414
+ return String(this).split('\n', n + 1).at(-1);
415
+ }
416
+
417
+ /**
418
+ * 在开头批量插入子节点
419
+ * @param {...this} elements 插入节点
420
+ * @complexity `n`
421
+ */
422
+ prepend(...elements) {
423
+ for (let i = 0; i < elements.length; i++) {
424
+ this.insertAt(elements[i], i);
425
+ }
426
+ }
427
+
428
+ /**
429
+ * 最近的祖先节点
430
+ * @param {string} selector
431
+ */
432
+ closest(selector) {
433
+ let {parentNode} = this;
434
+ while (parentNode) {
435
+ if (parentNode.matches(selector)) {
436
+ return parentNode;
437
+ }
438
+ ({parentNode} = parentNode);
439
+ }
440
+ return undefined;
441
+ }
442
+
443
+ /**
444
+ * 在末尾批量插入子节点
445
+ * @param {...this} elements 插入节点
446
+ * @complexity `n`
447
+ */
448
+ append(...elements) {
449
+ for (const element of elements) {
450
+ this.insertAt(element);
451
+ }
452
+ }
453
+
454
+ /**
455
+ * 批量替换子节点
456
+ * @param {...this} elements 新的子节点
457
+ * @complexity `n`
458
+ */
459
+ replaceChildren(...elements) {
460
+ for (let i = this.length - 1; i >= 0; i--) {
461
+ this.removeAt(i);
462
+ }
463
+ this.append(...elements);
464
+ }
465
+
466
+ /**
467
+ * 修改文本子节点
468
+ * @param {string} str 新文本
469
+ * @param {number} i 子节点位置
470
+ * @throws `RangeError` 对应位置的子节点不是文本节点
471
+ */
472
+ setText(str, i = 0) {
473
+ this.getAttribute('verifyChild')(i);
474
+ const /** @type {AstText} */ oldText = this.childNodes.at(i),
475
+ {type, data, constructor: {name}} = oldText;
476
+ if (type === 'text') {
477
+ oldText.replaceData(str);
478
+ return data;
479
+ }
480
+ throw new RangeError(`第 ${i} 个子节点是 ${name}!`);
481
+ }
482
+
483
+ /**
484
+ * 还原为wikitext
485
+ * @param {string} selector
486
+ * @param {string} separator 子节点间的连接符
487
+ * @returns {string}
488
+ */
489
+ toString(selector, separator = '') {
490
+ return selector && this.matches(selector)
491
+ ? ''
492
+ : this.childNodes.map(child => child.toString(selector)).join(separator);
493
+ }
494
+
495
+ static lintIgnoredExt = new Set([
496
+ 'nowiki',
497
+ 'pre',
498
+ 'charinsert',
499
+ 'score',
500
+ 'syntaxhighlight',
501
+ 'source',
502
+ 'math',
503
+ 'chem',
504
+ 'ce',
505
+ 'graph',
506
+ 'mapframe',
507
+ 'maplink',
508
+ 'quiz',
509
+ 'templatedata',
510
+ 'timeline',
511
+ ]);
512
+
513
+ /**
514
+ * Linter
515
+ * @param {number} start 起始位置
516
+ */
517
+ lint(start = 0) {
518
+ const SyntaxToken = require('../src/syntax');
519
+ if (this instanceof SyntaxToken || this.constructor.hidden
520
+ || this.type === 'ext-inner' && AstElement.lintIgnoredExt.has(this.name)
521
+ ) {
522
+ return [];
523
+ }
524
+ const /** @type {LintError[]} */ errors = [];
525
+ for (let i = 0, cur = start + this.getPadding(); i < this.length; i++) {
526
+ const child = this.childNodes[i];
527
+ errors.push(...child.lint(cur));
528
+ cur += String(child).length + this.getGaps(i);
529
+ }
530
+ return errors;
531
+ }
532
+
533
+ /**
534
+ * 以HTML格式打印
535
+ * @param {printOpt} opt 选项
536
+ * @returns {string}
537
+ */
538
+ print(opt = {}) {
539
+ return String(this)
540
+ ? `<span class="wpb-${opt.class || this.type}">${print(this.childNodes, opt)}</span>`
541
+ : '';
542
+ }
543
+
544
+ /**
545
+ * 保存为JSON
546
+ * @param {string} file 文件名
547
+ * @returns {Record<string, *>}
548
+ */
549
+ json(file) {
550
+ const {childNodes, ...prop} = this,
551
+ json = {
552
+ ...prop,
553
+ childNodes: childNodes.map(child => child.type === 'text' ? String(child) : child.json()),
554
+ };
555
+ if (typeof file === 'string') {
556
+ fs.writeFileSync(
557
+ path.join(__dirname.slice(0, -4), 'printed', `${file}${file.endsWith('.json') ? '' : '.json'}`),
558
+ JSON.stringify(json, null, 2),
559
+ );
560
+ }
561
+ return json;
562
+ }
563
+
564
+ /**
565
+ * 输出AST
566
+ * @param {number} depth 当前深度
567
+ * @returns {void}
568
+ */
569
+ echo(depth = 0) {
570
+ if (!Number.isInteger(depth) || depth < 0) {
571
+ this.typeError('print', 'Number');
572
+ }
573
+ const indent = ' '.repeat(depth),
574
+ str = String(this),
575
+ {childNodes, type, length} = this;
576
+ if (childNodes.every(child => child.type === 'text' || !String(child))) {
577
+ console.log(`${indent}\x1B[32m<%s>\x1B[0m${noWrap(str)}\x1B[32m</%s>\x1B[0m`, type, type);
578
+ return undefined;
579
+ }
580
+ Parser.info(`${indent}<${type}>`);
581
+ let i = this.getPadding();
582
+ if (i) {
583
+ console.log(`${indent} ${noWrap(str.slice(0, i))}`);
584
+ }
585
+ for (let j = 0; j < length; j++) {
586
+ const child = childNodes[j],
587
+ childStr = String(child),
588
+ gap = j === length - 1 ? 0 : this.getGaps(j);
589
+ if (!childStr) {
590
+ // pass
591
+ } else if (child.type === 'text') {
592
+ console.log(`${indent} ${noWrap(String(child))}`);
593
+ } else {
594
+ child.echo(depth + 1);
595
+ }
596
+ i += childStr.length;
597
+ if (gap) {
598
+ console.log(`${indent} ${noWrap(str.slice(i, i + gap))}`);
599
+ i += gap;
600
+ }
601
+ }
602
+ if (i < str.length) {
603
+ console.log(`${indent} ${noWrap(str.slice(i))}`);
604
+ }
605
+ Parser.info(`${indent}</${type}>`);
606
+ return undefined;
607
+ }
608
+ }
609
+
610
+ Parser.classes.AstElement = __filename;
611
+ module.exports = AstElement;