wikiparser-node 1.39.1 → 1.41.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 (59) hide show
  1. package/README.md +51 -5
  2. package/bundle/bundle-es8.min.js +30 -29
  3. package/bundle/bundle-lsp.min.js +28 -28
  4. package/bundle/bundle.min.js +16 -16
  5. package/config/.schema.json +5 -0
  6. package/config/default.json +11 -2
  7. package/config/enwiki.json +91 -89
  8. package/config/jawiki.json +168 -158
  9. package/config/llwiki.json +2 -1
  10. package/config/minimum.json +3 -81
  11. package/config/moegirl.json +2 -1
  12. package/config/zhwiki.json +128 -126
  13. package/data/signatures.json +1 -1
  14. package/dist/addon/attribute.js +1 -1
  15. package/dist/addon/table.js +1 -1
  16. package/dist/addon/transclude.js +3 -1
  17. package/dist/base.d.mts +11 -9
  18. package/dist/base.d.ts +11 -9
  19. package/dist/base.js +1 -0
  20. package/dist/base.mjs +1 -0
  21. package/dist/bin/config.js +11 -10
  22. package/dist/index.d.ts +5 -5
  23. package/dist/index.js +5 -1
  24. package/dist/lib/lintConfig.js +42 -23
  25. package/dist/lib/lsp.js +6 -12
  26. package/dist/lib/node.js +5 -2
  27. package/dist/lib/text.js +5 -2
  28. package/dist/lib/title.js +8 -4
  29. package/dist/mixin/elementLike.js +2 -2
  30. package/dist/parser/commentAndExt.js +2 -1
  31. package/dist/render/expand.js +3 -3
  32. package/dist/render/extension.js +15 -3
  33. package/dist/render/magicWords.js +13 -10
  34. package/dist/src/attribute.d.ts +4 -1
  35. package/dist/src/attribute.js +5 -4
  36. package/dist/src/attributes.js +29 -7
  37. package/dist/src/heading.js +8 -3
  38. package/dist/src/link/base.d.ts +1 -1
  39. package/dist/src/link/base.js +10 -6
  40. package/dist/src/link/file.js +2 -2
  41. package/dist/src/magicLink.js +20 -16
  42. package/dist/src/multiLine/gallery.d.ts +5 -3
  43. package/dist/src/multiLine/gallery.js +6 -4
  44. package/dist/src/nowiki/base.d.ts +1 -1
  45. package/dist/src/parameter.js +5 -2
  46. package/dist/src/table/tr.js +1 -1
  47. package/dist/src/tagPair/ext.js +12 -18
  48. package/dist/src/tagPair/index.d.ts +1 -1
  49. package/dist/src/transclude.d.ts +6 -1
  50. package/dist/src/transclude.js +27 -19
  51. package/dist/util/constants.js +2 -1
  52. package/dist/util/sharable.js +2 -0
  53. package/dist/util/sharable.mjs +3 -1
  54. package/extensions/dist/base.js +3 -2
  55. package/extensions/dist/env.js +34 -0
  56. package/i18n/en.json +1 -0
  57. package/i18n/zh-hans.json +1 -0
  58. package/i18n/zh-hant.json +1 -0
  59. package/package.json +20 -33
package/dist/index.js CHANGED
@@ -190,7 +190,11 @@ const Parser = {
190
190
  root.parseOnce(0, include).parseOnce();
191
191
  const t = new title_1.Title(root.firstChild.toString(), defaultNs, config, opt);
192
192
  root.build();
193
- for (const key of ['main', 'fragment']) {
193
+ const keys = [
194
+ 'main',
195
+ 'fragment',
196
+ ];
197
+ for (const key of keys) {
194
198
  const str = t[key];
195
199
  if (str?.includes('\0')) {
196
200
  const s = root.buildFromStr(str, constants_1.BuildMethod.Text);
@@ -15,8 +15,8 @@ const dict = new Map([
15
15
  ['error', 'error'],
16
16
  ]);
17
17
  const defaultLintRuleConfig = {
18
- 'arg-in-ext': 1,
19
- 'blank-alt': 1,
18
+ 'arg-in-ext': [1],
19
+ 'blank-alt': [1],
20
20
  'bold-header': [
21
21
  1,
22
22
  {
@@ -50,7 +50,7 @@ const defaultLintRuleConfig = {
50
50
  // value: 2,
51
51
  },
52
52
  ],
53
- 'insecure-style': 2,
53
+ 'insecure-style': [2],
54
54
  'invalid-gallery': [
55
55
  2,
56
56
  {
@@ -75,14 +75,14 @@ const defaultLintRuleConfig = {
75
75
  // name: 2,
76
76
  },
77
77
  ],
78
- 'invalid-isbn': 2,
78
+ 'invalid-isbn': [2],
79
79
  'invalid-json': [
80
80
  2,
81
81
  {
82
82
  duplicate: 1,
83
83
  },
84
84
  ],
85
- 'invalid-url': 1,
85
+ 'invalid-url': [1],
86
86
  'lonely-apos': [
87
87
  1,
88
88
  {
@@ -113,7 +113,7 @@ const defaultLintRuleConfig = {
113
113
  // ref: 2,
114
114
  },
115
115
  ],
116
- 'no-arg': 1,
116
+ 'no-arg': [1],
117
117
  'no-duplicate': [
118
118
  2,
119
119
  {
@@ -147,8 +147,8 @@ const defaultLintRuleConfig = {
147
147
  // references: 2,
148
148
  },
149
149
  ],
150
- 'obsolete-attr': 1,
151
- 'obsolete-tag': 1,
150
+ 'obsolete-attr': [1],
151
+ 'obsolete-tag': [1],
152
152
  'parsing-order': [
153
153
  2,
154
154
  {
@@ -166,6 +166,18 @@ const defaultLintRuleConfig = {
166
166
  // td: 1,
167
167
  },
168
168
  ],
169
+ 'required-attr': [
170
+ 2,
171
+ {
172
+ // indicator: 2,
173
+ // langconvert: 2,
174
+ // mapframe: 2,
175
+ // maplink: 2,
176
+ // phonos: 2,
177
+ // section: 2,
178
+ // templatestyles: 2,
179
+ },
180
+ ],
169
181
  'syntax-like': [
170
182
  2,
171
183
  {
@@ -173,7 +185,7 @@ const defaultLintRuleConfig = {
173
185
  // redirect: 2,
174
186
  },
175
187
  ],
176
- 'table-layout': 1,
188
+ 'table-layout': [1],
177
189
  'tag-like': [
178
190
  2,
179
191
  {
@@ -181,17 +193,17 @@ const defaultLintRuleConfig = {
181
193
  invalid: 1,
182
194
  },
183
195
  ],
184
- 'unbalanced-header': 2,
196
+ 'unbalanced-header': [2],
185
197
  'unclosed-comment': [
186
198
  1,
187
199
  {
188
200
  // include: 1,
189
201
  },
190
202
  ],
191
- 'unclosed-quote': 1,
192
- 'unclosed-table': 2,
193
- unescaped: 2,
194
- 'unknown-page': 1,
203
+ 'unclosed-quote': [1],
204
+ 'unclosed-table': [2],
205
+ unescaped: [2],
206
+ 'unknown-page': [1],
195
207
  'unmatched-tag': [
196
208
  1,
197
209
  {
@@ -238,7 +250,7 @@ const defaultLintRuleConfig = {
238
250
  warn: 1,
239
251
  },
240
252
  ],
241
- 'invalid-math': 2,
253
+ 'invalid-math': [2],
242
254
  };
243
255
  const defaultLintConfig = {
244
256
  configurationComment: 'lint',
@@ -255,8 +267,9 @@ const validateSeverity = (severity) => dict.has(severity);
255
267
  * 验证设置值是否符合规范
256
268
  * @param value 设置值
257
269
  */
258
- const validateConfigValue = (value) => validateSeverity(value)
259
- || Array.isArray(value) && validateSeverity(value[0]) && (value.length === 1 || typeof value[1] === 'object');
270
+ const validateConfigValue = (value) => validateSeverity(value) || (Array.isArray(value)
271
+ ? validateSeverity(value[0]) && (value.length === 1 || typeof value[1] === 'object')
272
+ : typeof value === 'object');
260
273
  /**
261
274
  * 设置语法检查规则
262
275
  * @param obj 语法检查设置对象
@@ -273,7 +286,16 @@ const set = (obj, key, value) => {
273
286
  return false;
274
287
  }
275
288
  if (validateConfigValue(value)) {
276
- obj[key] = value;
289
+ if (Array.isArray(value)) {
290
+ obj[key] = value;
291
+ }
292
+ else if (typeof value === 'object') {
293
+ const [base, options = {}] = defaultLintRuleConfig[key];
294
+ obj[key] = [base, { ...clone(options), ...value }];
295
+ }
296
+ else {
297
+ obj[key] = [value];
298
+ }
277
299
  return true;
278
300
  }
279
301
  /* c8 ignore next */
@@ -296,11 +318,8 @@ class LintRuleConfiguration {
296
318
  }
297
319
  /** @implements */
298
320
  getSeverity(rule, key) {
299
- const value = this[rule];
300
- if (typeof value !== 'object') {
301
- return dict.get(value);
302
- }
303
- return key ? dict.get(value[1]?.[key]) ?? dict.get(value[0]) : dict.get(value[0]);
321
+ const [base, options] = this[rule], severity = dict.get(base);
322
+ return key ? dict.get(options?.[key]) ?? severity : severity;
304
323
  }
305
324
  }
306
325
  /** 语法检查设置 */
package/dist/lib/lsp.js CHANGED
@@ -856,21 +856,16 @@ class LanguageService {
856
856
  end: { line: endLine, character: endCol },
857
857
  },
858
858
  severity: severity === 'error' ? 1 : 2,
859
- source:
860
- /* eslint-disable @stylistic/operator-linebreak */
861
- sources[rule] ??
859
+ source: sources[rule] ??
862
860
  'WikiLint',
863
861
  code: code ??
864
- /* eslint-enable @stylistic/operator-linebreak */
865
862
  rule,
866
863
  message,
867
864
  data: [
868
865
  ...fix ? [getQuickFix(root, fix, true)] : [],
869
866
  ...suggestions ? suggestions.map(suggestion => getQuickFix(root, suggestion)) : [],
870
867
  ],
871
- })),
872
- /* eslint-disable @stylistic/operator-linebreak */
873
- cssDiagnostics = stylelint ?
868
+ })), cssDiagnostics = stylelint ?
874
869
  await (async () => {
875
870
  NPM: {
876
871
  const tokens = this.findStyleTokens();
@@ -929,7 +924,6 @@ class LanguageService {
929
924
  return warning ? e : e.filter(({ severity }) => severity === 1);
930
925
  })) :
931
926
  [];
932
- /* eslint-enable @stylistic/operator-linebreak */
933
927
  /* NOT FOR BROWSER ONLY */
934
928
  let lilypondDiagnostics = [];
935
929
  if (this.lilypond) {
@@ -1013,7 +1007,7 @@ class LanguageService {
1013
1007
  * @param text source Wikitext / 源代码
1014
1008
  */
1015
1009
  async provideFoldingRanges(text) {
1016
- const root = await this.#queue(text), { length } = root.getLines(), ranges = [], levels = new Array(6), tokens = root.querySelectorAll('heading-title,table,template,magic-word');
1010
+ const root = await this.#queue(text), { length } = root.getLines(), ranges = [], levels = Array.from({ length: 6 }), tokens = root.querySelectorAll('heading-title,table,template,magic-word');
1017
1011
  for (const token of [...tokens].reverse()) { // 提高 getBoundingClientRect 的性能
1018
1012
  token.getRelativeIndex();
1019
1013
  }
@@ -1397,7 +1391,7 @@ class LanguageService {
1397
1391
  if (!this.data) {
1398
1392
  return undefined;
1399
1393
  }
1400
- const { line, character } = position, curLine = text.split(/\r?\n/u, line + 1)[line], { lastChild } = await this.#queueSignature(`${curLine.slice(0, character + /^[^{}<]*/u.exec(curLine.slice(character))[0].length)}}}`);
1394
+ const { line, character } = position, curLine = text.split(/\r?\n/u, line + 1)[line], { lastChild } = await this.#queueSignature(`${curLine.slice(0, character + (0, common_1.numLeadingSpaces)(curLine.slice(character), /[{}<]|$/u))}}}`);
1401
1395
  if (!lastChild.is('magic-word') || lastChild.length === 1) {
1402
1396
  return undefined;
1403
1397
  }
@@ -1561,7 +1555,7 @@ class LanguageService {
1561
1555
  * @param text source Wikitext / 源代码
1562
1556
  */
1563
1557
  async provideDocumentSymbols(text) {
1564
- const root = await this.#queue(text), lines = root.getLines(), { length } = lines, symbols = [], names = new Set(), sections = new Array(6), tokens = root.querySelectorAll('heading-title');
1558
+ const root = await this.#queue(text), lines = root.getLines(), { length } = lines, symbols = [], names = new Set(), sections = Array.from({ length: 6 }), tokens = root.querySelectorAll('heading-title');
1565
1559
  for (const token of tokens) {
1566
1560
  const { top, height, left, width } = token.getBoundingClientRect(), { level } = token.parentNode;
1567
1561
  for (let i = level - 1; i < 6; i++) {
@@ -1612,7 +1606,7 @@ class LanguageService {
1612
1606
  catch {
1613
1607
  this.config = await index_1.default.fetchConfig(site, `${host}/w`, user);
1614
1608
  }
1615
- Object.assign(this.config, { articlePath: `${host}/wiki/` });
1609
+ this.config.articlePath = `${host}/wiki/`;
1616
1610
  }
1617
1611
  /** @implements */
1618
1612
  [Symbol.dispose]() {
package/dist/lib/node.js CHANGED
@@ -200,7 +200,10 @@ let AstNode = (() => {
200
200
  /** @private */
201
201
  getChildNodes() {
202
202
  const { childNodes } = this;
203
- return Object.isFrozen(childNodes) ? [...childNodes] : childNodes;
203
+ if (Object.isFrozen(childNodes)) {
204
+ return [...childNodes];
205
+ }
206
+ return childNodes;
204
207
  }
205
208
  /** @private */
206
209
  getAttribute(key) {
@@ -636,7 +639,7 @@ let AstNode = (() => {
636
639
  if (this.getRootNode() !== other.getRootNode()) {
637
640
  throw new RangeError('Nodes to be compared are not in the same document!');
638
641
  }
639
- const aAncestors = [...this.getAncestors().reverse(), this], bAncestors = [...other.getAncestors().reverse(), other], depth = aAncestors.findIndex((ancestor, i) => bAncestors[i] !== ancestor), { childNodes } = aAncestors[depth - 1];
642
+ const aAncestors = [...this.getAncestors().toReversed(), this], bAncestors = [...other.getAncestors().toReversed(), other], depth = aAncestors.findIndex((ancestor, i) => bAncestors[i] !== ancestor), { childNodes } = aAncestors[depth - 1];
640
643
  return childNodes.indexOf(aAncestors[depth]) - childNodes.indexOf(bAncestors[depth]);
641
644
  }
642
645
  /** 获取当前节点的相对位置 */
package/dist/lib/text.js CHANGED
@@ -186,7 +186,10 @@ let AstText = (() => {
186
186
  }
187
187
  /** @private */
188
188
  toString(skip) {
189
- return skip && !this.parentNode?.getAttribute('built') ? (0, string_1.removeComment)(this.data) : this.data;
189
+ if (skip && !this.parentNode?.getAttribute('built')) {
190
+ return (0, string_1.removeComment)(this.data);
191
+ }
192
+ return this.data;
190
193
  }
191
194
  /** @private */
192
195
  text() {
@@ -217,7 +220,7 @@ let AstText = (() => {
217
220
  return [];
218
221
  }
219
222
  errorRegex.lastIndex = 0;
220
- const errors = [], nextType = nextSibling?.type, nextName = nextSibling?.name, previousType = previousSibling?.type, root = this.getRootNode(), rootStr = root.toString(), { ext, html, variants } = root.getAttribute('config'), { top, left } = root.posFromIndex(start), { lintConfig } = index_1.default, tagLike = lintConfig.rules['tag-like'], specified = typeof tagLike === 'object' && tagLike[1]
223
+ const errors = [], nextType = nextSibling?.type, nextName = nextSibling?.name, previousType = previousSibling?.type, root = this.getRootNode(), rootStr = root.toString(), { ext, html, variants } = root.getAttribute('config'), { top, left } = root.posFromIndex(start), { lintConfig } = index_1.default, tagLike = lintConfig.rules['tag-like'], specified = tagLike[1]
221
224
  ? new Set(Object.keys(tagLike[1]).filter(tag => tag !== 'invalid' && tag !== 'disallowed'))
222
225
  : new Set(), tags = new Set([
223
226
  'onlyinclude',
package/dist/lib/title.js CHANGED
@@ -136,7 +136,8 @@ class Title {
136
136
  catch /* c8 ignore next */ { }
137
137
  }
138
138
  title = (0, string_1.decodeHtml)(title).replace(/[_ ]+/gu, ' ').trim();
139
- if (subpage || page && trimmed.startsWith('/')) {
139
+ if (subpage
140
+ || page && trimmed.startsWith('/')) {
140
141
  this.#ns = 0;
141
142
  }
142
143
  else {
@@ -206,7 +207,9 @@ class Title {
206
207
  #getTitle(prefix, redirect = true) {
207
208
  let title = (prefix + this.main).replace(/ /gu, '_');
208
209
  if (title.startsWith('/')) {
209
- title = (this.page ?? '') + title.replace(/(.)\/$/u, '$1');
210
+ title =
211
+ (this.page ?? '') +
212
+ title.replace(/(.)\/$/u, '$1');
210
213
  }
211
214
  else if (title.startsWith('../') && this.page?.includes('/')) {
212
215
  const [level, sub] = resolve(title), dirs = this.page.split('/');
@@ -237,7 +240,7 @@ class Title {
237
240
  getRedirection() {
238
241
  const { prefix,
239
242
  /* NOT FOR BROWSER */
240
- main, interwiki, } = this, pre = interwiki + (interwiki && ':') + // eslint-disable-line @stylistic/operator-linebreak
243
+ main, interwiki, } = this, pre = interwiki + (interwiki && ':') +
241
244
  prefix, result = this.#getTitle(pre);
242
245
  /* NOT FOR BROWSER */
243
246
  if (result[0]) {
@@ -338,7 +341,8 @@ class Title {
338
341
  autoConvert() {
339
342
  const { conversionTable } = this;
340
343
  if (conversionTable.size > 0) {
341
- const regex = new RegExp([...conversionTable.keys()].sort((a, b) => b.localeCompare(a)).map(string_1.escapeRegExp).join('|'), 'gu');
344
+ const regex = new RegExp([...conversionTable.keys()].toSorted((a, b) => b.localeCompare(a)).map(string_1.escapeRegExp)
345
+ .join('|'), 'gu');
342
346
  this.main = this.main.replace(regex, p => conversionTable.get(p));
343
347
  }
344
348
  }
@@ -25,9 +25,9 @@ const elementLike = (constructor) => {
25
25
  /* NOT FOR BROWSER END */
26
26
  #getCondition(selector) {
27
27
  return (0, selector_1.getCondition)(selector,
28
- // eslint-disable-next-line unicorn/no-negated-condition, @stylistic/operator-linebreak
28
+ // eslint-disable-next-line unicorn/no-negated-condition
29
29
  !('type' in this) ?
30
- undefined : // eslint-disable-line @stylistic/operator-linebreak
30
+ undefined :
31
31
  this);
32
32
  }
33
33
  getElementBy(condition) {
@@ -96,7 +96,8 @@ const parseCommentAndExt = (wikitext, config, accum, includeOnly) => {
96
96
  ch = 'c';
97
97
  const closed = substr.endsWith('-->');
98
98
  // @ts-expect-error abstract class
99
- new comment_1.CommentToken((0, string_1.restore)(substr, accum, 1).slice(4, closed ? -3 : undefined), closed, config, accum);
99
+ new comment_1.CommentToken((0, string_1.restore)(substr, accum, 1)
100
+ .slice(4, closed ? -3 : undefined), closed, config, accum);
100
101
  }
101
102
  else if (include) {
102
103
  // @ts-expect-error abstract class
@@ -99,7 +99,7 @@ const expand = (wikitext, page, callPage, config, include, context, now = index_
99
99
  if (!/\0\d+g\x7F/u.test(data)) {
100
100
  continue;
101
101
  }
102
- const expanded = data.replace(/\0(\d+)g\x7F/gu, (_, i) => {
102
+ const expanded = data.replaceAll(/\0(\d+)g\x7F/gu, (_, i) => {
103
103
  const target = accum[i];
104
104
  if (target.type === 'onlyinclude') {
105
105
  clean(accum, target);
@@ -107,7 +107,7 @@ const expand = (wikitext, page, callPage, config, include, context, now = index_
107
107
  }
108
108
  const { lastChild } = target;
109
109
  clean(accum, lastChild);
110
- return lastChild.firstChild.toString().replace(/\0(\d+)c\x7F[\n ]|\0(\d+)n\x7F|^\n|\n$/gu, (m, p1, p2) => {
110
+ return lastChild.firstChild.toString().replaceAll(/\0(\d+)c\x7F[\n ]|\0(\d+)n\x7F|^\n|\n$/gu, (m, p1, p2) => {
111
111
  if (p1 !== undefined) {
112
112
  const { innerText } = accum[p1];
113
113
  return /^T:[^_/\n<>~]+$/u.test(innerText) ? '' : m;
@@ -132,7 +132,7 @@ const expand = (wikitext, page, callPage, config, include, context, now = index_
132
132
  if (!/\0\d+[tm!{}+~-]\x7F/u.test(data)) {
133
133
  continue;
134
134
  }
135
- const expanded = data.replace(/([^\x7F]?)\0(\d+)[tm!{}+~-]\x7F/gu, (m, prev, i) => {
135
+ const expanded = data.replaceAll(/([^\x7F]?)\0(\d+)[tm!{}+~-]\x7F/gu, (m, prev, i) => {
136
136
  const target = accum[i], { type, name, length, firstChild: f, childNodes } = target, isTemplate = type === 'template', args = childNodes.slice(1);
137
137
  if (type === 'arg') {
138
138
  const arg = (0, string_1.removeCommentLine)(f.toString()).trim();
@@ -1,5 +1,4 @@
1
1
  "use strict";
2
- /* eslint @stylistic/operator-linebreak: [2, "before", {overrides: {"=": "after"}}] */
3
2
  Object.defineProperty(exports, "__esModule", { value: true });
4
3
  exports.renderExt = void 0;
5
4
  const constants_1 = require("../util/constants");
@@ -12,12 +11,25 @@ const string_1 = require("../util/string");
12
11
  const renderExt = (token, opt) => {
13
12
  const { name, firstChild, lastChild } = token;
14
13
  switch (name) {
14
+ case 'nowiki': {
15
+ const html = lastChild.toHtmlInternal();
16
+ return token.closest('ext-inner')?.name === 'poem' ? html : (0, string_1.newline)(html);
17
+ }
18
+ case 'pre': {
19
+ const html = lastChild.toHtmlInternal({
20
+ ...opt,
21
+ nowrap: false,
22
+ });
23
+ return `<pre${firstChild.toHtmlInternal()}>${token.closest('ext-inner')?.name === 'poem' ? html : (0, string_1.newline)(html)}</pre>`;
24
+ }
25
+ case 'langconvert':
26
+ return lastChild.toHtmlInternal({ ...opt, nowrap: true });
15
27
  case 'poem': {
16
28
  const padding = firstChild.hasAttr('compact') ? '' : '\n';
17
29
  firstChild.classList.add('poem');
18
30
  return `<div${firstChild.toHtmlInternal()}>${padding}${lastChild.toHtmlInternal({ ...opt, nowrap: false })
19
- .replace(/(?<!^|<hr>)\n(?!$)/gu, '<br>\n')
20
- .replace(/^ +/gmu, p => '&nbsp;'.repeat(p.length))
31
+ .replaceAll(/(?<!^|<hr>)\n(?!$)/gu, '<br>\n')
32
+ .replaceAll(/^ +/gmu, p => '&nbsp;'.repeat(p.length))
21
33
  .trim()}${padding}</div>`;
22
34
  }
23
35
  case 'gallery': {
@@ -150,21 +150,24 @@ function urlFunction(config, args, local) {
150
150
  title.ns = 6;
151
151
  title.fragment = undefined;
152
152
  }
153
- const link = title.getUrl(config.testArticlePath), protocol = link.startsWith('//') ? 'https:' : '';
153
+ const { articlePath, server = '' } = config, path = articlePath && (/^\/(?!\/)/u.test(articlePath) ? server : '') + articlePath, link = title.getUrl(path), protocol = link.startsWith('//') ? 'https:' : '';
154
154
  try {
155
155
  const url = new URL(protocol + link);
156
- url.search = query ? `?${query.replace(/\s/gu, '_')}` : '';
156
+ url.search = query ? `?${query.replaceAll(/\s/gu, '_')}` : '';
157
157
  return local ? url : [url, protocol];
158
158
  }
159
159
  catch (e) {
160
160
  if (local) {
161
161
  title.fragment = undefined;
162
- return title.getUrl(config.testArticlePath) + (query ? `?${query}` : '');
162
+ return title.getUrl(path) + (query ? `?${query}` : '');
163
163
  }
164
164
  throw e;
165
165
  }
166
166
  }
167
- const parseUrl = ({ testServer = '', articlePath = testServer }) => {
167
+ const parseUrl = ({ server = '', articlePath = '' }) => {
168
+ if (/^\/(?!\/)/u.test(articlePath)) {
169
+ articlePath = server + articlePath;
170
+ }
168
171
  let offset = 0;
169
172
  if (articlePath.startsWith('//')) {
170
173
  offset = 6;
@@ -197,8 +200,8 @@ const parseUrl = ({ testServer = '', articlePath = testServer }) => {
197
200
  '3B': ';',
198
201
  40: '@',
199
202
  '7E': '~',
200
- }, strip = (s) => s.replace(/\0\d+.\x7F/gu, ''), wfUrlencode = (s) => encodeURIComponent(s.replaceAll(' ', '_'))
201
- .replace(/%(2[01489ACF]|3B|40|7E)/gu, (_, p) => dictUrl[p]), localurl = (config, args) => {
203
+ }, strip = (s) => s.replaceAll(/\0\d+.\x7F/gu, ''), wfUrlencode = (s) => encodeURIComponent(s.replaceAll(' ', '_'))
204
+ .replaceAll(/%(2[01489ACF]|3B|40|7E)/gu, (_, p) => dictUrl[p]), localurl = (config, args) => {
202
205
  const url = urlFunction(config, args, true);
203
206
  return typeof url === 'string' ? url : url.pathname + url.search;
204
207
  }, fullurl = (config, args) => {
@@ -246,7 +249,7 @@ const parseUrl = ({ testServer = '', articlePath = testServer }) => {
246
249
  if (!text) {
247
250
  return '';
248
251
  }
249
- let output = `\n${text}`.replace(/["&'=;_]|!!|__|:\/\/|~{3}|\n(?:[!#*:]|-{4})|(?:ISBN|PMID|RFC) /gu, m => dictHtml1[m]).slice(1);
252
+ let output = `\n${text}`.replaceAll(/["&'=;_]|!!|__|:\/\/|~{3}|\n(?:[!#*:]|-{4})|(?:ISBN|PMID|RFC) /gu, m => dictHtml1[m]).slice(1);
250
253
  output = output.charAt(0).replace(/[+_~-]/u, m => dictHtml2[m])
251
254
  + output.slice(1);
252
255
  output = output.slice(0, -1)
@@ -334,7 +337,7 @@ const expandMagicWord = (name, args, page = '', config = index_1.default.getConf
334
337
  case 'currentweek':
335
338
  return currentWeek(now, new Date(Date.UTC(now.getUTCFullYear(), 0, 1)));
336
339
  case 'currenttimestamp':
337
- return now.toISOString().slice(0, 19).replace(/[-:T]/gu, '');
340
+ return now.toISOString().slice(0, 19).replaceAll(/[-:T]/gu, '');
338
341
  case 'localyear':
339
342
  return localYear(now);
340
343
  case 'localmonth':
@@ -552,10 +555,10 @@ const expandMagicWord = (name, args, page = '', config = index_1.default.getConf
552
555
  case 'padright':
553
556
  return pad(args, 'padEnd');
554
557
  case 'anchorencode':
555
- return anchorencode((0, html_1.getId)(strip(arg0).replace(/\[\[([^[]+?)\]\]/gu, (_, p) => {
558
+ return anchorencode((0, html_1.getId)(strip(arg0).replaceAll(/\[\[([^[]+?)\]\]/gu, (_, p) => {
556
559
  const i = p.indexOf('|');
557
560
  return i <= 0 || i === p.length - 1 ? p : p.slice(i + 1);
558
- }))).replace(/%(?=[\da-f]{2})/giu, '%25');
561
+ }))).replaceAll(/%(?=[\da-f]{2})/giu, '%25');
559
562
  case 'special':
560
563
  return special(target, config);
561
564
  case 'speciale':
@@ -17,7 +17,10 @@ export type AttributeTypes = 'ext-attr' | 'html-attr' | 'table-attr';
17
17
  export declare abstract class AttributeToken extends Token {
18
18
  #private;
19
19
  readonly name: string;
20
- readonly childNodes: readonly [AtomToken, Token];
20
+ readonly childNodes: readonly [
21
+ AtomToken,
22
+ Token
23
+ ];
21
24
  abstract get firstChild(): AtomToken;
22
25
  abstract get lastChild(): Token;
23
26
  abstract get parentNode(): AttributesToken | undefined;
@@ -162,9 +162,10 @@ let AttributeToken = (() => {
162
162
  valueToken.setAttribute('stage', 1);
163
163
  }
164
164
  else {
165
- valueToken = new atom_1.AtomToken(value, 'attr-value', config, accum, {
166
- [`Stage-${exports.stages[type]}`]: ':',
167
- });
165
+ valueToken =
166
+ new atom_1.AtomToken(value, 'attr-value', config, accum, {
167
+ [`Stage-${exports.stages[type]}`]: ':',
168
+ });
168
169
  }
169
170
  super(undefined, config, accum);
170
171
  this.#type = type;
@@ -278,7 +279,7 @@ let AttributeToken = (() => {
278
279
  }
279
280
  else if (typeof value === 'string' && ((/^xmlns:[\w:.-]+$/u.test(name) || urlAttrs.has(name)) && evil.test(value)
280
281
  || simple
281
- && (name === 'href' || tag === 'img' && name === 'src')
282
+ && (name === 'href' || type === 'ext-attr' && tag === 'img' && name === 'src')
282
283
  && !new RegExp(String.raw `^(?:${this.getAttribute('config').protocol}|//)\S+$`, 'iu')
283
284
  .test(value))) {
284
285
  /* PRINT ONLY */
@@ -73,6 +73,15 @@ const wordRegex = /* #__PURE__ */ (() => {
73
73
  }
74
74
  /* c8 ignore stop */
75
75
  })();
76
+ const required = new Map([
77
+ ['indicator', ['name']],
78
+ ['langconvert', ['from', 'to']],
79
+ ['mapframe', ['width', 'height']],
80
+ ['maplink', ['width', 'height']],
81
+ ['phonos', [['ipa', 'file', 'wikibase']]],
82
+ ['section', [['begin', 'end']]],
83
+ ['templatestyles', ['src']],
84
+ ]);
76
85
  /**
77
86
  * attributes of extension and HTML tags
78
87
  *
@@ -174,16 +183,15 @@ let AttributesToken = (() => {
174
183
  let out = '', mt = regex.exec(attr), lastIndex = 0;
175
184
  const insertDirty = /** 插入无效属性 */ () => {
176
185
  if (out) {
177
- super.insertAt(new atom_1.AtomToken(out, toDirty(type), config, accum, {
178
- [`Stage-${stages[type]}`]: ':',
179
- }));
186
+ super.insertAt(new atom_1.AtomToken(out, toDirty(type), config, accum, { [`Stage-${stages[type]}`]: ':' }));
180
187
  out = '';
181
188
  }
182
189
  };
183
190
  while (mt) {
184
191
  const { index, 0: full, 1: key, 2: equal, 3: quoteStart, 4: quoted, 5: quoteEnd, 6: unquoted } = mt;
185
192
  out += attr.slice(lastIndex, index);
186
- if (/^(?:[\w:]|\0\d+t\x7F)(?:[\w:.-]|\0\d+t\x7F)*$/u.test((0, string_1.removeComment)(key).trim())) {
193
+ if (/^(?:[\w:]|\0\d+t\x7F)(?:[\w:.-]|\0\d+t\x7F)*$/u
194
+ .test((0, string_1.removeComment)(key).trim())) {
187
195
  const value = quoted ?? unquoted, quotes = [quoteStart, quoteEnd],
188
196
  // @ts-expect-error abstract class
189
197
  token = new attribute_1.AttributeToken((0, exports.toAttributeType)(type), name, key, quotes, config, equal, value, accum);
@@ -255,7 +263,7 @@ let AttributesToken = (() => {
255
263
  /** @private */
256
264
  lint(start = this.getAbsoluteIndex(), re) {
257
265
  LINT: {
258
- const errors = super.lint(start, re), { parentNode, childNodes } = this, attrs = new Map(), duplicated = new Set(), rect = new rect_1.BoundingRect(this, start), rules = ['no-ignored', 'no-duplicate'], { lintConfig } = index_1.default, { computeEditInfo, fix } = lintConfig, s = ['closingTag', 'invalidAttributes', 'nonWordAttributes']
266
+ const errors = super.lint(start, re), { parentNode, childNodes, type, name: tag } = this, attrs = new Map(), duplicated = new Set(), rect = new rect_1.BoundingRect(this, start), rules = ['no-ignored', 'required-attr', 'no-duplicate'], { lintConfig } = index_1.default, { computeEditInfo, fix } = lintConfig, s = ['closingTag', 'invalidAttributes', 'nonWordAttributes']
259
267
  .map(k => lintConfig.getSeverity(rules[0], k));
260
268
  if (s[0] && this.#lint()) {
261
269
  const e = (0, lint_1.generateForSelf)(this, rect, rules[0], 'attributes-of-closing-tag', s[0]);
@@ -290,7 +298,21 @@ let AttributesToken = (() => {
290
298
  }
291
299
  }
292
300
  }
293
- const severity = lintConfig.getSeverity(rules[1], 'attribute');
301
+ if (type === 'ext-attrs' && required.has(tag)) {
302
+ const severity = lintConfig.getSeverity(rules[1], tag);
303
+ if (severity) {
304
+ for (const key of required.get(tag)) {
305
+ const keys = typeof key === 'string' ? [key] : key, missing = keys.every(k => {
306
+ const value = this.getAttr(k);
307
+ return value === true || !value;
308
+ });
309
+ if (missing) {
310
+ errors.push((0, lint_1.generateForSelf)(this, rect, rules[1], index_1.default.msg('required-attribute', keys.join('/')), severity));
311
+ }
312
+ }
313
+ }
314
+ }
315
+ const severity = lintConfig.getSeverity(rules[2], 'attribute');
294
316
  if (severity && duplicated.size > 0) {
295
317
  for (const key of duplicated) {
296
318
  const pairs = attrs.get(key).map(attr => {
@@ -298,7 +320,7 @@ let AttributesToken = (() => {
298
320
  return [attr, value === true ? '' : value];
299
321
  });
300
322
  Array.prototype.push.apply(errors, pairs.map(([attr, value], i) => {
301
- const e = (0, lint_1.generateForChild)(attr, rect, rules[1], index_1.default.msg('duplicate-attribute', key), severity);
323
+ const e = (0, lint_1.generateForChild)(attr, rect, rules[2], index_1.default.msg('duplicate-attribute', key), severity);
302
324
  if (computeEditInfo || fix) {
303
325
  const remove = (0, lint_1.fixByRemove)(e);
304
326
  if (!value || pairs.slice(0, i).some(([, v]) => v === value)) {
@@ -38,6 +38,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
38
38
  };
39
39
  Object.defineProperty(exports, "__esModule", { value: true });
40
40
  exports.HeadingToken = void 0;
41
+ const common_1 = require("@bhsd/common");
41
42
  const lint_1 = require("../util/lint");
42
43
  const debug_1 = require("../util/debug");
43
44
  const rect_1 = require("../lib/rect");
@@ -170,10 +171,14 @@ let HeadingToken = (() => {
170
171
  //
171
172
  }
172
173
  else if (unbalancedStart) {
173
- const [extra] = /^=+/u.exec(innerStr), newLevel = level + extra.length;
174
- e.suggestions = [{ desc: `h${level}`, range: [e.startIndex, e.startIndex + extra.length], text: '' }];
174
+ const extra = (0, common_1.numLeadingSpaces)(innerStr, /[^=]|$/u), newLevel = level + extra;
175
+ e.suggestions = [{ desc: `h${level}`, range: [e.startIndex, e.startIndex + extra], text: '' }];
175
176
  if (newLevel < 7) {
176
- e.suggestions.push({ desc: `h${newLevel}`, range: [e.endIndex, e.endIndex], text: extra });
177
+ e.suggestions.push({
178
+ desc: `h${newLevel}`,
179
+ range: [e.endIndex, e.endIndex],
180
+ text: '='.repeat(extra),
181
+ });
177
182
  }
178
183
  }
179
184
  else {
@@ -12,7 +12,7 @@ import type { Title } from '../../lib/title';
12
12
  export declare abstract class LinkBaseToken extends Token {
13
13
  #private;
14
14
  readonly name: string;
15
- abstract get type(): 'link' | 'category' | 'file' | 'gallery-image' | 'imagemap-image' | 'redirect-target' | 'ext-inner';
15
+ abstract get type(): 'gallery-image' | 'link' | 'category' | 'file' | 'redirect-target' | 'ext-inner' | 'imagemap-image';
16
16
  readonly childNodes: readonly [AtomToken, ...Token[]];
17
17
  abstract get firstChild(): AtomToken;
18
18
  abstract get lastChild(): Token;