wikiparser-node 0.4.0 → 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 (65) hide show
  1. package/index.js +25 -2
  2. package/lib/element.js +69 -185
  3. package/lib/node.js +159 -1
  4. package/lib/ranges.js +1 -2
  5. package/lib/text.js +35 -6
  6. package/lib/title.js +1 -1
  7. package/mixin/fixedToken.js +4 -4
  8. package/mixin/sol.js +17 -7
  9. package/package.json +11 -1
  10. package/parser/commentAndExt.js +1 -1
  11. package/parser/converter.js +1 -1
  12. package/parser/externalLinks.js +1 -1
  13. package/parser/hrAndDoubleUnderscore.js +6 -5
  14. package/parser/links.js +1 -2
  15. package/parser/magicLinks.js +1 -1
  16. package/parser/selector.js +5 -5
  17. package/parser/table.js +12 -12
  18. package/src/arg.js +44 -20
  19. package/src/attribute.js +34 -7
  20. package/src/converter.js +13 -5
  21. package/src/converterFlags.js +42 -5
  22. package/src/converterRule.js +25 -19
  23. package/src/extLink.js +20 -14
  24. package/src/gallery.js +35 -4
  25. package/src/heading.js +28 -9
  26. package/src/html.js +46 -18
  27. package/src/imageParameter.js +13 -7
  28. package/src/index.js +22 -15
  29. package/src/link/category.js +6 -6
  30. package/src/link/file.js +25 -5
  31. package/src/link/index.js +36 -33
  32. package/src/magicLink.js +32 -4
  33. package/src/nowiki/comment.js +14 -0
  34. package/src/nowiki/doubleUnderscore.js +5 -0
  35. package/src/nowiki/quote.js +28 -1
  36. package/src/onlyinclude.js +5 -0
  37. package/src/parameter.js +48 -35
  38. package/src/table/index.js +37 -24
  39. package/src/table/td.js +23 -17
  40. package/src/table/tr.js +47 -30
  41. package/src/tagPair/ext.js +4 -5
  42. package/src/tagPair/include.js +10 -0
  43. package/src/tagPair/index.js +8 -0
  44. package/src/transclude.js +79 -46
  45. package/tool/index.js +1 -1
  46. package/{test/util.js → util/diff.js} +14 -18
  47. package/util/lint.js +40 -0
  48. package/util/string.js +20 -3
  49. package/.eslintrc.json +0 -714
  50. package/errors/README +0 -1
  51. package/jsconfig.json +0 -7
  52. package/printed/README +0 -1
  53. package/printed/example.json +0 -120
  54. package/test/api.js +0 -83
  55. package/test/real.js +0 -133
  56. package/test/test.js +0 -28
  57. package/typings/api.d.ts +0 -13
  58. package/typings/array.d.ts +0 -28
  59. package/typings/event.d.ts +0 -24
  60. package/typings/index.d.ts +0 -94
  61. package/typings/node.d.ts +0 -29
  62. package/typings/parser.d.ts +0 -16
  63. package/typings/table.d.ts +0 -14
  64. package/typings/token.d.ts +0 -22
  65. package/typings/tool.d.ts +0 -11
@@ -15,7 +15,7 @@ class ConverterRuleToken extends Token {
15
15
 
16
16
  /** 语言变体 */
17
17
  get variant() {
18
- return this.children.at(-2)?.text()?.trim() ?? '';
18
+ return this.childNodes.at(-2)?.text()?.trim() ?? '';
19
19
  }
20
20
 
21
21
  set variant(variant) {
@@ -72,7 +72,7 @@ class ConverterRuleToken extends Token {
72
72
  placeholder = placeholders[cloned.length - 1],
73
73
  token = Parser.run(() => new ConverterRuleToken(placeholder, placeholder, this.getAttribute('config')));
74
74
  for (let i = 0; i < cloned.length; i++) {
75
- token.children[i].safeReplaceWith(cloned[i]);
75
+ token.childNodes[i].safeReplaceWith(cloned[i]);
76
76
  }
77
77
  token.afterBuild();
78
78
  return token;
@@ -128,7 +128,7 @@ class ConverterRuleToken extends Token {
128
128
  */
129
129
  toString(selector) {
130
130
  if (this.childNodes.length === 3 && !(selector && this.matches(selector))) {
131
- const {children: [from, variant, to]} = this;
131
+ const {childNodes: [from, variant, to]} = this;
132
132
  return `${from.toString(selector)}=>${variant.toString(selector)}:${to.toString(selector)}`;
133
133
  }
134
134
  return super.toString(selector, ':');
@@ -144,13 +144,22 @@ class ConverterRuleToken extends Token {
144
144
  return i === 0 && length === 3 ? 2 : 1;
145
145
  }
146
146
 
147
+ /** @override */
148
+ print() {
149
+ if (this.childNodes.length === 3) {
150
+ const {childNodes: [from, variant, to]} = this;
151
+ return `<span class="wpb-converter-rule">${from.print()}=>${variant.print()}:${to.print()}</span>`;
152
+ }
153
+ return super.print({sep: ':'});
154
+ }
155
+
147
156
  /**
148
157
  * @override
149
158
  * @returns {string}
150
159
  */
151
160
  text() {
152
161
  if (this.childNodes.length === 3) {
153
- const {children: [from, variant, to]} = this;
162
+ const {childNodes: [from, variant, to]} = this;
154
163
  return `${from.text()}=>${variant.text()}:${to.text()}`;
155
164
  }
156
165
  return super.text(':');
@@ -174,16 +183,14 @@ class ConverterRuleToken extends Token {
174
183
  to = String(to);
175
184
  const config = this.getAttribute('config'),
176
185
  root = Parser.parse(`-{|${config.variants[0]}:${to}}-`, this.getAttribute('include'), undefined, config),
177
- {childNodes: {length}, firstElementChild} = root;
178
- if (length !== 1 || firstElementChild.type !== 'converter' || firstElementChild.childNodes.length !== 2
179
- || firstElementChild.lastElementChild.childNodes.length !== 2
180
- ) {
186
+ {childNodes: {length}, firstChild: converter} = root,
187
+ {lastChild: converterRule, type, childNodes: {length: converterLength}} = converter;
188
+ if (length !== 1 || type !== 'converter' || converterLength !== 2 || converterRule.childNodes.length !== 2) {
181
189
  throw new SyntaxError(`非法的转换目标:${noWrap(to)}`);
182
190
  }
183
- const {lastElementChild} = firstElementChild,
184
- {lastChild} = lastElementChild;
185
- lastElementChild.destroy(true);
186
- this.lastElementChild.safeReplaceWith(lastChild);
191
+ const {lastChild} = converterRule;
192
+ converterRule.destroy(true);
193
+ this.lastChild.safeReplaceWith(lastChild);
187
194
  }
188
195
 
189
196
  /**
@@ -202,7 +209,7 @@ class ConverterRuleToken extends Token {
202
209
  } else if (this.childNodes.length === 1) {
203
210
  super.insertAt(Parser.run(() => new AtomToken(variant, 'converter-rule-variant', config)), 0);
204
211
  } else {
205
- this.children.at(-2).setText(variant);
212
+ this.childNodes.at(-2).setText(variant);
206
213
  }
207
214
  }
208
215
 
@@ -220,16 +227,15 @@ class ConverterRuleToken extends Token {
220
227
  from = String(from);
221
228
  const config = this.getAttribute('config'),
222
229
  root = Parser.parse(`-{|${from}=>${variant}:}-`, this.getAttribute('include'), undefined, config),
223
- {childNodes: {length}, firstElementChild} = root;
224
- if (length !== 1 || firstElementChild.type !== 'converter' || firstElementChild.childNodes.length !== 2
225
- || firstElementChild.lastElementChild.childNodes.length !== 3
226
- ) {
230
+ {childNodes: {length}, firstChild: converter} = root,
231
+ {type, childNodes: {length: converterLength}, lastChild: converterRule} = converter;
232
+ if (length !== 1 || type !== 'converter' || converterLength !== 2 || converterRule.childNodes.length !== 3) {
227
233
  throw new SyntaxError(`非法的转换原文:${noWrap(from)}`);
228
234
  }
229
235
  if (unidirectional) {
230
- this.firstElementChild.safeReplaceWith(firstElementChild.lastElementChild.firstChild);
236
+ this.firstChild.safeReplaceWith(converterRule.firstChild);
231
237
  } else {
232
- super.insertAt(firstElementChild.lastElementChild.firstChild, 0);
238
+ super.insertAt(converterRule.firstChild, 0);
233
239
  }
234
240
  }
235
241
 
package/src/extLink.js CHANGED
@@ -41,7 +41,7 @@ class ExtLinkToken extends Token {
41
41
  /** 链接显示文字 */
42
42
  get innerText() {
43
43
  return this.childNodes.length > 1
44
- ? this.lastElementChild.text()
44
+ ? this.lastChild.text()
45
45
  : `[${this.getRootNode().querySelectorAll('ext-link[childElementCount=1]').indexOf(this) + 1}]`;
46
46
  }
47
47
 
@@ -67,7 +67,7 @@ class ExtLinkToken extends Token {
67
67
  cloneNode() {
68
68
  const [url, text] = this.cloneChildNodes(),
69
69
  token = Parser.run(() => new ExtLinkToken(undefined, '', '', this.getAttribute('config')));
70
- token.firstElementChild.safeReplaceWith(url);
70
+ token.firstChild.safeReplaceWith(url);
71
71
  if (text) {
72
72
  token.appendChild(text);
73
73
  }
@@ -78,7 +78,7 @@ class ExtLinkToken extends Token {
78
78
  #correct() {
79
79
  if (!this.#space && this.childNodes.length > 1
80
80
  // 都替换成`<`肯定不对,但无妨
81
- && /^[^[\]<>"{\0-\x1F\x7F\p{Zs}\uFFFD]/u.test(this.lastElementChild.text().replace(/&[lg]t;/u, '<'))
81
+ && /^[^[\]<>"{\0-\x1F\x7F\p{Zs}\uFFFD]/u.test(this.lastChild.text().replace(/&[lg]t;/u, '<'))
82
82
  ) {
83
83
  this.#space = ' ';
84
84
  }
@@ -95,7 +95,7 @@ class ExtLinkToken extends Token {
95
95
  return `[${super.toString(selector)}${this.#space}]`;
96
96
  }
97
97
  this.#correct();
98
- normalizeSpace(this.lastElementChild);
98
+ normalizeSpace(this.lastChild);
99
99
  return `[${super.toString(selector, this.#space)}]`;
100
100
  }
101
101
 
@@ -110,9 +110,15 @@ class ExtLinkToken extends Token {
110
110
  return this.#space.length;
111
111
  }
112
112
 
113
+ /** @override */
114
+ print() {
115
+ const {childNodes: {length}} = this;
116
+ return super.print(length > 1 ? {pre: '[', sep: this.#space, post: ']'} : {pre: '[', post: `${this.#space}]`});
117
+ }
118
+
113
119
  /** @override */
114
120
  text() {
115
- normalizeSpace(this.children[1]);
121
+ normalizeSpace(this.childNodes[1]);
116
122
  return `[${super.text(' ')}]`;
117
123
  }
118
124
 
@@ -132,13 +138,13 @@ class ExtLinkToken extends Token {
132
138
  setTarget(url) {
133
139
  url = String(url);
134
140
  const root = Parser.parse(`[${url}]`, this.getAttribute('include'), 8, this.getAttribute('config')),
135
- {childNodes: {length}, firstElementChild} = root;
136
- if (length !== 1 || firstElementChild?.type !== 'ext-link' || firstElementChild.childNodes.length !== 1) {
141
+ {childNodes: {length}, firstChild: extLink} = root;
142
+ if (length !== 1 || extLink.type !== 'ext-link' || extLink.childNodes.length !== 1) {
137
143
  throw new SyntaxError(`非法的外链目标:${url}`);
138
144
  }
139
- const {firstChild} = firstElementChild;
140
- firstElementChild.destroy(true);
141
- this.firstElementChild.safeReplaceWith(firstChild);
145
+ const {firstChild} = extLink;
146
+ extLink.destroy(true);
147
+ this.firstChild.safeReplaceWith(firstChild);
142
148
  }
143
149
 
144
150
  /**
@@ -149,15 +155,15 @@ class ExtLinkToken extends Token {
149
155
  setLinkText(text) {
150
156
  text = String(text);
151
157
  const root = Parser.parse(`[//url ${text}]`, this.getAttribute('include'), 8, this.getAttribute('config')),
152
- {childNodes: {length}, firstElementChild} = root;
153
- if (length !== 1 || firstElementChild?.type !== 'ext-link' || firstElementChild.childNodes.length !== 2) {
158
+ {childNodes: {length}, firstChild: extLink} = root;
159
+ if (length !== 1 || extLink.type !== 'ext-link' || extLink.childNodes.length !== 2) {
154
160
  throw new SyntaxError(`非法的外链文字:${noWrap(text)}`);
155
161
  }
156
- const {lastChild} = firstElementChild;
162
+ const {lastChild} = extLink;
157
163
  if (this.childNodes.length === 1) {
158
164
  this.appendChild(lastChild);
159
165
  } else {
160
- this.lastElementChild.safeReplaceWith(lastChild);
166
+ this.lastChild.safeReplaceWith(lastChild);
161
167
  }
162
168
  this.#space ||= ' ';
163
169
  }
package/src/gallery.js CHANGED
@@ -61,6 +61,11 @@ class GalleryToken extends Token {
61
61
  return 1;
62
62
  }
63
63
 
64
+ /** @override */
65
+ print() {
66
+ return super.print({sep: '\n'});
67
+ }
68
+
64
69
  /** @override */
65
70
  text() {
66
71
  return super.text('\n').replaceAll(/\n\s*\n/gu, '\n');
@@ -79,11 +84,37 @@ class GalleryToken extends Token {
79
84
  } catch {
80
85
  title = this.normalizeTitle(file, 6, true);
81
86
  }
82
- if (!title.valid) {
83
- throw new SyntaxError(`非法的文件名:${file}`);
87
+ if (title.valid) {
88
+ const token = Parser.run(() => new GalleryImageToken(file, undefined, title, this.getAttribute('config')));
89
+ return this.insertAt(token, i);
90
+ }
91
+ throw new SyntaxError(`非法的文件名:${file}`);
92
+ }
93
+
94
+ /**
95
+ * @override
96
+ * @param {number} start 起始位置
97
+ */
98
+ lint(start = 0) {
99
+ const {top, left} = this.getRootNode().posFromIndex(start),
100
+ /** @type {LintError[]} */ errors = [];
101
+ for (let i = 0, cur = start; i < this.childNodes.length; i++) {
102
+ const child = this.childNodes[i],
103
+ str = String(child);
104
+ if (child.type === 'hidden' && str.trim() && !/^<!--.*-->$/u.test(str)) {
105
+ errors.push({
106
+ message: '图库中的无效内容',
107
+ startLine: top + i,
108
+ endLine: top + i,
109
+ startCol: i ? 0 : left,
110
+ endCol: i ? str.length : left + str.length,
111
+ });
112
+ } else if (child.type !== 'hidden' && child.type !== 'text') {
113
+ errors.push(...child.lint(cur));
114
+ }
115
+ cur += str.length + 1;
84
116
  }
85
- const token = Parser.run(() => new GalleryImageToken(file, undefined, title, this.getAttribute('config')));
86
- return this.insertAt(token, i);
117
+ return errors;
87
118
  }
88
119
  }
89
120
 
package/src/heading.js CHANGED
@@ -1,6 +1,7 @@
1
1
  'use strict';
2
2
 
3
- const fixedToken = require('../mixin/fixedToken'),
3
+ const {generateForSelf} = require('../util/lint'),
4
+ fixedToken = require('../mixin/fixedToken'),
4
5
  sol = require('../mixin/sol'),
5
6
  Parser = require('..'),
6
7
  Token = require('.');
@@ -14,7 +15,7 @@ class HeadingToken extends fixedToken(sol(Token)) {
14
15
 
15
16
  /** 内部wikitext */
16
17
  get innerText() {
17
- return this.firstElementChild.text();
18
+ return this.firstChild.text();
18
19
  }
19
20
 
20
21
  /**
@@ -39,8 +40,8 @@ class HeadingToken extends fixedToken(sol(Token)) {
39
40
  cloneNode() {
40
41
  const [title, trail] = this.cloneChildNodes(),
41
42
  token = Parser.run(() => new HeadingToken(Number(this.name), [], this.getAttribute('config')));
42
- token.firstElementChild.safeReplaceWith(title);
43
- token.lastElementChild.safeReplaceWith(trail);
43
+ token.firsthild.safeReplaceWith(title);
44
+ token.lastChild.safeReplaceWith(trail);
44
45
  return token;
45
46
  }
46
47
 
@@ -54,8 +55,8 @@ class HeadingToken extends fixedToken(sol(Token)) {
54
55
  return selector && this.matches(selector)
55
56
  ? ''
56
57
  : `${this.prependNewLine()}${equals}${
57
- this.firstElementChild.toString(selector)
58
- }${equals}${this.lastElementChild.toString(selector)}${this.appendNewLine()}`;
58
+ this.firstChild.toString(selector)
59
+ }${equals}${this.lastChild.toString(selector)}${this.appendNewLine()}`;
59
60
  }
60
61
 
61
62
  /** @override */
@@ -68,6 +69,24 @@ class HeadingToken extends fixedToken(sol(Token)) {
68
69
  return Number(this.name);
69
70
  }
70
71
 
72
+ /** @override */
73
+ print() {
74
+ const equals = '='.repeat(Number(this.name));
75
+ return super.print({pre: equals, sep: equals});
76
+ }
77
+
78
+ /**
79
+ * @override
80
+ * @param {number} start 起始位置
81
+ */
82
+ lint(start = 0) {
83
+ const errors = super.lint(start);
84
+ if (this.name === '1') {
85
+ errors.push(generateForSelf(this, this.getRootNode().posFromIndex(start), '<h1>'));
86
+ }
87
+ return errors;
88
+ }
89
+
71
90
  /**
72
91
  * @override
73
92
  * @this {HeadingToken & {prependNewLine(): ''|'\n', appendNewLine(): ''|'\n'}}
@@ -75,7 +94,7 @@ class HeadingToken extends fixedToken(sol(Token)) {
75
94
  */
76
95
  text() {
77
96
  const equals = '='.repeat(Number(this.name));
78
- return `${this.prependNewLine()}${equals}${this.firstElementChild.text()}${equals}${this.appendNewLine()}`;
97
+ return `${this.prependNewLine()}${equals}${this.firstChild.text()}${equals}${this.appendNewLine()}`;
79
98
  }
80
99
 
81
100
  /**
@@ -87,12 +106,12 @@ class HeadingToken extends fixedToken(sol(Token)) {
87
106
  this.typeError('setLevel', 'Number');
88
107
  }
89
108
  n = Math.min(Math.max(n, 1), 6);
90
- this.setAttribute('name', String(n)).firstElementChild.setAttribute('name', this.name);
109
+ this.setAttribute('name', String(n)).firstChild.setAttribute('name', this.name);
91
110
  }
92
111
 
93
112
  /** 移除标题后的不可见内容 */
94
113
  removeTrail() {
95
- this.lastElementChild.replaceChildren();
114
+ this.lastChild.replaceChildren();
96
115
  }
97
116
  }
98
117
 
package/src/html.js CHANGED
@@ -1,6 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  const {noWrap} = require('../util/string'),
4
+ {generateForSelf} = require('../util/lint'),
4
5
  fixedToken = require('../mixin/fixedToken'),
5
6
  attributeParent = require('../mixin/attributeParent'),
6
7
  Parser = require('..'),
@@ -104,6 +105,34 @@ class HtmlToken extends attributeParent(fixedToken(Token)) {
104
105
  return this.#tag.length + (this.#closing ? 2 : 1);
105
106
  }
106
107
 
108
+ /** @override */
109
+ print() {
110
+ return super.print({
111
+ pre: `&lt;${this.#closing ? '/' : ''}${this.#tag}`, post: `${this.#selfClosing ? '/' : ''}&gt;`,
112
+ });
113
+ }
114
+
115
+ /**
116
+ * @override
117
+ * @param {number} start 起始位置
118
+ */
119
+ lint(start = 0) {
120
+ const errors = super.lint(start);
121
+ let /** @type {LintError} */ refError;
122
+ if (this.name === 'h1' && !this.#closing) {
123
+ refError = generateForSelf(this, this.getRootNode().posFromIndex(start), '<h1>');
124
+ errors.push(refError);
125
+ }
126
+ try {
127
+ this.findMatchingTag();
128
+ } catch ({message: errorMsg}) {
129
+ const [message] = errorMsg.split(':');
130
+ refError ||= generateForSelf(this, this.getRootNode().posFromIndex(start), '');
131
+ errors.push({...refError, message, severity: message === '未闭合的标签' ? 'warning' : 'error'});
132
+ }
133
+ return errors;
134
+ }
135
+
107
136
  /** @override */
108
137
  text() {
109
138
  return `<${this.#closing ? '/' : ''}${this.#tag}${super.text()}${this.#selfClosing ? '/' : ''}>`;
@@ -131,24 +160,23 @@ class HtmlToken extends attributeParent(fixedToken(Token)) {
131
160
  */
132
161
  findMatchingTag() {
133
162
  const {html} = this.getAttribute('config'),
134
- {name, parentNode, closing, selfClosing} = this,
163
+ {name: tagName, parentNode} = this,
135
164
  string = noWrap(String(this));
136
- if (closing && selfClosing) {
165
+ if (this.#closing && this.#selfClosing) {
137
166
  throw new SyntaxError(`同时闭合和自封闭的标签:${string}`);
138
- } else if (html[2].includes(name) || selfClosing && html[1].includes(name)) { // 自封闭标签
167
+ } else if (html[2].includes(tagName) || this.#selfClosing && html[1].includes(tagName)) { // 自封闭标签
139
168
  return this;
140
- } else if (selfClosing && html[0].includes(name)) {
169
+ } else if (this.#selfClosing && html[0].includes(tagName)) {
141
170
  throw new SyntaxError(`无效自封闭标签:${string}`);
142
171
  } else if (!parentNode) {
143
172
  return undefined;
144
173
  }
145
- const {children} = parentNode,
146
- i = children.indexOf(this),
147
- selector = `html#${name}`,
148
- siblings = closing
149
- ? children.slice(0, i).reverse().filter(child => child.matches(selector))
150
- : children.slice(i + 1).filter(child => child.matches(selector));
151
- let imbalance = closing ? -1 : 1;
174
+ const {childNodes} = parentNode,
175
+ i = childNodes.indexOf(this),
176
+ siblings = this.#closing
177
+ ? childNodes.slice(0, i).reverse().filter(({type, name}) => type === 'html' && name === tagName)
178
+ : childNodes.slice(i + 1).filter(({type, name}) => type === 'html' && name === tagName);
179
+ let imbalance = this.#closing ? -1 : 1;
152
180
  for (const token of siblings) {
153
181
  if (token.closing) {
154
182
  imbalance--;
@@ -159,7 +187,7 @@ class HtmlToken extends attributeParent(fixedToken(Token)) {
159
187
  return token;
160
188
  }
161
189
  }
162
- throw new SyntaxError(`未${closing ? '匹配的闭合' : '闭合的'}标签:${string}`);
190
+ throw new SyntaxError(`未${this.#closing ? '匹配的闭合' : '闭合的'}标签:${string}`);
163
191
  }
164
192
 
165
193
  /** 局部闭合 */
@@ -176,17 +204,17 @@ class HtmlToken extends attributeParent(fixedToken(Token)) {
176
204
  */
177
205
  fix() {
178
206
  const config = this.getAttribute('config'),
179
- {parentNode, name, firstElementChild} = this;
180
- if (!parentNode || !this.#selfClosing || !config.html[0].includes(name)) {
207
+ {parentNode, name: tagName, firstChild} = this;
208
+ if (!parentNode || !this.#selfClosing || !config.html[0].includes(tagName)) {
181
209
  return;
182
- } else if (firstElementChild.text().trim()) {
210
+ } else if (firstChild.text().trim()) {
183
211
  this.#localMatch();
184
212
  return;
185
213
  }
186
- const {children} = parentNode,
187
- i = children.indexOf(this),
214
+ const {childNodes} = parentNode,
215
+ i = childNodes.indexOf(this),
188
216
  /** @type {HtmlToken[]} */
189
- prevSiblings = children.slice(0, i).filter(child => child.matches(`html#${name}`)),
217
+ prevSiblings = childNodes.slice(0, i).filter(({type, name}) => type === 'html' && name === tagName),
190
218
  imbalance = prevSiblings.reduce((acc, {closing}) => acc + (closing ? 1 : -1), 0);
191
219
  if (imbalance < 0) {
192
220
  this.#selfClosing = false;
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- const {text, noWrap, extUrlChar} = require('../util/string'),
3
+ const {text, noWrap, print, extUrlChar} = require('../util/string'),
4
4
  Parser = require('..'),
5
5
  Token = require('.'),
6
6
  AstText = require('../lib/text');
@@ -89,7 +89,6 @@ class ImageParameterToken extends Token {
89
89
  }
90
90
  str.splitText(str.data.indexOf('x'));
91
91
  str.nextSibling.splitText(1);
92
- // eslint-disable-next-line unicorn/consistent-destructuring
93
92
  return {width: text(token.childNodes.slice(0, i + 1)), height: text(token.childNodes.slice(i + 2))};
94
93
  }
95
94
  return undefined;
@@ -191,6 +190,13 @@ class ImageParameterToken extends Token {
191
190
  return Math.max(0, this.#syntax.indexOf('$1'));
192
191
  }
193
192
 
193
+ /** @override */
194
+ print() {
195
+ return this.#syntax
196
+ ? `<span class="wpb-image-parameter">${this.#syntax.replace('$1', print(this.childNodes))}</span>`
197
+ : super.print({class: 'image-caption'});
198
+ }
199
+
194
200
  /** @override */
195
201
  text() {
196
202
  return this.#syntax ? this.#syntax.replace('$1', super.text()).trim() : super.text().trim();
@@ -239,14 +245,14 @@ class ImageParameterToken extends Token {
239
245
  const root = Parser.parse(`[[File:F|${
240
246
  this.#syntax ? this.#syntax.replace('$1', value) : value
241
247
  }]]`, this.getAttribute('include'), 6, this.getAttribute('config')),
242
- {childNodes: {length}, firstElementChild} = root,
243
- param = firstElementChild?.lastElementChild;
244
- if (length !== 1 || !firstElementChild?.matches('file#File\\:F')
245
- || firstElementChild.childNodes.length !== 2 || param.name !== this.name
248
+ {childNodes: {length}, firstChild: file} = root,
249
+ {lastChild: imageParameter, type, name, childNodes: {length: fileLength}} = file;
250
+ if (length !== 1 || type !== 'file' || name !== 'File:F' || fileLength !== 2
251
+ || imageParameter.name !== this.name
246
252
  ) {
247
253
  throw new SyntaxError(`非法的 ${this.name} 参数:${noWrap(value)}`);
248
254
  }
249
- this.replaceChildren(...param.childNodes);
255
+ this.replaceChildren(...imageParameter.childNodes);
250
256
  }
251
257
  }
252
258
 
package/src/index.js CHANGED
@@ -3,7 +3,7 @@
3
3
  /*
4
4
  * PHP解析器的步骤:
5
5
  * -1. 替换签名和`{{subst:}}`,参见Parser::preSaveTransform;这在revision中不可能保留,可以跳过
6
- * 0. 移除特定字符`\0`和`\x7f`,参见Parser::parse
6
+ * 0. 移除特定字符`\0`和`\x7F`,参见Parser::parse
7
7
  * 1. 注释/扩展标签('<'相关),参见Preprocessor_Hash::buildDomTreeArrayFromText和Sanitizer::decodeTagAttributes
8
8
  * 2. 模板/模板变量/标题,注意rightmost法则,以及`-{`和`[[`可以破坏`{{`或`{{{`语法,
9
9
  * 参见Preprocessor_Hash::buildDomTreeArrayFromText
@@ -19,7 +19,7 @@
19
19
  */
20
20
 
21
21
  /*
22
- * \0\d+.\x7f标记Token:
22
+ * \0\d+.\x7F标记Token:
23
23
  * e: ExtToken
24
24
  * c: CommentToken、NoIncludeToken和IncludeToken
25
25
  * !: `{{!}}`专用
@@ -58,7 +58,7 @@ class Token extends AstElement {
58
58
  type = 'root';
59
59
  #stage = 0; // 解析阶段,参见顶部注释。只对plain Token有意义。
60
60
  #config;
61
- // 这个数组起两个作用:1. 数组中的Token会在build时替换`/\0\d+.\x7f/`标记;2. 数组中的Token会依次执行parseOnce和build方法。
61
+ // 这个数组起两个作用:1. 数组中的Token会在build时替换`/\0\d+.\x7F/`标记;2. 数组中的Token会依次执行parseOnce和build方法。
62
62
  #accum;
63
63
  /** @type {Record<string, Ranges>} */ #acceptable;
64
64
  #protectedChildren = new Ranges();
@@ -167,7 +167,7 @@ class Token extends AstElement {
167
167
  constructor(wikitext, config = Parser.getConfig(), halfParsed = false, accum = [], acceptable = null) {
168
168
  super();
169
169
  if (typeof wikitext === 'string') {
170
- this.appendChild(halfParsed ? wikitext : wikitext.replaceAll('\0', '').replaceAll('\x7F', ''));
170
+ this.appendChild(halfParsed ? wikitext : wikitext.replaceAll(/[\0\x7F]/gu, ''));
171
171
  }
172
172
  this.#config = config;
173
173
  this.#accum = accum;
@@ -521,27 +521,28 @@ class Token extends AstElement {
521
521
  if (!parentNode) {
522
522
  return undefined;
523
523
  }
524
- const {children} = parentNode,
525
- index = children.indexOf(this);
524
+ const {childNodes} = parentNode,
525
+ index = childNodes.indexOf(this);
526
526
  let i;
527
527
  for (i = index - 1; i >= 0; i--) {
528
- if (children[i].matches(`html${tag && '#'}${tag ?? ''}[selfClosing=false][closing=false]`)) {
528
+ const {type, name, selfClosing, closing} = childNodes[i];
529
+ if (type === 'html' && (!tag || name === tag) && selfClosing === false && closing === false) {
529
530
  break;
530
531
  }
531
532
  }
532
533
  if (i === -1) {
533
534
  return parentNode.findEnclosingHtml(tag);
534
535
  }
535
- const opening = children[i],
536
- {name} = opening;
537
- for (i = index + 1; i < children.length; i++) {
538
- if (children[i].matches(`html#${name}[selfClosing=false][closing=true]`)) {
536
+ const opening = childNodes[i];
537
+ for (i = index + 1; i < childNodes.length; i++) {
538
+ const {type, name, selfClosing, closing} = childNodes[i];
539
+ if (type === 'html' && name === opening.name && selfClosing === false && closing === true) {
539
540
  break;
540
541
  }
541
542
  }
542
- return i === children.length
543
+ return i === childNodes.length
543
544
  ? parentNode.findEnclosingHtml(tag)
544
- : [opening, children[i]];
545
+ : [opening, childNodes[i]];
545
546
  }
546
547
 
547
548
  /**
@@ -654,7 +655,9 @@ class Token extends AstElement {
654
655
  /** 解析花括号 */
655
656
  #parseBrackets() {
656
657
  const parseBrackets = require('../parser/brackets');
657
- this.setText(parseBrackets(String(this), this.#config, this.#accum));
658
+ const str = this.type === 'root' ? String(this) : `\0${String(this)}`,
659
+ parsed = parseBrackets(str, this.#config, this.#accum);
660
+ this.setText(this.type === 'root' ? parsed : parsed.slice(1));
658
661
  }
659
662
 
660
663
  /** 解析HTML标签 */
@@ -718,9 +721,13 @@ class Token extends AstElement {
718
721
 
719
722
  /** 解析列表 */
720
723
  #parseList() {
724
+ if (this.type === 'image-parameter') {
725
+ return;
726
+ }
721
727
  const parseList = require('../parser/list');
722
728
  const lines = String(this).split('\n');
723
- for (let i = this.type === 'root' ? 0 : 1; i < lines.length; i++) {
729
+ let i = this.type === 'root' || this.type === 'ext-inner' && this.type === 'poem' ? 0 : 1;
730
+ for (; i < lines.length; i++) {
724
731
  lines[i] = parseList(lines[i], this.#config, this.#accum);
725
732
  }
726
733
  this.setText(lines.join('\n'));
@@ -2,8 +2,7 @@
2
2
 
3
3
  const Title = require('../../lib/title'),
4
4
  Parser = require('../..'),
5
- LinkToken = require('.'),
6
- Token = require('..');
5
+ LinkToken = require('.');
7
6
 
8
7
  /**
9
8
  * 分类
@@ -19,10 +18,11 @@ class CategoryToken extends LinkToken {
19
18
 
20
19
  /** 分类排序关键字 */
21
20
  get sortkey() {
22
- return this.children[1]?.text()
23
- ?.replaceAll(/&#(\d+);/gu, /** @param {string} p */ (_, p) => String.fromCodePoint(Number(p)))
24
- ?.replaceAll(/&#x([\da-f]+);/giu, /** @param {string} p */ (_, p) => String.fromCodePoint(parseInt(p, 16)))
25
- ?.replaceAll('\n', '') ?? '';
21
+ return this.childNodes[1]?.text()?.replaceAll(
22
+ /&#(\d+|x[\da-f]+);|\n/gu,
23
+ /** @param {string} p */
24
+ (_, p) => p ? String.fromCodePoint(p[0] === 'x' ? parseInt(p.slice(1), 16) : Number(p)) : '',
25
+ );
26
26
  }
27
27
 
28
28
  set sortkey(text) {
package/src/link/file.js CHANGED
@@ -3,6 +3,7 @@
3
3
  const Title = require('../../lib/title'),
4
4
  {explode, noWrap} = require('../../util/string'),
5
5
  {externalUse} = require('../../util/debug'),
6
+ {generateForChild} = require('../../util/lint'),
6
7
  Parser = require('../..'),
7
8
  LinkToken = require('.'),
8
9
  ImageParameterToken = require('../imageParameter');
@@ -78,6 +79,26 @@ class FileToken extends LinkToken {
78
79
  this.seal(['setLangLink', 'setFragment', 'asSelfLink', 'setLinkText', 'pipeTrick'], true);
79
80
  }
80
81
 
82
+ /**
83
+ * @override
84
+ * @param {number} start 起始位置
85
+ */
86
+ lint(start = 0) {
87
+ const errors = super.lint(start),
88
+ frameArgs = this.getFrameArgs(),
89
+ captions = this.getArgs('caption');
90
+ if (frameArgs.length > 1 || captions.size > 1) {
91
+ const rect = this.getRootNode().posFromIndex(start);
92
+ if (frameArgs.length > 1) {
93
+ errors.push(...frameArgs.map(arg => generateForChild(arg, rect, '重复或冲突的图片框架参数')));
94
+ }
95
+ if (captions.size > 1) {
96
+ errors.push(...[...captions].map(arg => generateForChild(arg, rect, '重复的图片说明')));
97
+ }
98
+ }
99
+ return errors;
100
+ }
101
+
81
102
  /**
82
103
  * @override
83
104
  * @param {number} i 移除位置
@@ -251,13 +272,12 @@ class FileToken extends LinkToken {
251
272
  }
252
273
  const wikitext = `[[File:F|${syntax ? syntax.replace('$1', value) : value}]]`,
253
274
  root = Parser.parse(wikitext, this.getAttribute('include'), 6, config),
254
- {childNodes: {length}, firstElementChild} = root;
255
- if (length !== 1 || !firstElementChild?.matches('file#File\\:F')
256
- || firstElementChild.childNodes.length !== 2 || firstElementChild.lastElementChild.name !== key
257
- ) {
275
+ {childNodes: {length}, firstChild: file} = root,
276
+ {name, type, childNodes: {length: fileLength}, lastChild: imageParameter} = file;
277
+ if (length !== 1 || type !== 'file' || name !== 'File:F' || fileLength !== 2 || imageParameter.name !== key) {
258
278
  throw new SyntaxError(`非法的 ${key} 参数:${noWrap(value)}`);
259
279
  }
260
- this.appendChild(firstElementChild.lastChild);
280
+ this.appendChild(imageParameter);
261
281
  }
262
282
  }
263
283