wikiparser-node 0.10.0 → 0.11.0-b

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