wikiparser-node 0.0.3 → 0.2.1

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.
@@ -0,0 +1,214 @@
1
+ 'use strict';
2
+
3
+ const /** @type {Parser} */ Parser = require('..'),
4
+ Token = require('.'),
5
+ AtomToken = require('./atom');
6
+
7
+ /**
8
+ * 转换flags
9
+ * @classdesc `{childNodes: ...AtomToken}`
10
+ */
11
+ class ConverterFlagsToken extends Token {
12
+ type = 'converter-flags';
13
+ /** @type {string[]} */ #flags;
14
+
15
+ /**
16
+ * @param {string[]} flags
17
+ * @param {accum} accum
18
+ */
19
+ constructor(flags, config = Parser.getConfig(), accum = []) {
20
+ super(undefined, config, false, accum, {AtomToken: ':'});
21
+ this.append(...flags.map(flag => new AtomToken(flag, 'converter-flag', config, accum)));
22
+ }
23
+
24
+ cloneNode() {
25
+ const cloned = this.cloneChildren(),
26
+ token = Parser.run(() => new ConverterFlagsToken([], this.getAttribute('config')));
27
+ token.append(...cloned);
28
+ token.afterBuild();
29
+ return token;
30
+ }
31
+
32
+ /** @complexity `n` */
33
+ afterBuild() {
34
+ this.#flags = this.children.map(child => child.text().trim());
35
+ const that = this,
36
+ /** @type {AstListener} */ converterFlagsListener = ({prevTarget}) => {
37
+ if (prevTarget) {
38
+ that.#flags[that.childNodes.indexOf(prevTarget)] = prevTarget.text().trim();
39
+ }
40
+ };
41
+ this.addEventListener(['remove', 'insert', 'text', 'replace'], converterFlagsListener);
42
+ }
43
+
44
+ /**
45
+ * @template {string} T
46
+ * @param {T} key
47
+ * @returns {TokenAttribute<T>}
48
+ */
49
+ getAttribute(key) {
50
+ if (key === 'flags') {
51
+ return Parser.debugging ? this.#flags : [...this.#flags];
52
+ }
53
+ return super.getAttribute(key);
54
+ }
55
+
56
+ /**
57
+ * @param {number} i
58
+ * @complexity `n`
59
+ */
60
+ removeAt(i) {
61
+ const /** @type {AtomToken} */ token = super.removeAt(i);
62
+ this.#flags?.splice(i, 1);
63
+ return token;
64
+ }
65
+
66
+ /**
67
+ * @param {AtomToken} token
68
+ * @complexity `n`
69
+ */
70
+ insertAt(token, i = this.childNodes.length) {
71
+ super.insertAt(token, i);
72
+ this.#flags?.splice(i, 0, token.text());
73
+ return token;
74
+ }
75
+
76
+ toString() {
77
+ return super.toString(';');
78
+ }
79
+
80
+ getGaps() {
81
+ return 1;
82
+ }
83
+
84
+ text() {
85
+ return super.text(';');
86
+ }
87
+
88
+ /** @returns {[number, string][]} */
89
+ plain() {
90
+ return [];
91
+ }
92
+
93
+ /**
94
+ * @param {string} flag
95
+ * @returns {AtomToken[]}
96
+ * @complexity `n`
97
+ */
98
+ getFlagToken(flag) {
99
+ return this.#flags.includes(flag) ? this.children.filter(child => child.text().trim() === flag) : [];
100
+ }
101
+
102
+ getAllFlags() {
103
+ return new Set(this.#flags);
104
+ }
105
+
106
+ /** @complexity `n` */
107
+ getEffectiveFlags() {
108
+ const {variants} = this.getAttribute('config'),
109
+ variantFlags = this.#flags.filter(flag => variants.includes(flag)),
110
+ unknownFlags = this.#flags.filter(flag => /{{.+}}/.test(flag));
111
+ if (variantFlags.length) {
112
+ return new Set([...variantFlags, ...unknownFlags]);
113
+ }
114
+ const validFlags = ['A', 'T', 'R', 'D', '-', 'H', 'N'],
115
+ flags = new Set([...this.#flags.filter(flag => validFlags.includes(flag)), ...unknownFlags]);
116
+ if (flags.size === 0) {
117
+ return new Set('S');
118
+ } else if (flags.has('R')) {
119
+ return new Set('R');
120
+ } else if (flags.has('N')) {
121
+ return new Set('N');
122
+ } else if (flags.has('-')) {
123
+ return new Set('-');
124
+ } else if (flags.has('H')) {
125
+ const hasT = flags.has('T'),
126
+ hasD = flags.has('D');
127
+ if (hasT && hasD) {
128
+ return new Set(['+', 'H', 'T', 'D']);
129
+ }
130
+ return new Set(['+', 'H', ...hasT ? ['T'] : [], ...hasD ? ['D'] : [], ...unknownFlags]);
131
+ }
132
+ if (flags.size === 1 && flags.has('T')) {
133
+ flags.add('H');
134
+ }
135
+ if (flags.has('A')) {
136
+ flags.add('+');
137
+ flags.add('S');
138
+ }
139
+ if (flags.has('D')) {
140
+ flags.delete('S');
141
+ }
142
+ return flags;
143
+ }
144
+
145
+ /** @complexity `n` */
146
+ getUnknownFlags() {
147
+ return [...this.getFlags()].filter(flag => /{{.+}}/.test(flag));
148
+ }
149
+
150
+ /** @param {string} flag */
151
+ hasFlag(flag) {
152
+ if (typeof flag !== 'string') {
153
+ this.typeError('hasFlag', 'String');
154
+ }
155
+ return this.#flags.includes(flag);
156
+ }
157
+
158
+ /**
159
+ * @param {string} flag
160
+ * @complexity `n`
161
+ */
162
+ hasEffectiveFlag(flag) {
163
+ if (typeof flag !== 'string') {
164
+ this.typeError('hasFlag', 'String');
165
+ }
166
+ return this.getEffectiveFlags().has(flag);
167
+ }
168
+
169
+ /**
170
+ * @param {string} flag
171
+ * @complexity `n²`
172
+ */
173
+ removeFlag(flag) {
174
+ for (const token of this.getFlagToken(flag)) {
175
+ token.remove();
176
+ }
177
+ }
178
+
179
+ /**
180
+ * @param {string} flag
181
+ * @complexity `n`
182
+ */
183
+ #newFlag(flag) {
184
+ const token = Parser.run(() => new AtomToken(flag, 'converter-flag', this.getAttribute('config')));
185
+ this.appendChild(token);
186
+ }
187
+
188
+ /**
189
+ * @param {string} flag
190
+ * @complexity `n`
191
+ */
192
+ setFlag(flag) {
193
+ if (!this.#flags.includes(flag)) {
194
+ this.#newFlag(flag);
195
+ } else if (!this.getEffectiveFlags().has(flag)) {
196
+ Parser.error('此 flag 不会生效', flag);
197
+ }
198
+ }
199
+
200
+ /**
201
+ * @param {string} flag
202
+ * @complexity `n²`
203
+ */
204
+ toggleFlag(flag) {
205
+ if (!this.#flags.includes(flag)) {
206
+ this.#newFlag(flag);
207
+ } else {
208
+ this.removeFlag(flag);
209
+ }
210
+ }
211
+ }
212
+
213
+ Parser.classes.ConverterFlagsToken = __filename;
214
+ module.exports = ConverterFlagsToken;
@@ -0,0 +1,209 @@
1
+ 'use strict';
2
+
3
+ const {undo} = require('../util/debug'),
4
+ {noWrap} = require('../util/string'),
5
+ /** @type {Parser} */ Parser = require('..'),
6
+ Token = require('.'),
7
+ AtomToken = require('./atom');
8
+
9
+ /**
10
+ * 转换规则
11
+ * @classdesc `{childNodes: ...AtomToken)}`
12
+ */
13
+ class ConverterRuleToken extends Token {
14
+ type = 'converter-rule';
15
+ variant = '';
16
+ unidirectional = false;
17
+ bidirectional = false;
18
+
19
+ /**
20
+ * @param {string} rule
21
+ * @param {accum} accum
22
+ */
23
+ constructor(rule, hasColon = true, config = Parser.getConfig(), accum = []) {
24
+ super(undefined, config, true, accum, {AtomToken: ':'});
25
+ if (!hasColon) {
26
+ super.insertAt(new AtomToken(rule, 'converter-rule-noconvert', config, accum));
27
+ } else {
28
+ const i = rule.indexOf(':'),
29
+ j = rule.slice(0, i).indexOf('=>'),
30
+ v = (j === -1 ? rule.slice(0, i) : rule.slice(j + 2, i)).trim(),
31
+ {variants} = config;
32
+ if (!variants.includes(v)) {
33
+ super.insertAt(new AtomToken(rule, 'converter-rule-noconvert', config, accum));
34
+ } else {
35
+ super.insertAt(new AtomToken(v, 'converter-rule-variant', config, accum));
36
+ super.insertAt(new AtomToken(rule.slice(i + 1), 'converter-rule-to', config, accum));
37
+ if (j === -1) {
38
+ this.bidirectional = true;
39
+ } else {
40
+ super.insertAt(new AtomToken(rule.slice(0, j), 'converter-rule-from', config, accum), 0);
41
+ this.unidirectional = true;
42
+ }
43
+ }
44
+ }
45
+ this.seal(['variant', 'unidirectional', 'bidirectional']);
46
+ }
47
+
48
+ cloneNode() {
49
+ const cloned = this.cloneChildren(),
50
+ placeholders = ['', ':', '=>:'],
51
+ placeholder = placeholders[cloned.length - 1],
52
+ token = Parser.run(() => new ConverterRuleToken(placeholder, placeholder, this.getAttribute('config')));
53
+ for (let i = 0; i < cloned.length; i++) {
54
+ token.children[i].safeReplaceWith(cloned[i]);
55
+ }
56
+ token.afterBuild();
57
+ return token;
58
+ }
59
+
60
+ afterBuild() {
61
+ if (this.childNodes.length > 1) {
62
+ this.setAttribute('variant', this.children.at(-2).text().trim());
63
+ }
64
+ const that = this,
65
+ /** @type {AstListener} */ converterRuleListener = (e, data) => {
66
+ const {childNodes} = that,
67
+ {prevTarget} = e;
68
+ if (childNodes.length > 1 && childNodes.at(-2) === prevTarget) {
69
+ const v = prevTarget.text().trim(),
70
+ {variants} = that.getAttribute('config');
71
+ if (variants.includes(v)) {
72
+ that.setAttribute('variant', v);
73
+ } else {
74
+ undo(e, data);
75
+ throw new Error(`无效的语言变体:${v}`);
76
+ }
77
+ }
78
+ };
79
+ this.addEventListener(['remove', 'insert', 'text', 'replace'], converterRuleListener);
80
+ }
81
+
82
+ /**
83
+ * @param {number} i
84
+ * @returns {AtomToken}
85
+ */
86
+ removeAt(i) {
87
+ if (i !== 0 && i !== -this.childNodes.length) {
88
+ throw new RangeError(`${this.constructor.name} 禁止移除第 ${i} 个子节点!`);
89
+ }
90
+ return super.removeAt(i);
91
+ }
92
+
93
+ insertAt() {
94
+ throw new Error('转换规则语法复杂,请勿尝试手动插入子节点!');
95
+ }
96
+
97
+ /** @returns {string} */
98
+ toString() {
99
+ if (this.childNodes.length === 3) {
100
+ const [from, variant, to] = this.children;
101
+ return `${from.toString()}=>${variant.toString()}:${to.toString()}`;
102
+ }
103
+ return super.toString(':');
104
+ }
105
+
106
+ /** @param {number} i */
107
+ getGaps(i = 0) {
108
+ const {length} = this.childNodes;
109
+ i = i < 0 ? i + length : i;
110
+ return i === 0 && length === 3 ? 2 : 1;
111
+ }
112
+
113
+ /** @returns {string} */
114
+ text() {
115
+ if (this.childNodes.length === 3) {
116
+ const [from, variant, to] = this.children;
117
+ return `${from.text()}=>${variant.text()}:${to.text()}`;
118
+ }
119
+ return super.text(':');
120
+ }
121
+
122
+ /** @returns {[number, string][]} */
123
+ plain() {
124
+ const {firstElementChild, lastElementChild, unidirectional} = this;
125
+ if (unidirectional) {
126
+ return [...firstElementChild.plain(), ...lastElementChild.plain()];
127
+ }
128
+ return lastElementChild.plain();
129
+ }
130
+
131
+ noConvert() {
132
+ for (let i = this.childNodes.length - 2; i >= 0; i--) {
133
+ super.removeAt(i);
134
+ }
135
+ this.setAttribute('unidirectional', false).setAttribute('bidirectional', false).setAttribute('variant', '');
136
+ }
137
+
138
+ /** @param {string} to */
139
+ setTo(to) {
140
+ to = String(to);
141
+ const config = this.getAttribute('config'),
142
+ root = Parser.parse(`-{|${config.variants[0]}:${to}}-`, this.getAttribute('include'), undefined, config),
143
+ {childNodes: {length}, firstElementChild} = root;
144
+ if (length !== 1 || firstElementChild.type !== 'converter' || firstElementChild.childNodes.length !== 2
145
+ || firstElementChild.lastElementChild.childNodes.length !== 2
146
+ ) {
147
+ throw new SyntaxError(`非法的转换目标:${noWrap(to)}`);
148
+ }
149
+ const {lastChild} = firstElementChild.lastElementChild;
150
+ firstElementChild.lastElementChild.removeAt(0);
151
+ this.lastElementChild.safeReplaceWith(lastChild);
152
+ }
153
+
154
+ /** @param {string} variant */
155
+ setVariant(variant) {
156
+ if (typeof variant !== 'string') {
157
+ this.typeError('setVariant', 'String');
158
+ }
159
+ const config = this.getAttribute('config'),
160
+ v = variant.trim();
161
+ if (!config.variants.includes(v)) {
162
+ throw new RangeError(`无效的语言变体:${v}`);
163
+ } else if (this.childNodes.length === 1) {
164
+ super.insertAt(Parser.run(() => new AtomToken(variant, 'converter-rule-variant', config)), 0);
165
+ this.setAttribute('bidirectional', true);
166
+ } else {
167
+ this.children.at(-2).setText(variant);
168
+ }
169
+ this.setAttribute('variant', v);
170
+ }
171
+
172
+ /** @param {string} from */
173
+ setFrom(from) {
174
+ const {variant} = this;
175
+ if (!variant) {
176
+ throw new Error('请先指定语言变体!');
177
+ }
178
+ from = String(from);
179
+ const config = this.getAttribute('config'),
180
+ root = Parser.parse(`-{|${from}=>${variant}:}-`, this.getAttribute('include'), undefined, config),
181
+ {childNodes: {length}, firstElementChild} = root;
182
+ if (length !== 1 || firstElementChild.type !== 'converter' || firstElementChild.childNodes.length !== 2
183
+ || firstElementChild.lastElementChild.childNodes.length !== 3
184
+ ) {
185
+ throw new SyntaxError(`非法的转换原文:${noWrap(from)}`);
186
+ }
187
+ if (this.unidirectional) {
188
+ this.firstElementChild.safeReplaceWith(firstElementChild.lastElementChild.firstChild);
189
+ } else {
190
+ super.insertAt(firstElementChild.lastElementChild.firstChild, 0);
191
+ this.setAttribute('unidirectional', true).setAttribute('bidirectional', false);
192
+ }
193
+ }
194
+
195
+ /** @param {string} from */
196
+ makeUnidirectional(from) {
197
+ this.setFrom(from);
198
+ }
199
+
200
+ makeBidirectional() {
201
+ if (this.unidirectional) {
202
+ super.removeAt(0);
203
+ this.setAttribute('unidirectional', false).setAttribute('bidirectional', true);
204
+ }
205
+ }
206
+ }
207
+
208
+ Parser.classes.ConverterRuleToken = __filename;
209
+ module.exports = ConverterRuleToken;
package/src/extLink.js CHANGED
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- const {noWrap} = require('../util/string'),
3
+ const {noWrap, normalizeSpace} = require('../util/string'),
4
4
  /** @type {Parser} */ Parser = require('..'),
5
5
  Token = require('.'),
6
6
  MagicLinkToken = require('./magicLink');
@@ -33,9 +33,9 @@ class ExtLinkToken extends Token {
33
33
  this.appendChild(new MagicLinkToken(url, true, config, accum));
34
34
  this.#space = space;
35
35
  if (text) {
36
- const inner = new Token(text, config, true, accum);
36
+ const inner = new Token(text, config, true, accum, {'Stage-7': ':', ConverterToken: ':'});
37
37
  inner.type = 'ext-link-text';
38
- this.appendChild(inner.setAttribute('stage', 8));
38
+ this.appendChild(inner.setAttribute('stage', Parser.MAX_STAGE - 1));
39
39
  }
40
40
  this.protectChildren(0);
41
41
  }
@@ -49,20 +49,32 @@ class ExtLinkToken extends Token {
49
49
  }
50
50
  }
51
51
 
52
+ #correct() {
53
+ if (!this.#space && this.childNodes.length > 1
54
+ // 都替换成`<`肯定不对,但无妨
55
+ && /^[^[\]<>"{\x00-\x20\x7f\p{Zs}\ufffd]/u.test(this.lastElementChild.text().replace(/&[lg]t;/, '<'))
56
+ ) {
57
+ this.#space = ' ';
58
+ }
59
+ }
60
+
52
61
  toString() {
53
- return `[${this.firstElementChild.toString()}${this.#space}${this.children[1]?.toString() ?? ''}]`;
62
+ this.#correct();
63
+ return `[${this.firstElementChild.toString()}${this.#space}${normalizeSpace(this.children[1])}]`;
54
64
  }
55
65
 
56
66
  getPadding() {
67
+ this.#correct();
57
68
  return 1;
58
69
  }
59
70
 
60
71
  getGaps() {
72
+ this.#correct();
61
73
  return this.#space.length;
62
74
  }
63
75
 
64
76
  text() {
65
- return `[${super.text(' ')}]`;
77
+ return `[${super.text(' ').replaceAll('\n', ' ')}]`;
66
78
  }
67
79
 
68
80
  /**
@@ -70,7 +82,7 @@ class ExtLinkToken extends Token {
70
82
  * @complexity `n`
71
83
  */
72
84
  plain() {
73
- return this.childElementCount === 1 ? [] : this.lastElementChild.plain();
85
+ return this.childNodes.length === 1 ? [] : this.lastElementChild.plain();
74
86
  }
75
87
 
76
88
  /** @this {ExtLinkToken & {firstElementChild: MagicLinkToken}} */
@@ -83,7 +95,7 @@ class ExtLinkToken extends Token {
83
95
  url = String(url);
84
96
  const root = Parser.parse(`[${url}]`, this.getAttribute('include'), 8, this.getAttribute('config')),
85
97
  {childNodes: {length}, firstElementChild} = root;
86
- if (length !== 1 || firstElementChild?.type !== 'ext-link' || firstElementChild.childElementCount !== 1) {
98
+ if (length !== 1 || firstElementChild?.type !== 'ext-link' || firstElementChild.childNodes.length !== 1) {
87
99
  throw new SyntaxError(`非法的外链目标:${url}`);
88
100
  }
89
101
  const {firstChild} = firstElementChild;
@@ -97,11 +109,11 @@ class ExtLinkToken extends Token {
97
109
  text = String(text);
98
110
  const root = Parser.parse(`[//url ${text}]`, this.getAttribute('include'), 8, this.getAttribute('config')),
99
111
  {childNodes: {length}, firstElementChild} = root;
100
- if (length !== 1 || firstElementChild?.type !== 'ext-link' || firstElementChild.childElementCount !== 2) {
112
+ if (length !== 1 || firstElementChild?.type !== 'ext-link' || firstElementChild.childNodes.length !== 2) {
101
113
  throw new SyntaxError(`非法的外链文字:${noWrap(text)}`);
102
114
  }
103
115
  const {lastChild} = firstElementChild;
104
- if (this.childElementCount === 1) {
116
+ if (this.childNodes.length === 1) {
105
117
  this.appendChild(lastChild);
106
118
  } else {
107
119
  this.lastElementChild.replaceWith(lastChild);
package/src/heading.js CHANGED
@@ -1,6 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  const fixedToken = require('../mixin/fixedToken'),
4
+ sol = require('../mixin/sol'),
4
5
  /** @type {Parser} */ Parser = require('..'),
5
6
  Token = require('.');
6
7
 
@@ -8,7 +9,7 @@ const fixedToken = require('../mixin/fixedToken'),
8
9
  * 章节标题
9
10
  * @classdesc `{childNodes: [Token, HiddenToken]}`
10
11
  */
11
- class HeadingToken extends fixedToken(Token) {
12
+ class HeadingToken extends fixedToken(sol(Token)) {
12
13
  type = 'heading';
13
14
 
14
15
  /**
@@ -37,34 +38,29 @@ class HeadingToken extends fixedToken(Token) {
37
38
  return token;
38
39
  }
39
40
 
41
+ /** @this {HeadingToken & {prependNewLine(): ''|'\n', appendNewLine(): ''|'\n'}} */
40
42
  toString() {
41
- const equals = '='.repeat(Number(this.name)),
42
- {previousVisibleSibling, nextVisibleSibling} = this;
43
- return `${
44
- typeof previousVisibleSibling === 'string' && !previousVisibleSibling.endsWith('\n')
45
- || previousVisibleSibling instanceof Token
46
- ? '\n'
47
- : ''
48
- }${equals}${super.toString(equals)}${
49
- typeof nextVisibleSibling === 'string' && !nextVisibleSibling.startsWith('\n')
50
- || nextVisibleSibling instanceof Token
51
- ? '\n'
52
- : ''
53
- }`;
43
+ const equals = '='.repeat(Number(this.name));
44
+ return `${this.prependNewLine()}${equals}${
45
+ this.firstElementChild.toString()
46
+ }${equals}${this.lastElementChild.toString()}${this.appendNewLine()}`;
54
47
  }
55
48
 
56
49
  getPadding() {
57
- return Number(this.name);
50
+ return super.getPadding() + Number(this.name);
58
51
  }
59
52
 
60
53
  getGaps() {
61
54
  return Number(this.name);
62
55
  }
63
56
 
64
- /** @returns {string} */
57
+ /**
58
+ * @this {HeadingToken & {prependNewLine(): ''|'\n', appendNewLine(): ''|'\n'}}
59
+ * @returns {string}
60
+ */
65
61
  text() {
66
62
  const equals = '='.repeat(Number(this.name));
67
- return `${equals}${this.firstElementChild.text()}${equals}`;
63
+ return `${this.prependNewLine()}${equals}${this.firstElementChild.text()}${equals}${this.appendNewLine()}`;
68
64
  }
69
65
 
70
66
  /** @returns {[number, string][]} */
@@ -213,7 +213,7 @@ class ImageParameterToken extends Token {
213
213
  {childNodes: {length}, firstElementChild} = root,
214
214
  param = firstElementChild?.lastElementChild;
215
215
  if (length !== 1 || !firstElementChild?.matches('file#File:F')
216
- || firstElementChild.childElementCount !== 2 || param.name !== this.name
216
+ || firstElementChild.childNodes.length !== 2 || param.name !== this.name
217
217
  ) {
218
218
  throw new SyntaxError(`非法的 ${this.name} 参数:${noWrap(value)}`);
219
219
  }