wikiparser-node 0.3.1 → 0.5.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 (80) hide show
  1. package/README.md +1 -1
  2. package/config/default.json +13 -17
  3. package/config/llwiki.json +11 -79
  4. package/config/moegirl.json +7 -1
  5. package/config/zhwiki.json +1269 -0
  6. package/index.js +130 -97
  7. package/lib/element.js +410 -518
  8. package/lib/node.js +493 -115
  9. package/lib/ranges.js +27 -19
  10. package/lib/text.js +175 -0
  11. package/lib/title.js +14 -6
  12. package/mixin/attributeParent.js +70 -24
  13. package/mixin/fixedToken.js +18 -10
  14. package/mixin/hidden.js +6 -4
  15. package/mixin/sol.js +39 -12
  16. package/package.json +17 -4
  17. package/parser/brackets.js +18 -18
  18. package/parser/commentAndExt.js +16 -14
  19. package/parser/converter.js +14 -13
  20. package/parser/externalLinks.js +12 -11
  21. package/parser/hrAndDoubleUnderscore.js +24 -14
  22. package/parser/html.js +8 -7
  23. package/parser/links.js +13 -13
  24. package/parser/list.js +12 -11
  25. package/parser/magicLinks.js +11 -10
  26. package/parser/quotes.js +6 -5
  27. package/parser/selector.js +175 -0
  28. package/parser/table.js +31 -24
  29. package/src/arg.js +91 -43
  30. package/src/atom/hidden.js +5 -2
  31. package/src/atom/index.js +17 -9
  32. package/src/attribute.js +210 -101
  33. package/src/converter.js +78 -43
  34. package/src/converterFlags.js +104 -45
  35. package/src/converterRule.js +136 -78
  36. package/src/extLink.js +81 -27
  37. package/src/gallery.js +63 -20
  38. package/src/heading.js +58 -20
  39. package/src/html.js +138 -48
  40. package/src/imageParameter.js +93 -58
  41. package/src/index.js +314 -186
  42. package/src/link/category.js +22 -54
  43. package/src/link/file.js +83 -32
  44. package/src/link/galleryImage.js +21 -7
  45. package/src/link/index.js +170 -81
  46. package/src/magicLink.js +64 -14
  47. package/src/nowiki/comment.js +36 -10
  48. package/src/nowiki/dd.js +37 -22
  49. package/src/nowiki/doubleUnderscore.js +21 -7
  50. package/src/nowiki/hr.js +11 -7
  51. package/src/nowiki/index.js +16 -9
  52. package/src/nowiki/list.js +2 -2
  53. package/src/nowiki/noinclude.js +8 -4
  54. package/src/nowiki/quote.js +38 -7
  55. package/src/onlyinclude.js +24 -7
  56. package/src/parameter.js +102 -62
  57. package/src/syntax.js +23 -20
  58. package/src/table/index.js +282 -174
  59. package/src/table/td.js +112 -61
  60. package/src/table/tr.js +135 -74
  61. package/src/tagPair/ext.js +30 -23
  62. package/src/tagPair/include.js +26 -11
  63. package/src/tagPair/index.js +72 -29
  64. package/src/transclude.js +235 -127
  65. package/tool/index.js +42 -32
  66. package/util/debug.js +21 -18
  67. package/util/diff.js +76 -0
  68. package/util/lint.js +40 -0
  69. package/util/string.js +56 -26
  70. package/.eslintrc.json +0 -319
  71. package/errors/README +0 -1
  72. package/jsconfig.json +0 -7
  73. package/printed/README +0 -1
  74. package/typings/element.d.ts +0 -28
  75. package/typings/index.d.ts +0 -52
  76. package/typings/node.d.ts +0 -23
  77. package/typings/parser.d.ts +0 -9
  78. package/typings/table.d.ts +0 -14
  79. package/typings/token.d.ts +0 -22
  80. package/typings/tool.d.ts +0 -10
@@ -2,26 +2,94 @@
2
2
 
3
3
  const assert = require('assert/strict'),
4
4
  {noWrap} = require('../../util/string'),
5
- /** @type {Parser} */ Parser = require('../..'),
5
+ {generateForChild} = require('../../util/lint'),
6
+ Parser = require('../..'),
6
7
  Token = require('..'),
7
8
  TrToken = require('./tr'),
8
9
  TdToken = require('./td'),
9
10
  SyntaxToken = require('../syntax'),
10
11
  AttributeToken = require('../attribute');
11
12
 
13
+ const openingPattern = /^(?:\{\||\{\{\{\s*!\s*\}\}|\{\{\s*\(!\s*\}\})$/u,
14
+ closingPattern = /^\n[^\S\n]*(?:\|\}|\{\{\s*!\s*\}\}\}|\{\{\s*!\)\s*\}\})$/u;
15
+
12
16
  /**
13
- * @param {TableCoords} coords1
14
- * @param {TableCoords} coords2
17
+ * 比较两个表格坐标
18
+ * @param {TableCoords} coords1 坐标1
19
+ * @param {TableCoords} coords2 坐标2
15
20
  */
16
21
  const cmpCoords = (coords1, coords2) => {
17
22
  const diff = coords1.row - coords2.row;
18
23
  return diff === 0 ? coords1.column - coords2.column : diff;
19
24
  };
20
- const isRowEnd = /** @param {Token} */ ({type}) => ['tr', 'table-syntax'].includes(type);
25
+
26
+ /**
27
+ * 是否是行尾
28
+ * @param {Token} cell 表格单元格
29
+ */
30
+ const isRowEnd = ({type}) => type === 'tr' || type === 'table-syntax';
31
+
32
+ /**
33
+ * 是否是合并单元格的第一列
34
+ * @param {TableCoords[]} rowLayout 行布局
35
+ * @param {number} i 单元格序号
36
+ * @param {boolean} oneCol 是否仅有一列
37
+ */
38
+ const isStartCol = (rowLayout, i, oneCol = false) => {
39
+ const coords = rowLayout[i];
40
+ return rowLayout[i - 1] !== coords && (!oneCol || rowLayout[i + 1] !== coords);
41
+ };
42
+
43
+ /**
44
+ * 设置表格格式
45
+ * @param {Map<TdToken, boolean>} cells 单元格
46
+ * @param {string|Record<string, string|boolean>} attr 属性
47
+ * @param {boolean} multi 是否对所有单元格设置,或是仅对行首单元格设置
48
+ * @complexity `n`
49
+ */
50
+ const format = (cells, attr = {}, multi = false) => {
51
+ for (const [token, start] of cells) {
52
+ if (multi || start) {
53
+ if (typeof attr === 'string') {
54
+ token.setSyntax(attr);
55
+ } else {
56
+ for (const [k, v] of Object.entries(attr)) {
57
+ token.setAttr(k, v);
58
+ }
59
+ }
60
+ }
61
+ }
62
+ };
63
+
64
+ /**
65
+ * 填补缺失单元格
66
+ * @param {number} y 行号
67
+ * @param {TrToken} rowToken 表格行
68
+ * @param {TableCoords[][]} layout 表格布局
69
+ * @param {number} maxCol 最大列数
70
+ * @param {Token} token 待填充的单元格
71
+ * @complexity `n`
72
+ */
73
+ const fill = (y, rowToken, layout, maxCol, token) => {
74
+ const rowLayout = layout[y],
75
+ {childNodes} = rowToken,
76
+ lastIndex = childNodes.findLastIndex(child => child instanceof TdToken && child.subtype !== 'caption') + 1
77
+ || undefined;
78
+ Parser.run(() => {
79
+ for (let i = 0; i < maxCol; i++) {
80
+ if (!rowLayout[i]) {
81
+ rowToken.insertAt(token.cloneNode(), lastIndex);
82
+ }
83
+ }
84
+ });
85
+ };
21
86
 
22
87
  /** @extends {Array<TableCoords[]>} */
23
88
  class Layout extends Array {
24
- /** @complexity `n` */
89
+ /**
90
+ * 打印表格布局
91
+ * @complexity `n`
92
+ */
25
93
  print() {
26
94
  const hBorders = new Array(this.length + 1).fill().map((_, i) => {
27
95
  const prev = this[i - 1] ?? [],
@@ -37,10 +105,9 @@ class Layout extends Array {
37
105
  // eslint-disable-next-line no-sparse-arrays
38
106
  border = [' ',,, '┌',, '┐', '─', '┬',, '│', '└', '├', '┘', '┤', '┴', '┼'];
39
107
  for (let j = 0; j <= hBorder.length; j++) {
40
- /* eslint-disable no-bitwise */
108
+ // eslint-disable-next-line no-bitwise
41
109
  const bit = (vBorderTop[j] << 3) + (vBorderBottom[j] << 0) + (hBorder[j - 1] << 2) + (hBorder[j] << 1);
42
110
  out += `${border[bit]}${hBorder[j] ? '─' : ' '}`;
43
- /* eslint-enable no-bitwise */
44
111
  }
45
112
  out += '\n';
46
113
  }
@@ -55,52 +122,79 @@ class Layout extends Array {
55
122
  class TableToken extends TrToken {
56
123
  type = 'table';
57
124
 
58
- static openingPattern = /^(?:\{\||\{\{\{\s*!\s*\}\}|\{\{\s*\(!\s*\}\})$/;
59
- static closingPattern = /^\n[^\S\n]*(?:\|\}|\{\{\s*!\s*\}\}\}|\{\{\s*!\)\s*\}\})$/;
125
+ /** 表格是否闭合 */
126
+ get closed() {
127
+ return this.lastChild.type === 'table-syntax';
128
+ }
129
+
130
+ set closed(closed) {
131
+ if (closed === true && !this.closed) {
132
+ this.close(this.closest('parameter') ? '\n{{!)}}' : '\n|}');
133
+ }
134
+ }
60
135
 
61
136
  /**
62
- * @param {string} syntax
137
+ * @param {string} syntax 表格语法
138
+ * @param {string} attr 表格属性
63
139
  * @param {accum} accum
64
140
  */
65
141
  constructor(syntax, attr = '', config = Parser.getConfig(), accum = []) {
66
- super(syntax, attr, config, accum, TableToken.openingPattern);
142
+ super(syntax, attr, config, accum, openingPattern);
67
143
  this.setAttribute('acceptable', {
68
144
  Token: 2, SyntaxToken: [0, -1], AttributeToken: 1, TdToken: '2:', TrToken: '2:',
69
145
  });
70
146
  }
71
147
 
72
148
  /**
149
+ * @override
150
+ * @param {number} start 起始位置
151
+ */
152
+ lint(start = 0) {
153
+ const errors = super.lint(start);
154
+ if (!this.closed) {
155
+ errors.push(generateForChild(this.firstChild, this.getRootNode().posFromIndex(start), '未闭合的表格'));
156
+ }
157
+ return errors;
158
+ }
159
+
160
+ /**
161
+ * @override
73
162
  * @template {TrToken|SyntaxToken} T
74
- * @param {T} token
163
+ * @param {T} token 待插入的子节点
164
+ * @param {number} i 插入位置
75
165
  * @returns {T}
76
166
  * @complexity `n`
167
+ * @throws `SyntaxError` 表格的闭合部分非法
77
168
  */
78
169
  insertAt(token, i = this.childNodes.length) {
79
- const previous = this.children.at(i - 1),
80
- {closingPattern} = TableToken;
170
+ const previous = this.childNodes.at(i - 1);
81
171
  if (token.type === 'td' && previous.type === 'tr') {
82
172
  Parser.warn('改为将单元格插入当前行。');
83
173
  return previous.appendChild(token);
84
174
  } else if (!Parser.running && i === this.childNodes.length && token instanceof SyntaxToken
85
175
  && (token.getAttribute('pattern') !== closingPattern || !closingPattern.test(token.text()))
86
176
  ) {
87
- throw new SyntaxError(`表格的闭合部分不符合语法!${noWrap(token.toString())}`);
177
+ throw new SyntaxError(`表格的闭合部分不符合语法!${noWrap(String(token))}`);
88
178
  }
89
179
  return super.insertAt(token, i);
90
180
  }
91
181
 
92
- /** @complexity `n` */
182
+ /**
183
+ * 闭合表格语法
184
+ * @complexity `n`
185
+ * @param {string} syntax 表格结尾语法
186
+ * @throws `SyntaxError` 表格的闭合部分不符合语法
187
+ */
93
188
  close(syntax = '\n|}', halfParsed = false) {
94
189
  halfParsed &&= Parser.running;
95
190
  const config = this.getAttribute('config'),
96
191
  accum = this.getAttribute('accum'),
97
192
  inner = !halfParsed && Parser.parse(syntax, this.getAttribute('include'), 2, config),
98
- {lastElementChild} = this,
99
- {closingPattern} = TableToken;
193
+ {lastChild} = this;
100
194
  if (!halfParsed && !closingPattern.test(inner.text())) {
101
195
  throw new SyntaxError(`表格的闭合部分不符合语法!${noWrap(syntax)}`);
102
- } else if (lastElementChild instanceof SyntaxToken) {
103
- lastElementChild.replaceChildren(...inner.childNodes);
196
+ } else if (lastChild instanceof SyntaxToken) {
197
+ lastChild.replaceChildren(...inner.childNodes);
104
198
  } else {
105
199
  this.appendChild(Parser.run(() => {
106
200
  const token = new SyntaxToken(syntax, closingPattern, 'table-syntax', config, accum, {
@@ -115,27 +209,38 @@ class TableToken extends TrToken {
115
209
  }
116
210
 
117
211
  /**
212
+ * @override
118
213
  * @returns {number}
119
214
  * @complexity `n`
120
215
  */
121
216
  getRowCount() {
122
217
  return super.getRowCount()
123
- + this.children.filter(child => child.type === 'tr' && child.getRowCount()).length;
218
+ + this.childNodes.filter(child => child.type === 'tr' && child.getRowCount()).length;
124
219
  }
125
220
 
126
- getPreviousRow() {}
221
+ /** @override */
222
+ getPreviousRow() { // eslint-disable-line class-methods-use-this
223
+ return undefined;
224
+ }
127
225
 
128
- /** @complexity `n` */
226
+ /**
227
+ * @override
228
+ * @complexity `n`
229
+ */
129
230
  getNextRow() {
130
231
  return this.getNthRow(super.getRowCount() ? 1 : 0);
131
232
  }
132
233
 
133
234
  /**
134
- * @param {number} n
235
+ * 获取第n
236
+ * @param {number} n 行号
237
+ * @param {boolean} force 是否将表格自身视为第一行
238
+ * @param {boolean} insert 是否用于判断插入新行的位置
135
239
  * @returns {TrToken}
136
240
  * @complexity `n`
241
+ * @throws `RangeError` 不存在该行
137
242
  */
138
- getNthRow(n, force = false, insert = false) {
243
+ getNthRow(n, force, insert) {
139
244
  if (typeof n !== 'number') {
140
245
  this.typeError('getNthRow', 'Number');
141
246
  }
@@ -149,7 +254,7 @@ class TableToken extends TrToken {
149
254
  } else if (isRow) {
150
255
  n--;
151
256
  }
152
- for (const child of this.children.slice(2)) {
257
+ for (const child of this.childNodes.slice(2)) {
153
258
  if (child.type === 'tr' && child.getRowCount()) {
154
259
  n--;
155
260
  if (n < 0) {
@@ -159,21 +264,24 @@ class TableToken extends TrToken {
159
264
  return child;
160
265
  }
161
266
  }
267
+ return undefined;
162
268
  }
163
269
 
164
270
  /**
271
+ * 获取所有行
165
272
  * @returns {TrToken[]}
166
273
  * @complexity `n`
167
274
  */
168
275
  getAllRows() {
169
276
  return [
170
277
  ...super.getRowCount() ? [this] : [],
171
- ...this.children.filter(child => child.type === 'tr' && child.getRowCount()),
278
+ ...this.childNodes.filter(child => child.type === 'tr' && child.getRowCount()),
172
279
  ];
173
280
  }
174
281
 
175
282
  /**
176
- * @param {TableCoords & TableRenderedCoords} coords
283
+ * 获取指定坐标的单元格
284
+ * @param {TableCoords & TableRenderedCoords} coords 表格坐标
177
285
  * @complexity `n`
178
286
  */
179
287
  getNthCell(coords) {
@@ -184,14 +292,15 @@ class TableToken extends TrToken {
184
292
  }
185
293
 
186
294
  /**
187
- * @param {TableCoords & TableRenderedCoords} stop
295
+ * 获取表格布局
296
+ * @param {TableCoords & TableRenderedCoords} stop 中止条件
188
297
  * @complexity `n`
189
298
  */
190
299
  getLayout(stop = {}) {
191
300
  const rows = this.getAllRows(),
192
301
  {length} = rows,
193
302
  /** @type {Layout} */ layout = new Layout(length).fill().map(() => []);
194
- for (const [i, rowToken] of rows.entries()) {
303
+ for (let i = 0; i < length; i++) {
195
304
  if (i > (stop.row ?? stop.y)) {
196
305
  break;
197
306
  }
@@ -199,7 +308,7 @@ class TableToken extends TrToken {
199
308
  let j = 0,
200
309
  k = 0,
201
310
  last;
202
- for (const cell of rowToken.children.slice(2)) {
311
+ for (const cell of rows[i].childNodes.slice(2)) {
203
312
  if (cell instanceof TdToken) {
204
313
  if (cell.isIndependent()) {
205
314
  last = cell.subtype !== 'caption';
@@ -233,13 +342,17 @@ class TableToken extends TrToken {
233
342
  return layout;
234
343
  }
235
344
 
236
- /** @complexity `n` */
345
+ /**
346
+ * 打印表格布局
347
+ * @complexity `n`
348
+ */
237
349
  printLayout() {
238
350
  this.getLayout().print();
239
351
  }
240
352
 
241
353
  /**
242
- * @param {TableCoords}
354
+ * 转换为渲染后的表格坐标
355
+ * @param {TableCoords} coord wikitext中的表格坐标
243
356
  * @returns {TableRenderedCoords}
244
357
  * @complexity `n`
245
358
  */
@@ -253,7 +366,8 @@ class TableToken extends TrToken {
253
366
  }
254
367
 
255
368
  /**
256
- * @param {TableRenderedCoords}
369
+ * 转换为wikitext中的表格坐标
370
+ * @param {TableRenderedCoords} coord 渲染后的表格坐标
257
371
  * @complexity `n`
258
372
  */
259
373
  toRawCoords({x, y}) {
@@ -264,15 +378,17 @@ class TableToken extends TrToken {
264
378
  coords = rowLayout?.[x];
265
379
  if (coords) {
266
380
  return {...coords, start: coords.row === y && rowLayout[x - 1] !== coords};
267
- } else if (!rowLayout && y === 0) {
268
- return {row: 0, column: 0, start: true};
269
- } else if (x === rowLayout?.length) {
270
- return {row: y, column: (rowLayout.findLast(({row}) => row === y)?.column ?? -1) + 1, start: true};
381
+ } else if (rowLayout || y > 0) {
382
+ return x === rowLayout?.length
383
+ ? {row: y, column: (rowLayout.findLast(({row}) => row === y)?.column ?? -1) + 1, start: true}
384
+ : undefined;
271
385
  }
386
+ return {row: 0, column: 0, start: true};
272
387
  }
273
388
 
274
389
  /**
275
- * @param {number} y
390
+ * 获取完整行
391
+ * @param {number} y 行号
276
392
  * @complexity `n²`
277
393
  */
278
394
  getFullRow(y) {
@@ -286,7 +402,8 @@ class TableToken extends TrToken {
286
402
  }
287
403
 
288
404
  /**
289
- * @param {number} x
405
+ * 获取完整列
406
+ * @param {number} x 列号
290
407
  * @complexity `n`
291
408
  */
292
409
  getFullCol(x) {
@@ -294,7 +411,7 @@ class TableToken extends TrToken {
294
411
  this.typeError('getFullCol', 'Number');
295
412
  }
296
413
  const layout = this.getLayout(),
297
- colLayout = layout.map(row => row[x]).filter(coords => coords),
414
+ colLayout = layout.map(row => row[x]).filter(Boolean),
298
415
  rows = this.getAllRows();
299
416
  return new Map(
300
417
  colLayout.map(coords => [rows[coords.row].getNthCol(coords.column), layout[coords.row][x - 1] !== coords]),
@@ -302,69 +419,33 @@ class TableToken extends TrToken {
302
419
  }
303
420
 
304
421
  /**
305
- * @param {Map<TdToken, boolean>} cells
306
- * @param {string|Record<string, string|boolean>} attr
307
- * @complexity `n`
308
- */
309
- #format(cells, attr = {}, multi = false) {
310
- for (const [token, start] of cells) {
311
- if (multi || start) {
312
- if (typeof attr === 'string') {
313
- token.setSyntax(attr);
314
- } else {
315
- for (const [k, v] of Object.entries(attr)) {
316
- token.setAttr(k, v);
317
- }
318
- }
319
- }
320
- }
321
- }
322
-
323
- /**
324
- * @param {number} y
325
- * @param {string|Record<string, string|boolean>} attry
422
+ * 设置行格式
423
+ * @param {number} y 行号
424
+ * @param {string|Record<string, string|boolean>} attr 表格属性
425
+ * @param {boolean} multiRow 是否对所有单元格设置,或是仅对行首单元格设置
326
426
  * @complexity `n²`
327
427
  */
328
428
  formatTableRow(y, attr = {}, multiRow = false) {
329
- this.#format(this.getFullRow(y), attr, multiRow);
429
+ format(this.getFullRow(y), attr, multiRow);
330
430
  }
331
431
 
332
432
  /**
333
- * @param {number} x
334
- * @param {string|Record<string, string|boolean>} attry
433
+ * 设置列格式
434
+ * @param {number} x 列号
435
+ * @param {string|Record<string, string|boolean>} attr 表格属性
436
+ * @param {boolean} multiCol 是否对所有单元格设置,或是仅对行首单元格设置
335
437
  * @complexity `n`
336
438
  */
337
439
  formatTableCol(x, attr = {}, multiCol = false) {
338
- this.#format(this.getFullCol(x), attr, multiCol);
440
+ format(this.getFullCol(x), attr, multiCol);
339
441
  }
340
442
 
341
443
  /**
342
- * @param {number} y
343
- * @param {TrToken} rowToken
344
- * @param {TableCoords[][]} layout
345
- * @param {number} maxCol
346
- * @param {Token} token
347
- * @complexity `n`
348
- */
349
- #fill(y, rowToken, layout, maxCol, token) {
350
- const rowLayout = layout[y],
351
- {childNodes} = rowToken;
352
- let lastIndex = childNodes.findLastIndex(child => child instanceof TdToken && child.subtype !== 'caption');
353
- lastIndex = lastIndex === -1 ? undefined : lastIndex - childNodes.length;
354
- Parser.run(() => {
355
- for (let i = 0; i < maxCol; i++) {
356
- if (!rowLayout[i]) {
357
- rowToken.insertAt(token.cloneNode(), lastIndex);
358
- }
359
- }
360
- });
361
- }
362
-
363
- /**
364
- * @param {number} y
365
- * @param {string|Token} inner
366
- * @param {'td'|'th'|'caption'} subtype
367
- * @param {Record<string, string>} attr
444
+ * 填补表格行
445
+ * @param {number} y 行号
446
+ * @param {string|Token} inner 填充内容
447
+ * @param {'td'|'th'|'caption'} subtype 单元格类型
448
+ * @param {Record<string, string>} attr 表格属性
368
449
  * @complexity `n`
369
450
  */
370
451
  fillTableRow(y, inner, subtype = 'td', attr = {}) {
@@ -372,13 +453,14 @@ class TableToken extends TrToken {
372
453
  layout = this.getLayout({y}),
373
454
  maxCol = Math.max(...layout.map(row => row.length)),
374
455
  token = TdToken.create(inner, subtype, attr, this.getAttribute('include'), this.getAttribute('config'));
375
- this.#fill(y, rowToken, layout, maxCol, token);
456
+ fill(y, rowToken, layout, maxCol, token);
376
457
  }
377
458
 
378
459
  /**
379
- * @param {string|Token} inner
380
- * @param {'td'|'th'|'caption'} subtype
381
- * @param {Record<string, string>} attr
460
+ * 填补表格
461
+ * @param {string|Token} inner 填充内容
462
+ * @param {'td'|'th'|'caption'} subtype 单元格类型
463
+ * @param {Record<string, string>} attr 表格属性
382
464
  * @complexity `n`
383
465
  */
384
466
  fillTable(inner, subtype = 'td', attr = {}) {
@@ -386,18 +468,20 @@ class TableToken extends TrToken {
386
468
  layout = this.getLayout(),
387
469
  maxCol = Math.max(...layout.map(({length}) => length)),
388
470
  token = TdToken.create(inner, subtype, attr, this.getAttribute('include'), this.getAttribute('config'));
389
- for (const [y, rowToken] of rowTokens.entries()) {
390
- this.#fill(y, rowToken, layout, maxCol, token);
471
+ for (let y = 0; y < rowTokens.length; y++) {
472
+ fill(y, rowTokens[y], layout, maxCol, token);
391
473
  }
392
474
  }
393
475
 
394
476
  /**
395
- * @param {string|Token} inner
396
- * @param {TableCoords & TableRenderedCoords} coords
397
- * @param {'td'|'th'|'caption'} subtype
398
- * @param {Record<string, string|boolean>} attr
477
+ * @override
478
+ * @param {string|Token} inner 单元格内部wikitext
479
+ * @param {TableCoords & TableRenderedCoords} coords 单元格坐标
480
+ * @param {'td'|'th'|'caption'} subtype 单元格类型
481
+ * @param {Record<string, string|boolean>} attr 单元格属性
399
482
  * @returns {TdToken}
400
483
  * @complexity `n`
484
+ * @throws `RangeError` 指定的坐标不是单元格起始点
401
485
  */
402
486
  insertTableCell(inner, coords, subtype = 'td', attr = {}) {
403
487
  if (coords.column === undefined) {
@@ -408,19 +492,21 @@ class TableToken extends TrToken {
408
492
  }
409
493
  }
410
494
  const rowToken = this.getNthRow(coords.row ?? 0, true);
411
- if (rowToken === this) {
412
- return super.insertTableCell(inner, coords, subtype, attr);
413
- }
414
- return rowToken.insertTableCell(inner, coords, subtype, attr);
495
+ return rowToken === this
496
+ ? super.insertTableCell(inner, coords, subtype, attr)
497
+ : rowToken.insertTableCell(inner, coords, subtype, attr);
415
498
  }
416
499
 
417
- /** @complexity `n` */
500
+ /**
501
+ * 在开头插入一行
502
+ * @complexity `n`
503
+ */
418
504
  #prependTableRow() {
419
505
  const row = Parser.run(() => new TrToken('\n|-', undefined, this.getAttribute('config'))),
420
- {children} = this,
421
- [,, plain] = children,
506
+ {childNodes} = this,
507
+ [,, plain] = childNodes,
422
508
  start = plain?.isPlain() ? 3 : 2,
423
- /** @type {TdToken[]} */ tdChildren = children.slice(start),
509
+ /** @type {TdToken[]} */ tdChildren = childNodes.slice(start),
424
510
  index = tdChildren.findIndex(({type}) => type !== 'td');
425
511
  this.insertAt(row, index === -1 ? -1 : index + start);
426
512
  Parser.run(() => {
@@ -434,11 +520,12 @@ class TableToken extends TrToken {
434
520
  }
435
521
 
436
522
  /**
437
- * @param {number} y
438
- * @param {Record<string, string|boolean>} attr
439
- * @param {string|Token} inner
440
- * @param {'td'|'th'|'caption'} subtype
441
- * @param {Record<string, string|boolean>} attr
523
+ * 插入表格行
524
+ * @param {number} y 行号
525
+ * @param {Record<string, string|boolean>} attr 表格行属性
526
+ * @param {string|Token} inner 内部wikitext
527
+ * @param {'td'|'th'|'caption'} subtype 单元格类型
528
+ * @param {Record<string, string|boolean>} innerAttr 单元格属性
442
529
  * @complexity `n`
443
530
  */
444
531
  insertTableRow(y, attr = {}, inner = undefined, subtype = 'td', innerAttr = {}) {
@@ -479,11 +566,13 @@ class TableToken extends TrToken {
479
566
  }
480
567
 
481
568
  /**
482
- * @param {number} x
483
- * @param {string|Token} inner
484
- * @param {'td'|'th'|'caption'} subtype
485
- * @param {Record<string, string>} attr
569
+ * 插入表格列
570
+ * @param {number} x 列号
571
+ * @param {string|Token} inner 内部wikitext
572
+ * @param {'td'|'th'|'caption'} subtype 单元格类型
573
+ * @param {Record<string, string>} attr 单元格属性
486
574
  * @complexity `n²`
575
+ * @throws `RangeError` 列号过大
487
576
  */
488
577
  insertTableCol(x, inner, subtype = 'td', attr = {}) {
489
578
  if (typeof x !== 'number') {
@@ -511,14 +600,17 @@ class TableToken extends TrToken {
511
600
  }
512
601
 
513
602
  /**
514
- * @param {number} y
603
+ * 移除表格行
604
+ * @param {number} y 行号
515
605
  * @complexity `n²`
516
606
  */
517
607
  removeTableRow(y) {
518
608
  const rows = this.getAllRows(),
519
609
  layout = this.getLayout(),
610
+ rowLayout = layout[y],
520
611
  /** @type {Set<TableCoords>} */ set = new Set();
521
- for (const [x, coords] of [...layout[y].entries()].reverse()) {
612
+ for (let x = rowLayout.length - 1; x >= 0; x--) {
613
+ const coords = rowLayout[x];
522
614
  if (set.has(coords)) {
523
615
  continue;
524
616
  }
@@ -546,16 +638,17 @@ class TableToken extends TrToken {
546
638
  }
547
639
 
548
640
  /**
549
- * @param {number} x
641
+ * 移除表格列
642
+ * @param {number} x 列号
550
643
  * @complexity `n²`
551
644
  */
552
645
  removeTableCol(x) {
553
646
  for (const [token, start] of this.getFullCol(x)) {
554
- const {colspan} = token;
647
+ const {colspan, lastChild} = token;
555
648
  if (colspan > 1) {
556
649
  token.colspan = colspan - 1;
557
650
  if (start) {
558
- token.lastElementChild.replaceChildren();
651
+ lastChild.replaceChildren();
559
652
  }
560
653
  } else {
561
654
  token.remove();
@@ -564,9 +657,11 @@ class TableToken extends TrToken {
564
657
  }
565
658
 
566
659
  /**
567
- * @param {[number, number]} xlim
568
- * @param {[number, number]} ylim
660
+ * 合并单元格
661
+ * @param {[number, number]} xlim 列范围
662
+ * @param {[number, number]} ylim 行范围
569
663
  * @complexity `n²`
664
+ * @throws `RangeError` 待合并区域与外侧区域有重叠
570
665
  */
571
666
  mergeCells(xlim, ylim) {
572
667
  if ([...xlim, ...ylim].some(arg => typeof arg !== 'number')) {
@@ -597,9 +692,11 @@ class TableToken extends TrToken {
597
692
  }
598
693
 
599
694
  /**
600
- * @param {TableCoords & TableRenderedCoords} coords
601
- * @param {Set<'rowspan'|'colspan'>} dirs
695
+ * 分裂单元格
696
+ * @param {TableCoords & TableRenderedCoords} coords 单元格坐标
697
+ * @param {Set<'rowspan'|'colspan'>} dirs 分裂方向
602
698
  * @complexity `n²`
699
+ * @throws `RangeError` 指定的坐标不是单元格起始点
603
700
  */
604
701
  #split(coords, dirs) {
605
702
  const cell = this.getNthCell(coords),
@@ -634,10 +731,10 @@ class TableToken extends TrToken {
634
731
  try {
635
732
  this.insertTableCell('', {x: i, y: j}, subtype, attr);
636
733
  } catch (e) {
637
- if (!(e instanceof RangeError) || !e.message.startsWith('指定的坐标不是单元格起始点:')) {
638
- throw e;
734
+ if (e instanceof RangeError && e.message.startsWith('指定的坐标不是单元格起始点:')) {
735
+ break;
639
736
  }
640
- break;
737
+ throw e;
641
738
  }
642
739
  }
643
740
  }
@@ -645,7 +742,8 @@ class TableToken extends TrToken {
645
742
  }
646
743
 
647
744
  /**
648
- * @param {TableCoords & TableRenderedCoords} coords
745
+ * 分裂成多行
746
+ * @param {TableCoords & TableRenderedCoords} coords 单元格坐标
649
747
  * @complexity `n²`
650
748
  */
651
749
  splitIntoRows(coords) {
@@ -653,7 +751,8 @@ class TableToken extends TrToken {
653
751
  }
654
752
 
655
753
  /**
656
- * @param {TableCoords & TableRenderedCoords} coords
754
+ * 分裂成多列
755
+ * @param {TableCoords & TableRenderedCoords} coords 单元格坐标
657
756
  * @complexity `n²`
658
757
  */
659
758
  splitIntoCols(coords) {
@@ -661,7 +760,8 @@ class TableToken extends TrToken {
661
760
  }
662
761
 
663
762
  /**
664
- * @param {TableCoords & TableRenderedCoords} coords
763
+ * 分裂成单元格
764
+ * @param {TableCoords & TableRenderedCoords} coords 单元格坐标
665
765
  * @complexity `n²`
666
766
  */
667
767
  splitIntoCells(coords) {
@@ -670,7 +770,7 @@ class TableToken extends TrToken {
670
770
 
671
771
  /**
672
772
  * 复制一行并插入该行之前
673
- * @param {number} row
773
+ * @param {number} row 行号
674
774
  * @complexity `n²`
675
775
  */
676
776
  replicateTableRow(row) {
@@ -691,7 +791,7 @@ class TableToken extends TrToken {
691
791
 
692
792
  /**
693
793
  * 复制一列并插入该列之前
694
- * @param {number} x
794
+ * @param {number} x 列号
695
795
  * @complexity `n`
696
796
  */
697
797
  replicateTableCol(x) {
@@ -710,20 +810,23 @@ class TableToken extends TrToken {
710
810
  }
711
811
 
712
812
  /**
713
- * @param {number} y
714
- * @param {number} before
813
+ * 移动表格行
814
+ * @param {number} y 行号
815
+ * @param {number} before 新位置
715
816
  * @complexity `n²`
817
+ * @throws `RangeError` 无法移动
716
818
  */
717
819
  moveTableRowBefore(y, before) {
718
820
  if (typeof y !== 'number' || typeof before !== 'number') {
719
821
  this.typeError('moveTableRowBefore', 'Number');
720
822
  }
721
- const layout = this.getLayout(),
722
- /**
723
- * @type {(i: number) => number[]}
724
- * @complexity `n`
725
- */
726
- occupied = i => layout[i].map(({row}, j) => row === i ? j : null).filter(j => j !== null);
823
+ const layout = this.getLayout();
824
+
825
+ /**
826
+ * @type {(i: number) => number[]}
827
+ * @complexity `n`
828
+ */
829
+ const occupied = i => layout[i].map(({row}, j) => row === i ? j : null).filter(j => j !== null);
727
830
  try {
728
831
  assert.deepStrictEqual(occupied(y), occupied(before));
729
832
  } catch (e) {
@@ -747,9 +850,11 @@ class TableToken extends TrToken {
747
850
  }
748
851
 
749
852
  /**
750
- * @param {number} y
751
- * @param {number} after
853
+ * 移动表格行
854
+ * @param {number} y 行号
855
+ * @param {number} after 新位置
752
856
  * @complexity `n²`
857
+ * @throws `RangeError` 无法移动
753
858
  */
754
859
  moveTableRowAfter(y, after) {
755
860
  if (typeof y !== 'number' || typeof after !== 'number') {
@@ -758,14 +863,15 @@ class TableToken extends TrToken {
758
863
  const layout = this.getLayout(),
759
864
  afterToken = this.getNthRow(after),
760
865
  /** @type {TdToken[]} */
761
- cells = afterToken.children.filter(child => child instanceof TdToken && child.subtype !== 'caption'),
762
- /**
763
- * @type {(i: number, oneRow?: boolean) => number[]}
764
- * @complexity `n`
765
- */
766
- occupied = (i, oneRow = false) => layout[i].map(({row, column}, j) =>
767
- row === i && (!oneRow || cells[column].rowspan === 1) ? j : null,
768
- ).filter(j => j !== null);
866
+ cells = afterToken.childNodes.filter(child => child instanceof TdToken && child.subtype !== 'caption');
867
+
868
+ /**
869
+ * @type {(i: number, oneRow?: boolean) => number[]}
870
+ * @complexity `n`
871
+ */
872
+ const occupied = (i, oneRow = false) => layout[i].map(
873
+ ({row, column}, j) => row === i && (!oneRow || cells[column].rowspan === 1) ? j : null,
874
+ ).filter(j => j !== null);
769
875
  try {
770
876
  assert.deepStrictEqual(occupied(y), occupied(after, true));
771
877
  } catch (e) {
@@ -796,30 +902,29 @@ class TableToken extends TrToken {
796
902
  }
797
903
 
798
904
  /**
799
- * @param {number} x
800
- * @param {number} reference
905
+ * 移动表格列
906
+ * @param {number} x 列号
907
+ * @param {number} reference 新位置
908
+ * @param {boolean} after 在新位置之后或之前
801
909
  * @complexity `n`
910
+ * @throws `RangeError` 无法移动
802
911
  */
803
912
  #moveCol(x, reference, after = false) {
804
913
  if (typeof x !== 'number' || typeof reference !== 'number') {
805
914
  this.typeError(`moveTableCol${after ? 'After' : 'Before'}`, 'Number');
806
915
  }
807
- const layout = this.getLayout(),
808
- /** @type {(rowLayout: TableCoords[], i: number, oneCol?: boolean) => boolean} */
809
- isStart = (rowLayout, i, oneCol = false) => {
810
- const coords = rowLayout[i];
811
- return rowLayout[i - 1] !== coords && (!oneCol || rowLayout[i + 1] !== coords);
812
- };
813
- if (layout.some(rowLayout => isStart(rowLayout, x) !== isStart(rowLayout, reference, after))) {
916
+ const layout = this.getLayout();
917
+ if (layout.some(rowLayout => isStartCol(rowLayout, x) !== isStartCol(rowLayout, reference, after))) {
814
918
  throw new RangeError(`第 ${x} 列与第 ${reference} 列的构造不同,无法移动!`);
815
919
  }
816
920
  const /** @type {Set<TableCoords>} */ setX = new Set(),
817
921
  /** @type {Set<TableCoords>} */ setRef = new Set(),
818
922
  rows = this.getAllRows();
819
- for (const [i, rowLayout] of layout.entries()) {
820
- const coords = rowLayout[x],
923
+ for (let i = 0; i < layout.length; i++) {
924
+ const rowLayout = layout[i],
925
+ coords = rowLayout[x],
821
926
  refCoords = rowLayout[reference],
822
- start = isStart(rowLayout, x);
927
+ start = isStartCol(rowLayout, x);
823
928
  if (refCoords && !start && !setRef.has(refCoords)) {
824
929
  setRef.add(refCoords);
825
930
  rows[refCoords.row].getNthCol(refCoords.column).colspan++;
@@ -834,15 +939,16 @@ class TableToken extends TrToken {
834
939
  if (start) {
835
940
  const original = token;
836
941
  token = token.cloneNode();
837
- original.lastElementChild.replaceChildren();
942
+ original.lastChild.replaceChildren();
838
943
  token.colspan = 1;
839
944
  }
840
945
  }
841
946
  if (start) {
842
947
  const col = rowLayout.slice(reference + Number(after)).find(({row}) => row === i)?.column;
843
- rowToken.insertBefore(token, col === undefined && rowToken.type === 'table'
844
- ? rowToken.children.slice(2).find(isRowEnd)
845
- : col !== undefined && rowToken.getNthCol(col),
948
+ rowToken.insertBefore(
949
+ token, col === undefined && rowToken.type === 'table'
950
+ ? rowToken.childNodes.slice(2).find(isRowEnd)
951
+ : col !== undefined && rowToken.getNthCol(col),
846
952
  );
847
953
  }
848
954
  }
@@ -850,8 +956,9 @@ class TableToken extends TrToken {
850
956
  }
851
957
 
852
958
  /**
853
- * @param {number} x
854
- * @param {number} before
959
+ * 移动表格列
960
+ * @param {number} x 列号
961
+ * @param {number} before 新位置
855
962
  * @complexity `n`
856
963
  */
857
964
  moveTableColBefore(x, before) {
@@ -859,8 +966,9 @@ class TableToken extends TrToken {
859
966
  }
860
967
 
861
968
  /**
862
- * @param {number} x
863
- * @param {number} after
969
+ * 移动表格列
970
+ * @param {number} x 列号
971
+ * @param {number} after 新位置
864
972
  * @complexity `n`
865
973
  */
866
974
  moveTableColAfter(x, after) {