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
package/mixin/sol.js CHANGED
@@ -9,25 +9,35 @@ const Parser = require('..'),
9
9
  * @param {T} Constructor 基类
10
10
  * @returns {T}
11
11
  */
12
- const sol = Constructor => class extends Constructor {
12
+ const sol = Constructor => class SolToken extends Constructor {
13
13
  /**
14
- * 在前方插入newline
14
+ * 是否可以视为root节点
15
15
  * @this {Token}
16
+ * @param {boolean} includeHeading 是否包括HeadingToken
17
+ */
18
+ #isRoot(includeHeading) {
19
+ const {parentNode, type} = this;
20
+ return parentNode?.type === 'root'
21
+ || parentNode?.type === 'ext-inner' && parentNode?.name === 'poem'
22
+ && (includeHeading || type !== 'heading');
23
+ }
24
+
25
+ /**
26
+ * 在前方插入newline
27
+ * @this {SolToken & Token}
16
28
  */
17
29
  prependNewLine() {
18
- const {previousVisibleSibling = '', parentNode} = this;
19
- return (previousVisibleSibling || parentNode?.type !== 'root') && String(previousVisibleSibling).at(-1) !== '\n'
30
+ return (this.previousVisibleSibling || !this.#isRoot()) && String(this.previousVisibleSibling).at(-1) !== '\n'
20
31
  ? '\n'
21
32
  : '';
22
33
  }
23
34
 
24
35
  /**
25
36
  * 在后方插入newline
26
- * @this {Token}
37
+ * @this {SolToken & Token}
27
38
  */
28
39
  appendNewLine() {
29
- const {nextVisibleSibling = '', parentNode} = this;
30
- return (nextVisibleSibling || parentNode?.type !== 'root') && String(nextVisibleSibling ?? '')[0] !== '\n'
40
+ return (this.nextVisibleSibling || !this.#isRoot(true)) && String(this.nextVisibleSibling ?? '')[0] !== '\n'
31
41
  ? '\n'
32
42
  : '';
33
43
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wikiparser-node",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "A Node.js parser for MediaWiki markup with AST",
5
5
  "keywords": [
6
6
  "mediawiki",
@@ -13,6 +13,16 @@
13
13
  },
14
14
  "license": "GPL-3.0",
15
15
  "author": "Bhsd",
16
+ "files": [
17
+ "/index.js",
18
+ "/parser/",
19
+ "/util/",
20
+ "/lib/",
21
+ "/src/",
22
+ "/config/",
23
+ "/mixin/",
24
+ "/tool/"
25
+ ],
16
26
  "repository": {
17
27
  "type": "git",
18
28
  "url": "git+https://github.com/bhsd-harry/wikiparser-node.git"
@@ -35,7 +35,7 @@ const parseCommentAndExt = (text, config = Parser.getConfig(), accum = [], inclu
35
35
  + `<(${noincludeRegex})(\\s[^>]*?)?(?:/>|>(.*?)(?:</(\\5\\s*)>|$))`, // <noinclude>
36
36
  'gisu',
37
37
  );
38
- return text.replace(
38
+ return text.replaceAll(
39
39
  regex,
40
40
  /** @type {function(...string): string} */
41
41
  (substr, name, attr, inner, closing, include, includeAttr, includeInner, includeClosing) => {
@@ -22,7 +22,7 @@ const parseConverter = (wikitext, config = Parser.getConfig(), accum = []) => {
22
22
  str = wikitext.slice(top.index + 2, index),
23
23
  i = str.indexOf('|'),
24
24
  [flags, text] = i === -1 ? [[], str] : [str.slice(0, i).split(';'), str.slice(i + 1)],
25
- temp = text.replace(/(&[#a-z\d]+);/iu, '$1\x01'),
25
+ temp = text.replaceAll(/(&[#a-z\d]+);/giu, '$1\x01'),
26
26
  variants = `(?:${config.variants.join('|')})`,
27
27
  rules = temp.split(new RegExp(`;(?=\\s*(?:${variants}|[^;]*?=>\\s*${variants})\\s*:)`, 'u'))
28
28
  .map(rule => rule.replaceAll('\x01', ';'));
@@ -14,7 +14,7 @@ const parseExternalLinks = (wikitext, config = Parser.getConfig(), accum = []) =
14
14
  `\\[((?:${config.protocol}|//)${extUrlChar})(\\p{Zs}*)([^\\]\x01-\x08\x0A-\x1F\uFFFD]*)\\]`,
15
15
  'giu',
16
16
  );
17
- return wikitext.replace(regex, /** @type {function(...string): string} */ (_, url, space, text) => {
17
+ return wikitext.replaceAll(regex, /** @type {function(...string): string} */ (_, url, space, text) => {
18
18
  const {length} = accum,
19
19
  mt = /&[lg]t;/u.exec(url);
20
20
  if (mt) {
@@ -1,18 +1,19 @@
1
1
  'use strict';
2
2
 
3
3
  const Parser = require('..'),
4
- AstText = require('../lib/text');
4
+ AstText = require('../lib/text'),
5
+ Token = require('../src');
5
6
 
6
7
  /**
7
8
  * 解析\<hr\>和状态开关
8
- * @param {{firstChild: AstText, type: string}} root 根节点
9
+ * @param {Token & {firstChild: AstText}} root 根节点
9
10
  * @param {accum} accum
10
11
  */
11
- const parseHrAndDoubleUnderscore = ({firstChild: {data}, type}, config = Parser.getConfig(), accum = []) => {
12
+ const parseHrAndDoubleUnderscore = ({firstChild: {data}, type, name}, config = Parser.getConfig(), accum = []) => {
12
13
  const HrToken = require('../src/nowiki/hr'),
13
14
  DoubleUnderscoreToken = require('../src/nowiki/doubleUnderscore');
14
15
  const {doubleUnderscore} = config;
15
- if (type !== 'root') {
16
+ if (type !== 'root' && (type !== 'ext-inner' || name !== 'poem')) {
16
17
  data = `\0${data}`;
17
18
  }
18
19
  data = data.replaceAll(/^((?:\0\d+c\x7F)*)(-{4,})/gmu, (_, lead, m) => {
@@ -28,7 +29,7 @@ const parseHrAndDoubleUnderscore = ({firstChild: {data}, type}, config = Parser.
28
29
  return m;
29
30
  },
30
31
  );
31
- return type === 'root' ? data : data.slice(1);
32
+ return type === 'root' || type === 'ext-inner' && name === 'poem' ? data : data.slice(1);
32
33
  };
33
34
 
34
35
  Parser.parsers.parseHrAndDoubleUnderscore = __filename;
package/parser/links.js CHANGED
@@ -1,7 +1,6 @@
1
1
  'use strict';
2
2
 
3
- const Parser = require('..'),
4
- Token = require('../src');
3
+ const Parser = require('..');
5
4
 
6
5
  /**
7
6
  * 解析内部链接
@@ -11,7 +11,7 @@ const {extUrlChar} = require('../util/string'),
11
11
  const parseMagicLinks = (wikitext, config = Parser.getConfig(), accum = []) => {
12
12
  const MagicLinkToken = require('../src/magicLink');
13
13
  const regex = new RegExp(`\\b(?:${config.protocol})(${extUrlChar})`, 'giu');
14
- return wikitext.replace(regex, /** @param {string} p1 */ (m, p1) => {
14
+ return wikitext.replaceAll(regex, /** @param {string} p1 */ (m, p1) => {
15
15
  let trail = '',
16
16
  url = m;
17
17
  const m2 = /&(?:lt|gt|nbsp|#x0*(?:3[ce]|a0)|#0*(?:6[02]|160));/iu.exec(url);
@@ -46,7 +46,7 @@ const /** @type {pseudo[]} */ simplePseudos = [
46
46
  ['&', '&amp;'],
47
47
  ],
48
48
  pseudoRegex = new RegExp(`:(${complexPseudos.join('|')})$`, 'u'),
49
- regularRegex = /[[(,>+~]|\s+/u, // eslint-disable-line regexp/no-super-linear-move
49
+ regularRegex = /[[(,>+~]|\s+/u,
50
50
  attributeRegex = /^\s*(\w+)\s*(?:([~|^$*!]?=)\s*("[^"]*"|'[^']*'|[^\s[\]]+)(?:\s+(i))?\s*)?\]/u,
51
51
  functionRegex = /^(\s*"[^"]*"\s*|\s*'[^']*'\s*|[^()]*)\)/u;
52
52
 
@@ -164,11 +164,11 @@ const parseSelector = selector => {
164
164
  }
165
165
  mt = regex.exec(sanitized);
166
166
  }
167
- if (regex !== regularRegex) {
168
- throw new SyntaxError(`非法的选择器!\n${selector}\n检测到未闭合的'${regex === attributeRegex ? '[' : '('}'`);
167
+ if (regex === regularRegex) {
168
+ pushSimple(step, sanitized);
169
+ return stack;
169
170
  }
170
- pushSimple(step, sanitized);
171
- return stack;
171
+ throw new SyntaxError(`非法的选择器!\n${selector}\n检测到未闭合的'${regex === attributeRegex ? '[' : '('}'`);
172
172
  };
173
173
 
174
174
  Parser.parsers.parseSelector = __filename;
package/parser/table.js CHANGED
@@ -1,22 +1,22 @@
1
1
  'use strict';
2
2
 
3
3
  const Parser = require('..'),
4
- AstText = require('../lib/text');
4
+ AstText = require('../lib/text'),
5
+ Token = require('../src'),
6
+ TableToken = require('../src/table'),
7
+ TrToken = require('../src/table/tr'),
8
+ TdToken = require('../src/table/td'),
9
+ DdToken = require('../src/nowiki/dd');
5
10
 
6
11
  /**
7
12
  * 解析表格,注意`tr`和`td`包含开头的换行
8
- * @param {{firstChild: AstText, type: string}} root 根节点
13
+ * @param {Token & {firstChild: AstText}} root 根节点
9
14
  * @param {accum} accum
10
15
  */
11
- const parseTable = ({firstChild: {data}, type}, config = Parser.getConfig(), accum = []) => {
12
- const Token = require('../src'),
13
- TableToken = require('../src/table'),
14
- TrToken = require('../src/table/tr'),
15
- TdToken = require('../src/table/td'),
16
- DdToken = require('../src/nowiki/dd');
16
+ const parseTable = ({firstChild: {data}, type, name}, config = Parser.getConfig(), accum = []) => {
17
17
  const /** @type {TrToken[]} */ stack = [],
18
18
  lines = data.split('\n');
19
- let out = type === 'root' ? '' : `\n${lines.shift()}`;
19
+ let out = type === 'root' || type === 'ext-inner' && name === 'poem' ? '' : `\n${lines.shift()}`;
20
20
 
21
21
  /**
22
22
  * 向表格中插入纯文本
@@ -28,9 +28,9 @@ const parseTable = ({firstChild: {data}, type}, config = Parser.getConfig(), acc
28
28
  out += str;
29
29
  return;
30
30
  }
31
- const {lastElementChild} = top;
32
- if (lastElementChild.isPlain()) {
33
- lastElementChild.setText(String(lastElementChild) + str);
31
+ const /** @type {Token & {firstChild: AstText}} */ {lastChild} = top;
32
+ if (lastChild.isPlain()) {
33
+ lastChild.firstChild.appendData(str);
34
34
  } else {
35
35
  const token = new Token(str, config, true, accum);
36
36
  token.type = 'table-inter';
package/src/arg.js CHANGED
@@ -1,6 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  const {text, noWrap} = require('../util/string'),
4
+ {generateForChild} = require('../util/lint'),
4
5
  Parser = require('..'),
5
6
  Token = require('.');
6
7
 
@@ -20,10 +21,10 @@ class ArgToken extends Token {
20
21
  super(undefined, config, true, accum, {AtomToken: 0, Token: 1, HiddenToken: '2:'});
21
22
  for (let i = 0; i < parts.length; i++) {
22
23
  if (i === 0 || i > 1) {
23
- const AtomToken = i === 0 ? require('./atom') : require('./atom/hidden'),
24
- token = new AtomToken(parts[i], i === 0 ? 'arg-name' : undefined, config, accum, {
25
- 'Stage-2': ':', '!HeadingToken': '',
26
- });
24
+ const AtomToken = i === 0 ? require('./atom') : require('./atom/hidden');
25
+ const token = new AtomToken(parts[i], i === 0 ? 'arg-name' : undefined, config, accum, {
26
+ 'Stage-2': ':', '!HeadingToken': '',
27
+ });
27
28
  this.appendChild(token);
28
29
  } else {
29
30
  const token = new Token(parts[i], config, true, accum);
@@ -39,7 +40,7 @@ class ArgToken extends Token {
39
40
  const [name, ...cloned] = this.cloneChildNodes();
40
41
  return Parser.run(() => {
41
42
  const token = new ArgToken([''], this.getAttribute('config'));
42
- token.firstElementChild.safeReplaceWith(name);
43
+ token.firstChild.safeReplaceWith(name);
43
44
  token.append(...cloned);
44
45
  return token.afterBuild();
45
46
  });
@@ -47,9 +48,9 @@ class ArgToken extends Token {
47
48
 
48
49
  /** @override */
49
50
  afterBuild() {
50
- this.setAttribute('name', this.firstElementChild.text().trim());
51
+ this.setAttribute('name', this.firstChild.text().trim());
51
52
  const /** @type {AstListener} */ argListener = ({prevTarget}) => {
52
- if (prevTarget === this.firstElementChild) {
53
+ if (prevTarget === this.firstChild) {
53
54
  this.setAttribute('name', prevTarget.text().trim());
54
55
  }
55
56
  };
@@ -75,12 +76,17 @@ class ArgToken extends Token {
75
76
  return 1;
76
77
  }
77
78
 
79
+ /** @override */
80
+ print() {
81
+ return super.print({pre: '{{{', post: '}}}', sep: '|'});
82
+ }
83
+
78
84
  /**
79
85
  * @override
80
86
  * @returns {string}
81
87
  */
82
88
  text() {
83
- return `{{{${text(this.children.slice(0, 2), '|')}}}}`;
89
+ return `{{{${text(this.childNodes.slice(0, 2), '|')}}}}`;
84
90
  }
85
91
 
86
92
  /**
@@ -95,6 +101,24 @@ class ArgToken extends Token {
95
101
  });
96
102
  }
97
103
 
104
+ /**
105
+ * @override
106
+ * @param {number} start 起始位置
107
+ * @returns {LintError[]}
108
+ */
109
+ lint(start = 0) {
110
+ const {childNodes: [argName, argDefault, ...rest]} = this,
111
+ errors = argName.lint(start + 3);
112
+ if (argDefault) {
113
+ errors.push(...argDefault.lint(start + 4 + String(argName).length));
114
+ }
115
+ if (rest.length > 0) {
116
+ const rect = this.getRootNode().posFromIndex(start);
117
+ errors.push(...rest.map(child => generateForChild(child, rect, '三重括号内的不可见部分')));
118
+ }
119
+ return errors;
120
+ }
121
+
98
122
  /**
99
123
  * 移除子节点,且在移除`arg-default`子节点时自动移除全部多余子节点
100
124
  * @param {number} i 移除位置
@@ -133,13 +157,13 @@ class ArgToken extends Token {
133
157
  setName(name) {
134
158
  name = String(name);
135
159
  const root = Parser.parse(`{{{${name}}}}`, this.getAttribute('include'), 2, this.getAttribute('config')),
136
- {childNodes: {length}, firstElementChild} = root;
137
- if (length !== 1 || firstElementChild?.type !== 'arg' || firstElementChild.childNodes.length !== 1) {
160
+ {childNodes: {length}, firstChild: arg} = root;
161
+ if (length !== 1 || arg.type !== 'arg' || arg.childNodes.length !== 1) {
138
162
  throw new SyntaxError(`非法的参数名称:${noWrap(name)}`);
139
163
  }
140
- const {firstElementChild: newName} = firstElementChild;
141
- firstElementChild.destroy(true);
142
- this.firstElementChild.safeReplaceWith(newName);
164
+ const {firstChild} = arg;
165
+ arg.destroy(true);
166
+ this.firstChild.safeReplaceWith(firstChild);
143
167
  }
144
168
 
145
169
  /**
@@ -150,17 +174,17 @@ class ArgToken extends Token {
150
174
  setDefault(value) {
151
175
  value = String(value);
152
176
  const root = Parser.parse(`{{{|${value}}}}`, this.getAttribute('include'), 2, this.getAttribute('config')),
153
- {childNodes: {length}, firstElementChild} = root;
154
- if (length !== 1 || firstElementChild?.type !== 'arg' || firstElementChild.childNodes.length !== 2) {
177
+ {childNodes: {length}, firstChild: arg} = root;
178
+ if (length !== 1 || arg.type !== 'arg' || arg.childNodes.length !== 2) {
155
179
  throw new SyntaxError(`非法的参数预设值:${noWrap(value)}`);
156
180
  }
157
- const {children: [, oldDefault]} = this,
158
- {lastElementChild} = firstElementChild;
159
- firstElementChild.destroy(true);
181
+ const {childNodes: [, oldDefault]} = this,
182
+ {lastChild} = arg;
183
+ arg.destroy(true);
160
184
  if (oldDefault) {
161
- oldDefault.safeReplaceWith(lastElementChild);
185
+ oldDefault.safeReplaceWith(lastChild);
162
186
  } else {
163
- this.appendChild(lastElementChild);
187
+ this.appendChild(lastChild);
164
188
  }
165
189
  }
166
190
  }
package/src/attribute.js CHANGED
@@ -1,6 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  const {externalUse} = require('../util/debug'),
4
+ {generateForSelf} = require('../util/lint'),
4
5
  {toCase, removeComment, normalizeSpace} = require('../util/string'),
5
6
  Parser = require('..'),
6
7
  Token = require('.');
@@ -14,6 +15,7 @@ const stages = {'ext-attr': 0, 'html-attr': 2, 'table-attr': 3};
14
15
  class AttributeToken extends Token {
15
16
  /** @type {Map<string, string|true>} */ #attr = new Map();
16
17
  #sanitized = true;
18
+ #quoteBalance = true;
17
19
 
18
20
  /**
19
21
  * @override
@@ -88,11 +90,10 @@ class AttributeToken extends Token {
88
90
  */
89
91
  #updateFromAttr() {
90
92
  let equal = '=';
91
- const ParameterToken = require('./parameter');
92
- const parent = this.closest('ext, parameter');
93
- if (parent instanceof ParameterToken && parent.anon
94
- && parent.parentNode?.matches('template, magic-word#invoke')
95
- ) {
93
+ const ParameterToken = require('./parameter'),
94
+ TranscludeToken = require('./transclude');
95
+ const /** @type {ParameterToken & {parentNode: TranscludeToken}} */ parent = this.closest('ext, parameter');
96
+ if (parent instanceof ParameterToken && parent.anon && parent.parentNode?.isTemplate()) {
96
97
  equal = '{{=}}';
97
98
  }
98
99
  return [...this.#attr].map(([k, v]) => {
@@ -117,6 +118,7 @@ class AttributeToken extends Token {
117
118
  this.replaceChildren(...token.childNodes, true);
118
119
  });
119
120
  this.#sanitized = true;
121
+ this.#quoteBalance = true;
120
122
  }
121
123
 
122
124
  /**
@@ -146,11 +148,13 @@ class AttributeToken extends Token {
146
148
  */
147
149
  const build = str =>
148
150
  typeof str === 'boolean' || !token ? str : token.getAttribute('buildFromStr')(str).map(String).join('');
149
- for (const [, key,, quoted, unquoted] of string
150
- .matchAll(/([^\s/][^\s/=]*)(?:\s*=\s*(?:(["'])(.*?)(?:\2|$)|(\S*)))?/gsu)
151
+ for (const [, key, quoteStart, quoted, quoteEnd, unquoted] of string
152
+ .matchAll(/([^\s/][^\s/=]*)(?:\s*=\s*(?:(["'])(.*?)(\2|$)|(\S*)))?/gsu)
151
153
  ) {
152
154
  if (!this.setAttr(build(key), build(quoted ?? unquoted ?? true), true)) {
153
155
  this.#sanitized = false;
156
+ } else if (quoteStart !== quoteEnd) {
157
+ this.#quoteBalance = false;
154
158
  }
155
159
  }
156
160
  }
@@ -355,6 +359,29 @@ class AttributeToken extends Token {
355
359
  return this.#leadingSpace().length;
356
360
  }
357
361
 
362
+ /**
363
+ * @override
364
+ * @this {AttributeToken & {parentNode: HtmlToken}}
365
+ * @param {number} start 起始位置
366
+ */
367
+ lint(start = 0) {
368
+ const HtmlToken = require('./html');
369
+ const errors = super.lint(start);
370
+ let /** @type {{top: number, left: number}} */ rect;
371
+ if (this.type === 'html-attr' && this.parentNode.closing && this.text().trim()) {
372
+ rect = this.getRootNode().posFromIndex(start);
373
+ errors.push(generateForSelf(this, rect, '位于闭合标签的属性'));
374
+ }
375
+ if (!this.#sanitized) {
376
+ rect ||= this.getRootNode().posFromIndex(start);
377
+ errors.push(generateForSelf(this, rect, '包含无效属性'));
378
+ } else if (!this.#quoteBalance) {
379
+ rect ||= this.getRootNode().posFromIndex(start);
380
+ errors.push(generateForSelf(this, rect, '未闭合的引号', 'warning'));
381
+ }
382
+ return errors;
383
+ }
384
+
358
385
  /** @override */
359
386
  text() {
360
387
  if (this.type === 'table-attr') {
package/src/converter.js CHANGED
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- const {text} = require('../util/string'),
3
+ const {text, print} = require('../util/string'),
4
4
  Parser = require('..'),
5
5
  Token = require('.'),
6
6
  ConverterFlagsToken = require('./converterFlags'),
@@ -47,7 +47,7 @@ class ConverterToken extends Token {
47
47
  cloneNode() {
48
48
  const [flags, ...rules] = this.cloneChildNodes(),
49
49
  token = Parser.run(() => new ConverterToken([], [], this.getAttribute('config')));
50
- token.firstElementChild.safeReplaceWith(flags);
50
+ token.firstChild.safeReplaceWith(flags);
51
51
  token.append(...rules);
52
52
  return token;
53
53
  }
@@ -57,7 +57,7 @@ class ConverterToken extends Token {
57
57
  * @param {string} selector
58
58
  */
59
59
  toString(selector) {
60
- const {children: [flags, ...rules]} = this;
60
+ const {childNodes: [flags, ...rules]} = this;
61
61
  return selector && this.matches(selector)
62
62
  ? ''
63
63
  : `-{${flags.toString(selector)}${flags.childNodes.length > 0 ? '|' : ''}${rules.map(String).join(';')}}-`;
@@ -74,12 +74,20 @@ class ConverterToken extends Token {
74
74
  */
75
75
  getGaps(i = 0) {
76
76
  i = i < 0 ? i + this.childNodes.length : i;
77
- return i || this.firstElementChild.childNodes.length > 0 ? 1 : 0;
77
+ return i || this.firstChild.childNodes.length > 0 ? 1 : 0;
78
78
  }
79
79
 
80
80
  /** @override */
81
- text() {
81
+ print() {
82
82
  const {children: [flags, ...rules]} = this;
83
+ return `<span class="wpb-converter">-{${flags.print()}${
84
+ flags.childNodes.length > 0 ? '|' : ''
85
+ }${print(rules, {sep: ';'})}}-</span>`;
86
+ }
87
+
88
+ /** @override */
89
+ text() {
90
+ const {childNodes: [flags, ...rules]} = this;
83
91
  return `-{${flags.text()}|${text(rules, ';')}}-`;
84
92
  }
85
93
 
@@ -1,6 +1,7 @@
1
1
  'use strict';
2
2
 
3
- const Parser = require('..'),
3
+ const {generateForChild} = require('../util/lint'),
4
+ Parser = require('..'),
4
5
  Token = require('.'),
5
6
  AtomToken = require('./atom');
6
7
 
@@ -35,7 +36,7 @@ class ConverterFlagsToken extends Token {
35
36
  * @complexity `n`
36
37
  */
37
38
  afterBuild() {
38
- this.#flags = this.children.map(child => child.text().trim());
39
+ this.#flags = this.childNodes.map(child => child.text().trim());
39
40
  const /** @type {AstListener} */ converterFlagsListener = ({prevTarget}) => {
40
41
  if (prevTarget) {
41
42
  this.#flags[this.childNodes.indexOf(prevTarget)] = prevTarget.text().trim();
@@ -94,6 +95,37 @@ class ConverterFlagsToken extends Token {
94
95
  return 1;
95
96
  }
96
97
 
98
+ /** @override */
99
+ print() {
100
+ return super.print({sep: ';'});
101
+ }
102
+
103
+ /**
104
+ * @override
105
+ * @param {number} start 起始位置
106
+ */
107
+ lint(start = 0) {
108
+ const variantFlags = this.getVariantFlags(),
109
+ unknownFlags = this.getUnknownFlags(),
110
+ emptyFlags = this.#flags.filter(flag => !flag),
111
+ validFlags = this.#flags.filter(flag => ['A', 'T', 'R', 'D', '-', 'H', 'N'].includes(flag)),
112
+ knownFlagCount = this.#flags.length - unknownFlags.length - emptyFlags,
113
+ errors = super.lint(start);
114
+ if (variantFlags.length === knownFlagCount || validFlags.length === knownFlagCount) {
115
+ return errors;
116
+ }
117
+ const rect = this.getRootNode().posFromIndex(start);
118
+ for (const child of this.childNodes) {
119
+ const flag = child.text().trim();
120
+ if (flag && !variantFlags.includes(flag) && !unknownFlags.includes(flag)
121
+ && (variantFlags.length > 0 || !validFlags.includes(flag))
122
+ ) {
123
+ errors.push(generateForChild(child, rect, '无效的转换标记'));
124
+ }
125
+ }
126
+ return errors;
127
+ }
128
+
97
129
  /** @override */
98
130
  text() {
99
131
  return super.text(';');
@@ -106,7 +138,7 @@ class ConverterFlagsToken extends Token {
106
138
  * @complexity `n`
107
139
  */
108
140
  getFlagToken(flag) {
109
- return this.#flags.includes(flag) ? this.children.filter(child => child.text().trim() === flag) : [];
141
+ return this.#flags.includes(flag) ? this.childNodes.filter(child => child.text().trim() === flag) : [];
110
142
  }
111
143
 
112
144
  /** 获取所有转换类型标记 */
@@ -122,13 +154,18 @@ class ConverterFlagsToken extends Token {
122
154
  return this.#flags.filter(flag => /\{\{[^{}]+\}\}/u.test(flag));
123
155
  }
124
156
 
157
+ /** 获取指定语言变体的转换标记 */
158
+ getVariantFlags() {
159
+ const {variants} = this.getAttribute('config');
160
+ return this.#flags.filter(flag => variants.includes(flag));
161
+ }
162
+
125
163
  /**
126
164
  * 获取有效转换类型标记
127
165
  * @complexity `n`
128
166
  */
129
167
  getEffectiveFlags() {
130
- const {variants} = this.getAttribute('config'),
131
- variantFlags = this.#flags.filter(flag => variants.includes(flag)),
168
+ const variantFlags = this.getVariantFlags(),
132
169
  unknownFlags = this.getUnknownFlags();
133
170
  if (variantFlags.length > 0) {
134
171
  return new Set([...variantFlags, ...unknownFlags]);