├── test
├── templates
│ ├── Main_Page.wiki
│ ├── MediaWiki꞉Ok.wiki
│ ├── Template꞉''.wiki
│ ├── Template꞉1x.wiki
│ ├── Template꞉Blank.wiki
│ ├── Template꞉Foo.wiki
│ ├── Template꞉Just_tr.wiki
│ ├── Template꞉Pipe.wiki
│ ├── Template꞉Tbl-end.wiki
│ ├── Template꞉Bullet.wiki
│ ├── Template꞉Emptytemplate.wiki
│ ├── Template꞉Foo–bar.wiki
│ ├── Template꞉Loop1.wiki
│ ├── Template꞉Loop2.wiki
│ ├── Template꞉Tbl-start.wiki
│ ├── MediaWiki꞉Fake.wiki
│ ├── MediaWiki꞉Mainpage.wiki
│ ├── Template꞉=.wiki
│ ├── Template꞉Inner_list.wiki
│ ├── Template꞉Linktest2.wiki
│ ├── Template꞉Pipe_page.wiki
│ ├── Template꞉Safesubst.wiki
│ ├── Template꞉Temp%3F.wiki
│ ├── Template꞉Templa.wiki
│ ├── MediaWiki꞉History_short.wiki
│ ├── MediaWiki꞉Loop1.wiki
│ ├── MediaWiki꞉Loop2.wiki
│ ├── MediaWiki꞉T34057.wiki
│ ├── Template꞉Precedence5.wiki
│ ├── Template꞉Quote.wiki
│ ├── Template꞉Templatesimple.wiki
│ ├── Template꞉1x_with_div.wiki
│ ├── Template꞉Blank_param.wiki
│ ├── Template꞉Linktest.wiki
│ ├── Template꞉Refinref.wiki
│ ├── Template꞉Test.wiki
│ ├── Template꞉With_a_section%3F.wiki
│ ├── MediaWiki꞉New-messages.wiki
│ ├── Subpage_test
│ │ └── L1
│ │ │ └── L2
│ │ │ └── L3Sibling.wiki
│ ├── Template꞉Paramtestnum.wiki
│ ├── Template꞉Templateasargtest2.wiki
│ ├── Template꞉Templateasargtestnum.wiki
│ ├── Template꞉With꞉_Colon.wiki
│ ├── Template꞉Map-one-parameter.wiki
│ ├── Template꞉One-parameter.wiki
│ ├── Template꞉T290526.wiki
│ ├── Template꞉Identical.wiki
│ ├── Template꞉Includes2.wiki
│ ├── Template꞉Nested_special.wiki
│ ├── Template꞉Sections.wiki
│ ├── Template꞉Table_cell_content.wiki
│ ├── Template꞉Template_with_pagename.wiki
│ ├── Template꞉Templateasargtest.wiki
│ ├── Template꞉Scribunto_all_args.wiki
│ ├── Template꞉Table.wiki
│ ├── Template꞉SubstTest.wiki
│ ├── Template꞉Templateasargtestdefault.wiki
│ ├── Template꞉Dangerous.wiki
│ ├── Template꞉Dangerous_attribute.wiki
│ ├── Template꞉Div_style.wiki
│ ├── Template꞉Paramtest.wiki
│ ├── Template꞉Scribunto_frame_caching.wiki
│ ├── Template꞉Complextemplate.wiki
│ ├── Template꞉SafeSubstTest.wiki
│ ├── Template꞉Table_attribs.wiki
│ ├── Template꞉Includes.wiki
│ ├── Template꞉Paramtest2.wiki
│ ├── Template꞉Table_attribs_4.wiki
│ ├── Template꞉Table_attribs_5.wiki
│ ├── Template꞉Dangerous_style_attribute.wiki
│ ├── Template꞉Includes3.wiki
│ ├── Template꞉Table_attribs_6.wiki
│ ├── Template꞉Includes4.wiki
│ ├── Template꞉Mixed_attr_content_template.wiki
│ ├── Template꞉Table_attribs_3.wiki
│ ├── Template꞉Table_cells.wiki
│ ├── Template꞉Includeonly_section.wiki
│ ├── Template꞉Table_attribs_2.wiki
│ ├── Template꞉Image_attribs.wiki
│ ├── Template꞉Preprocessor_precedence_9.wiki
│ ├── Template꞉Table_header_cells.wiki
│ ├── Template꞉Mixed_attr_content_template_2.wiki
│ └── Template꞉Preprocessor_precedence_10.wiki
├── stylelint.ts
├── hooks.ts
├── core
│ ├── pfeqParserTests.txt
│ ├── parserFunctionTests.txt
│ ├── stringFunctionTests.txt
│ ├── extTags.txt
│ └── badCharacters.txt
├── prof.ts
├── real.ts
├── single.ts
└── parserTests.ts
├── printed
└── README
├── .mocharc.json
├── errors
└── README
├── sed.sh
├── tsconfig.dist.json
├── extensions
├── tsconfig.json
└── typings.d.ts
├── bin
└── config.js
├── .gitignore
├── diff.sh
├── .markdownlint.json
├── util
├── search.ts
├── constants.ts
├── selector.ts
└── diff.ts
├── typings
├── parser.d.ts
├── event.d.ts
├── node.d.ts
└── index.d.ts
├── mixin
├── gapped.ts
├── noEscape.ts
├── readOnly.ts
├── clone.ts
├── padded.ts
├── cached.ts
├── singleLine.ts
├── fixed.ts
├── hidden.ts
├── sol.ts
├── nodeLike.ts
└── syntax.ts
├── src
├── nowiki
│ ├── dd.ts
│ ├── hr.ts
│ ├── commentLine.ts
│ ├── base.ts
│ ├── list.ts
│ ├── doubleUnderscore.ts
│ ├── noinclude.ts
│ └── comment.ts
├── hidden.ts
├── multiLine
│ ├── inputbox.ts
│ ├── index.ts
│ └── paramTag.ts
├── syntax.ts
├── onlyinclude.ts
├── atom.ts
├── link
│ ├── category.ts
│ ├── categorytree.ts
│ ├── redirectTarget.ts
│ └── index.ts
├── commented.ts
├── imagemapLink.ts
├── table
│ ├── tr.ts
│ └── base.ts
├── paramLine.ts
├── pre.ts
├── tagPair
│ ├── index.ts
│ └── include.ts
├── redirect.ts
└── tag
│ └── tvar.ts
├── script
├── util.ts
├── coverage.ts
├── toc.ts
└── declaration.ts
├── tsconfig.json
├── lib
├── rect.ts
├── redirectMap.ts
├── attributes.ts
└── ranges.ts
├── coverage
└── badge.svg
├── .github
└── workflows
│ ├── node.js.yml
│ └── codeql.yml
├── bump.sh
├── parser
├── redirect.ts
├── html.ts
├── externalLinks.ts
├── converter.ts
├── hrAndDoubleUnderscore.ts
├── magicLinks.ts
├── quotes.ts
├── list.ts
└── links.ts
├── data
├── .schema.json
└── ext
│ └── ThirdPartyNotices.txt
├── i18n
├── zh-hans.json
├── zh-hant.json
└── en.json
├── internal.ts
├── addon
└── link.ts
├── render
└── html.ts
├── README-(ZH).md
└── eslint.config.mjs
/test/templates/Main_Page.wiki:
--------------------------------------------------------------------------------
1 | blah blah
--------------------------------------------------------------------------------
/test/templates/MediaWiki꞉Ok.wiki:
--------------------------------------------------------------------------------
1 | OK
--------------------------------------------------------------------------------
/test/templates/Template꞉''.wiki:
--------------------------------------------------------------------------------
1 | bar
--------------------------------------------------------------------------------
/test/templates/Template꞉1x.wiki:
--------------------------------------------------------------------------------
1 | {{{1}}}
--------------------------------------------------------------------------------
/test/templates/Template꞉Blank.wiki:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/templates/Template꞉Foo.wiki:
--------------------------------------------------------------------------------
1 | FOO
--------------------------------------------------------------------------------
/test/templates/Template꞉Just_tr.wiki:
--------------------------------------------------------------------------------
1 | |-
--------------------------------------------------------------------------------
/test/templates/Template꞉Pipe.wiki:
--------------------------------------------------------------------------------
1 | |
--------------------------------------------------------------------------------
/test/templates/Template꞉Tbl-end.wiki:
--------------------------------------------------------------------------------
1 | |}
--------------------------------------------------------------------------------
/test/templates/Template꞉Bullet.wiki:
--------------------------------------------------------------------------------
1 | *Bar
--------------------------------------------------------------------------------
/test/templates/Template꞉Emptytemplate.wiki:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/templates/Template꞉Foo–bar.wiki:
--------------------------------------------------------------------------------
1 | foo
--------------------------------------------------------------------------------
/test/templates/Template꞉Loop1.wiki:
--------------------------------------------------------------------------------
1 | {{loop2}}
--------------------------------------------------------------------------------
/test/templates/Template꞉Loop2.wiki:
--------------------------------------------------------------------------------
1 | {{loop1}}
--------------------------------------------------------------------------------
/test/templates/Template꞉Tbl-start.wiki:
--------------------------------------------------------------------------------
1 | {|
--------------------------------------------------------------------------------
/test/stylelint.ts:
--------------------------------------------------------------------------------
1 | export default undefined;
2 |
--------------------------------------------------------------------------------
/test/templates/MediaWiki꞉Fake.wiki:
--------------------------------------------------------------------------------
1 | ==header==
--------------------------------------------------------------------------------
/test/templates/MediaWiki꞉Mainpage.wiki:
--------------------------------------------------------------------------------
1 | Main Page
--------------------------------------------------------------------------------
/test/templates/Template꞉=.wiki:
--------------------------------------------------------------------------------
1 |
This uses =. 17 |
18 | !! end 19 | 20 | !! test 21 | Using {{Template:=}} doesn't invoke the parser function 22 | !! wikitext 23 | This uses {{Template:=}}. 24 | !! html 25 |This uses no. 26 |
27 | !! end -------------------------------------------------------------------------------- /mixin/readOnly.ts: -------------------------------------------------------------------------------- 1 | import {Shadow} from '../util/debug'; 2 | import {mixins} from '../util/constants'; 3 | import Parser from '../index'; 4 | 5 | /** 6 | * 只读或可写的方法 7 | * @param readonly 是否只读 8 | */ 9 | export const readOnly = (readonly = false) => 10 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 11 | (method: Function) => function(this: unknown, ...args: unknown[]): any { 12 | const {viewOnly} = Parser; 13 | if (!Shadow.running) { 14 | Parser.viewOnly = readonly; 15 | } 16 | const result = method.apply(this, args); 17 | if (!Shadow.running) { 18 | Parser.viewOnly = viewOnly; 19 | } 20 | return result; 21 | }; 22 | 23 | mixins['readOnly'] = __filename; 24 | -------------------------------------------------------------------------------- /mixin/clone.ts: -------------------------------------------------------------------------------- 1 | import {Shadow} from '../util/debug'; 2 | import {mixins} from '../util/constants'; 3 | import type {Token} from '../internal'; 4 | 5 | /** 6 | * 深拷贝节点 7 | * @param method 方法 8 | */ 9 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 10 | export const clone = (method: (this: Token) => Token) => function(this: Token): any { 11 | const cloned = this.cloneChildNodes(), 12 | {type, name} = this; 13 | return Shadow.run(() => { 14 | const newToken = method.call(this); 15 | newToken.safeAppend(cloned); 16 | if (type === 'ext-inner' && name) { 17 | newToken.setAttribute('name', name); 18 | } 19 | return newToken; 20 | }); 21 | }; 22 | 23 | mixins['clone'] = __filename; 24 | -------------------------------------------------------------------------------- /mixin/padded.ts: -------------------------------------------------------------------------------- 1 | import {mixin} from '../util/debug'; 2 | 3 | /* NOT FOR BROWSER */ 4 | 5 | import {mixins} from '../util/constants'; 6 | 7 | /* NOT FOR BROWSER END */ 8 | 9 | /** 10 | * 给定 padding 的类 11 | * @param padding padding 字符串 12 | * @param padding.length 13 | */ 14 | export const padded = ({length}: string) =>[1] 15 | false or error 16 |
17 |0 20 | 1 21 | 4 22 |
23 | !! end 24 | 25 | !! test 26 | #urldecode 27 | !! wikitext 28 | {{#urldecode:}} 29 | {{#urldecode:foo%20bar}} 30 | {{#urldecode:%D0%9C%D0%B5%D0%B4%D0%B8%D0%B0%D0%92%D0%B8%D0%BA%D0%B8}} 31 | {{#urldecode: some unescaped string}} 32 | !! html 33 |foo bar 34 | МедиаВики 35 | some unescaped string 36 |
37 | !! end 38 | 39 | !! test 40 | #pos 41 | !! wikitext 42 | {{#pos:Žmržlina|žlina}} 43 | {{#pos:stringstring|str|4}} 44 | !! html 45 |3 46 | 6 47 |
48 | !! end 49 | 50 | !! test 51 | #pos with too large offset (T209600) 52 | !! wikitext 53 | {{#pos:FooBar|Foo|32}} 54 | !! html 55 | 56 | !! end -------------------------------------------------------------------------------- /coverage/badge.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /script/toc.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | 4 | const {argv: [,, filename]} = process; 5 | if (!filename) { 6 | throw new RangeError('请指定文档文件!'); 7 | } 8 | const fullpath = path.resolve('wiki', `${filename}.md`), 9 | isEnglish = filename.endsWith('-(EN)'); 10 | if (!fs.existsSync(fullpath)) { 11 | throw new RangeError(`文档 ${filename}.md 不存在!`); 12 | } 13 | 14 | const content = fs.readFileSync(fullpath, 'utf8'); 15 | if (/^- \[[^\]]+\]\(#[^)]+\)$/mu.test(content)) { 16 | throw new Error(`文档 ${filename}.md 中已包含目录!`); 17 | } 18 | 19 | const toc = content.split('\n').filter(line => line.startsWith('#')).map(line => line.replace( 20 | /^(#+)\s+(\S.*)$/u, 21 | (_, {length}: string, title: string) => `${'\t'.repeat(length - 1)}- [${title}](#${ 22 | title.toLowerCase().replace(/[ .]/gu, m => m === ' ' ? '-' : '') 23 | })`, 24 | )).join('\n'); 25 | 26 | fs.writeFileSync(fullpath, `abc 14 |
15 | !! end 16 | 17 | !! test 18 | Extension tags in extension content:
19 | !! wikitext
20 | {{#tag:pre|ab c}}
21 | !! html
22 | abc
23 | !! end
24 |
25 | !! test
26 | Extension tags in extension content:
27 | !! wikitext
28 | {{#tag:indicator|ab c|name=foo}}
29 | !! options
30 | showindicators
31 | !! metadata
32 | foo=abc
33 | !! html
34 | !! end
35 |
36 | !! test
37 | Extension tags in extension attributes:
38 | !! wikitext
39 | {{#tag:pre|content|class=ab c}}
40 | !! html
41 | content
42 | !! end
43 |
44 | !! test
45 | Extension tags in extension attributes:
46 | !! wikitext
47 | {{#tag:indicator|content|name=ab c}}
48 | !! options
49 | showindicators
50 | !! metadata
51 | ac=content
52 | !! html
53 | !! end
54 |
55 | !! test
56 | Extension tags in extension attributes: bogus tag name
57 | !! wikitext
58 | {{#tag:does-not-exist|content |name=ab c}}
59 | !! html
60 | <does-not-exist name="ac">content</does-not-exist>
61 |
62 | !! end
63 |
64 | !! test
65 | Extension tags in extension name
66 | !! wikitext
67 | {{#tag:noignored wiki|content}}
68 | !! html
69 | content
70 |
71 | !! end
72 |
73 | !! test
74 | Doubly-nested tags
75 | !! wikitext
76 | {{#tag:nowiki|more {{#tag:nowiki|content here }}}}
77 | !! html
78 | more content here
79 |
80 | !! end
--------------------------------------------------------------------------------
/src/syntax.ts:
--------------------------------------------------------------------------------
1 | import {Token} from './index';
2 | import type {Config, LintError} from '../base';
3 |
4 | /* NOT FOR BROWSER */
5 |
6 | import {classes} from '../util/constants';
7 | import {clone} from '../mixin/clone';
8 | import {syntax} from '../mixin/syntax';
9 | import type {SyntaxBase} from '../mixin/syntax';
10 |
11 | export interface SyntaxToken extends SyntaxBase {}
12 |
13 | /* NOT FOR BROWSER END */
14 |
15 | declare type SyntaxTypes = 'heading-trail'
16 | | 'magic-word-name'
17 | | 'table-syntax'
18 | | 'redirect-syntax'
19 | | 'translate-attr'
20 | | 'tvar-name';
21 |
22 | /**
23 | * plain token that satisfies specific grammar syntax
24 | *
25 | * 满足特定语法格式的plain Token
26 | */
27 | @syntax()
28 | export class SyntaxToken extends Token {
29 | readonly #type;
30 |
31 | override get type(): SyntaxTypes {
32 | return this.#type;
33 | }
34 |
35 | /**
36 | * @class
37 | * @param pattern 语法正则
38 | */
39 | constructor(
40 | wikitext: string | undefined,
41 | pattern: RegExp,
42 | type: SyntaxTypes,
43 | config?: Config,
44 | accum?: Token[],
45 | acceptable?: WikiParserAcceptable,
46 | ) {
47 | super(wikitext, config, accum, acceptable);
48 | this.#type = type;
49 |
50 | /* NOT FOR BROWSER */
51 |
52 | this.setAttribute('pattern', pattern);
53 | }
54 |
55 | /** @private */
56 | override lint(start = this.getAbsoluteIndex()): LintError[] {
57 | LINT: return super.lint(start, false);
58 | }
59 |
60 | /* NOT FOR BROWSER */
61 |
62 | @clone
63 | override cloneNode(): this {
64 | return new SyntaxToken(
65 | undefined,
66 | this.pattern,
67 | this.type,
68 | this.getAttribute('config'),
69 | [],
70 | this.getAcceptable(),
71 | ) as this;
72 | }
73 | }
74 |
75 | classes['SyntaxToken'] = __filename;
76 |
--------------------------------------------------------------------------------
/parser/html.ts:
--------------------------------------------------------------------------------
1 | import {AttributesToken} from '../src/attributes';
2 | import {HtmlToken} from '../src/tag/html';
3 | import type {Config} from '../base';
4 | import type {Token} from '../internal';
5 |
6 | /* NOT FOR BROWSER */
7 |
8 | import {parsers} from '../util/constants';
9 |
10 | /* NOT FOR BROWSER END */
11 |
12 | const regex = /^(\/?)([a-z][^\s/>]*)((?:\s|\/(?!>))[^>]*?)?(\/?>)([^<]*)$/iu;
13 |
14 | /**
15 | * 解析HTML标签
16 | * @param wikitext
17 | * @param config
18 | * @param accum
19 | */
20 | export const parseHtml = (wikitext: string, config: Config, accum: Token[]): string => {
21 | const {html} = config;
22 | config.htmlElements ??= new Set([...html[0], ...html[1], ...html[2]]);
23 | const bits = wikitext.split('<');
24 | let text = bits.shift()!;
25 | for (const x of bits) {
26 | const mt = regex.exec(x) as [string, string, string, string | undefined, string, string] | null,
27 | t = mt?.[2],
28 | name = t?.toLowerCase();
29 | if (!mt || !config.htmlElements.has(name!)) {
30 | text += `<${x}`;
31 | continue;
32 | }
33 | const [, slash,, params = '', brace, rest] = mt,
34 | {length} = accum,
35 | // @ts-expect-error abstract class
36 | attrs: AttributesToken = new AttributesToken(params, 'html-attrs', name!, config, accum),
37 | itemprop = attrs.hasAttr('itemprop');
38 | if (
39 | name === 'meta' && !(itemprop && attrs.hasAttr('content'))
40 | || name === 'link' && !(itemprop && attrs.hasAttr('href'))
41 | ) {
42 | text += `<${x}`;
43 | accum.length = length;
44 | continue;
45 | }
46 | text += `\0${accum.length}x\x7F${rest}`;
47 | // @ts-expect-error abstract class
48 | new HtmlToken(t!, attrs, slash === '/', brace === '/>', config, accum);
49 | }
50 | return text;
51 | };
52 |
53 | parsers['parseHtml'] = __filename;
54 |
--------------------------------------------------------------------------------
/parser/externalLinks.ts:
--------------------------------------------------------------------------------
1 | import {zs, extUrlChar, extUrlCharFirst} from '../util/string';
2 | import {ExtLinkToken} from '../src/extLink';
3 | import {MagicLinkToken} from '../src/magicLink';
4 | import type {Config} from '../base';
5 | import type {Token} from '../internal';
6 |
7 | /* NOT FOR BROWSER */
8 |
9 | import {parsers} from '../util/constants';
10 |
11 | /* NOT FOR BROWSER END */
12 |
13 | /**
14 | * 解析外部链接
15 | * @param wikitext
16 | * @param config
17 | * @param accum
18 | * @param inFile 是否在图链中
19 | */
20 | export const parseExternalLinks = (wikitext: string, config: Config, accum: Token[], inFile?: boolean): string => {
21 | // eslint-disable-next-line @typescript-eslint/no-unused-expressions
22 | /\[((?:\0\d+[cn]\x7F)*(?:\0\d+f\x7F|(?:(?:ftp:\/\/|\/\/)(?:\[[\da-f:.]+\]|[^[\]<>"\t\n\p{Zs}])|\0\d+m\x7F)[^[\]<>"\t\n\p{Zs}]*(?=[[\]<>"\t\p{Zs}]|\0\d)))(\p{Zs}*(?!\p{Zs}))([^\]\n]*)\]/giu;
23 | config.regexExternalLinks ??= new RegExp(
24 | String.raw`\[((?:\0\d+[cn]\x7F)*(?:\0\d+f\x7F|(?:(?:${config.protocol}|//)${extUrlCharFirst}|\0\d+m\x7F)${
25 | extUrlChar
26 | }(?=[[\]<>"\t${zs}]|\0\d)))([${zs}]*(?![${zs}]))([^\]\x01-\x08\x0A-\x1F\uFFFD]*)\]`,
27 | 'giu',
28 | );
29 | return wikitext.replace(config.regexExternalLinks, (_, url: string, space: string, text: string) => {
30 | const {length} = accum,
31 | mt = /&[lg]t;/u.exec(url);
32 | if (mt) {
33 | space = '';
34 | text = url.slice(mt.index) + space + text;
35 | url = url.slice(0, mt.index);
36 | }
37 | if (inFile) {
38 | // @ts-expect-error abstract class
39 | new MagicLinkToken(url, 'ext-link-url', config, accum);
40 | return `[\0${length}f\x7F${space}${text}]`;
41 | }
42 | // @ts-expect-error abstract class
43 | new ExtLinkToken(url, space, text, config, accum);
44 | return `\0${length}w\x7F`;
45 | });
46 | };
47 |
48 | parsers['parseExternalLinks'] = __filename;
49 |
--------------------------------------------------------------------------------
/src/onlyinclude.ts:
--------------------------------------------------------------------------------
1 | import {padded} from '../mixin/padded';
2 | import {noEscape} from '../mixin/noEscape';
3 | import {Token} from './index';
4 |
5 | /* NOT FOR BROWSER */
6 |
7 | import {classes} from '../util/constants';
8 | import {clone} from '../mixin/clone';
9 | import Parser from '../index';
10 |
11 | /* NOT FOR BROWSER END */
12 |
13 | /**
14 | * `` during transclusion
15 | *
16 | * 嵌入时的``
17 | * @classdesc `{childNodes: (AstText|Token)[]}`
18 | */
19 | @noEscape @padded('')
20 | export class OnlyincludeToken extends Token {
21 | override get type(): 'onlyinclude' {
22 | return 'onlyinclude';
23 | }
24 |
25 | /* NOT FOR BROWSER */
26 |
27 | /** inner wikitext / 内部wikitext */
28 | get innerText(): string {
29 | return this.text();
30 | }
31 |
32 | /** @throws `RangeError` 不允许包含` ` */
33 | set innerText(text) {
34 | /* istanbul ignore if */
35 | if (text.includes(' ')) {
36 | throw new RangeError('" " is not allowed in the text!');
37 | }
38 | const {childNodes} = Parser.parseWithRef(text, this, undefined, true);
39 | this.safeReplaceChildren(childNodes);
40 | }
41 |
42 | /* NOT FOR BROWSER END */
43 |
44 | /** @private */
45 | override toString(skip?: boolean): string {
46 | return `${super.toString(skip)} `;
47 | }
48 |
49 | /** @private */
50 | override isPlain(): true {
51 | return true;
52 | }
53 |
54 | /** @private */
55 | override print(): string {
56 | PRINT: return super.print({
57 | pre: '<onlyinclude>',
58 | post: '</onlyinclude>',
59 | });
60 | }
61 |
62 | /* NOT FOR BROWSER */
63 |
64 | @clone
65 | override cloneNode(): this {
66 | return new OnlyincludeToken(undefined, this.getAttribute('config')) as this;
67 | }
68 | }
69 |
70 | classes['OnlyincludeToken'] = __filename;
71 |
--------------------------------------------------------------------------------
/parser/converter.ts:
--------------------------------------------------------------------------------
1 | import {ConverterToken} from '../src/converter';
2 | import type {Config} from '../base';
3 | import type {Token} from '../internal';
4 |
5 | /* NOT FOR BROWSER */
6 |
7 | import {parsers} from '../util/constants';
8 |
9 | /* NOT FOR BROWSER END */
10 |
11 | /**
12 | * 解析语言变体转换
13 | * @param text
14 | * @param config
15 | * @param accum
16 | */
17 | export const parseConverter = (text: string, config: Config, accum: Token[]): string => {
18 | // eslint-disable-next-line @typescript-eslint/no-unused-expressions
19 | /;(?=(?:[^;]*?=>)?\s*zh\s*:|(?:\s|\0\d+[cn]\x7F)*$)/u;
20 | config.regexConverter ??= new RegExp(
21 | String.raw`;(?=(?:[^;]*?=>)?\s*(?:${config.variants.join('|')})\s*:|(?:\s|\0\d+[cn]\x7F)*$)`,
22 | 'iu',
23 | );
24 | const regex1 = /-\{/gu,
25 | regex2 = /-\{|\}-/gu,
26 | stack: RegExpExecArray[] = [];
27 | let regex = regex1,
28 | mt = regex.exec(text);
29 | while (mt) {
30 | const {0: syntax, index} = mt;
31 | if (syntax === '}-') {
32 | const top = stack.pop()!,
33 | {length} = accum,
34 | str = text.slice(top.index + 2, index),
35 | i = str.indexOf('|'),
36 | [flags, raw] = i === -1 ? [[], str] : [str.slice(0, i).split(';'), str.slice(i + 1)],
37 | temp = raw.replace(/(&[#a-z\d]+);/giu, '$1\x01'),
38 | rules = temp.split(config.regexConverter)
39 | .map(rule => rule.replace(/\x01/gu, ';')) as [string, ...string[]];
40 | // @ts-expect-error abstract class
41 | new ConverterToken(flags, rules, config, accum);
42 | text = `${text.slice(0, top.index)}\0${length}v\x7F${text.slice(index + 2)}`;
43 | if (stack.length === 0) {
44 | regex = regex1;
45 | }
46 | regex.lastIndex = top.index + 3 + String(length).length;
47 | } else {
48 | stack.push(mt);
49 | regex = regex2;
50 | regex.lastIndex = index + 2;
51 | }
52 | mt = regex.exec(text);
53 | }
54 | return text;
55 | };
56 |
57 | parsers['parseConverter'] = __filename;
58 |
--------------------------------------------------------------------------------
/src/nowiki/base.ts:
--------------------------------------------------------------------------------
1 | import {noEscape} from '../../mixin/noEscape';
2 | import {Token} from '../index';
3 | import type {Config} from '../../base';
4 | import type {AstText} from '../../internal';
5 |
6 | /* NOT FOR BROWSER */
7 |
8 | import {Shadow} from '../../util/debug';
9 | import {classes} from '../../util/constants';
10 | import {fixedToken} from '../../mixin/fixed';
11 |
12 | /* NOT FOR BROWSER END */
13 |
14 | declare type NowikiTypes = 'ext-inner'
15 | | 'comment'
16 | | 'dd'
17 | | 'double-underscore'
18 | | 'hr'
19 | | 'list'
20 | | 'noinclude'
21 | | 'quote';
22 |
23 | /**
24 | * text-only token that will not be parsed
25 | *
26 | * 纯文字Token,不会被解析
27 | * @classdesc `{childNodes: [AstText]}`
28 | */
29 | @fixedToken
30 | @noEscape
31 | export abstract class NowikiBaseToken extends Token {
32 | abstract override get type(): NowikiTypes;
33 | declare readonly childNodes: readonly [AstText];
34 | abstract override get firstChild(): AstText;
35 | abstract override get lastChild(): AstText;
36 |
37 | /* NOT FOR BROWSER */
38 |
39 | abstract override get children(): [];
40 | abstract override get firstElementChild(): undefined;
41 | abstract override get lastElementChild(): undefined;
42 |
43 | /* NOT FOR BROWSER END */
44 |
45 | /** text content / 纯文本部分 */
46 | get innerText(): string {
47 | return this.firstChild.data;
48 | }
49 |
50 | /** @param wikitext default: `''` */
51 | constructor(wikitext = '', config?: Config, accum?: Token[]) {
52 | super(wikitext, config, accum);
53 | }
54 |
55 | /* NOT FOR BROWSER */
56 |
57 | override cloneNode(): this {
58 | return Shadow.run(() => {
59 | const C = this.constructor as new (...args: any[]) => this,
60 | token = new C(this.innerText, this.getAttribute('config'));
61 | if (this.name && !token.name) {
62 | token.setAttribute('name', this.name);
63 | }
64 | return token;
65 | });
66 | }
67 |
68 | /**
69 | * @override
70 | * @param str new text / 新文本
71 | */
72 | override setText(str: string): string {
73 | return super.setText(str);
74 | }
75 | }
76 |
77 | classes['NowikiBaseToken'] = __filename;
78 |
--------------------------------------------------------------------------------
/typings/index.d.ts:
--------------------------------------------------------------------------------
1 | import type {
2 | LintError,
3 |
4 | /* NOT FOR BROWSER */
5 |
6 | Config,
7 | } from '../base';
8 | import type {
9 | AstNodes,
10 |
11 | /* NOT FOR BROWSER */
12 |
13 | Token,
14 | } from '../internal';
15 |
16 | /* NOT FOR BROWSER */
17 |
18 | import type {Ranges} from '../lib/ranges';
19 |
20 | /* NOT FOR BROWSER END */
21 |
22 | declare global {
23 | type WikiParserAcceptable = Record;
24 |
25 | type AstConstructor = abstract new (...args: any[]) => {
26 | readonly childNodes: readonly AstNodes[];
27 | getAttribute(key: T): TokenAttribute;
28 | toString(skip?: boolean, separator?: string): string;
29 | text(separator?: string): string;
30 | lint(): LintError[];
31 | print(opt?: PrintOpt): string;
32 |
33 | /* NOT FOR BROWSER */
34 |
35 | readonly parentNode: Token | undefined;
36 | afterBuild(): void;
37 | insertAt(token: unknown, i?: number): unknown;
38 | setAttribute(key: T, value: TokenAttribute): void;
39 | addEventListener(events: string | string[], listener: (...args: any[]) => void): void;
40 | dispatchEvent(e: Event, data: unknown): void;
41 | safeReplaceChildren(elements: readonly (AstNodes | string)[]): void;
42 | constructorError(msg: string): never;
43 | seal(key: string, permanent?: boolean): void;
44 | toHtmlInternal(opt?: HtmlOpt): string;
45 | };
46 |
47 | interface PrintOpt {
48 | readonly pre?: string;
49 | readonly post?: string;
50 | readonly sep?: string;
51 | readonly class?: string;
52 | }
53 |
54 | /* NOT FOR BROWSER */
55 |
56 | interface ParsingError {
57 | readonly stage: number;
58 | readonly include: boolean;
59 | readonly config: Config;
60 | readonly page?: string;
61 | }
62 |
63 | interface State {
64 | headings: Set;
65 | categories: Set;
66 | }
67 |
68 | /** 注意`nocc`只用于`HeadingToken.id` */
69 | interface HtmlOpt {
70 | readonly nowrap?: boolean | undefined;
71 | readonly nocc?: boolean | undefined;
72 | removeBlank?: boolean | undefined;
73 | }
74 | }
75 |
76 | export {};
77 |
--------------------------------------------------------------------------------
/src/multiLine/paramTag.ts:
--------------------------------------------------------------------------------
1 | import {parseCommentAndExt} from '../../parser/commentAndExt';
2 | import {MultiLineToken} from './index';
3 | import {ParamLineToken} from '../paramLine';
4 | import type {Config} from '../../base';
5 | import type {Token} from '../../internal';
6 |
7 | /* NOT FOR BROWSER */
8 |
9 | import {classes} from '../../util/constants';
10 | import {clone} from '../../mixin/clone';
11 |
12 | /* NOT FOR BROWSER END */
13 |
14 | /**
15 | * ``
16 | * @classdesc `{childNodes: ParamLineToken[]}`
17 | */
18 | export abstract class ParamTagToken extends MultiLineToken {
19 | declare readonly childNodes: readonly ParamLineToken[];
20 | abstract override get firstChild(): ParamLineToken | undefined;
21 | abstract override get lastChild(): ParamLineToken | undefined;
22 |
23 | /* NOT FOR BROWSER */
24 |
25 | abstract override get children(): ParamLineToken[];
26 | abstract override get firstElementChild(): ParamLineToken | undefined;
27 | abstract override get lastElementChild(): ParamLineToken | undefined;
28 |
29 | /* NOT FOR BROWSER END */
30 |
31 | /** @param name 扩展标签名 */
32 | constructor(
33 | name: string,
34 | include: boolean,
35 | wikitext: string | undefined,
36 | config: Config,
37 | accum: Token[] = [],
38 | acceptable?: WikiParserAcceptable,
39 | ) {
40 | super(undefined, config, accum, {
41 | ParamLineToken: ':',
42 | });
43 | if (wikitext) {
44 | this.safeAppend(
45 | wikitext.split('\n')
46 | .map(line => acceptable ? line : parseCommentAndExt(line, config, accum, include))
47 | // @ts-expect-error abstract class
48 | .map((line): ParamLineToken => new ParamLineToken(name, line, config, accum, {
49 | 'Stage-1': ':', ...acceptable ?? {'!ExtToken': ''},
50 | })),
51 | );
52 | }
53 | accum.splice(accum.indexOf(this), 1);
54 | accum.push(this);
55 | }
56 |
57 | /* NOT FOR BROWSER */
58 |
59 | @clone
60 | override cloneNode(): this {
61 | const C = this.constructor as new (...args: any[]) => this;
62 | return new C(this.name, this.getAttribute('include'), undefined, this.getAttribute('config'));
63 | }
64 | }
65 |
66 | classes['ParamTagToken'] = __filename;
67 |
--------------------------------------------------------------------------------
/src/nowiki/list.ts:
--------------------------------------------------------------------------------
1 | import {generateForSelf} from '../../util/lint';
2 | import Parser from '../../index';
3 | import {ListBaseToken} from './listBase';
4 | import type {LintError, TokenTypes} from '../../base';
5 |
6 | /* NOT FOR BROWSER */
7 |
8 | import {classes} from '../../util/constants';
9 | import {sol} from '../../mixin/sol';
10 | import {syntax} from '../../mixin/syntax';
11 | import type {SyntaxBase} from '../../mixin/syntax';
12 | import type {ListRangeToken} from './listBase';
13 |
14 | export interface ListToken extends SyntaxBase {}
15 |
16 | /* NOT FOR BROWSER END */
17 |
18 | const linkTypes = new Set(['link', 'category', 'file']);
19 |
20 | /**
21 | * `;:*#` at the start of a line
22 | *
23 | * 位于行首的`;:*#`
24 | */
25 | @sol(true) @syntax(/^[;:*#]+[^\S\n]*$/u)
26 | export abstract class ListToken extends ListBaseToken {
27 | override get type(): 'list' {
28 | return 'list';
29 | }
30 |
31 | override lint(start = this.getAbsoluteIndex()): LintError[] {
32 | LINT: {
33 | const rule = 'syntax-like',
34 | s = Parser.lintConfig.getSeverity(rule, 'redirect'),
35 | {innerText} = this;
36 | if (s && innerText === '#') {
37 | let {nextSibling} = this;
38 |
39 | /* NOT FOR BROWSER */
40 |
41 | if (nextSibling?.is('list-range')) {
42 | nextSibling = nextSibling.firstChild;
43 | }
44 |
45 | /* NOT FOR BROWSER END */
46 |
47 | if (nextSibling?.type === 'text' && linkTypes.has(nextSibling.nextSibling?.type)) {
48 | /^redirect\s*(?::\s*)?$/iu; // eslint-disable-line @typescript-eslint/no-unused-expressions
49 | const re = new RegExp(
50 | String.raw`^(?:${
51 | this.getAttribute('config').redirection.join('|')
52 | })\s*(?::\s*)?$`,
53 | 'iu',
54 | );
55 | if (re.test(`#${nextSibling.data}`)) {
56 | const e = generateForSelf(nextSibling, {start: start + 1}, rule, 'redirect-like', s);
57 | e.startIndex--;
58 | e.startCol--;
59 | e.endIndex += 2;
60 | e.endCol += 2;
61 | return [e];
62 | }
63 | }
64 | }
65 | return [];
66 | }
67 | }
68 | }
69 |
70 | classes['ListToken'] = __filename;
71 |
--------------------------------------------------------------------------------
/src/atom.ts:
--------------------------------------------------------------------------------
1 | import {Token} from './index';
2 | import type {Config} from '../base';
3 |
4 | /* PRINT ONLY */
5 |
6 | import type {ConverterFlagsToken} from '../internal';
7 |
8 | /* PRINT ONLY END */
9 |
10 | /* NOT FOR BROWSER */
11 |
12 | import {classes} from '../util/constants';
13 | import {clone} from '../mixin/clone';
14 |
15 | /* NOT FOR BROWSER END */
16 |
17 | const atomTypes = [
18 | 'arg-name',
19 | 'attr-key',
20 | 'attr-value',
21 | 'ext-attr-dirty',
22 | 'html-attr-dirty',
23 | 'table-attr-dirty',
24 | 'converter-flag',
25 | 'converter-rule-variant',
26 | 'invoke-function',
27 | 'invoke-module',
28 | 'template-name',
29 | 'link-target',
30 | ] as const;
31 |
32 | declare type AtomTypes = typeof atomTypes[number];
33 |
34 | /**
35 | * plain Token that will not be parsed further
36 | *
37 | * 不会被继续解析的plain Token
38 | */
39 | export class AtomToken extends Token {
40 | #type;
41 |
42 | override get type(): AtomTypes {
43 | return this.#type;
44 | }
45 |
46 | override set type(value) {
47 | /* NOT FOR BROWSER */
48 |
49 | /* istanbul ignore if */
50 | if (!atomTypes.includes(value)) {
51 | throw new RangeError(`"${value}" is not a valid type for AtomToken!`);
52 | }
53 |
54 | /* NOT FOR BROWSER END */
55 |
56 | this.#type = value;
57 | }
58 |
59 | /** @class */
60 | constructor(
61 | wikitext: string | undefined,
62 | type: AtomTypes,
63 | config?: Config,
64 | accum?: Token[],
65 | acceptable?: WikiParserAcceptable,
66 | ) {
67 | super(wikitext, config, accum, acceptable);
68 | this.#type = type;
69 | }
70 |
71 | /* PRINT ONLY */
72 |
73 | /** @private */
74 | override getAttribute(key: T): TokenAttribute {
75 | return key === 'invalid'
76 | ? (
77 | this.type === 'converter-flag'
78 | && Boolean((this.parentNode as ConverterFlagsToken | undefined)?.isInvalidFlag(this))
79 | ) as TokenAttribute
80 | : super.getAttribute(key);
81 | }
82 |
83 | /* PRINT ONLY END */
84 |
85 | /* NOT FOR BROWSER */
86 |
87 | @clone
88 | override cloneNode(): this {
89 | return new AtomToken(
90 | undefined,
91 | this.type,
92 | this.getAttribute('config'),
93 | [],
94 | this.getAcceptable(),
95 | ) as this;
96 | }
97 | }
98 |
99 | classes['AtomToken'] = __filename;
100 |
--------------------------------------------------------------------------------
/lib/attributes.ts:
--------------------------------------------------------------------------------
1 | import {classes} from '../util/constants';
2 | import type {TokenTypes} from '../base';
3 | import type {Title} from '../lib/title';
4 | import type {Token} from '../internal';
5 |
6 | declare type Target = Token & {link?: string | Title};
7 |
8 | /** 用于选择器的属性 */
9 | export class Attributes {
10 | token: Target;
11 | type: TokenTypes;
12 | #link: string | Title | undefined;
13 | #invalid: boolean | undefined;
14 | #siblings: Token[] | undefined;
15 | #siblingsOfType: Token[] | undefined;
16 | #siblingsCount: number | undefined;
17 | #siblingsCountOfType: number | undefined;
18 | #index: number | undefined;
19 | #indexOfType: number | undefined;
20 | #lastIndex: number | undefined;
21 | #lastIndexOfType: number | undefined;
22 |
23 | constructor(token: Token) {
24 | this.token = token;
25 | this.type = token.type;
26 | }
27 |
28 | get link(): string | Title | undefined {
29 | this.#link ??= this.token.link;
30 | return this.#link;
31 | }
32 |
33 | get invalid(): boolean {
34 | this.#invalid ??= this.token.getAttribute('invalid');
35 | return this.#invalid;
36 | }
37 |
38 | get siblings(): Token[] | undefined {
39 | this.#siblings ??= this.token.parentNode?.children;
40 | return this.#siblings;
41 | }
42 |
43 | get siblingsOfType(): Token[] | undefined {
44 | this.#siblingsOfType ??= this.siblings?.filter(({type}) => type === this.type);
45 | return this.#siblingsOfType;
46 | }
47 |
48 | get siblingsCount(): number {
49 | this.#siblingsCount ??= this.siblings?.length ?? 1;
50 | return this.#siblingsCount;
51 | }
52 |
53 | get siblingsCountOfType(): number {
54 | this.#siblingsCountOfType ??= this.siblingsOfType?.length ?? 1;
55 | return this.#siblingsCountOfType;
56 | }
57 |
58 | get index(): number {
59 | this.#index ??= (this.siblings?.indexOf(this.token) ?? 0) + 1;
60 | return this.#index;
61 | }
62 |
63 | get indexOfType(): number {
64 | this.#indexOfType ??= (this.siblingsOfType?.indexOf(this.token) ?? 0) + 1;
65 | return this.#indexOfType;
66 | }
67 |
68 | get lastIndex(): number {
69 | this.#lastIndex ??= this.siblingsCount - this.index + 1;
70 | return this.#lastIndex;
71 | }
72 |
73 | get lastIndexOfType(): number {
74 | this.#lastIndexOfType ??= this.siblingsCountOfType - this.indexOfType + 1;
75 | return this.#lastIndexOfType;
76 | }
77 | }
78 |
79 | classes['Attributes'] = __filename;
80 |
--------------------------------------------------------------------------------
/src/nowiki/doubleUnderscore.ts:
--------------------------------------------------------------------------------
1 | import {hiddenToken} from '../../mixin/hidden';
2 | import {padded} from '../../mixin/padded';
3 | import {NowikiBaseToken} from './base';
4 | import type {Config} from '../../base';
5 | import type {Token} from '../../internal';
6 |
7 | /* NOT FOR BROWSER */
8 |
9 | import {Shadow} from '../../util/debug';
10 | import {classes} from '../../util/constants';
11 | import {syntax} from '../../mixin/syntax';
12 | import type {SyntaxBase} from '../../mixin/syntax';
13 |
14 | export interface DoubleUnderscoreToken extends SyntaxBase {}
15 |
16 | /* NOT FOR BROWSER END */
17 |
18 | /**
19 | * behavior switch
20 | *
21 | * 状态开关
22 | */
23 | @syntax()
24 | @hiddenToken() @padded('__')
25 | export abstract class DoubleUnderscoreToken extends NowikiBaseToken {
26 | declare readonly name: string;
27 | readonly #fullWidth;
28 |
29 | /* NOT FOR BROWSER */
30 |
31 | readonly #sensitive;
32 |
33 | /* NOT FOR BROWSER END */
34 |
35 | override get type(): 'double-underscore' {
36 | return 'double-underscore';
37 | }
38 |
39 | /**
40 | * @param word 状态开关名
41 | * @param sensitive 是否固定大小写
42 | * @param fullWidth 是否为全角下划线
43 | */
44 | constructor(word: string, sensitive: boolean, fullWidth: boolean, config: Config, accum?: Token[]) {
45 | super(word, config, accum);
46 | const lc = word.toLowerCase(),
47 | {doubleUnderscore: [,, iAlias, sAlias]} = config;
48 | this.setAttribute('name', (sensitive ? sAlias?.[word]?.toLowerCase() : iAlias?.[lc]) ?? lc);
49 | this.#fullWidth = fullWidth;
50 |
51 | /* NOT FOR BROWSER */
52 |
53 | this.#sensitive = sensitive;
54 | this.setAttribute('pattern', new RegExp(`^${word}$`, sensitive ? 'u' : 'iu'));
55 | }
56 |
57 | /** @private */
58 | override toString(): string {
59 | const underscore = this.#fullWidth ? '__' : '__';
60 | return underscore + this.innerText + underscore;
61 | }
62 |
63 | /** @private */
64 | override print(): string {
65 | PRINT: {
66 | const underscore = this.#fullWidth ? '__' : '__';
67 | return super.print({pre: underscore, post: underscore});
68 | }
69 | }
70 |
71 | /* NOT FOR BROWSER */
72 |
73 | override cloneNode(): this {
74 | // @ts-expect-error abstract class
75 | return Shadow.run((): this => new DoubleUnderscoreToken(
76 | this.innerText,
77 | this.#sensitive,
78 | this.#fullWidth,
79 | this.getAttribute('config'),
80 | ));
81 | }
82 | }
83 |
84 | classes['DoubleUnderscoreToken'] = __filename;
85 |
--------------------------------------------------------------------------------
/src/link/category.ts:
--------------------------------------------------------------------------------
1 | import {LinkBaseToken} from './base';
2 | import type {Title} from '../../lib/title';
3 | import type {Token, AtomToken} from '../../internal';
4 |
5 | /* PRINT ONLY */
6 |
7 | import {decodeHtml} from '../../util/string';
8 | import type {AST} from '../../base';
9 |
10 | /* PRINT ONLY END */
11 |
12 | /* NOT FOR BROWSER */
13 |
14 | import {classes, states} from '../../util/constants';
15 | import {cached} from '../../mixin/cached';
16 |
17 | /* NOT FOR BROWSER END */
18 |
19 | /**
20 | * category
21 | *
22 | * 分类
23 | * @classdesc `{childNodes: [AtomToken, ?Token]}`
24 | */
25 | export abstract class CategoryToken extends LinkBaseToken {
26 | declare readonly childNodes: readonly [AtomToken] | readonly [AtomToken, Token];
27 | abstract override get link(): Title;
28 |
29 | /* NOT FOR BROWSER */
30 |
31 | abstract override get children(): [AtomToken] | [AtomToken, Token];
32 | abstract override set link(link: string);
33 |
34 | /* NOT FOR BROWSER END */
35 |
36 | override get type(): 'category' {
37 | return 'category';
38 | }
39 |
40 | /* PRINT ONLY */
41 |
42 | /** sort key / 分类排序关键字 */
43 | get sortkey(): string | undefined {
44 | LSP: {
45 | const {childNodes: [, child]} = this;
46 | return child && decodeHtml(child.text());
47 | }
48 | }
49 |
50 | /* PRINT ONLY END */
51 |
52 | /* NOT FOR BROWSER */
53 |
54 | set sortkey(text) {
55 | this.setSortkey(text);
56 | }
57 |
58 | /**
59 | * link text
60 | *
61 | * 链接显示文字
62 | * @since v1.32.0
63 | */
64 | get innerText(): string {
65 | return this.link.main;
66 | }
67 |
68 | /* NOT FOR BROWSER END */
69 |
70 | /** @private */
71 | override json(_?: string, start = this.getAbsoluteIndex()): AST {
72 | LSP: {
73 | const json = super.json(undefined, start),
74 | {sortkey} = this;
75 | if (sortkey) {
76 | json['sortkey'] = sortkey;
77 | }
78 | return json;
79 | }
80 | }
81 |
82 | /* NOT FOR BROWSER */
83 |
84 | /**
85 | * Set the sort key
86 | *
87 | * 设置排序关键字
88 | * @param text sort key / 排序关键字
89 | */
90 | setSortkey(text?: string): void {
91 | this.setLinkText(text);
92 | }
93 |
94 | /** @private */
95 | @cached()
96 | override toHtmlInternal(): '' {
97 | states.get(this.getRootNode())?.categories.add(super.toHtmlInternal());
98 | return '';
99 | }
100 | }
101 |
102 | classes['CategoryToken'] = __filename;
103 |
--------------------------------------------------------------------------------
/test/real.ts:
--------------------------------------------------------------------------------
1 | import {getPages, reset} from '@bhsd/test-util';
2 | import {error, info, diff} from '../util/diff';
3 | import single from './single';
4 | import lsp from './lsp';
5 |
6 | /* NOT FOR BROWSER ONLY */
7 |
8 | import Parser from '../index';
9 |
10 | /* NOT FOR BROWSER ONLY END */
11 |
12 | /* NOT FOR BROWSER */
13 |
14 | Parser.warning = false;
15 |
16 | /* NOT FOR BROWSER END */
17 |
18 | const i18n: Record = require('../../i18n/zh-hans');
19 | Parser.i18n = i18n;
20 |
21 | const {argv: [,, site = '']} = process,
22 | apis = ([
23 | ['LLWiki', 'https://llwiki.org/mediawiki', 'llwiki'],
24 | ['维基百科', 'https://zh.wikipedia.org/w', 'zhwiki'],
25 | ['Wikipedia', 'https://en.wikipedia.org/w', 'enwiki'],
26 | ['ウィキペディア', 'https://ja.wikipedia.org/w', 'jawiki'],
27 | ['MediaWiki', 'https://www.mediawiki.org/w', 'mediawikiwiki'],
28 | ] as const).filter(([name]) => name.toLowerCase().includes(site.toLowerCase()));
29 |
30 | (async () => {
31 | const failures = new Map();
32 | for (const [name, url, config] of apis) {
33 | info(`开始检查${name}:\n`);
34 | Parser.config = config;
35 | reset();
36 | try {
37 | let failed = 0;
38 | for (const page of await getPages(`${url}/api.php`, name, '10')) {
39 | const {pageid, title, content} = page;
40 | try {
41 | const errors = await single(page);
42 | if (!errors) {
43 | throw new Error('解析错误');
44 | } else if (errors.length > 0) {
45 | console.log(errors.map(({message, severity}) => ({message, severity})));
46 | errors.sort(({startIndex: a}, {startIndex: b}) => b - a);
47 | let text = content,
48 | firstStart = Infinity;
49 | for (const {startIndex, endIndex} of errors) {
50 | if (endIndex <= firstStart) {
51 | text = text.slice(0, startIndex) + text.slice(endIndex);
52 | firstStart = startIndex;
53 | } else {
54 | firstStart = Math.min(firstStart, startIndex);
55 | }
56 | }
57 | await diff(content, text, pageid);
58 | }
59 | if (name !== 'MediaWiki') {
60 | await lsp(page, true);
61 | }
62 | } catch (e) {
63 | error(`解析 ${title} 页面时出错!`, e);
64 | failed++;
65 | }
66 | console.log();
67 | }
68 | if (failed) {
69 | failures.set(name, failed);
70 | }
71 | } catch (e) {
72 | error(`访问${name}的API端口时出错!`, e);
73 | }
74 | }
75 | if (failures.size > 0) {
76 | let total = 0;
77 | for (const [name, failed] of failures) {
78 | error(`${name}:${failed} 个页面解析失败!`);
79 | total += failed;
80 | }
81 | throw new Error(`共有 ${total} 个页面解析失败!`);
82 | }
83 | })();
84 |
--------------------------------------------------------------------------------
/src/nowiki/noinclude.ts:
--------------------------------------------------------------------------------
1 | import {generateForSelf, fixByRemove} from '../../util/lint';
2 | import {hiddenToken} from '../../mixin/hidden';
3 | import Parser from '../../index';
4 | import {NowikiBaseToken} from './base';
5 | import type {
6 | LintError,
7 |
8 | /* NOT FOR BROWSER */
9 |
10 | Config,
11 | } from '../../base';
12 |
13 | /* NOT FOR BROWSER */
14 |
15 | import {classes} from '../../util/constants';
16 | import {Shadow} from '../../util/debug';
17 | import type {Token} from '../../internal';
18 |
19 | /* NOT FOR BROWSER END */
20 |
21 | /**
22 | * `` or ` ` that allows no modification
23 | *
24 | * ``或` `,不可进行任何更改
25 | */
26 | @hiddenToken(false)
27 | export abstract class NoincludeToken extends NowikiBaseToken {
28 | /* NOT FOR BROWSER */
29 |
30 | #fixed;
31 |
32 | /* NOT FOR BROWSER END */
33 |
34 | override get type(): 'noinclude' {
35 | return 'noinclude';
36 | }
37 |
38 | /* NOT FOR BROWSER */
39 |
40 | /** @param fixed 是否不可更改 */
41 | constructor(wikitext: string, config?: Config, accum?: Token[], fixed = false) {
42 | super(wikitext, config, accum);
43 | this.#fixed = fixed;
44 | }
45 |
46 | /* NOT FOR BROWSER END */
47 |
48 | /** @private */
49 | override toString(skip?: boolean): string {
50 | return skip ? '' : super.toString();
51 | }
52 |
53 | override lint(start = this.getAbsoluteIndex()): LintError[] {
54 | LINT: {
55 | const {lintConfig} = Parser,
56 | rule = 'no-ignored',
57 | s = lintConfig.getSeverity(rule, 'include');
58 | if (s) {
59 | const {innerText} = this,
60 | mt = /^<(noinclude|includeonly|onlyinclude)\s+(?:[^\s>/]|\/(?!>))[^>]*>$/iu.exec(innerText);
61 | if (mt) {
62 | const e = generateForSelf(this, {start}, rule, 'useless-attribute', s),
63 | {computeEditInfo} = lintConfig,
64 | before = mt[1]!.length + 1,
65 | after = innerText.endsWith('/>') ? 2 : 1;
66 | e.startIndex += before;
67 | e.startCol += before;
68 | e.endIndex -= after;
69 | e.endCol -= after;
70 | if (computeEditInfo) {
71 | e.suggestions = [fixByRemove(e)];
72 | }
73 | return [e];
74 | }
75 | }
76 | return [];
77 | }
78 | }
79 |
80 | /* NOT FOR BROWSER */
81 |
82 | override cloneNode(): this {
83 | return Shadow.run(() => {
84 | const C = this.constructor as new (...args: any[]) => this;
85 | return new C(this.innerText, this.getAttribute('config'), [], this.#fixed);
86 | });
87 | }
88 |
89 | /* istanbul ignore next */
90 | override setText(str: string): string {
91 | return this.#fixed ? this.constructorError('cannot change the text content') : super.setText(str);
92 | }
93 | }
94 |
95 | classes['NoincludeToken'] = __filename;
96 |
--------------------------------------------------------------------------------
/extensions/typings.d.ts:
--------------------------------------------------------------------------------
1 | import type {
2 | Diagnostic as DiagnosticBase,
3 | } from '@codemirror/lint';
4 | import type {editor} from 'monaco-editor';
5 | import type {CodeJar} from 'codejar-async';
6 | import type {
7 | ColorInformation,
8 | ColorPresentation,
9 | CodeAction,
10 | } from 'vscode-languageserver-types';
11 | // 必须写在一行内
12 | import type {Config, ConfigData, LintConfig, LintError, AST, LanguageService} from '../base';
13 |
14 | export type Diagnostic = DiagnosticBase & {rule: LintError.Rule};
15 |
16 | export interface PrinterBase {
17 | include: boolean;
18 | }
19 |
20 | export interface LinterBase {
21 | include: boolean;
22 | queue(wikitext: string): Promise;
23 | codemirror(wikitext: string): Promise;
24 | monaco(wikitext: string): Promise;
25 | }
26 |
27 | export type CodeJarAsync = CodeJar & {
28 | include: boolean;
29 | editor: HTMLElement;
30 | };
31 |
32 | export type codejar = (textbox: HTMLTextAreaElement, include?: boolean, linenums?: boolean) => CodeJarAsync;
33 |
34 | export interface LanguageServiceBase extends Omit<
35 | LanguageService,
36 | 'provideDocumentSymbols' | 'provideCodeAction'
37 | > {
38 | provideDocumentColors(text: string): Promise;
39 | provideColorPresentations(color: ColorInformation): Promise;
40 | resolveCodeAction(rule?: string): Promise;
41 | findStyleTokens(): Promise;
42 | }
43 |
44 | /* eslint-disable @typescript-eslint/method-signature-style */
45 | export interface wikiparse {
46 | version: string;
47 | CDN: string;
48 | setI18N: (i18n?: Record) => void;
49 | setLintConfig: (config?: LintConfig) => void;
50 | setConfig: (config: ConfigData) => void;
51 | getConfig: () => Promise;
52 | json: (wikitext: string, include: boolean, qid?: number, stage?: number) => Promise;
53 | print: (wikitext: string, include?: boolean, stage?: number, qid?: number) => Promise<[number, string, string][]>;
54 | lint: (wikitext: string, include?: boolean, qid?: number) => Promise;
55 | lineNumbers: (html: HTMLElement, start?: number, paddingTop?: string, paddingBottom?: string) => void;
56 | highlight?: (ele: HTMLElement, include?: boolean, linenums?: boolean, start?: number) => Promise;
57 | edit?: (textbox: HTMLTextAreaElement, include?: boolean) => PrinterBase;
58 | codejar?: codejar | Promise;
59 | Printer?: new (preview: HTMLDivElement, textbox: HTMLTextAreaElement, include?: boolean) => PrinterBase;
60 | Linter?: new (include?: boolean) => LinterBase;
61 | LanguageService?: new (include?: boolean) => LanguageServiceBase;
62 | }
63 | /* eslint-enable @typescript-eslint/method-signature-style */
64 |
65 | declare global {
66 | const wikiparse: wikiparse;
67 | }
68 |
--------------------------------------------------------------------------------
/src/commented.ts:
--------------------------------------------------------------------------------
1 | import {Token} from './index';
2 | import {CommentToken} from './nowiki/comment';
3 | import type {Config, LintError} from '../base';
4 | import type {AstText, AttributesToken, ExtToken} from '../internal';
5 |
6 | /* NOT FOR BROWSER */
7 |
8 | import {classes} from '../util/constants';
9 | import {clone} from '../mixin/clone';
10 |
11 | /* NOT FOR BROWSER END */
12 |
13 | /**
14 | * ``
15 | * @classdesc `{childNodes: (AstText|CommentToken)[]}`
16 | */
17 | export abstract class CommentedToken extends Token {
18 | declare readonly childNodes: readonly (CommentToken | AstText)[];
19 | abstract override get firstChild(): CommentToken | AstText | undefined;
20 | abstract override get lastChild(): CommentToken | AstText | undefined;
21 | abstract override get nextSibling(): undefined;
22 | abstract override get previousSibling(): AttributesToken | undefined;
23 | abstract override get parentNode(): ExtToken | undefined;
24 |
25 | /* NOT FOR BROWSER */
26 |
27 | abstract override get children(): CommentToken[];
28 | abstract override get firstElementChild(): CommentToken | undefined;
29 | abstract override get lastElementChild(): CommentToken | undefined;
30 | abstract override get previousElementSibling(): AttributesToken | undefined;
31 | abstract override get nextElementSibling(): undefined;
32 | abstract override get parentElement(): ExtToken | undefined;
33 |
34 | /* NOT FOR BROWSER END */
35 |
36 | override get type(): 'ext-inner' {
37 | return 'ext-inner';
38 | }
39 |
40 | /** @class */
41 | constructor(wikitext?: string, config?: Config, accum: Token[] = []) {
42 | super(undefined, config, accum, {
43 | AstText: ':', CommentToken: ':',
44 | });
45 | if (wikitext) {
46 | let i = wikitext.indexOf('', i + 4),
48 | lastIndex = 0;
49 | while (j !== false && j !== -1) {
50 | if (i > lastIndex) {
51 | this.insertAt(wikitext.slice(lastIndex, i));
52 | }
53 | // @ts-expect-error abstract class
54 | this.insertAt(new CommentToken(wikitext.slice(i + 4, j), true, config, accum));
55 | lastIndex = j + 3;
56 | i = wikitext.indexOf('', i + 4);
58 | }
59 | if (lastIndex < wikitext.length) {
60 | this.insertAt(wikitext.slice(lastIndex));
61 | }
62 | }
63 | }
64 |
65 | /** @private */
66 | override lint(start = this.getAbsoluteIndex()): LintError[] {
67 | LINT: return super.lint(start, new RegExp(String.raw`<\s*(?:\/\s*)?(${this.name})\b`, 'giu'));
68 | }
69 |
70 | /* NOT FOR BROWSER */
71 |
72 | @clone
73 | override cloneNode(): this {
74 | // @ts-expect-error abstract class
75 | return new CommentedToken(undefined, this.getAttribute('config'));
76 | }
77 | }
78 |
79 | classes['CommentedToken'] = __filename;
80 |
--------------------------------------------------------------------------------
/src/link/categorytree.ts:
--------------------------------------------------------------------------------
1 | import {generateForSelf} from '../../util/lint';
2 | import Parser from '../../index';
3 | import {LinkBaseToken} from './base';
4 | import type {Config, LintError} from '../../base';
5 | import type {Title} from '../../lib/title';
6 | import type {Token, AtomToken, AttributesToken, ExtToken} from '../../internal';
7 |
8 | /* NOT FOR BROWSER */
9 |
10 | import {classes} from '../../util/constants';
11 | import {cached} from '../../mixin/cached';
12 | import {fixedToken} from '../../mixin/fixed';
13 |
14 | /* NOT FOR BROWSER END */
15 |
16 | /**
17 | * ``
18 | * @classdesc `{childNodes: [AtomToken]}`
19 | */
20 | @fixedToken
21 | export abstract class CategorytreeToken extends LinkBaseToken {
22 | declare readonly childNodes: readonly [AtomToken];
23 | abstract override get lastChild(): AtomToken;
24 | abstract override get nextSibling(): undefined;
25 | abstract override get previousSibling(): AttributesToken | undefined;
26 | abstract override get parentNode(): ExtToken | undefined;
27 | abstract override get link(): Title;
28 |
29 | /* NOT FOR BROWSER */
30 |
31 | abstract override get children(): [AtomToken];
32 | abstract override get lastElementChild(): AtomToken;
33 | abstract override get previousElementSibling(): AttributesToken | undefined;
34 | abstract override get nextElementSibling(): undefined;
35 | abstract override get parentElement(): ExtToken | undefined;
36 | abstract override set link(link: string);
37 |
38 | /* NOT FOR BROWSER END */
39 |
40 | override get type(): 'ext-inner' {
41 | return 'ext-inner';
42 | }
43 |
44 | /** @param link 链接标题 */
45 | constructor(link: string, linkText?: undefined, config?: Config, accum: Token[] = []) {
46 | super(link, linkText, config, accum);
47 | this.setAttribute('bracket', false);
48 |
49 | /* NOT FOR BROWSER */
50 |
51 | // @ts-expect-error abstract getter override
52 | this.firstChild.setAttribute('acceptable', {AstText: 0});
53 | }
54 |
55 | override getTitle(): Title {
56 | const target = this.firstChild.toString().trim(),
57 | opt = {halfParsed: true},
58 | title = this.normalizeTitle(target, 14, opt);
59 | return title.valid && title.ns === 14
60 | && !title.interwiki
61 | ? title
62 | : this.normalizeTitle(`Category:${target}`, 0, opt);
63 | }
64 |
65 | override lint(start = this.getAbsoluteIndex()): LintError[] {
66 | LINT: {
67 | const rule = 'no-ignored',
68 | s = Parser.lintConfig.getSeverity(rule, 'categorytree');
69 | if (s) {
70 | const {link} = this;
71 | if (
72 | !link.valid || link.ns !== 14
73 | || link.interwiki
74 | ) {
75 | return [generateForSelf(this, {start}, rule, 'invalid-category', s)];
76 | }
77 | }
78 | return super.lint(start, false);
79 | }
80 | }
81 |
82 | /* NOT FOR BROWSER */
83 |
84 | /** @private */
85 | @cached()
86 | override toHtmlInternal(): '' {
87 | return '';
88 | }
89 | }
90 |
91 | classes['CategorytreeToken'] = __filename;
92 |
--------------------------------------------------------------------------------
/util/diff.ts:
--------------------------------------------------------------------------------
1 | import fs from 'fs/promises';
2 | import {spawn} from 'child_process';
3 | import type {ChildProcessWithoutNullStreams} from 'child_process';
4 | import type {Chalk} from 'chalk';
5 |
6 | /* istanbul ignore next */
7 | process.on('unhandledRejection', e => {
8 | console.error(e);
9 | });
10 |
11 | export type log = (msg: string, ...args: unknown[]) => void;
12 |
13 | /* istanbul ignore next */
14 | /**
15 | * 将shell命令转化为Promise对象
16 | * @param command shell指令
17 | * @param args shell输入参数
18 | */
19 | export const cmd = (command: string, args: readonly string[]): Promise => new Promise(resolve => {
20 | let timer: NodeJS.Timeout | undefined,
21 | shell: ChildProcessWithoutNullStreams | undefined;
22 |
23 | /**
24 | * 清除进程并返回
25 | * @param val 返回值
26 | */
27 | const r = (val?: string): void => {
28 | clearTimeout(timer);
29 | shell?.kill('SIGINT');
30 | resolve(val);
31 | };
32 | try {
33 | shell = spawn(command, args);
34 | timer = setTimeout(() => {
35 | shell!.kill('SIGINT');
36 | }, 60 * 1e3);
37 | let buf = '';
38 | shell.stdout.on('data', data => {
39 | buf += String(data);
40 | });
41 | shell.stdout.on('end', () => {
42 | r(buf);
43 | });
44 | shell.on('exit', () => {
45 | r(shell!.killed ? undefined : '');
46 | });
47 | shell.on('error', () => {
48 | r(undefined);
49 | });
50 | } catch {
51 | r(undefined);
52 | }
53 | });
54 |
55 | /* istanbul ignore next */
56 | /**
57 | * 比较两个文件
58 | * @param oldStr 旧文本
59 | * @param newStr 新文本
60 | * @param uid 唯一标识
61 | */
62 | export const diff = async (oldStr: string, newStr: string, uid: number): Promise => {
63 | if (oldStr === newStr) {
64 | return;
65 | }
66 | const oldFile = `diffOld${uid}`,
67 | newFile = `diffNew${uid}`;
68 | await Promise.all([fs.writeFile(oldFile, oldStr), fs.writeFile(newFile, newStr)]);
69 | const stdout = await cmd('git', [
70 | 'diff',
71 | '--color-words=[\xC0-\xFF][\x80-\xBF]+|/?\\w+/?>?|[^[:space:]]',
72 | '-U0',
73 | '--no-index',
74 | oldFile,
75 | newFile,
76 | ]);
77 | console.log(stdout?.split('\n').slice(4).join('\n'));
78 | await Promise.allSettled([fs.unlink(oldFile), fs.unlink(newFile)]);
79 | };
80 |
81 | let chalk: Chalk | null | undefined;
82 | /* istanbul ignore next */
83 | export const loadChalk = /** @ignore */ (): Chalk | null => {
84 | if (chalk === undefined) {
85 | try {
86 | chalk = require('chalk') as Chalk;
87 | } catch {
88 | chalk = null;
89 | }
90 | }
91 | return chalk;
92 | };
93 |
94 | /* istanbul ignore next */
95 | /** @implements */
96 | export const error: log = (msg, ...args) => {
97 | console.error(loadChalk()?.red(msg) ?? msg, ...args);
98 | };
99 |
100 | /* istanbul ignore next */
101 | /** @implements */
102 | export const info: log = (msg, ...args) => {
103 | console.info(loadChalk()?.green(msg) ?? msg, ...args);
104 | };
105 |
--------------------------------------------------------------------------------
/parser/hrAndDoubleUnderscore.ts:
--------------------------------------------------------------------------------
1 | import {isUnderscore} from '@bhsd/cm-util';
2 | import {HrToken} from '../src/nowiki/hr';
3 | import {DoubleUnderscoreToken} from '../src/nowiki/doubleUnderscore';
4 | import {HeadingToken} from '../src/heading';
5 | import type {Config} from '../base';
6 | import type {AstText, Token} from '../internal';
7 |
8 | /* NOT FOR BROWSER */
9 |
10 | import {parsers} from '../util/constants';
11 |
12 | /* NOT FOR BROWSER END */
13 |
14 | /**
15 | * 解析`
`和状态开关
16 | * @param {Token} root 根节点
17 | * @param config
18 | * @param accum
19 | */
20 | export const parseHrAndDoubleUnderscore = (
21 | {firstChild: {data}, type, name}: Token & {firstChild: AstText},
22 | config: Config,
23 | accum: Token[],
24 | ): string => {
25 | const {doubleUnderscore: [insensitive, sensitive, aliases]} = config,
26 | all = [...insensitive, ...sensitive];
27 | config.insensitiveDoubleUnderscore ??= new Set(insensitive.filter(isUnderscore));
28 | config.sensitiveDoubleUnderscore ??= new Set(sensitive.filter(isUnderscore));
29 | // eslint-disable-next-line @typescript-eslint/no-unused-expressions
30 | /^((?:\0\d+[cno]\x7F)*)(-{4,})|__(toc|notoc)__|_{2}(目次)_{2}/gimu;
31 | config.regexHrAndDoubleUnderscore ??= new RegExp(
32 | String.raw`^((?:\0\d+[cno]\x7F)*)(-{4,})|__(${
33 | all.filter(isUnderscore).join('|')
34 | })__|_{2}(${
35 | all.filter(s => !isUnderscore(s)).map(s => s.slice(2, -2)).join('|')
36 | })_{2}`,
37 | 'gimu',
38 | );
39 | if (type !== 'root' && (type !== 'ext-inner' || name !== 'poem')) {
40 | data = `\0${data}`;
41 | }
42 | data = data.replace(config.regexHrAndDoubleUnderscore, (m, p1?: string, p2?: string, p3?: string, p4?: string) => {
43 | if (p2) {
44 | // @ts-expect-error abstract class
45 | new HrToken(p2, config, accum);
46 | return `${p1}\0${accum.length - 1}r\x7F`;
47 | }
48 | const key = p3 ?? p4!,
49 | caseSensitive = config.sensitiveDoubleUnderscore!.has(key),
50 | lc = key.toLowerCase(),
51 | caseInsensitive = config.insensitiveDoubleUnderscore!.has(lc);
52 | if (caseSensitive || caseInsensitive) {
53 | // @ts-expect-error abstract class
54 | new DoubleUnderscoreToken(key, caseSensitive, Boolean(p4), config, accum);
55 | return `\0${accum.length - 1}${
56 | caseInsensitive && (aliases?.[lc] ?? /* istanbul ignore next */ lc) === 'toc' ? 'u' : 'n'
57 | }\x7F`;
58 | }
59 | return m;
60 | });
61 | if (!config.excludes.includes('heading')) {
62 | data = data.replace(
63 | /^((?:\0\d+[cn]\x7F)*)(={1,6})(.+)\2((?:\s|\0\d+[cn]\x7F)*)$/gmu,
64 | (_, lead: string, equals: string, heading: string, trail: string) => {
65 | const text = `${lead}\0${accum.length}h\x7F`;
66 | // @ts-expect-error abstract class
67 | new HeadingToken(equals.length, [heading, trail], config, accum);
68 | return text;
69 | },
70 | );
71 | }
72 | return type === 'root' || type === 'ext-inner' && name === 'poem' ? data : data.slice(1);
73 | };
74 |
75 | parsers['parseHrAndDoubleUnderscore'] = __filename;
76 |
--------------------------------------------------------------------------------
/i18n/zh-hans.json:
--------------------------------------------------------------------------------
1 | {
2 | "argument-in-ext": "扩展标签内的模板参数",
3 | "attributes-of-closing-tag": "结束标签的属性",
4 | "bold-apostrophes": "粗体撇号",
5 | "bold-in-header": "章节标题中的加粗文本",
6 | "chem-required": "需要chem属性",
7 | "close": "闭合",
8 | "closing-and-self-closing": "同时闭合和自封闭的标签",
9 | "comment": "注释",
10 | "conflicting-image-parameter": "冲突的图像$1参数",
11 | "content-outside-table": "将被移出表格的内容",
12 | "decode": "解码",
13 | "delink": "移除链接",
14 | "duplicate-attribute": "重复的$1属性",
15 | "duplicate-category": "重复的分类",
16 | "duplicate-id": "重复的HTML id属性",
17 | "duplicate-image-parameter": "重复的图像$1参数",
18 | "duplicate-parameter": "重复的模板参数",
19 | "encode": "编码",
20 | "escape": "转义",
21 | "expand": "展开",
22 | "ext-in-html": "HTML标签属性中的扩展标签",
23 | "frame": "框架",
24 | "full-width-punctuation": "全角标点",
25 | "header-in-html": "HTML标签属性中的章节标题",
26 | "header-like": "纯文本中的疑似章节标题语法",
27 | "horizontal-alignment": "水平对齐",
28 | "html-in-table": "表格属性中的HTML标签",
29 | "illegal-attribute-name": "无效的属性名称",
30 | "illegal-attribute-value": "无效的属性值",
31 | "illegal-module": "无效的Scribunto模块名称",
32 | "imagemap-without-image": "没有图像的",
33 | "in-url": "URL中的$1",
34 | "inconsistent-table": "不一致的表格布局",
35 | "insecure-style": "不安全的样式",
36 | "invalid-attribute": "包含无效属性名称的元素",
37 | "invalid-category": "无效的分类名",
38 | "invalid-content": "<$1>中的无效内容",
39 | "invalid-conversion-flag": "无效的转换旗标",
40 | "invalid-gallery": "无效的图库图像",
41 | "invalid-image-parameter": "无效的图像参数",
42 | "invalid-imagemap-link": "中的无效链接",
43 | "invalid-isbn": "无效的ISBN",
44 | "invalid-parameter": "无效的<$1>参数",
45 | "invalid-self-closing": "无效自封闭标签",
46 | "invalid-thumb": "无效的缩略图文件名",
47 | "invalid-url": "无效的URL",
48 | "invisible-triple-braces": "三重括号内的不可见内容",
49 | "italic-apostrophes": "斜体撇号",
50 | "left-bracket": "左括号",
51 | "link-in-extlink": "外部链接中的内部链接",
52 | "lonely": "孤立的\"$1\"",
53 | "missing-extension": "缺少文件扩展名",
54 | "missing-function": "缺少Scribunto模块函数名",
55 | "newline": "换行",
56 | "no-self-closing": "移除自封闭",
57 | "nonzero-tabindex": "非零的tabindex",
58 | "nothing-in": "<$1>不应有任何内容",
59 | "obsolete-attribute": "过时的属性",
60 | "obsolete-tag": "过时的HTML标签",
61 | "open": "开始",
62 | "prefix": "前缀",
63 | "pipe-in-link": "链接文本中的额外\"|\"",
64 | "pipe-in-table": "表格单元格中的额外\"|\"",
65 | "redirect-like": "列表项中的疑似重定向语法",
66 | "ref-in-link": "内部或外部链接中的",
67 | "remove": "移除",
68 | "template-in-link": "内部链接目标中的模板",
69 | "unbalanced-in-section-header": "章节标题中未成对的$1",
70 | "unclosed": "未闭合的$1",
71 | "unclosed-comment": "未闭合的HTML注释",
72 | "unclosed-quotes": "未闭合的引号",
73 | "unclosed-table": "未闭合的表格",
74 | "unclosed-tag": "未闭合的标签",
75 | "unescaped-query": "匿名参数中的未转义查询字符串",
76 | "unexpected-argument": "未预期的模板参数",
77 | "unmatched-closing": "未匹配的结束标签",
78 | "unnecessary-encoding": "内部链接中不必要的百分号编码",
79 | "uppercase": "大写",
80 | "useless-attribute": "无用的属性",
81 | "useless-fragment": "无用的片段",
82 | "useless-link-text": "无用的链接文本",
83 | "variable-anchor": "章节标题中的变量锚点",
84 | "vertical-alignment": "垂直对齐",
85 | "whitespace": "空格"
86 | }
87 |
--------------------------------------------------------------------------------
/i18n/zh-hant.json:
--------------------------------------------------------------------------------
1 | {
2 | "argument-in-ext": "擴充標籤內的模板參數",
3 | "attributes-of-closing-tag": "結束標籤的屬性",
4 | "bold-apostrophes": "粗體撇號",
5 | "bold-in-header": "章節標題中的粗體文字",
6 | "chem-required": "需要chem屬性",
7 | "close": "關閉",
8 | "closing-and-self-closing": "同時關閉和自封閉的標籤",
9 | "comment": "註解",
10 | "conflicting-image-parameter": "起衝突的圖片$1參數",
11 | "content-outside-table": "有會被移出表格外的內容",
12 | "decode": "解碼",
13 | "delink": "解除連結",
14 | "duplicate-attribute": "重複的$1屬性",
15 | "duplicate-category": "重複的分類",
16 | "duplicate-id": "重複的HTML id屬性",
17 | "duplicate-image-parameter": "重複的圖片$1參數",
18 | "duplicate-parameter": "重複的模板參數",
19 | "encode": "編碼",
20 | "escape": "跳脫",
21 | "expand": "展開",
22 | "ext-in-html": "HTML標籤屬性裡的擴充標籤",
23 | "frame": "框架",
24 | "full-width-punctuation": "全形標點符號",
25 | "header-in-html": "HTML標籤屬性中的章節標題",
26 | "header-like": "純文字中的疑似章節標題語法",
27 | "horizontal-alignment": "水平對齊",
28 | "html-in-table": "表格屬性裡的HTML標籤",
29 | "illegal-attribute-name": "無效的屬性名稱",
30 | "illegal-attribute-value": "無效的屬性值",
31 | "illegal-module": "無效的Scribunto模組名稱",
32 | "imagemap-without-image": "沒有圖片",
33 | "in-url": "URL中有$1",
34 | "inconsistent-table": "表格版面不一致",
35 | "insecure-style": "不安全的樣式",
36 | "invalid-attribute": "包含無效屬性名稱的元素",
37 | "invalid-category": "無效的分類名",
38 | "invalid-content": "<$1>有無效內容",
39 | "invalid-conversion-flag": "無效的轉換旗標",
40 | "invalid-gallery": "無效的圖庫圖片",
41 | "invalid-image-parameter": "無效的圖片參數",
42 | "invalid-imagemap-link": "裡有無效連結",
43 | "invalid-isbn": "無效的ISBN",
44 | "invalid-parameter": "無效的<$1>參數",
45 | "invalid-self-closing": "無效自封閉標籤",
46 | "invalid-thumb": "無效的縮略圖檔名",
47 | "invalid-url": "無效的URL",
48 | "invisible-triple-braces": "三重括號內有不可見內容",
49 | "italic-apostrophes": "斜體撇號",
50 | "left-bracket": "左括號",
51 | "link-in-extlink": "外部連結裡的內部連結",
52 | "lonely": "孤立字元\"$1\"",
53 | "missing-extension": "缺少副檔名",
54 | "missing-function": "缺少Scribunto模組函式名稱",
55 | "newline": "換行",
56 | "no-self-closing": "移除自封閉",
57 | "nonzero-tabindex": "非零的tabindex",
58 | "nothing-in": "<$1>不應有任何內容",
59 | "obsolete-attribute": "過時的屬性",
60 | "obsolete-tag": "過時的HTML標籤",
61 | "open": "起始",
62 | "prefix": "前綴",
63 | "pipe-in-link": "連結文字有額外的\"|\"字元",
64 | "pipe-in-table": "表格儲存格有額外的\"|\"字元",
65 | "redirect-like": "列表項中的疑似重新導向語法",
66 | "ref-in-link": "內部或外部連結裡的",
67 | "remove": "移除",
68 | "template-in-link": "內部連結目標中的模板",
69 | "unbalanced-in-section-header": "章節標題裡的$1未成對",
70 | "unclosed": "有未關閉的$1",
71 | "unclosed-comment": "有未關閉的HTML註解",
72 | "unclosed-quotes": "有未關閉的引號",
73 | "unclosed-table": "有未關閉的表格",
74 | "unclosed-tag": "有未關閉的標籤",
75 | "unescaped-query": "匿名參數裡有未跳脫的查詢字元",
76 | "unexpected-argument": "未預期的模板參數",
77 | "unmatched-closing": "未匹配的結束標籤",
78 | "unnecessary-encoding": "內部連結中有不需要的百分比編碼",
79 | "uppercase": "大寫",
80 | "useless-attribute": "無用屬性",
81 | "useless-fragment": "無用的片段內容",
82 | "useless-link-text": "無用的連結文字",
83 | "variable-anchor": "章節標題有可變錨點",
84 | "vertical-alignment": "垂直對齊",
85 | "whitespace": "空格"
86 | }
87 |
--------------------------------------------------------------------------------
/src/imagemapLink.ts:
--------------------------------------------------------------------------------
1 | import {Token} from './index';
2 | import {NoincludeToken} from './nowiki/noinclude';
3 | import {LinkToken} from './link/index';
4 | import {ExtLinkToken} from './extLink';
5 | import type {Config} from '../base';
6 | import type {AstText, ImagemapToken, GalleryImageToken} from '../internal';
7 |
8 | /* NOT FOR BROWSER */
9 |
10 | import {classes} from '../util/constants';
11 | import {fixedToken} from '../mixin/fixed';
12 | import {singleLine} from '../mixin/singleLine';
13 | import type {Title} from '../lib/title';
14 |
15 | /* NOT FOR BROWSER END */
16 |
17 | /**
18 | * link inside the ``
19 | *
20 | * ``内的链接
21 | * @classdesc `{childNodes: [AstText, LinkToken|ExtLinkToken, NoincludeToken]}`
22 | */
23 | @fixedToken @singleLine
24 | export abstract class ImagemapLinkToken extends Token {
25 | declare readonly childNodes: readonly [AstText, LinkToken | ExtLinkToken, NoincludeToken];
26 | abstract override get firstChild(): AstText;
27 | abstract override get lastChild(): NoincludeToken;
28 | abstract override get parentNode(): ImagemapToken | undefined;
29 | abstract override get previousSibling(): GalleryImageToken | this | NoincludeToken | AstText | undefined;
30 | abstract override get nextSibling(): this | NoincludeToken | AstText | undefined;
31 |
32 | /* NOT FOR BROWSER */
33 |
34 | abstract override get children(): [LinkToken | ExtLinkToken, NoincludeToken];
35 | abstract override get firstElementChild(): LinkToken | ExtLinkToken;
36 | abstract override get lastElementChild(): NoincludeToken;
37 | abstract override get parentElement(): ImagemapToken | undefined;
38 | abstract override get previousElementSibling(): GalleryImageToken | this | NoincludeToken | undefined;
39 | abstract override get nextElementSibling(): this | NoincludeToken | undefined;
40 |
41 | /* NOT FOR BROWSER END */
42 |
43 | override get type(): 'imagemap-link' {
44 | return 'imagemap-link';
45 | }
46 |
47 | /* NOT FOR BROWSER */
48 |
49 | /** internal or external link / 内外链接 */
50 | get link(): string | Title {
51 | return this.childNodes[1].link;
52 | }
53 |
54 | set link(link: string) {
55 | this.childNodes[1].link = link;
56 | }
57 |
58 | /* NOT FOR BROWSER END */
59 |
60 | /**
61 | * @param pre 链接前的文本
62 | * @param linkStuff 内外链接
63 | * @param post 链接后的文本
64 | */
65 | constructor(
66 | pre: string,
67 | linkStuff: readonly [string, string | undefined, string | undefined] | readonly [string, string | undefined],
68 | post: string,
69 | config: Config,
70 | accum: Token[] = [],
71 | ) {
72 | super(undefined, config, accum);
73 | this.append(
74 | pre,
75 | linkStuff.length === 2
76 | // @ts-expect-error abstract class
77 | ? new LinkToken(...linkStuff, config, accum) as LinkToken
78 | // @ts-expect-error abstract class
79 | : new ExtLinkToken(...linkStuff, config, accum) as ExtLinkToken,
80 | // @ts-expect-error abstract class
81 | new NoincludeToken(post, config, accum) as NoincludeToken,
82 | );
83 | }
84 | }
85 |
86 | classes['ImagemapLinkToken'] = __filename;
87 |
--------------------------------------------------------------------------------
/src/nowiki/comment.ts:
--------------------------------------------------------------------------------
1 | import {generateForSelf, fixByClose} from '../../util/lint';
2 | import {hiddenToken} from '../../mixin/hidden';
3 | import {padded} from '../../mixin/padded';
4 | import Parser from '../../index';
5 | import {NowikiBaseToken} from './base';
6 | import type {LintError, Config} from '../../base';
7 | import type {Token} from '../../internal';
8 |
9 | /* NOT FOR BROWSER */
10 |
11 | import {Shadow} from '../../util/debug';
12 | import {classes} from '../../util/constants';
13 |
14 | /* NOT FOR BROWSER END */
15 |
16 | /**
17 | * invisible HTML comment
18 | *
19 | * HTML注释,不可见
20 | */
21 | @hiddenToken(false) @padded('')];
64 | }
65 | return [e];
66 | }
67 | }
68 |
69 | /** @private */
70 | override toString(skip?: boolean): string {
71 | /* NOT FOR BROWSER */
72 |
73 | /* istanbul ignore if */
74 | if (!this.closed && this.nextSibling) {
75 | Parser.error('Auto-closing HTML comment', this);
76 | this.closed = true;
77 | }
78 |
79 | /* NOT FOR BROWSER END */
80 |
81 | return skip ? '' : `' : ''}`;
82 | }
83 |
84 | /** @private */
85 | override print(): string {
86 | PRINT: return super.print({pre: '<!--', post: this.closed ? '-->' : ''});
87 | }
88 |
89 | /* NOT FOR BROWSER */
90 |
91 | override cloneNode(): this {
92 | return Shadow.run(
93 | // @ts-expect-error abstract class
94 | (): this => new CommentToken(this.innerText, this.closed, this.getAttribute('config')),
95 | );
96 | }
97 |
98 | /** @private */
99 | override setText(text: string): string {
100 | /* istanbul ignore if */
101 | if (text.includes('-->')) {
102 | throw new RangeError('Do not contain "-->" in the comment!');
103 | }
104 | return super.setText(text);
105 | }
106 | }
107 |
108 | classes['CommentToken'] = __filename;
109 |
--------------------------------------------------------------------------------
/internal.ts:
--------------------------------------------------------------------------------
1 | export type {AstNodes} from './lib/node';
2 | export type {AstText} from './lib/text';
3 | export type {Token} from './src/index';
4 | export type {RedirectToken} from './src/redirect';
5 | export type {RedirectTargetToken} from './src/link/redirectTarget';
6 | export type {OnlyincludeToken} from './src/onlyinclude';
7 | export type {NoincludeToken} from './src/nowiki/noinclude';
8 | export type {IncludeToken} from './src/tagPair/include';
9 | export type {CommentToken} from './src/nowiki/comment';
10 | export type {AtomToken} from './src/atom';
11 | export type {AttributeToken} from './src/attribute';
12 | export type {AttributesToken} from './src/attributes';
13 | export type {ExtToken} from './src/tagPair/ext';
14 | export type {HiddenToken} from './src/hidden';
15 | export type {ArgToken} from './src/arg';
16 | export type {SyntaxToken} from './src/syntax';
17 | export type {ParameterToken} from './src/parameter';
18 | export type {TranscludeToken} from './src/transclude';
19 | export type {HeadingToken} from './src/heading';
20 | export type {HtmlToken} from './src/tag/html';
21 | export type {TdToken} from './src/table/td';
22 | export type {TrToken} from './src/table/tr';
23 | export type {TableToken} from './src/table/index';
24 | export type {HrToken} from './src/nowiki/hr';
25 | export type {DoubleUnderscoreToken} from './src/nowiki/doubleUnderscore';
26 | export type {LinkToken} from './src/link/index';
27 | export type {CategoryToken} from './src/link/category';
28 | export type {ImageParameterToken} from './src/imageParameter';
29 | export type {FileToken} from './src/link/file';
30 | export type {GalleryImageToken} from './src/link/galleryImage';
31 | export type {QuoteToken} from './src/nowiki/quote';
32 | export type {MagicLinkToken} from './src/magicLink';
33 | export type {ExtLinkToken} from './src/extLink';
34 | export type {DdToken} from './src/nowiki/dd';
35 | export type {ListToken} from './src/nowiki/list';
36 | export type {ConverterFlagsToken} from './src/converterFlags';
37 | export type {ConverterRuleToken} from './src/converterRule';
38 | export type {ConverterToken} from './src/converter';
39 | export type {NowikiToken} from './src/nowiki/index';
40 | export type {PreToken} from './src/pre';
41 | export type {ParamTagToken} from './src/multiLine/paramTag';
42 | export type {InputboxToken} from './src/multiLine/inputbox';
43 | export type {NestedToken} from './src/nested';
44 | export type {GalleryToken} from './src/multiLine/gallery';
45 | export type {ImagemapLinkToken} from './src/imagemapLink';
46 | export type {ImagemapToken} from './src/multiLine/imagemap';
47 | export type {CommentedToken} from './src/commented';
48 | export type {TranslateToken} from './src/tagPair/translate';
49 | export type {TvarToken} from './src/tag/tvar';
50 |
51 | /* NOT FOR BROWSER */
52 |
53 | export type {ListRangeToken} from './src/nowiki/listBase';
54 |
55 | import type {TranscludeToken} from './src/transclude';
56 | import type {ExtToken} from './src/tagPair/ext';
57 |
58 | export type FunctionHook = (token: TranscludeToken, context?: TranscludeToken) => string;
59 | export type TagHook = (token: ExtToken) => string;
60 |
--------------------------------------------------------------------------------
/src/table/tr.ts:
--------------------------------------------------------------------------------
1 | import {TrBaseToken} from './trBase';
2 | import type {Config} from '../../base';
3 | import type {Token, TdToken, TableToken, SyntaxToken, AttributesToken} from '../../internal';
4 |
5 | /* NOT FOR BROWSER */
6 |
7 | import {classes} from '../../util/constants';
8 |
9 | /* NOT FOR BROWSER END */
10 |
11 | /**
12 | * table row that contains the newline at the beginning but not at the end
13 | *
14 | * 表格行,含开头的换行,不含结尾的换行
15 | * @classdesc `{childNodes: [SyntaxToken, AttributesToken, ?Token, ...TdToken[]]}`
16 | */
17 | export abstract class TrToken extends TrBaseToken {
18 | declare readonly childNodes: readonly [SyntaxToken, AttributesToken, ...TdToken[]];
19 | abstract override get lastChild(): AttributesToken | TdToken;
20 | abstract override get parentNode(): TableToken | undefined;
21 | abstract override get nextSibling(): SyntaxToken | this | undefined;
22 | abstract override get previousSibling(): Token | undefined;
23 |
24 | /* NOT FOR BROWSER */
25 |
26 | abstract override get children(): [SyntaxToken, AttributesToken, ...TdToken[]];
27 | abstract override get lastElementChild(): AttributesToken | TdToken;
28 | abstract override get parentElement(): TableToken | undefined;
29 | abstract override get nextElementSibling(): SyntaxToken | this | undefined;
30 | abstract override get previousElementSibling(): Token | undefined;
31 |
32 | /* NOT FOR BROWSER END */
33 |
34 | override get type(): 'tr' {
35 | return 'tr';
36 | }
37 |
38 | /**
39 | * @param syntax 表格语法
40 | * @param attr 表格属性
41 | */
42 | constructor(syntax?: string, attr?: string, config?: Config, accum?: Token[]) {
43 | super(
44 | /^\n[^\S\n]*(?:\|-+|\{\{\s*!\s*\}\}-+|\{\{\s*!-\s*\}\}-*)$/u,
45 | syntax,
46 | 'tr',
47 | attr,
48 | config,
49 | accum,
50 | {Token: 2, SyntaxToken: 0, AttributesToken: 1, TdToken: '2:'},
51 | );
52 | }
53 |
54 | /* NOT FOR BROWSER */
55 |
56 | /** @private */
57 | override text(): string {
58 | const str = super.text();
59 | return str.trim().includes('\n') ? str : '';
60 | }
61 |
62 | /**
63 | * 获取相邻行
64 | * @param subset 筛选兄弟节点的方法
65 | */
66 | #getSiblingRow(subset: (childNodes: readonly Token[], index: number) => Token[]): TrToken | undefined {
67 | const {parentNode} = this;
68 | if (!parentNode) {
69 | return undefined;
70 | }
71 | const {childNodes} = parentNode,
72 | index = childNodes.indexOf(this);
73 | for (const child of subset(childNodes, index)) {
74 | if (child instanceof TrToken && child.getRowCount()) {
75 | return child;
76 | }
77 | }
78 | return undefined;
79 | }
80 |
81 | /**
82 | * Get the next row
83 | *
84 | * 获取下一行
85 | */
86 | getNextRow(): TrToken | undefined {
87 | return this.#getSiblingRow((childNodes, index) => childNodes.slice(index + 1));
88 | }
89 |
90 | /**
91 | * Get the previous row
92 | *
93 | * 获取前一行
94 | */
95 | getPreviousRow(): TrToken | undefined {
96 | return this.#getSiblingRow((childNodes, index) => childNodes.slice(0, index).reverse());
97 | }
98 | }
99 |
100 | classes['TrToken'] = __filename;
101 |
--------------------------------------------------------------------------------
/parser/magicLinks.ts:
--------------------------------------------------------------------------------
1 | import {zs, extUrlChar, extUrlCharFirst} from '../util/string';
2 | import {MagicLinkToken} from '../src/magicLink';
3 | import type {Config} from '../base';
4 | import type {Token} from '../internal';
5 |
6 | /* NOT FOR BROWSER */
7 |
8 | import {parsers} from '../util/constants';
9 |
10 | /* NOT FOR BROWSER END */
11 |
12 | const space = String.raw`[${zs}\t]| |*160;|*a0;`,
13 | sp = `(?:${space})+`,
14 | spdash = `(?:${space}|-)`,
15 | magicLinkPattern = String.raw`(?:RFC|PMID)${sp}\d+\b|ISBN${sp}(?:97[89]${spdash}?)?(?:\d${spdash}?){9}[\dx]\b`;
16 |
17 | /**
18 | * 解析自由外链
19 | * @param wikitext
20 | * @param config
21 | * @param accum
22 | */
23 | export const parseMagicLinks = (wikitext: string, config: Config, accum: Token[]): string => {
24 | if (!config.regexMagicLinks) {
25 | try {
26 | // eslint-disable-next-line @typescript-eslint/no-unused-expressions
27 | /(^|[^\p{L}\p{N}_])(?:(?:ftp:\/\/|http:\/\/)((?:\[[\da-f:.]+\]|[^[\]<>"\t\n\p{Zs}])[^[\]<>"\0\t\n\p{Zs}]*)|(?:rfc|pmid)[\p{Zs}\t]+\d+\b|isbn[\p{Zs}\t]+(?:97[89][\p{Zs}\t-]?)?(?:\d[\p{Zs}\t-]?){9}[\dx]\b)/giu;
28 | config.regexMagicLinks = new RegExp(
29 | String.raw`(^|[^\p{L}\p{N}_])(?:(?:${
30 | config.protocol
31 | })(${extUrlCharFirst}${extUrlChar})|${magicLinkPattern})`,
32 | 'giu',
33 | );
34 | } catch /* istanbul ignore next */ {
35 | // eslint-disable-next-line @typescript-eslint/no-unused-expressions
36 | /(^|\W)(?:(?:ftp:\/\/|http:\/\/)((?:\[[\da-f:.]+\]|[^[\]<>"\s])[^[\]<>"\0\s]*)|(?:rfc|pmid)\s+\d+\b|isbn\s+(?:97[89][\s-]?)?(?:\d[\s-]?){9}[\dx]\b)/giu;
37 | config.regexMagicLinks = new RegExp(
38 | String.raw`(^|\W)(?:(?:${config.protocol})(${extUrlCharFirst}${extUrlChar})|${magicLinkPattern})`,
39 | 'giu',
40 | );
41 | }
42 | }
43 | return wikitext.replace(config.regexMagicLinks, (m, lead: string, p1: string | undefined) => {
44 | let url = lead ? m.slice(lead.length) : m;
45 | if (p1) {
46 | let trail = '';
47 | const m2 = /&(?:lt|gt|nbsp|#x0*(?:3[ce]|a0)|#0*(?:6[02]|160));/iu.exec(url);
48 | if (m2) {
49 | trail = url.slice(m2.index);
50 | url = url.slice(0, m2.index);
51 | }
52 | const sep = url.includes('(') ? /[^,;\\.:!?][,;\\.:!?]+$/u : /[^,;\\.:!?)][,;\\.:!?)]+$/u,
53 | sepChars = sep.exec(url);
54 | if (sepChars) {
55 | let correction = 1;
56 | if (
57 | sepChars[0][1] === ';'
58 | && /&(?:[a-z]+|#x[\da-f]+|#\d+)$/iu.test(url.slice(0, sepChars.index))
59 | ) {
60 | correction = 2;
61 | }
62 | trail = url.slice(sepChars.index + correction) + trail;
63 | url = url.slice(0, sepChars.index + correction);
64 | }
65 | if (trail.length >= p1.length) {
66 | return m;
67 | }
68 | // @ts-expect-error abstract class
69 | new MagicLinkToken(url, undefined, config, accum);
70 | return `${lead}\0${accum.length - 1}w\x7F${trail}`;
71 | } else if (!/^(?:RFC|PMID|ISBN)/u.test(url)) {
72 | return m;
73 | }
74 | // @ts-expect-error abstract class
75 | new MagicLinkToken(url, 'magic-link', config, accum);
76 | return `${lead}\0${accum.length - 1}i\x7F`;
77 | });
78 | };
79 |
80 | parsers['parseMagicLinks'] = __filename;
81 |
--------------------------------------------------------------------------------
/test/single.ts:
--------------------------------------------------------------------------------
1 | import {diff, error} from '../util/diff';
2 | import type {SimplePage} from '@bhsd/test-util';
3 | import type {LintError} from '../base';
4 |
5 | /* NOT FOR BROWSER ONLY */
6 |
7 | import Parser from '../index';
8 |
9 | /* NOT FOR BROWSER ONLY END */
10 |
11 | /* NOT FOR BROWSER */
12 |
13 | Parser.viewOnly = true;
14 | Parser.internal = true;
15 |
16 | /* NOT FOR BROWSER END */
17 |
18 | /* PRINT ONLY */
19 |
20 | const entities = {lt: '<', gt: '>', amp: '&'};
21 |
22 | /* PRINT ONLY END */
23 |
24 | const ignored = new Set(['obsolete-attr', 'obsolete-tag', 'table-layout']);
25 |
26 | /**
27 | * 测试单个页面
28 | * @param page 页面
29 | * @param page.pageid 页面ID
30 | * @param page.title 页面标题
31 | * @param page.ns 页面命名空间
32 | * @param page.content 页面源代码
33 | * @param method 方法
34 | */
35 | // eslint-disable-next-line @typescript-eslint/no-invalid-void-type
36 | export default async ({pageid, title, ns, content}: SimplePage, method?: string): Promise => {
37 | content = content.replace(/[\0\x7F]|\r$/gmu, '');
38 | const include = ns === 10 || title.endsWith('/doc');
39 |
40 | /* NOT FOR BROWSER ONLY */
41 |
42 | console.time(`parse: ${title}`);
43 | const token = Parser.parse(content, title, include);
44 | console.timeEnd(`parse: ${title}`);
45 | const parsed = token.toString();
46 | if (parsed !== content) {
47 | error('解析过程中不可逆地修改了原始文本!');
48 | return diff(content, parsed, pageid);
49 | }
50 |
51 | if (method === undefined) {
52 | const set = new Set();
53 | for (const t of token.querySelectorAll('')) {
54 | if (!t.getAttribute('built')) {
55 | set.add(`${t.type}#${t.name ?? ''}`);
56 | }
57 | }
58 | if (set.size > 0) {
59 | error('未构建的节点:', set);
60 | }
61 | }
62 |
63 | /* NOT FOR BROWSER ONLY END */
64 |
65 | /* PRINT ONLY */
66 |
67 | if (!method || method === 'print') {
68 | console.time(`print: ${title}`);
69 | const printed = token.print();
70 | console.timeEnd(`print: ${title}`);
71 | const restored = printed.replace(
72 | /<[^<]+?>|&([lg]t|amp);/gu,
73 | (_, s?: keyof typeof entities) => s ? entities[s] : '',
74 | );
75 | if (restored !== content) {
76 | error('高亮过程中不可逆地修改了原始文本!');
77 | return diff(content, restored, pageid);
78 | }
79 | }
80 |
81 | if (!method || method === 'json') {
82 | console.time(`json: ${title}`);
83 | token.json();
84 | console.timeEnd(`json: ${title}`);
85 | }
86 |
87 | /* PRINT ONLY END */
88 |
89 | /* NOT FOR BROWSER */
90 |
91 | if (!method || method === 'html') {
92 | console.time(`html: ${title}`);
93 | token.toHtml();
94 | console.timeEnd(`html: ${title}`);
95 | const reparsed = token.toString();
96 | if (reparsed !== content) {
97 | error('渲染HTML过程中不可逆地修改了原始文本!');
98 | return diff(content, reparsed, pageid);
99 | }
100 | }
101 |
102 | /* NOT FOR BROWSER END */
103 |
104 | if (!method || method === 'lint') {
105 | console.time(`lint: ${title}`);
106 | const errors = token.lint()
107 | .filter(({rule}) => !ignored.has(rule));
108 | console.timeEnd(`lint: ${title}`);
109 | return errors;
110 | }
111 | return undefined;
112 | };
113 |
--------------------------------------------------------------------------------
/test/parserTests.ts:
--------------------------------------------------------------------------------
1 | import assert from 'assert';
2 | import type {
3 | Test,
4 |
5 | /* NOT FOR BROWSER ONLY */
6 |
7 | SimplePage,
8 | } from '@bhsd/test-util';
9 |
10 | /* NOT FOR BROWSER ONLY */
11 |
12 | import Parser from '../index';
13 | import lsp from './lsp';
14 |
15 | /* NOT FOR BROWSER ONLY END */
16 |
17 | /* NOT FOR BROWSER */
18 |
19 | import {prepare} from '../script/util';
20 |
21 | prepare(Parser);
22 |
23 | /**
24 | * 合并`wpb-list`元素
25 | * @param html HTML字符串
26 | */
27 | const merge = (html: string): string =>
28 | html.replaceAll('', '');
29 |
30 | /* NOT FOR BROWSER END */
31 |
32 | /* PRINT ONLY */
33 |
34 | Parser.internal = true;
35 |
36 | const entities = {lt: '<', gt: '>', amp: '&'};
37 |
38 | /**
39 | * 移除HTML标签
40 | * @param str HTML字符串
41 | */
42 | const deprint = (str: string): string => str.replace(
43 | /<[^<]+?>|&([lg]t|amp);/gu,
44 | (_, s?: keyof typeof entities) => s ? entities[s] : '',
45 | );
46 |
47 | /**
48 | * HTML字符串分行
49 | * @param str HTML字符串
50 | */
51 | const split = (str: string): string[] => str
52 | .replace(/(?:[^<]+<\/span>)+/gu, merge)
53 | .split(/(?<=<\/\w+>)(?!$)|(? {
59 | for (const {desc, title = 'Parser test', wikitext, print, render} of tests) {
60 | if (wikitext && (print || render)) {
61 | it(desc, () => {
62 | /* NOT FOR BROWSER */
63 |
64 | Parser.viewOnly = false;
65 |
66 | assert.strictEqual(
67 | tests.filter(({desc: d}) => d === desc).length,
68 | 1,
69 | `测试用例描述重复:${desc}`,
70 | );
71 |
72 | /* NOT FOR BROWSER END */
73 |
74 | const root = Parser.parse(wikitext, title),
75 | tidied = wikitext.replaceAll('\0', '');
76 |
77 | /* NOT FOR BROWSER */
78 |
79 | root.buildLists();
80 |
81 | /* NOT FOR BROWSER END */
82 |
83 | try {
84 | assert.strictEqual(
85 | root.toString(),
86 | tidied,
87 | '解析过程中不可逆地修改了原始文本!',
88 | );
89 |
90 | /* PRINT ONLY */
91 |
92 | if (print) {
93 | const printed = root.print();
94 | assert.strictEqual(
95 | deprint(printed),
96 | tidied,
97 | '高亮过程中不可逆地修改了原始文本!',
98 | );
99 | assert.deepStrictEqual(split(printed), split(print));
100 | }
101 |
102 | /* PRINT ONLY END */
103 |
104 | /* NOT FOR BROWSER */
105 |
106 | if (render) {
107 | assert.deepStrictEqual(split(root.toHtml()), split(render));
108 | }
109 |
110 | /* NOT FOR BROWSER END */
111 | } catch (e) {
112 | if (e instanceof assert.AssertionError) {
113 | e.cause = {message: `\n${wikitext}`};
114 | }
115 | throw e;
116 | }
117 | });
118 |
119 | /* NOT FOR BROWSER ONLY */
120 |
121 | if (process.env['LSP'] !== '0') {
122 | it(`LSP: ${desc}`, async () => {
123 | /* NOT FOR BROWSER */
124 |
125 | Parser.viewOnly = true;
126 |
127 | /* NOT FOR BROWSER END */
128 |
129 | await lsp({title, content: wikitext} as SimplePage, true, true);
130 | });
131 | }
132 | }
133 | }
134 | });
135 |
--------------------------------------------------------------------------------
/parser/quotes.ts:
--------------------------------------------------------------------------------
1 | import {QuoteToken} from '../src/nowiki/quote';
2 | import type {Config} from '../base';
3 | import type {Token} from '../internal';
4 |
5 | /* NOT FOR BROWSER */
6 |
7 | import {parsers} from '../util/constants';
8 |
9 | /* NOT FOR BROWSER END */
10 |
11 | /**
12 | * 解析单引号
13 | * @param wikitext
14 | * @param config
15 | * @param accum
16 | * @param tidy 是否整理
17 | */
18 | export const parseQuotes = (wikitext: string, config: Config, accum: Token[], tidy?: boolean): string => {
19 | const arr = wikitext.split(/('{2,})/u),
20 | {length} = arr;
21 | if (length === 1) {
22 | return wikitext;
23 | }
24 | let nBold = 0,
25 | nItalic = 0,
26 | firstSingle: number | undefined,
27 | firstMulti: number | undefined,
28 | firstSpace: number | undefined;
29 | for (let i = 1; i < length; i += 2) {
30 | const len = arr[i]!.length;
31 | switch (len) {
32 | case 2:
33 | nItalic++;
34 | break;
35 | case 4:
36 | arr[i - 1] += `'`;
37 | arr[i] = `'''`;
38 | // fall through
39 | case 3:
40 | nBold++;
41 | if (firstSingle !== undefined) {
42 | break;
43 | } else if (arr[i - 1]!.endsWith(' ')) {
44 | if (firstMulti === undefined && firstSpace === undefined) {
45 | firstSpace = i;
46 | }
47 | } else if (arr[i - 1]!.slice(-2, -1) === ' ') {
48 | firstSingle = i;
49 | } else {
50 | firstMulti ??= i;
51 | }
52 | break;
53 | default:
54 | arr[i - 1] += `'`.repeat(len - 5);
55 | arr[i] = `'''''`;
56 | nItalic++;
57 | nBold++;
58 | }
59 | }
60 | if (nItalic % 2 === 1 && nBold % 2 === 1) {
61 | const i = firstSingle ?? firstMulti ?? firstSpace;
62 | if (i !== undefined) {
63 | arr[i] = `''`;
64 | arr[i - 1] += `'`;
65 | }
66 | }
67 | let bold: QuoteToken | false = false,
68 | italic: QuoteToken | false = false;
69 | for (let i = 1; i < length; i += 2) {
70 | const n = arr[i]!.length,
71 | isBold = n !== 2,
72 | isItalic = n !== 3,
73 | // @ts-expect-error abstract class
74 | token: QuoteToken = new QuoteToken(
75 | arr[i],
76 | {bold: isBold && Boolean(bold), italic: isItalic && Boolean(italic)},
77 | config,
78 | accum,
79 | );
80 | if (isBold) {
81 | /* NOT FOR BROWSER */
82 |
83 | if (!tidy && bold) {
84 | bold.setAttribute('bold', token);
85 | token.setAttribute('bold', bold);
86 | }
87 |
88 | /* NOT FOR BROWSER END */
89 |
90 | bold = !bold && token;
91 | }
92 | if (isItalic) {
93 | /* NOT FOR BROWSER */
94 |
95 | if (!tidy && italic) {
96 | italic.setAttribute('italic', token);
97 | token.setAttribute('italic', italic);
98 | }
99 |
100 | /* NOT FOR BROWSER END */
101 |
102 | italic = !italic && token;
103 | }
104 | arr[i] = `\0${accum.length - 1}q\x7F`;
105 | }
106 |
107 | /* NOT FOR BROWSER */
108 |
109 | if (tidy && (bold || italic)) {
110 | // @ts-expect-error abstract class
111 | new QuoteToken(
112 | (bold ? `'''` : '') + (italic ? `''` : ''),
113 | {bold, italic},
114 | config,
115 | accum,
116 | );
117 | arr.push(`\0${accum.length - 1}q\x7F`);
118 | }
119 |
120 | /* NOT FOR BROWSER END */
121 |
122 | return arr.join('');
123 | };
124 |
125 | parsers['parseQuotes'] = __filename;
126 |
--------------------------------------------------------------------------------
/src/paramLine.ts:
--------------------------------------------------------------------------------
1 | import {generateForSelf, fixByRemove} from '../util/lint';
2 | import {extParams} from '../util/sharable';
3 | import Parser from '../index';
4 | import {Token} from './index';
5 | import type {Config, LintError} from '../base';
6 | import type {ParamTagToken} from '../internal';
7 |
8 | /* NOT FOR BROWSER */
9 |
10 | import {classes} from '../util/constants';
11 | import {singleLine} from '../mixin/singleLine';
12 | import {clone} from '../mixin/clone';
13 |
14 | /* NOT FOR BROWSER END */
15 |
16 | /**
17 | * parameter of certain extension tags
18 | *
19 | * 某些扩展标签的参数
20 | */
21 | @singleLine
22 | export abstract class ParamLineToken extends Token {
23 | abstract override get parentNode(): ParamTagToken | undefined;
24 | abstract override get nextSibling(): this | undefined;
25 | abstract override get previousSibling(): this | undefined;
26 |
27 | /* NOT FOR BROWSER */
28 |
29 | abstract override get parentElement(): ParamTagToken | undefined;
30 | abstract override get nextElementSibling(): this | undefined;
31 | abstract override get previousElementSibling(): this | undefined;
32 |
33 | /* NOT FOR BROWSER END */
34 |
35 | override get type(): 'param-line' {
36 | return 'param-line';
37 | }
38 |
39 | /** @param name 扩展标签名 */
40 | constructor(
41 | name: string,
42 | wikitext: string | undefined,
43 | config: Config,
44 | accum: Token[],
45 | acceptable: WikiParserAcceptable,
46 | ) {
47 | super(wikitext, config, accum, acceptable);
48 | this.setAttribute('name', name);
49 | }
50 |
51 | /** @private */
52 | override lint(start = this.getAbsoluteIndex()): LintError[] {
53 | LINT: {
54 | const rule = 'no-ignored',
55 | {lintConfig} = Parser,
56 | {name, childNodes} = this,
57 | s = lintConfig.getSeverity(rule, name);
58 | if (!s) {
59 | return [];
60 | }
61 | const msg = Parser.msg('invalid-parameter', name);
62 | if (childNodes.some(({type}) => type === 'ext')) {
63 | return [generateForSelf(this, {start}, rule, msg, s)];
64 | }
65 | const children = childNodes
66 | .filter(({type}) => type !== 'comment' && type !== 'include' && type !== 'noinclude'),
67 | isInputbox = name === 'inputbox',
68 | i = isInputbox ? children.findIndex(({type}) => type !== 'text') : -1;
69 | let str = children.slice(0, i === -1 ? undefined : i).map(String).join('').trim();
70 | if (str) {
71 | if (isInputbox) {
72 | str = str.toLowerCase();
73 | }
74 | const j = str.indexOf('='),
75 | key = str.slice(0, j === -1 ? undefined : j).trim(),
76 | params = extParams[name!]!;
77 | if (j === -1 ? i === -1 || !params.some(p => p.startsWith(key)) : !params.includes(key)) {
78 | const e = generateForSelf(this, {start}, rule, msg, s);
79 | if (lintConfig.computeEditInfo) {
80 | e.suggestions = [fixByRemove(e)];
81 | }
82 | return [e];
83 | }
84 | }
85 | return super.lint(start, false);
86 | }
87 | }
88 |
89 | /* NOT FOR BROWSER */
90 |
91 | @clone
92 | override cloneNode(): this {
93 | // @ts-expect-error abstract class
94 | return new ParamLineToken(
95 | this.name,
96 | undefined,
97 | this.getAttribute('config'),
98 | [],
99 | this.getAcceptable(),
100 | ) as this;
101 | }
102 | }
103 |
104 | classes['ParamLineToken'] = __filename;
105 |
--------------------------------------------------------------------------------
/src/link/redirectTarget.ts:
--------------------------------------------------------------------------------
1 | import {generateForChild, fixByRemove} from '../../util/lint';
2 | import Parser from '../../index';
3 | import {LinkBaseToken} from './base';
4 | import {NoincludeToken} from '../nowiki/noinclude';
5 | import type {LintError, Config} from '../../base';
6 | import type {Title} from '../../lib/title';
7 | import type {Token, AtomToken} from '../../internal';
8 |
9 | /* NOT FOR BROWSER */
10 |
11 | import {classes} from '../../util/constants';
12 |
13 | /* NOT FOR BROWSER END */
14 |
15 | /**
16 | * target of a redirect
17 | *
18 | * 重定向目标
19 | * @classdesc `{childNodes: [AtomToken, ?NoincludeToken]}`
20 | */
21 | export abstract class RedirectTargetToken extends LinkBaseToken {
22 | declare readonly childNodes: readonly [AtomToken] | readonly [AtomToken, NoincludeToken];
23 | abstract override get lastChild(): AtomToken | NoincludeToken;
24 | abstract override get link(): Title;
25 |
26 | /* NOT FOR BROWSER */
27 |
28 | abstract override get children(): [AtomToken] | [AtomToken, NoincludeToken];
29 | abstract override get lastElementChild(): AtomToken | NoincludeToken;
30 | abstract override set link(link: string);
31 |
32 | /* NOT FOR BROWSER END */
33 |
34 | override get type(): 'redirect-target' {
35 | return 'redirect-target';
36 | }
37 |
38 | /* NOT FOR BROWSER */
39 |
40 | /**
41 | * link text
42 | *
43 | * 链接显示文字
44 | * @since v1.10.0
45 | */
46 | get innerText(): string {
47 | return this.link.toString(true);
48 | }
49 |
50 | /* NOT FOR BROWSER END */
51 |
52 | /**
53 | * @param link 链接标题
54 | * @param linkText 链接显示文字
55 | */
56 | constructor(link: string, linkText?: string, config?: Config, accum?: Token[]) {
57 | super(link, undefined, config, accum);
58 | if (linkText !== undefined) {
59 | // @ts-expect-error abstract class
60 | this.insertAt(new NoincludeToken(linkText, config, accum));
61 | }
62 |
63 | /* NOT FOR BROWSER */
64 |
65 | this.setAttribute('acceptable', {AtomToken: 0, NoincludeToken: 1});
66 | // @ts-expect-error abstract getter
67 | this.firstChild.setAttribute('acceptable', {AstText: ':'});
68 | }
69 |
70 | /** @private */
71 | override getTitle(): Title {
72 | return this.normalizeTitle(
73 | this.firstChild.toString(),
74 | 0,
75 | {halfParsed: true, decode: true, page: ''},
76 | );
77 | }
78 |
79 | /** @private */
80 | override lint(start = this.getAbsoluteIndex()): LintError[] {
81 | LINT: {
82 | const errors = super.lint(start, false),
83 | rule = 'no-ignored',
84 | {lintConfig} = Parser,
85 | s = lintConfig.getSeverity(rule, 'redirect');
86 | if (s && this.length === 2) {
87 | const e = generateForChild(this.lastChild, {start}, rule, 'useless-link-text', s);
88 | e.startIndex--;
89 | e.startCol--;
90 | if (lintConfig.computeEditInfo || lintConfig.fix) {
91 | e.fix = fixByRemove(e);
92 | }
93 | errors.push(e);
94 | }
95 | return errors;
96 | }
97 | }
98 |
99 | /* NOT FOR BROWSER */
100 |
101 | /** @private */
102 | override setTarget(link: string): void {
103 | this.firstChild.setText(link);
104 | }
105 |
106 | /** @private */
107 | override setLinkText(linkStr?: string): void {
108 | if (!linkStr) {
109 | this.childNodes[1]?.remove();
110 | }
111 | }
112 | }
113 |
114 | classes['RedirectTargetToken'] = __filename;
115 |
--------------------------------------------------------------------------------
/src/link/index.ts:
--------------------------------------------------------------------------------
1 | import {rawurldecode} from '@bhsd/common';
2 | import {generateForSelf, fixBy} from '../../util/lint';
3 | import Parser from '../../index';
4 | import {LinkBaseToken} from './base';
5 | import type {LintError} from '../../base';
6 | import type {Title} from '../../lib/title';
7 | import type {Token, AtomToken} from '../../internal';
8 |
9 | /* NOT FOR BROWSER */
10 |
11 | import {classes} from '../../util/constants';
12 |
13 | /* NOT FOR BROWSER END */
14 |
15 | /**
16 | * internal link
17 | *
18 | * 内链
19 | * @classdesc `{childNodes: [AtomToken, ?Token]}`
20 | */
21 | export abstract class LinkToken extends LinkBaseToken {
22 | declare readonly childNodes: readonly [AtomToken] | readonly [AtomToken, Token];
23 | abstract override get link(): Title;
24 |
25 | /* NOT FOR BROWSER */
26 |
27 | abstract override get children(): [AtomToken] | [AtomToken, Token];
28 | abstract override set link(link: string);
29 |
30 | /* NOT FOR BROWSER END */
31 |
32 | override get type(): 'link' {
33 | return 'link';
34 | }
35 |
36 | /** link text / 链接显示文字 */
37 | get innerText(): string {
38 | LINT: return this.length > 1
39 | ? this.lastChild.text()
40 | : rawurldecode(this.firstChild.text().replace(/^\s*:?/u, ''));
41 | }
42 |
43 | /* NOT FOR BROWSER */
44 |
45 | set innerText(text) {
46 | this.setLinkText(text);
47 | }
48 |
49 | /** whether to be a self link / 是否链接到自身 */
50 | get selfLink(): boolean {
51 | const {title, fragment} = this.link;
52 | return !title && Boolean(fragment);
53 | }
54 |
55 | set selfLink(selfLink) {
56 | if (selfLink) {
57 | this.asSelfLink();
58 | }
59 | }
60 |
61 | /* NOT FOR BROWSER END */
62 |
63 | /** @private */
64 | override lint(start = this.getAbsoluteIndex(), re?: RegExp): LintError[] {
65 | LINT: {
66 | const errors = super.lint(start, re),
67 | rule = 'nested-link',
68 | {lintConfig} = Parser,
69 | s = lintConfig.getSeverity(rule);
70 | if (s && this.isInside('ext-link-text')) {
71 | const e = generateForSelf(this, {start}, rule, 'link-in-extlink', s);
72 | if (lintConfig.computeEditInfo || lintConfig.fix) {
73 | e.fix = fixBy(e, 'delink', this.innerText);
74 | }
75 | errors.push(e);
76 | }
77 | return errors;
78 | }
79 | }
80 |
81 | /* NOT FOR BROWSER */
82 |
83 | /* istanbul ignore next */
84 | /**
85 | * Set the interlanguage link
86 | *
87 | * 设置跨语言链接
88 | * @param lang language prefix / 语言前缀
89 | * @param link page title / 页面标题
90 | * @throws `SyntaxError` 仅有片段标识符
91 | */
92 | setLangLink(lang: string, link: string): void {
93 | require('../../addon/link');
94 | this.setLangLink(lang, link);
95 | }
96 |
97 | /* istanbul ignore next */
98 | /**
99 | * Convert to a self link
100 | *
101 | * 修改为到自身的链接
102 | * @param fragment URI fragment / 片段标识符
103 | * @throws `RangeError` 空的片段标识符
104 | */
105 | asSelfLink(fragment?: string): void {
106 | require('../../addon/link');
107 | this.asSelfLink(fragment);
108 | }
109 |
110 | /* istanbul ignore next */
111 | /**
112 | * Automatically generate the link text after the pipe
113 | *
114 | * 自动生成管道符后的链接文字
115 | * @throws `Error` 带有"#"或"%"时不可用
116 | */
117 | pipeTrick(): void {
118 | require('../../addon/link');
119 | this.pipeTrick();
120 | }
121 | }
122 |
123 | classes['LinkToken'] = __filename;
124 |
--------------------------------------------------------------------------------
/addon/link.ts:
--------------------------------------------------------------------------------
1 | /* eslint @stylistic/operator-linebreak: [2, "before", {overrides: {"=": "after"}}] */
2 |
3 | import {classes} from '../util/constants';
4 | import {Shadow, isLink} from '../util/debug';
5 | import {encode} from '../util/string';
6 | import Parser from '../index';
7 | import {Token} from '../src/index';
8 | import {LinkBaseToken} from '../src/link/base';
9 | import {LinkToken} from '../src/link/index';
10 | import {AtomToken} from '../src/atom';
11 |
12 | LinkBaseToken.prototype.setTarget =
13 | /** @implements */
14 | function(link): void {
15 | const {childNodes} = Parser.parseWithRef(link, this, 2),
16 | token = Shadow.run(() => new AtomToken(
17 | undefined,
18 | 'link-target',
19 | this.getAttribute('config'),
20 | [],
21 | {'Stage-2': ':', '!ExtToken': '', '!HeadingToken': ''},
22 | ));
23 | token.concat(childNodes); // eslint-disable-line unicorn/prefer-spread
24 | this.firstChild.safeReplaceWith(token);
25 | };
26 |
27 | LinkBaseToken.prototype.setFragment =
28 | /** @implements */
29 | function(fragment): void {
30 | const {type, name} = this;
31 | if (fragment === undefined || isLink(type)) {
32 | fragment &&= encode(fragment);
33 | this.setTarget(name + (fragment === undefined ? '' : `#${fragment}`));
34 | }
35 | };
36 |
37 | LinkBaseToken.prototype.setLinkText =
38 | /** @implements */
39 | function(linkStr): void {
40 | if (linkStr === undefined) {
41 | this.childNodes[1]?.remove();
42 | return;
43 | } else if (this.length === 1) {
44 | this.insertAt(Shadow.run(() => {
45 | const inner = new Token(undefined, this.getAttribute('config'), [], {
46 | 'Stage-5': ':', QuoteToken: ':', ConverterToken: ':',
47 | });
48 | inner.type = 'link-text';
49 | return inner;
50 | }));
51 | }
52 | this.lastChild.safeReplaceChildren(Parser.parseWithRef(linkStr, this).childNodes);
53 | };
54 |
55 | LinkToken.prototype.setLangLink =
56 | /** @implements */
57 | function(lang, link): void {
58 | link = link.trim();
59 | /* istanbul ignore if */
60 | if (link.startsWith('#')) {
61 | throw new SyntaxError('An interlanguage link cannot be fragment only!');
62 | }
63 | this.setTarget(lang + (link.startsWith(':') ? '' : ':') + link);
64 | };
65 |
66 | LinkToken.prototype.asSelfLink =
67 | /** @implements */
68 | function(fragment): void {
69 | fragment ??= this.fragment;
70 | /* istanbul ignore if */
71 | if (!fragment?.trim()) {
72 | throw new RangeError('LinkToken.asSelfLink method must specify a non-empty fragment!');
73 | }
74 | this.setTarget(`#${encode(fragment)}`);
75 | };
76 |
77 | LinkToken.prototype.pipeTrick =
78 | /** @implements */
79 | function(): void {
80 | const linkText = this.firstChild.text();
81 | /* istanbul ignore if */
82 | if (linkText.includes('#') || linkText.includes('%')) {
83 | throw new Error('Pipe trick cannot be used with "#" or "%"!');
84 | }
85 | const m1 = /^:?(?:[ \w\x80-\xFF-]+:)?([^(]+?) ?\(.+\)$/u.exec(linkText) as [string, string] | null;
86 | if (m1) {
87 | this.setLinkText(m1[1]);
88 | return;
89 | }
90 | const m2 = /^:?(?:[ \w\x80-\xFF-]+:)?([^(]+?) ?(.+)$/u.exec(linkText) as [string, string] | null;
91 | if (m2) {
92 | this.setLinkText(m2[1]);
93 | return;
94 | }
95 | const m3 = /^:?(?:[ \w\x80-\xFF-]+:)?(.*?)(?: ?(? {
24 | states.set(token, {headings: new Set(), categories: new Set()});
25 | const lines = token.toHtmlInternal().split('\n');
26 | let output = '',
27 | inBlockElem = false,
28 | pendingPTag: string | false = false,
29 | inBlockquote = false,
30 | lastParagraph = '';
31 | const /** @ignore */ closeParagraph = (): string => {
32 | if (lastParagraph) {
33 | const result = `${lastParagraph}>\n`;
34 | lastParagraph = '';
35 | return result;
36 | }
37 | return '';
38 | };
39 | for (let line of lines) {
40 | const openMatch = openRegex.test(line),
41 | closeMatch = closeRegex.test(line);
42 | if (openMatch || closeMatch) {
43 | const blockquote = /<(\/?)blockquote[\s>](?!.*<\/?blockquote[\s>])/iu.exec(line)?.[1];
44 | inBlockquote = blockquote === undefined ? inBlockquote : !blockquote;
45 | pendingPTag = false;
46 | output += closeParagraph();
47 | inBlockElem = !closeMatch;
48 | } else if (!inBlockElem) {
49 | if (line.startsWith(' ') && (lastParagraph === 'pre' || line.trim()) && !inBlockquote) {
50 | if (lastParagraph !== 'pre') {
51 | pendingPTag = false;
52 | output += `${closeParagraph()}`;
53 | lastParagraph = 'pre';
54 | }
55 | line = line.slice(1);
56 | } else if (/^(?:]*>\s*)+$/iu.test(line)) {
57 | if (pendingPTag) {
58 | output += closeParagraph();
59 | pendingPTag = false;
60 | }
61 | } else if (!line.trim()) {
62 | if (pendingPTag) {
63 | output += `${pendingPTag}
`;
64 | pendingPTag = false;
65 | lastParagraph = 'p';
66 | } else if (lastParagraph === 'p') {
67 | pendingPTag = '';
68 | } else {
69 | output += closeParagraph();
70 | pendingPTag = '
';
71 | }
72 | } else if (pendingPTag) {
73 | output += pendingPTag;
74 | pendingPTag = false;
75 | lastParagraph = 'p';
76 | } else if (lastParagraph !== 'p') {
77 | output += `${closeParagraph()}
`;
78 | lastParagraph = 'p';
79 | }
80 | }
81 | if (!pendingPTag) {
82 | output += `${line}\n`;
83 | }
84 | }
85 | output += closeParagraph();
86 | const {categories} = states.get(token)!;
87 | states.delete(token);
88 | let html = output.trimEnd();
89 | if (categories.size > 0) {
90 | html += `
91 |
Categories: ${
94 | [...categories].map(catlink => `- ${catlink}
`).join('')
95 | }
`;
96 | }
97 | return html;
98 | };
99 |
100 | parsers['toHtml'] = __filename;
101 |
--------------------------------------------------------------------------------
/src/pre.ts:
--------------------------------------------------------------------------------
1 | import {
2 | MAX_STAGE,
3 |
4 | /* NOT FOR BROWSER */
5 |
6 | classes,
7 | } from '../util/constants';
8 | import {Token} from './index';
9 | import {NoincludeToken} from './nowiki/noinclude';
10 | import type {Config, LintError} from '../base';
11 | import type {AstText, AttributesToken, ExtToken, ConverterToken} from '../internal';
12 |
13 | /* NOT FOR BROWSER */
14 |
15 | import {clone} from '../mixin/clone';
16 |
17 | /* NOT FOR BROWSER END */
18 |
19 | /**
20 | * ``
21 | * @classdesc `{childNodes: (AstText|NoincludeToken|ConverterToken)[]}`
22 | */
23 | export abstract class PreToken extends Token {
24 | declare readonly name: 'pre';
25 |
26 | declare readonly childNodes: readonly (NoincludeToken | ConverterToken | AstText)[];
27 | abstract override get firstChild(): NoincludeToken | ConverterToken | AstText | undefined;
28 | abstract override get lastChild(): NoincludeToken | ConverterToken | AstText | undefined;
29 | abstract override get nextSibling(): undefined;
30 | abstract override get previousSibling(): AttributesToken | undefined;
31 | abstract override get parentNode(): ExtToken | undefined;
32 |
33 | /* NOT FOR BROWSER */
34 |
35 | abstract override get children(): (NoincludeToken | ConverterToken)[];
36 | abstract override get firstElementChild(): NoincludeToken | ConverterToken | undefined;
37 | abstract override get lastElementChild(): NoincludeToken | ConverterToken | undefined;
38 | abstract override get previousElementSibling(): AttributesToken | undefined;
39 | abstract override get nextElementSibling(): undefined;
40 | abstract override get parentElement(): ExtToken | undefined;
41 |
42 | /* NOT FOR BROWSER END */
43 |
44 | override get type(): 'ext-inner' {
45 | return 'ext-inner';
46 | }
47 |
48 | /** @class */
49 | constructor(wikitext?: string, config?: Config, accum: Token[] = []) {
50 | if (wikitext) {
51 | const opening = //giu,
52 | closing = /<\/nowiki>/giu,
53 | {length} = opening.source;
54 | let i = opening.exec(wikitext);
55 | if (i) {
56 | closing.lastIndex = i.index + length;
57 | }
58 | let j = closing.exec(wikitext),
59 | lastIndex = 0,
60 | str = '';
61 | while (i && j) {
62 | // @ts-expect-error abstract class
63 | new NoincludeToken(i[0], config, accum, true);
64 | // @ts-expect-error abstract class
65 | new NoincludeToken(j[0], config, accum, true);
66 | str += `${wikitext.slice(lastIndex, i.index)}\0${accum.length - 1}n\x7F${
67 | wikitext.slice(i.index + length, j.index)
68 | }\0${accum.length}n\x7F`;
69 | lastIndex = j.index + length + 1;
70 | opening.lastIndex = lastIndex;
71 | i = opening.exec(wikitext);
72 | if (i) {
73 | closing.lastIndex = i.index + length;
74 | }
75 | j = closing.exec(wikitext);
76 | }
77 | wikitext = str + wikitext.slice(lastIndex);
78 | }
79 | super(wikitext, config, accum, {
80 | AstText: ':', NoincludeToken: ':', ConverterToken: ':',
81 | });
82 | this.setAttribute('stage', MAX_STAGE - 1);
83 | }
84 |
85 | /** @private */
86 | override isPlain(): true {
87 | return true;
88 | }
89 |
90 | /** @private */
91 | override lint(start = this.getAbsoluteIndex()): LintError[] {
92 | LINT: return super.lint(start, /<\s*\/\s*(pre)\b/giu);
93 | }
94 |
95 | /* NOT FOR BROWSER */
96 |
97 | @clone
98 | override cloneNode(): this {
99 | // @ts-expect-error abstract class
100 | return new PreToken(undefined, this.getAttribute('config'));
101 | }
102 | }
103 |
104 | classes['PreToken'] = __filename;
105 |
--------------------------------------------------------------------------------
/README-(ZH).md:
--------------------------------------------------------------------------------
1 | [](https://www.npmjs.com/package/wikiparser-node)
2 | [](https://github.com/bhsd-harry/wikiparser-node/actions/workflows/codeql.yml)
3 | [](https://github.com/bhsd-harry/wikiparser-node/actions/workflows/node.js.yml)
4 | [](https://www.npmjs.com/package/wikiparser-node)
5 | [](https://app.codacy.com/gh/bhsd-harry/wikiparser-node/dashboard)
6 | 
7 |
8 | # Other Languages
9 |
10 | - [English](./README.md)
11 |
12 | # 简介
13 |
14 | WikiParser-Node 是一款由 Bhsd 开发的基于 [Node.js](https://nodejs.org/) 环境的离线[维基文本](https://www.mediawiki.org/wiki/Wikitext)语法解析器,可以解析几乎全部的维基语法并生成[语法树](https://en.wikipedia.org/wiki/Abstract_syntax_tree)([在线解析](https://bhsd-harry.github.io/wikiparser-node/#editor)),还可以很方便地对语法树进行查询和修改,最后返回修改后的维基文本。
15 |
16 | 尽管 WikiParser-Node 并非专门用于将维基文本转换为 HTML,但它提供了有限的转换能力。[这里](https://bhsd-harry.github.io/wikiparser-website/)是一个使用这个库渲染的 HTML 示例页面列表。
17 |
18 | # 其他版本
19 |
20 | ## Mini (又名 [WikiLint](https://www.npmjs.com/package/wikilint))
21 |
22 | 提供了 [CLI](https://en.wikipedia.org/wiki/Command-line_interface),但仅保留了解析功能和语法错误分析功能,解析生成的语法树不能修改。这个版本为 [Wikitext 语言服务器协议](https://www.npmjs.com/package/wikitext-lsp)提供支持,可为 [VS Code](https://marketplace.visualstudio.com/items?itemName=Bhsd.vscode-extension-wikiparser)、[Sublime Text](https://lsp.sublimetext.io/language_servers/#mediawiki) 和 [Helix](https://github.com/helix-editor/helix/wiki/Language-Server-Configurations#wikitext) 等编辑器提供多种语言服务。
23 |
24 | 可用的语法检查规则列表请见[这里](https://github.com/bhsd-harry/wikiparser-node/wiki/Rules)。
25 |
26 | ## Browser-compatible
27 |
28 | 兼容浏览器的版本,可用于代码高亮或是搭配 [CodeMirror](https://www.npmjs.com/package/@bhsd/codemirror-mediawiki) 和 [Monaco](https://www.npmjs.com/package/monaco-wiki) 等编辑器作为语法分析插件([使用实例展示](https://bhsd-harry.github.io/wikiparser-node))。自 1.45 版本起已集成到 MediaWiki 官方 [CodeMirror 扩展](https://www.mediawiki.org/wiki/Extension:CodeMirror)中。
29 |
30 | # 安装方法
31 |
32 | ## Node.js
33 |
34 | 请根据需要需要安装对应的版本(`WikiParser-Node` 或 `WikiLint`),如:
35 |
36 | ```sh
37 | npm i wikiparser-node
38 | ```
39 |
40 | 或
41 |
42 | ```sh
43 | npm i wikilint
44 | ```
45 |
46 | ## 浏览器
47 |
48 | 可以通过 CDN 下载代码,如:
49 |
50 | ```html
51 |
52 | ```
53 |
54 | 或
55 |
56 | ```html
57 |
58 | ```
59 |
60 | 更多浏览器端可用的插件请查阅对应[文档](https://github.com/bhsd-harry/wikiparser-node/wiki/Browser)。
61 |
62 | # 使用方法
63 |
64 | ## CLI 使用方法
65 |
66 | 对于安装了 [CodeMirror 扩展](https://mediawiki.org/wiki/Extension:CodeMirror)的 MediaWiki 站点,如不同语言版本的维基百科和其他[由维基媒体基金会托管的站点](https://meta.wikimedia.org/wiki/Special:SiteMatrix),可以使用以下命令获取解析器配置:
67 |
68 | ```sh
69 | npx getParserConfig