wikiparser-node 0.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.
Files changed (65) hide show
  1. package/.eslintrc.json +229 -0
  2. package/LICENSE +674 -0
  3. package/README.md +1896 -0
  4. package/config/default.json +766 -0
  5. package/config/llwiki.json +686 -0
  6. package/config/moegirl.json +721 -0
  7. package/index.js +159 -0
  8. package/jsconfig.json +7 -0
  9. package/lib/element.js +690 -0
  10. package/lib/node.js +357 -0
  11. package/lib/ranges.js +122 -0
  12. package/lib/title.js +57 -0
  13. package/mixin/attributeParent.js +67 -0
  14. package/mixin/fixedToken.js +32 -0
  15. package/mixin/hidden.js +22 -0
  16. package/package.json +30 -0
  17. package/parser/brackets.js +107 -0
  18. package/parser/commentAndExt.js +61 -0
  19. package/parser/externalLinks.js +30 -0
  20. package/parser/hrAndDoubleUnderscore.js +26 -0
  21. package/parser/html.js +41 -0
  22. package/parser/links.js +92 -0
  23. package/parser/magicLinks.js +40 -0
  24. package/parser/quotes.js +63 -0
  25. package/parser/table.js +97 -0
  26. package/src/arg.js +150 -0
  27. package/src/atom/hidden.js +10 -0
  28. package/src/atom/index.js +33 -0
  29. package/src/attribute.js +342 -0
  30. package/src/extLink.js +116 -0
  31. package/src/heading.js +91 -0
  32. package/src/html.js +144 -0
  33. package/src/imageParameter.js +172 -0
  34. package/src/index.js +602 -0
  35. package/src/link/category.js +88 -0
  36. package/src/link/file.js +201 -0
  37. package/src/link/index.js +214 -0
  38. package/src/listToken.js +47 -0
  39. package/src/magicLink.js +66 -0
  40. package/src/nowiki/comment.js +45 -0
  41. package/src/nowiki/doubleUnderscore.js +42 -0
  42. package/src/nowiki/hr.js +41 -0
  43. package/src/nowiki/index.js +37 -0
  44. package/src/nowiki/noinclude.js +24 -0
  45. package/src/nowiki/quote.js +37 -0
  46. package/src/onlyinclude.js +42 -0
  47. package/src/parameter.js +165 -0
  48. package/src/syntax.js +80 -0
  49. package/src/table/index.js +867 -0
  50. package/src/table/td.js +259 -0
  51. package/src/table/tr.js +244 -0
  52. package/src/tagPair/ext.js +85 -0
  53. package/src/tagPair/include.js +45 -0
  54. package/src/tagPair/index.js +91 -0
  55. package/src/transclude.js +627 -0
  56. package/tool/index.js +898 -0
  57. package/typings/element.d.ts +28 -0
  58. package/typings/index.d.ts +49 -0
  59. package/typings/node.d.ts +23 -0
  60. package/typings/parser.d.ts +9 -0
  61. package/typings/table.d.ts +14 -0
  62. package/typings/token.d.ts +21 -0
  63. package/typings/tool.d.ts +10 -0
  64. package/util/debug.js +70 -0
  65. package/util/string.js +60 -0
@@ -0,0 +1,867 @@
1
+ 'use strict';
2
+
3
+ const {typeError} = require('../../util/debug'),
4
+ assert = require('assert/strict'),
5
+ /** @type {Parser} */ Parser = require('../..'),
6
+ Token = require('..'), // eslint-disable-line no-unused-vars
7
+ TrToken = require('./tr'),
8
+ TdToken = require('./td'),
9
+ SyntaxToken = require('../syntax'),
10
+ AttributeToken = require('../attribute'); // eslint-disable-line no-unused-vars
11
+
12
+ /**
13
+ * @param {TableCoords} coords1
14
+ * @param {TableCoords} coords2
15
+ */
16
+ const cmpCoords = (coords1, coords2) => {
17
+ const diff = coords1?.row - coords2?.row;
18
+ return diff === 0 ? coords1?.column - coords2?.column : diff;
19
+ };
20
+ const isRowEnd = /** @param {Token} */ ({type}) => ['tr', 'table-syntax'].includes(type);
21
+
22
+ /** @extends {Array<TableCoords[]>} */
23
+ class Layout extends Array {
24
+ /** @complexity `n` */
25
+ print() {
26
+ const hBorders = new Array(this.length + 1).fill().map((_, i) => {
27
+ const prev = this[i - 1] ?? [],
28
+ next = this[i] ?? [];
29
+ return new Array(Math.max(prev.length, next.length)).fill().map((__, j) => prev[j] !== next[j]);
30
+ }),
31
+ vBorders = this.map(cur => new Array(cur.length + 1).fill().map((_, j) => cur[j - 1] !== cur[j]));
32
+ let out = '';
33
+ for (let i = 0; i <= this.length; i++) {
34
+ const hBorder = hBorders[i],
35
+ vBorderTop = vBorders[i - 1] ?? [],
36
+ vBorderBottom = vBorders[i] ?? [],
37
+ // eslint-disable-next-line no-sparse-arrays
38
+ border = [' ',,, '┌',, '┐', '─', '┬',, '│', '└', '├', '┘', '┤', '┴', '┼'];
39
+ for (let j = 0; j <= hBorder.length; j++) {
40
+ /* eslint-disable no-bitwise */
41
+ const bit = (vBorderTop[j] << 3) + (vBorderBottom[j] << 0) + (hBorder[j - 1] << 2) + (hBorder[j] << 1);
42
+ out += `${border[bit]}${hBorder[j] ? '─' : ' '}`;
43
+ /* eslint-enable no-bitwise */
44
+ }
45
+ out += '\n';
46
+ }
47
+ console.log(out.slice(0, -1));
48
+ }
49
+ }
50
+
51
+ /**
52
+ * 表格
53
+ * @classdesc `{childNodes: [SyntaxToken, AttributeToken, ?(string|Token), ...TdToken, ...TrToken, ?SyntaxToken]}`
54
+ */
55
+ class TableToken extends TrToken {
56
+ type = 'table';
57
+
58
+ static openingPattern = /^(?:{\||{{{\s*!\s*}}|{{\s*\(!\s*}})$/;
59
+ static closingPattern = /^\n[^\S\n]*(?:\|}|{{\s*!\s*}}}|{{\s*!\)\s*}})$/;
60
+
61
+ /**
62
+ * @param {string} syntax
63
+ * @param {accum} accum
64
+ */
65
+ constructor(syntax, attr = '', config = Parser.getConfig(), accum = []) {
66
+ super(syntax, attr, config, accum, TableToken.openingPattern);
67
+ this.setAttribute('acceptable', {
68
+ String: 2, Token: 2, SyntaxToken: [0, -1], AttributeToken: 1, TdToken: '2:', TrToken: '2:',
69
+ });
70
+ }
71
+
72
+ /**
73
+ * @template {string|Token} T
74
+ * @param {T} token
75
+ * @returns {T}
76
+ * @complexity `n`
77
+ */
78
+ insertAt(token, i = this.childNodes.length) {
79
+ const previous = this.childNodes.at(i - 1),
80
+ {closingPattern} = TableToken;
81
+ if (token instanceof TrToken && token.type === 'td' && previous instanceof TrToken && previous.type === 'tr') {
82
+ Parser.warn('改为将单元格插入当前行。');
83
+ return previous.appendChild(token);
84
+ } else if (!Parser.running && i === this.childNodes.length && token instanceof SyntaxToken
85
+ && (token.getAttribute('pattern') !== closingPattern || !closingPattern.test(token.text()))
86
+ ) {
87
+ throw new SyntaxError(`表格的闭合部分不符合语法!${token.toString().replaceAll('\n', '\\n')}`);
88
+ }
89
+ return super.insertAt(token, i);
90
+ }
91
+
92
+ /** @complexity `n` */
93
+ close(syntax = '\n|}') {
94
+ const config = this.getAttribute('config'),
95
+ inner = Parser.parse(syntax, this.getAttribute('include'), 2, config),
96
+ {lastElementChild} = this;
97
+ if (!TableToken.closingPattern.test(inner.text())) {
98
+ throw new SyntaxError(`表格的闭合部分不符合语法!${syntax.replaceAll('\n', '\\n')}`);
99
+ } else if (lastElementChild instanceof SyntaxToken) {
100
+ lastElementChild.replaceChildren(...inner.childNodes);
101
+ } else {
102
+ this.appendChild(Parser.run(() => {
103
+ const token = new SyntaxToken(undefined, TableToken.closingPattern, 'table-syntax', config, [], {
104
+ 'Stage-1': ':', '!ExtToken': '', TranscludeToken: ':',
105
+ });
106
+ token.replaceChildren(...inner.childNodes);
107
+ return token;
108
+ }));
109
+ }
110
+ }
111
+
112
+ /**
113
+ * @returns {number}
114
+ * @complexity `n`
115
+ */
116
+ getRowCount() {
117
+ return super.getRowCount()
118
+ + this.children.filter(child => child.type === 'tr' && child.getRowCount()).length;
119
+ }
120
+
121
+ getPreviousRow() {}
122
+
123
+ /** @complexity `n` */
124
+ getNextRow() {
125
+ return this.getNthRow(super.getRowCount() ? 1 : 0);
126
+ }
127
+
128
+ /**
129
+ * @param {number} n
130
+ * @returns {TrToken}
131
+ * @complexity `n`
132
+ */
133
+ getNthRow(n, force = false, insert = false) {
134
+ if (typeof n !== 'number') {
135
+ typeError(this, 'getNthRow', 'Number');
136
+ }
137
+ const nRows = this.getRowCount(),
138
+ isRow = super.getRowCount();
139
+ n = n < 0 ? n + nRows : n;
140
+ if (n === 0 && (isRow || force && nRows === 0)) {
141
+ return this;
142
+ } else if (n < 0 || n > nRows || n === nRows && !insert) {
143
+ throw new RangeError(`不存在第 ${n} 行!`);
144
+ } else if (isRow) {
145
+ n--;
146
+ }
147
+ for (const child of this.children.slice(2)) {
148
+ if (child.type === 'tr' && child.getRowCount()) {
149
+ n--;
150
+ if (n < 0) {
151
+ return child;
152
+ }
153
+ } else if (child.type === 'table-syntax') {
154
+ return child;
155
+ }
156
+ }
157
+ }
158
+
159
+ /**
160
+ * @returns {TrToken[]}
161
+ * @complexity `n`
162
+ */
163
+ getAllRows() {
164
+ return [
165
+ ...super.getRowCount() ? [this] : [],
166
+ ...this.children.filter(child => child.type === 'tr' && child.getRowCount()),
167
+ ];
168
+ }
169
+
170
+ /**
171
+ * @param {TableCoords & TableRenderedCoords} coords
172
+ * @complexity `n`
173
+ */
174
+ getNthCell(coords) {
175
+ if (coords.row === undefined) {
176
+ coords = this.toRawCoords(coords);
177
+ }
178
+ return coords && this.getNthRow(coords.row).getNthCol(coords.column);
179
+ }
180
+
181
+ /**
182
+ * @param {TableCoords & TableRenderedCoords} stop
183
+ * @complexity `n`
184
+ */
185
+ getLayout(stop = {}) {
186
+ const rows = this.getAllRows(),
187
+ {length} = rows,
188
+ /** @type {Layout} */ layout = new Layout(length).fill().map(() => []);
189
+ for (const [i, rowToken] of rows.entries()) {
190
+ if (i > stop.row ?? stop.y) {
191
+ break;
192
+ }
193
+ const rowLayout = layout[i];
194
+ let j = 0,
195
+ k = 0,
196
+ last;
197
+ for (const cell of rowToken.children.slice(2)) {
198
+ if (cell instanceof TdToken) {
199
+ if (cell.isIndependent()) {
200
+ last = cell.subtype !== 'caption';
201
+ }
202
+ if (last) {
203
+ const /** @type {TableCoords} */ coords = {row: i, column: j},
204
+ {rowspan, colspan} = cell;
205
+ j++;
206
+ while (rowLayout[k]) {
207
+ k++;
208
+ }
209
+ if (i === stop.row && j > stop.column) {
210
+ layout[i][k] = coords;
211
+ return layout;
212
+ }
213
+ for (let y = i; y < Math.min(i + rowspan, length); y++) {
214
+ for (let x = k; x < k + colspan; x++) {
215
+ layout[y][x] = coords;
216
+ }
217
+ }
218
+ k += colspan;
219
+ if (i === stop.y && k > stop.x) {
220
+ return layout;
221
+ }
222
+ }
223
+ } else if (isRowEnd(cell)) {
224
+ break;
225
+ }
226
+ }
227
+ }
228
+ return layout;
229
+ }
230
+
231
+ /** @complexity `n` */
232
+ printLayout() {
233
+ this.getLayout().print();
234
+ }
235
+
236
+ /**
237
+ * @param {TableCoords}
238
+ * @returns {TableRenderedCoords}
239
+ * @complexity `n`
240
+ */
241
+ toRenderedCoords({row, column}) {
242
+ if (typeof row !== 'number' || typeof column !== 'number') {
243
+ typeError(this, 'toRenderedCoords', 'Number');
244
+ }
245
+ const rowLayout = this.getLayout({row, column})[row],
246
+ x = rowLayout?.findIndex(coords => cmpCoords(coords, {row, column}) === 0);
247
+ return rowLayout && (x === -1 ? undefined : {y: row, x});
248
+ }
249
+
250
+ /**
251
+ * @param {TableRenderedCoords}
252
+ * @complexity `n`
253
+ */
254
+ toRawCoords({x, y}) {
255
+ if (typeof x !== 'number' || typeof y !== 'number') {
256
+ typeError(this, 'toRawCoords', 'Number');
257
+ }
258
+ const rowLayout = this.getLayout({x, y})[y],
259
+ coords = rowLayout?.[x];
260
+ if (coords) {
261
+ return {...coords, start: coords.row === y && rowLayout[x - 1] !== coords};
262
+ } else if (!rowLayout && y === 0) {
263
+ return {row: 0, column: 0, start: true};
264
+ } else if (x === rowLayout?.length) {
265
+ return {row: y, column: (rowLayout.findLast(({row}) => row === y)?.column ?? -1) + 1, start: true};
266
+ }
267
+ }
268
+
269
+ /**
270
+ * @param {number} y
271
+ * @complexity `n²`
272
+ */
273
+ getFullRow(y) {
274
+ if (typeof y !== 'number') {
275
+ typeError(this, 'getFullRow', 'Number');
276
+ }
277
+ const rows = this.getAllRows();
278
+ return new Map(
279
+ this.getLayout({y})[y]?.map(({row, column}) => [rows[row].getNthCol(column), row === y]),
280
+ );
281
+ }
282
+
283
+ /**
284
+ * @param {number} x
285
+ * @complexity `n`
286
+ */
287
+ getFullCol(x) {
288
+ if (typeof x !== 'number') {
289
+ typeError(this, 'getFullCol', 'Number');
290
+ }
291
+ const layout = this.getLayout(),
292
+ colLayout = layout.map(row => row[x]).filter(coords => coords),
293
+ rows = this.getAllRows();
294
+ return new Map(
295
+ colLayout.map(coords => [rows[coords.row].getNthCol(coords.column), layout[coords.row][x - 1] !== coords]),
296
+ );
297
+ }
298
+
299
+ /**
300
+ * @param {Map<TdToken, boolean>} cells
301
+ * @param {string|Record<string, string|boolean>} attr
302
+ * @complexity `n`
303
+ */
304
+ #format(cells, attr = {}, multi = false) {
305
+ for (const [token, start] of cells) {
306
+ if (multi || start) {
307
+ if (typeof attr === 'string') {
308
+ token.setSyntax(attr);
309
+ } else {
310
+ for (const [k, v] of Object.entries(attr)) {
311
+ token.setAttr(k, v);
312
+ }
313
+ }
314
+ }
315
+ }
316
+ }
317
+
318
+ /**
319
+ * @param {number} y
320
+ * @param {string|Record<string, string|boolean>} attry
321
+ * @complexity `n²`
322
+ */
323
+ formatTableRow(y, attr = {}, multiRow = false) {
324
+ this.#format(this.getFullRow(y), attr, multiRow);
325
+ }
326
+
327
+ /**
328
+ * @param {number} x
329
+ * @param {string|Record<string, string|boolean>} attry
330
+ * @complexity `n`
331
+ */
332
+ formatTableCol(x, attr = {}, multiCol = false) {
333
+ this.#format(this.getFullCol(x), attr, multiCol);
334
+ }
335
+
336
+ /**
337
+ * @param {number} y
338
+ * @param {TrToken} rowToken
339
+ * @param {TableCoords[][]} layout
340
+ * @param {number} maxCol
341
+ * @param {Token} token
342
+ * @complexity `n`
343
+ */
344
+ #fill(y, rowToken, layout, maxCol, token) {
345
+ const rowLayout = layout[y],
346
+ {childNodes} = rowToken;
347
+ let lastIndex = childNodes.findLastIndex(child => child instanceof TdToken && child.subtype !== 'caption');
348
+ lastIndex = lastIndex === -1 ? undefined : lastIndex - childNodes.length;
349
+ Parser.run(() => {
350
+ for (let i = 0; i < maxCol; i++) {
351
+ if (!rowLayout[i]) {
352
+ rowToken.insertAt(token.cloneNode(), lastIndex);
353
+ }
354
+ }
355
+ });
356
+ }
357
+
358
+ /**
359
+ * @param {number} y
360
+ * @param {string|Token} inner
361
+ * @param {'td'|'th'|'caption'} subtype
362
+ * @param {Record<string, string>} attr
363
+ * @complexity `n`
364
+ */
365
+ fillTableRow(y, inner, subtype = 'td', attr = {}) {
366
+ const rowToken = this.getNthRow(y),
367
+ layout = this.getLayout({y}),
368
+ maxCol = Math.max(...layout.map(row => row.length)),
369
+ token = TdToken.create(inner, subtype, attr, this.getAttribute('include'), this.getAttribute('config'));
370
+ this.#fill(y, rowToken, layout, maxCol, token);
371
+ }
372
+
373
+ /**
374
+ * @param {string|Token} inner
375
+ * @param {'td'|'th'|'caption'} subtype
376
+ * @param {Record<string, string>} attr
377
+ * @complexity `n`
378
+ */
379
+ fillTable(inner, subtype = 'td', attr = {}) {
380
+ const rowTokens = this.getAllRows(),
381
+ layout = this.getLayout(),
382
+ maxCol = Math.max(...layout.map(({length}) => length)),
383
+ token = TdToken.create(inner, subtype, attr, this.getAttribute('include'), this.getAttribute('config'));
384
+ for (const [y, rowToken] of rowTokens.entries()) {
385
+ this.#fill(y, rowToken, layout, maxCol, token);
386
+ }
387
+ }
388
+
389
+ /**
390
+ * @param {string|Token} inner
391
+ * @param {TableCoords & TableRenderedCoords} coords
392
+ * @param {'td'|'th'|'caption'} subtype
393
+ * @param {Record<string, string|boolean>} attr
394
+ * @returns {TdToken}
395
+ * @complexity `n`
396
+ */
397
+ insertTableCell(inner, coords, subtype = 'td', attr = {}) {
398
+ if (coords.column === undefined) {
399
+ const {x, y} = coords;
400
+ coords = this.toRawCoords(coords);
401
+ if (!coords?.start) {
402
+ throw new RangeError(`指定的坐标不是单元格起始点:(${x}, ${y})`);
403
+ }
404
+ }
405
+ const rowToken = this.getNthRow(coords.row ?? 0, true);
406
+ if (rowToken === this) {
407
+ return super.insertTableCell(inner, coords, subtype, attr);
408
+ }
409
+ return rowToken.insertTableCell(inner, coords, subtype, attr);
410
+ }
411
+
412
+ /** @complexity `n` */
413
+ #prependTableRow() {
414
+ const row = Parser.run(() => new TrToken('\n|-', undefined, this.getAttribute('config'))),
415
+ {childNodes} = this,
416
+ [,, plain] = childNodes,
417
+ start = typeof plain === 'string' || plain.isPlain() ? 3 : 2,
418
+ /** @type {TdToken[]} */ children = childNodes.slice(start),
419
+ index = children.findIndex(({type}) => type !== 'td');
420
+ this.insertAt(row, index === -1 ? -1 : index + start);
421
+ Parser.run(() => {
422
+ for (const cell of children.slice(0, index === -1 ? undefined : index)) {
423
+ if (cell.subtype !== 'caption') {
424
+ row.appendChild(cell);
425
+ }
426
+ }
427
+ });
428
+ return row;
429
+ }
430
+
431
+ /**
432
+ * @param {number} y
433
+ * @param {Record<string, string|boolean>} attr
434
+ * @param {string|Token} inner
435
+ * @param {'td'|'th'|'caption'} subtype
436
+ * @param {Record<string, string|boolean>} attr
437
+ * @complexity `n`
438
+ */
439
+ insertTableRow(y, attr = {}, inner = undefined, subtype = 'td', innerAttr = {}) {
440
+ if (typeof attr !== 'object') {
441
+ typeError(this, 'insertTableRow', 'Object');
442
+ }
443
+ let reference = this.getNthRow(y, false, true);
444
+ /** @type {TrToken & AttributeToken}} */
445
+ const token = Parser.run(() => new TrToken('\n|-', undefined, this.getAttribute('config')));
446
+ for (const [k, v] of Object.entries(attr)) {
447
+ token.setAttr(k, v);
448
+ }
449
+ if (reference.type === 'table') { // `row === 0`且表格自身是有效行
450
+ reference = this.#prependTableRow();
451
+ }
452
+ this.insertBefore(token, reference);
453
+ if (inner !== undefined) {
454
+ const td = token.insertTableCell(inner, {column: 0}, subtype, innerAttr),
455
+ /** @type {Set<TableCoords>} */ set = new Set(),
456
+ layout = this.getLayout({y}),
457
+ maxCol = Math.max(...layout.map(row => row.length)),
458
+ rowLayout = layout[y];
459
+ Parser.run(() => {
460
+ for (let i = 0; i < maxCol; i++) {
461
+ const coords = rowLayout[i];
462
+ if (!coords) {
463
+ token.appendChild(td.cloneNode());
464
+ } else if (!set.has(coords)) {
465
+ set.add(coords);
466
+ if (coords.row < y) {
467
+ this.getNthCell(coords).rowspan++;
468
+ }
469
+ }
470
+ }
471
+ });
472
+ }
473
+ return token;
474
+ }
475
+
476
+ /**
477
+ * @param {number} x
478
+ * @param {string|Token} inner
479
+ * @param {'td'|'th'|'caption'} subtype
480
+ * @param {Record<string, string>} attr
481
+ * @complexity `n²`
482
+ */
483
+ insertTableCol(x, inner, subtype = 'td', attr = {}) {
484
+ if (typeof x !== 'number') {
485
+ typeError(this, 'insertTableCol', 'Number');
486
+ }
487
+ const layout = this.getLayout(),
488
+ rowLength = layout.map(({length}) => length),
489
+ minCol = Math.min(...rowLength);
490
+ if (x > minCol) {
491
+ throw new RangeError(`表格第 ${rowLength.indexOf(minCol)} 行仅有 ${minCol} 列!`);
492
+ }
493
+ const token = TdToken.create(inner, subtype, attr, this.getAttribute('include'), this.getAttribute('config'));
494
+ for (let i = 0; i < layout.length; i++) {
495
+ const coords = layout[i][x],
496
+ prevCoords = x === 0 ? true : layout[i][x - 1];
497
+ if (!prevCoords) {
498
+ continue;
499
+ } else if (prevCoords !== coords) {
500
+ const rowToken = this.getNthRow(i);
501
+ rowToken.insertBefore(token.cloneNode(), rowToken.getNthCol(coords.column, true));
502
+ } else if (coords?.row === i) {
503
+ this.getNthCell(coords).colspan++;
504
+ }
505
+ }
506
+ }
507
+
508
+ /**
509
+ * @param {number} y
510
+ * @complexity `n²`
511
+ */
512
+ removeTableRow(y) {
513
+ const rows = this.getAllRows(),
514
+ layout = this.getLayout(),
515
+ /** @type {Set<TableCoords>} */ set = new Set();
516
+ for (const [x, coords] of [...layout[y].entries()].reverse()) {
517
+ if (set.has(coords)) {
518
+ continue;
519
+ }
520
+ set.add(coords);
521
+ const token = rows[coords.row].getNthCol(coords.column);
522
+ let {rowspan} = token;
523
+ if (rowspan > 1) {
524
+ token.rowspan = --rowspan;
525
+ if (coords.row === y) {
526
+ const {colspan, subtype} = token,
527
+ attr = token.getAttr();
528
+ for (let i = y + 1; rowspan && i < rows.length; i++, rowspan--) {
529
+ const {column} = layout[i].slice(x + colspan).find(({row}) => row === i) ?? {};
530
+ if (column !== undefined) {
531
+ rows[i].insertTableCell('', {column}, subtype, {...attr, rowspan});
532
+ break;
533
+ }
534
+ }
535
+ }
536
+ }
537
+ }
538
+ const rowToken = rows[y].type === 'table' ? this.#prependTableRow() : rows[y];
539
+ rowToken.remove();
540
+ return rowToken;
541
+ }
542
+
543
+ /**
544
+ * @param {number} x
545
+ * @complexity `n²`
546
+ */
547
+ removeTableCol(x) {
548
+ for (const [token, start] of this.getFullCol(x)) {
549
+ const {colspan} = token;
550
+ if (colspan > 1) {
551
+ token.colspan = colspan - 1;
552
+ if (start) {
553
+ token.lastElementChild.replaceChildren();
554
+ }
555
+ } else {
556
+ token.remove();
557
+ }
558
+ }
559
+ }
560
+
561
+ /**
562
+ * @param {[number, number]} xlim
563
+ * @param {[number, number]} ylim
564
+ * @complexity `n²`
565
+ */
566
+ mergeCells(xlim, ylim) {
567
+ if ([...xlim, ...ylim].some(arg => typeof arg !== 'number')) {
568
+ typeError(this, 'mergeCells', 'Number');
569
+ }
570
+ const layout = this.getLayout(),
571
+ maxCol = Math.max(...layout.map(({length}) => length));
572
+ xlim = xlim.map(x => x < 0 ? x + maxCol : x);
573
+ ylim = ylim.map(y => y < 0 ? y + layout.length : y);
574
+ const [xmin, xmax] = xlim.sort(),
575
+ [ymin, ymax] = ylim.sort(),
576
+ set = new Set(layout.slice(ymin, ymax).flatMap(rowLayout => rowLayout.slice(xmin, xmax)));
577
+ if ([...layout[ymin - 1] ?? [], ...layout[ymax] ?? []].some(coords => set.has(coords))
578
+ || layout.some(rowLayout => set.has(rowLayout[xmin - 1]) || set.has(rowLayout[xmax]))
579
+ ) {
580
+ throw new RangeError(`待合并区域与外侧区域有重叠!`);
581
+ }
582
+ const corner = layout[ymin][xmin],
583
+ rows = this.getAllRows(),
584
+ cornerCell = rows[corner.row].getNthCol(corner.column);
585
+ cornerCell.rowspan = ymax - ymin;
586
+ cornerCell.colspan = xmax - xmin;
587
+ set.delete(corner);
588
+ for (const token of [...set].map(({row, column}) => rows[row].getNthCol(column))) {
589
+ token.remove();
590
+ }
591
+ return cornerCell;
592
+ }
593
+
594
+ /**
595
+ * @param {TableCoords & TableRenderedCoords} coords
596
+ * @param {Set<'rowspan'|'colspan'>} dirs
597
+ * @complexity `n²`
598
+ */
599
+ #split(coords, dirs) {
600
+ const cell = this.getNthCell(coords),
601
+ attr = cell.getAttr(),
602
+ {subtype} = cell;
603
+ attr.rowspan = Number(attr.rowspan) || 1;
604
+ attr.colspan = Number(attr.colspan) || 1;
605
+ for (const dir of dirs) {
606
+ if (attr[dir] === 1) {
607
+ dirs.delete(dir);
608
+ }
609
+ }
610
+ if (dirs.size === 0) {
611
+ return;
612
+ }
613
+ let {x, y} = coords;
614
+ if (x !== undefined) {
615
+ coords = this.toRawCoords(coords);
616
+ }
617
+ if (coords.start === false || x === undefined) {
618
+ ({x, y} = this.toRenderedCoords(coords));
619
+ }
620
+ const splitting = {rowspan: 1, colspan: 1};
621
+ for (const dir of dirs) {
622
+ cell.setAttr(dir, 1);
623
+ splitting[dir] = attr[dir];
624
+ delete attr[dir];
625
+ }
626
+ for (let j = y; j < y + splitting.rowspan; j++) {
627
+ for (let i = x; i < x + splitting.colspan; i++) {
628
+ if (i > x || j > y) {
629
+ try {
630
+ this.insertTableCell('', {x: i, y: j}, subtype, attr);
631
+ } catch (e) {
632
+ if (!(e instanceof RangeError) || !e.message.startsWith('指定的坐标不是单元格起始点:')) {
633
+ throw e;
634
+ }
635
+ break;
636
+ }
637
+ }
638
+ }
639
+ }
640
+ }
641
+
642
+ /**
643
+ * @param {TableCoords & TableRenderedCoords} coords
644
+ * @complexity `n²`
645
+ */
646
+ splitIntoRows(coords) {
647
+ this.#split(coords, new Set(['rowspan']));
648
+ }
649
+
650
+ /**
651
+ * @param {TableCoords & TableRenderedCoords} coords
652
+ * @complexity `n²`
653
+ */
654
+ splitIntoCols(coords) {
655
+ this.#split(coords, new Set(['colspan']));
656
+ }
657
+
658
+ /**
659
+ * @param {TableCoords & TableRenderedCoords} coords
660
+ * @complexity `n²`
661
+ */
662
+ splitIntoCells(coords) {
663
+ this.#split(coords, new Set(['rowspan', 'colspan']));
664
+ }
665
+
666
+ /**
667
+ * 复制一行并插入该行之前
668
+ * @param {number} row
669
+ * @complexity `n²`
670
+ */
671
+ replicateTableRow(row) {
672
+ let rowToken = this.getNthRow(row);
673
+ if (rowToken.type === 'table') {
674
+ rowToken = this.#prependTableRow();
675
+ }
676
+ const /** @type {TrToken} */ replicated = this.insertBefore(rowToken.cloneNode(), rowToken);
677
+ for (const [token, start] of this.getFullRow(row)) {
678
+ if (start) {
679
+ token.rowspan = 1;
680
+ } else {
681
+ token.rowspan++;
682
+ }
683
+ }
684
+ return replicated;
685
+ }
686
+
687
+ /**
688
+ * 复制一列并插入该列之前
689
+ * @param {number} x
690
+ * @complexity `n`
691
+ */
692
+ replicateTableCol(x) {
693
+ const /** @type {TdToken[]} */ replicated = [];
694
+ for (const [token, start] of this.getFullCol(x)) {
695
+ if (start) {
696
+ const newToken = token.cloneNode();
697
+ newToken.colspan = 1;
698
+ token.before(newToken);
699
+ replicated.push(newToken);
700
+ } else {
701
+ token.colspan++;
702
+ }
703
+ }
704
+ return replicated;
705
+ }
706
+
707
+ /**
708
+ * @param {number} y
709
+ * @param {number} before
710
+ * @complexity `n²`
711
+ */
712
+ moveTableRowBefore(y, before) {
713
+ if (typeof y !== 'number' || typeof before !== 'number') {
714
+ typeError(this, 'moveTableRowBefore', 'Number');
715
+ }
716
+ const layout = this.getLayout(),
717
+ /**
718
+ * @type {(i: number) => number[]}
719
+ * @complexity `n`
720
+ */
721
+ occupied = i => layout[i].map(({row}, j) => row === i ? j : null).filter(j => j !== null);
722
+ try {
723
+ assert.deepStrictEqual(occupied(y), occupied(before));
724
+ } catch (e) {
725
+ if (e instanceof assert.AssertionError) {
726
+ throw new RangeError(`第 ${y} 行与第 ${before} 行的构造不同,无法移动!`);
727
+ }
728
+ throw e;
729
+ }
730
+ const rowToken = this.removeTableRow(y);
731
+ for (const coords of layout[before]) {
732
+ if (coords.row < before) {
733
+ this.getNthCell(coords).rowspan++;
734
+ }
735
+ }
736
+ let beforeToken = this.getNthRow(before);
737
+ if (beforeToken.type === 'table') {
738
+ beforeToken = this.#prependTableRow();
739
+ }
740
+ this.insertBefore(rowToken, beforeToken);
741
+ return rowToken;
742
+ }
743
+
744
+ /**
745
+ * @param {number} y
746
+ * @param {number} after
747
+ * @complexity `n²`
748
+ */
749
+ moveTableRowAfter(y, after) {
750
+ if (typeof y !== 'number' || typeof after !== 'number') {
751
+ typeError(this, 'moveTableRowAfter', 'Number');
752
+ }
753
+ const layout = this.getLayout(),
754
+ afterToken = this.getNthRow(after),
755
+ /** @type {TdToken[]} */
756
+ cells = afterToken.children.filter(child => child instanceof TdToken && child.subtype !== 'caption'),
757
+ /**
758
+ * @type {(i: number, oneRow?: boolean) => number[]}
759
+ * @complexity `n`
760
+ */
761
+ occupied = (i, oneRow = false) => layout[i].map(({row, column}, j) =>
762
+ row === i && (!oneRow || cells[column].rowspan === 1) ? j : null,
763
+ ).filter(j => j !== null);
764
+ try {
765
+ assert.deepStrictEqual(occupied(y), occupied(after, true));
766
+ } catch (e) {
767
+ if (e instanceof assert.AssertionError) {
768
+ throw new RangeError(`第 ${y} 行与第 ${after} 行的构造不同,无法移动!`);
769
+ }
770
+ throw e;
771
+ }
772
+ const rowToken = this.removeTableRow(y);
773
+ for (const coords of layout[after]) {
774
+ if (coords.row < after) {
775
+ this.getNthCell(coords).rowspan++;
776
+ } else {
777
+ const cell = cells[coords.column],
778
+ {rowspan} = cell;
779
+ if (rowspan > 1) {
780
+ cell.rowspan = rowspan + 1;
781
+ }
782
+ }
783
+ }
784
+ if (afterToken === this) {
785
+ const index = this.childNodes.slice(2).findIndex(isRowEnd);
786
+ this.insertAt(rowToken, index + 2);
787
+ } else {
788
+ this.insertBefore(rowToken, afterToken);
789
+ }
790
+ return rowToken;
791
+ }
792
+
793
+ /**
794
+ * @param {number} x
795
+ * @param {number} reference
796
+ * @complexity `n`
797
+ */
798
+ #moveCol(x, reference, after = false) {
799
+ if (typeof x !== 'number' || typeof reference !== 'number') {
800
+ typeError(this, `moveTableCol${after ? 'After' : 'Before'}`, 'Number');
801
+ }
802
+ const layout = this.getLayout(),
803
+ /** @type {(rowLayout: TableCoords[], i: number, oneCol?: boolean) => boolean} */
804
+ isStart = (rowLayout, i, oneCol = false) => {
805
+ const coords = rowLayout[i];
806
+ return rowLayout[i - 1] !== coords && (!oneCol || rowLayout[i + 1] !== coords);
807
+ };
808
+ if (layout.some(rowLayout => isStart(rowLayout, x) !== isStart(rowLayout, reference, after))) {
809
+ throw new RangeError(`第 ${x} 列与第 ${reference} 列的构造不同,无法移动!`);
810
+ }
811
+ const /** @type {Set<TableCoords>} */ setX = new Set(),
812
+ /** @type {Set<TableCoords>} */ setRef = new Set(),
813
+ rows = this.getAllRows();
814
+ for (const [i, rowLayout] of layout.entries()) {
815
+ const coords = rowLayout[x],
816
+ refCoords = rowLayout[reference],
817
+ start = isStart(rowLayout, x);
818
+ if (refCoords && !start && !setRef.has(refCoords)) {
819
+ setRef.add(refCoords);
820
+ rows[refCoords.row].getNthCol(refCoords.column).colspan++;
821
+ }
822
+ if (coords && !setX.has(coords)) {
823
+ setX.add(coords);
824
+ const rowToken = rows[i];
825
+ let token = rowToken.getNthCol(coords.column);
826
+ const {colspan} = token;
827
+ if (colspan > 1) {
828
+ token.colspan = colspan - 1;
829
+ if (start) {
830
+ const original = token;
831
+ token = token.cloneNode();
832
+ original.lastElementChild.replaceChildren();
833
+ token.colspan = 1;
834
+ }
835
+ }
836
+ if (start) {
837
+ const col = rowLayout.slice(reference + Number(after)).find(({row}) => row === i)?.column;
838
+ rowToken.insertBefore(token, col === undefined && rowToken.type === 'table'
839
+ ? rowToken.children.slice(2).find(isRowEnd)
840
+ : col !== undefined && rowToken.getNthCol(col),
841
+ );
842
+ }
843
+ }
844
+ }
845
+ }
846
+
847
+ /**
848
+ * @param {number} x
849
+ * @param {number} before
850
+ * @complexity `n`
851
+ */
852
+ moveTableColBefore(x, before) {
853
+ this.#moveCol(x, before);
854
+ }
855
+
856
+ /**
857
+ * @param {number} x
858
+ * @param {number} after
859
+ * @complexity `n`
860
+ */
861
+ moveTableColAfter(x, after) {
862
+ this.#moveCol(x, after, true);
863
+ }
864
+ }
865
+
866
+ Parser.classes.TableToken = __filename;
867
+ module.exports = TableToken;