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