├── 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 | = -------------------------------------------------------------------------------- /test/templates/Template꞉Inner_list.wiki: -------------------------------------------------------------------------------- 1 | * item 1 -------------------------------------------------------------------------------- /test/templates/Template꞉Linktest2.wiki: -------------------------------------------------------------------------------- 1 | Main Page -------------------------------------------------------------------------------- /test/templates/Template꞉Pipe_page.wiki: -------------------------------------------------------------------------------- 1 | Main|Page -------------------------------------------------------------------------------- /test/templates/Template꞉Safesubst.wiki: -------------------------------------------------------------------------------- 1 | {{{1}}} -------------------------------------------------------------------------------- /test/templates/Template꞉Temp%3F.wiki: -------------------------------------------------------------------------------- 1 | temp {{{1}}} -------------------------------------------------------------------------------- /test/templates/Template꞉Templa.wiki: -------------------------------------------------------------------------------- 1 | '''templ''' -------------------------------------------------------------------------------- /test/templates/MediaWiki꞉History_short.wiki: -------------------------------------------------------------------------------- 1 | History -------------------------------------------------------------------------------- /test/templates/MediaWiki꞉Loop1.wiki: -------------------------------------------------------------------------------- 1 | {{Identical|B}} -------------------------------------------------------------------------------- /test/templates/MediaWiki꞉Loop2.wiki: -------------------------------------------------------------------------------- 1 | {{Identical|A}} -------------------------------------------------------------------------------- /test/templates/MediaWiki꞉T34057.wiki: -------------------------------------------------------------------------------- 1 | == {{int:ok}} == -------------------------------------------------------------------------------- /test/templates/Template꞉Precedence5.wiki: -------------------------------------------------------------------------------- 1 | {{{{{1}}}}} -------------------------------------------------------------------------------- /test/templates/Template꞉Quote.wiki: -------------------------------------------------------------------------------- 1 | {{{quote|{{{1}}}}}} -------------------------------------------------------------------------------- /test/templates/Template꞉Templatesimple.wiki: -------------------------------------------------------------------------------- 1 | (test) -------------------------------------------------------------------------------- /test/templates/Template꞉1x_with_div.wiki: -------------------------------------------------------------------------------- 1 |
{{{1}}}
-------------------------------------------------------------------------------- /test/templates/Template꞉Blank_param.wiki: -------------------------------------------------------------------------------- 1 | {{{1}}} 2 | {{{}}} -------------------------------------------------------------------------------- /test/templates/Template꞉Linktest.wiki: -------------------------------------------------------------------------------- 1 | [[{{{param}}}|link]] -------------------------------------------------------------------------------- /test/templates/Template꞉Refinref.wiki: -------------------------------------------------------------------------------- 1 | {{{1}}} -------------------------------------------------------------------------------- /test/templates/Template꞉Test.wiki: -------------------------------------------------------------------------------- 1 | This is a test template -------------------------------------------------------------------------------- /test/templates/Template꞉With_a_section%3F.wiki: -------------------------------------------------------------------------------- 1 | === test === -------------------------------------------------------------------------------- /test/templates/MediaWiki꞉New-messages.wiki: -------------------------------------------------------------------------------- 1 | You have $1 ($2). -------------------------------------------------------------------------------- /test/templates/Subpage_test/L1/L2/L3Sibling.wiki: -------------------------------------------------------------------------------- 1 | Sibling article -------------------------------------------------------------------------------- /test/templates/Template꞉Paramtestnum.wiki: -------------------------------------------------------------------------------- 1 | [[{{{1}}}|{{{2}}}]] -------------------------------------------------------------------------------- /test/templates/Template꞉Templateasargtest2.wiki: -------------------------------------------------------------------------------- 1 | {{{{{templ}}}}} -------------------------------------------------------------------------------- /test/templates/Template꞉Templateasargtestnum.wiki: -------------------------------------------------------------------------------- 1 | {{{{{1}}}}} -------------------------------------------------------------------------------- /test/templates/Template꞉With꞉_Colon.wiki: -------------------------------------------------------------------------------- 1 | Template with colon -------------------------------------------------------------------------------- /test/templates/Template꞉Map-one-parameter.wiki: -------------------------------------------------------------------------------- 1 | {{{{{1}}}|{{{2}}}}} -------------------------------------------------------------------------------- /test/templates/Template꞉One-parameter.wiki: -------------------------------------------------------------------------------- 1 | (My parameter is: {{{1}}}) -------------------------------------------------------------------------------- /test/templates/Template꞉T290526.wiki: -------------------------------------------------------------------------------- 1 | Main Page{{!}}Something else -------------------------------------------------------------------------------- /test/templates/Template꞉Identical.wiki: -------------------------------------------------------------------------------- 1 | {{int:loop1}} 2 | {{int:loop2}} -------------------------------------------------------------------------------- /test/templates/Template꞉Includes2.wiki: -------------------------------------------------------------------------------- 1 | Foobar -------------------------------------------------------------------------------- /test/templates/Template꞉Nested_special.wiki: -------------------------------------------------------------------------------- 1 | {{Special:Prefixindex/Xyzzyx}} -------------------------------------------------------------------------------- /test/templates/Template꞉Sections.wiki: -------------------------------------------------------------------------------- 1 | ===Section 1=== 2 | ==Section 2== -------------------------------------------------------------------------------- /test/templates/Template꞉Table_cell_content.wiki: -------------------------------------------------------------------------------- 1 | {{{attr|}}}{{{cmt|}}}| foo -------------------------------------------------------------------------------- /test/templates/Template꞉Template_with_pagename.wiki: -------------------------------------------------------------------------------- 1 | This is {{PAGENAME}}. -------------------------------------------------------------------------------- /test/templates/Template꞉Templateasargtest.wiki: -------------------------------------------------------------------------------- 1 | {{template{{{templ}}}}} -------------------------------------------------------------------------------- /test/templates/Template꞉Scribunto_all_args.wiki: -------------------------------------------------------------------------------- 1 | {{#invoke:test|getParentArgs}} -------------------------------------------------------------------------------- /test/templates/Template꞉Table.wiki: -------------------------------------------------------------------------------- 1 | {| 2 | | 1 || 2 3 | |- 4 | | 3 || 4 5 | |} -------------------------------------------------------------------------------- /printed/README: -------------------------------------------------------------------------------- 1 | 这里存放以 JSON 格式打印的 AST。 2 | 3 | AST as JSON files are saved here. 4 | -------------------------------------------------------------------------------- /test/templates/Template꞉SubstTest.wiki: -------------------------------------------------------------------------------- 1 | {{subst:Includes}} -------------------------------------------------------------------------------- /test/templates/Template꞉Templateasargtestdefault.wiki: -------------------------------------------------------------------------------- 1 | {{{{{templ|templatesimple}}}}} -------------------------------------------------------------------------------- /test/templates/Template꞉Dangerous.wiki: -------------------------------------------------------------------------------- 1 | @Oh no -------------------------------------------------------------------------------- /test/templates/Template꞉Dangerous_attribute.wiki: -------------------------------------------------------------------------------- 1 | " onmouseover="alert(document.cookie) -------------------------------------------------------------------------------- /test/templates/Template꞉Div_style.wiki: -------------------------------------------------------------------------------- 1 |
Magic div
-------------------------------------------------------------------------------- /test/templates/Template꞉Paramtest.wiki: -------------------------------------------------------------------------------- 1 | This is a test template with parameter {{{param}}} -------------------------------------------------------------------------------- /test/templates/Template꞉Scribunto_frame_caching.wiki: -------------------------------------------------------------------------------- 1 | {{#invoke:test|testFrameCaching}} -------------------------------------------------------------------------------- /test/templates/Template꞉Complextemplate.wiki: -------------------------------------------------------------------------------- 1 | {{{1}}} {{paramtest| 2 | param ={{{param}}}}} -------------------------------------------------------------------------------- /test/templates/Template꞉SafeSubstTest.wiki: -------------------------------------------------------------------------------- 1 | {{safesubst:Includes}} -------------------------------------------------------------------------------- /test/templates/Template꞉Table_attribs.wiki: -------------------------------------------------------------------------------- 1 | 2 | |style="color:red;"|Foo -------------------------------------------------------------------------------- /.mocharc.json: -------------------------------------------------------------------------------- 1 | { 2 | "timeout": 5000, 3 | "diff": false, 4 | "reporter": "progress" 5 | } 6 | -------------------------------------------------------------------------------- /test/templates/Template꞉Includes.wiki: -------------------------------------------------------------------------------- 1 | Foozarbar -------------------------------------------------------------------------------- /test/templates/Template꞉Paramtest2.wiki: -------------------------------------------------------------------------------- 1 | including another template, {{paramtest|param={{{arg}}}}} -------------------------------------------------------------------------------- /test/templates/Template꞉Table_attribs_4.wiki: -------------------------------------------------------------------------------- 1 | | style="background-color:#DC241f;" width="10px" | -------------------------------------------------------------------------------- /test/templates/Template꞉Table_attribs_5.wiki: -------------------------------------------------------------------------------- 1 | 2 | |style="color:red;"||Bar -------------------------------------------------------------------------------- /test/templates/Template꞉Dangerous_style_attribute.wiki: -------------------------------------------------------------------------------- 1 | border-size: expression(alert(document.cookie)) -------------------------------------------------------------------------------- /test/templates/Template꞉Includes3.wiki: -------------------------------------------------------------------------------- 1 | Foobarzar -------------------------------------------------------------------------------- /test/templates/Template꞉Table_attribs_6.wiki: -------------------------------------------------------------------------------- 1 | style="background: 2 | 3 | 4 | red;" | -------------------------------------------------------------------------------- /test/templates/Template꞉Includes4.wiki: -------------------------------------------------------------------------------- 1 | FoobarBazquux -------------------------------------------------------------------------------- /test/templates/Template꞉Mixed_attr_content_template.wiki: -------------------------------------------------------------------------------- 1 | style="color:red;" title="T48811" 2 | |- 3 | |foo -------------------------------------------------------------------------------- /test/templates/Template꞉Table_attribs_3.wiki: -------------------------------------------------------------------------------- 1 | 2 | |style{{=}}"background:#f9f9f9;"|Foo -------------------------------------------------------------------------------- /test/templates/Template꞉Table_cells.wiki: -------------------------------------------------------------------------------- 1 | {{table_attribs}}||style='color:red;'|''Bar''||style='color:brown;'|''Foo'' and Baz -------------------------------------------------------------------------------- /test/templates/Template꞉Includeonly_section.wiki: -------------------------------------------------------------------------------- 1 | 2 | ==Includeonly section== 3 | 4 | ==Section T-1== -------------------------------------------------------------------------------- /test/templates/Template꞉Table_attribs_2.wiki: -------------------------------------------------------------------------------- 1 | 2 | |data-sort-value="" style="color:red;"|Foo 3 | |Bar||Baz -------------------------------------------------------------------------------- /test/templates/Template꞉Image_attribs.wiki: -------------------------------------------------------------------------------- 1 | 2 | [[File:foobar.jpg|right|Caption text]] -------------------------------------------------------------------------------- /test/templates/Template꞉Preprocessor_precedence_9.wiki: -------------------------------------------------------------------------------- 1 | ;4: {{{{1}}}} 2 | ;5: {{{{{2}}}}} 3 | ;6: {{{{{{3}}}}}} 4 | ;7: {{{{{{{4}}}}}}} -------------------------------------------------------------------------------- /test/templates/Template꞉Table_header_cells.wiki: -------------------------------------------------------------------------------- 1 | {{table_attribs}}!!style='color:red;'|''Bar''||style='color:brown;'|''Foo'' and Baz -------------------------------------------------------------------------------- /errors/README: -------------------------------------------------------------------------------- 1 | 这里记录解析失败时处于半解析状态的维基文本以及错误信息。 2 | 3 | When parsing fails, the half-parsed wikitext and the error message are recorded here. 4 | -------------------------------------------------------------------------------- /test/templates/Template꞉Mixed_attr_content_template_2.wiki: -------------------------------------------------------------------------------- 1 | style="color:red;" title="T249740" 2 | |- 3 | [[Category:Fostered Content]] 4 | |foo -------------------------------------------------------------------------------- /sed.sh: -------------------------------------------------------------------------------- 1 | #!/usr/local/bin/bash 2 | 3 | function is_gnu_sed() { 4 | sed --version >/dev/null 2>&1 5 | } 6 | 7 | if is_gnu_sed 8 | then 9 | sed "$@" 10 | else 11 | gsed "$@" 12 | fi 13 | -------------------------------------------------------------------------------- /tsconfig.dist.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": [ 4 | "dist/**/*.d.ts" 5 | ], 6 | "exclude": [], 7 | "compilerOptions": { 8 | "noEmit": true 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /test/templates/Template꞉Preprocessor_precedence_10.wiki: -------------------------------------------------------------------------------- 1 | ;1: -{R|raw}- 2 | ;2: -{{Bullet}}- 3 | ;3: -{{{1}}}- 4 | ;4: -{{{{2}}}}- 5 | ;5: -{{{{{3}}}}}- 6 | ;6: -{{{{{{4}}}}}}- 7 | ;7: -{{{{{{{5}}}}}}}- -------------------------------------------------------------------------------- /extensions/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig", 3 | "include": [ 4 | "*.ts" 5 | ], 6 | "compilerOptions": { 7 | "target": "es2019", 8 | "outDir": "dist", 9 | "skipLibCheck": true 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /bin/config.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 'use strict'; 3 | const /** @type {import('./config.ts').default} */ fetchConfig = require('../dist/bin/config.js').default; 4 | const [,, site, url, email, force] = process.argv; 5 | fetchConfig(site, url, email, force); 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | /bundle/bundle-es*.min.js 3 | /bundle/*.map 4 | /coverage/* 5 | !/coverage/badge.svg 6 | /errors/* 7 | /printed/* 8 | /.vscode/ 9 | /.eslintcache* 10 | /.wikilintcache 11 | /.nyc_output/ 12 | /.cache/ 13 | /wiki/ 14 | /test/*.wiki 15 | /test/prof*.json 16 | /build 17 | !README 18 | /config/testwiki.json 19 | /diff.html 20 | -------------------------------------------------------------------------------- /diff.sh: -------------------------------------------------------------------------------- 1 | #!/usr/local/bin/bash 2 | if (( $# > 2 )) 3 | then 4 | git diff --ignore-all-space --color-moved --minimal "$@" 5 | else 6 | git diff --ignore-all-space --color-moved --minimal --diff-filter=ad "$@" -- *.ts \ 7 | bin/ \ 8 | config/ data/ i18n/ lib/ mixin/ parser/ src/ test/ typings/ util/ \ 9 | | diff2html -i stdin -F diff.html 10 | fi 11 | -------------------------------------------------------------------------------- /.markdownlint.json: -------------------------------------------------------------------------------- 1 | { 2 | "no-hard-tabs": { 3 | "spaces_per_tab": 4 4 | }, 5 | "line-length": false, 6 | "no-inline-html": { 7 | "allowed_elements": [ 8 | "br", 9 | "details", 10 | "summary" 11 | ] 12 | }, 13 | "no-emphasis-as-heading": false, 14 | "first-line-h1": { 15 | "allow_preamble": true 16 | }, 17 | "descriptive-link-text": false 18 | } 19 | -------------------------------------------------------------------------------- /test/hooks.ts: -------------------------------------------------------------------------------- 1 | import {pathToFileURL} from 'url'; 2 | import path from 'path'; 3 | 4 | declare type Resolve = (specifier: string, context: unknown, defaultResolve: Resolve) => {url: string}; 5 | 6 | export const resolve: Resolve = (specifier, context, defaultResolve) => specifier === 'stylelint' 7 | ? {url: pathToFileURL(path.join(__dirname, 'stylelint.js')).toString()} 8 | : defaultResolve(specifier, context, defaultResolve); 9 | -------------------------------------------------------------------------------- /util/search.ts: -------------------------------------------------------------------------------- 1 | import bs from 'binary-search'; 2 | 3 | /** 4 | * 二分法查找索引 5 | * @param haystack 数组 6 | * @param needle 目标值 7 | * @param comparator 比较函数 8 | */ 9 | export default ( 10 | haystack: T[], 11 | needle: number, 12 | comparator: (item: T, needle: number) => number, 13 | ): number => { 14 | const found = bs(haystack, needle, comparator); 15 | return found < 0 ? ~found : found; // eslint-disable-line no-bitwise 16 | }; 17 | -------------------------------------------------------------------------------- /typings/parser.d.ts: -------------------------------------------------------------------------------- 1 | declare global { 2 | interface BraceExecArray extends RegExpExecArray { 3 | parts?: string[][]; 4 | findEqual?: boolean; 5 | pos?: number; 6 | } 7 | 8 | type BraceExecArrayOrEmpty = Partial; 9 | 10 | /* NOT FOR BROWSER */ 11 | 12 | interface SelectorArray extends Array< 13 | string 14 | | [string, string] 15 | | [string, string | undefined, string | undefined, string | undefined] 16 | > { 17 | relation?: string; 18 | } 19 | } 20 | 21 | export {}; 22 | -------------------------------------------------------------------------------- /mixin/gapped.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 | * 给定 gap 的类 11 | * @param gap 12 | */ 13 | export const gapped = (gap = 1) => (constructor: S): S => { 14 | abstract class GappedToken extends constructor { 15 | getGaps(): number { 16 | return gap; 17 | } 18 | } 19 | mixin(GappedToken, constructor); 20 | return GappedToken; 21 | }; 22 | 23 | mixins['gapped'] = __filename; 24 | -------------------------------------------------------------------------------- /src/nowiki/dd.ts: -------------------------------------------------------------------------------- 1 | import {ListBaseToken} from './listBase'; 2 | 3 | /* NOT FOR BROWSER */ 4 | 5 | import {classes} from '../../util/constants'; 6 | import {syntax} from '../../mixin/syntax'; 7 | import type {SyntaxBase} from '../../mixin/syntax'; 8 | 9 | export interface DdToken extends SyntaxBase {} 10 | 11 | /* NOT FOR BROWSER END */ 12 | 13 | /** `:` */ 14 | @syntax(/^:+$/u) 15 | export abstract class DdToken extends ListBaseToken { 16 | override get type(): 'dd' { 17 | return 'dd'; 18 | } 19 | } 20 | 21 | classes['DdToken'] = __filename; 22 | -------------------------------------------------------------------------------- /mixin/noEscape.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 | * 不需要转义的类 11 | * @ignore 12 | */ 13 | export const noEscape = (constructor: T): T => { 14 | LSP: { 15 | abstract class NoEscapeToken extends constructor { 16 | escape(): void { 17 | // 18 | } 19 | } 20 | mixin(NoEscapeToken, constructor); 21 | return NoEscapeToken; 22 | } 23 | }; 24 | 25 | mixins['noEscape'] = __filename; 26 | -------------------------------------------------------------------------------- /test/core/pfeqParserTests.txt: -------------------------------------------------------------------------------- 1 | !! Version 2 2 | !! article 3 | Template:= 4 | !! text 5 | no 6 | !! endarticle 7 | 8 | # {{=}} is a parser function which expands to `=`, although 9 | # referencing a template explicitly as {{Template:=}} is fine 10 | # regardless of what it expands to. (T91154) 11 | !! test 12 | Basic usage of {{=}} 13 | !! wikitext 14 | This uses {{=}}. 15 | !! html 16 |

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) => (constructor: S): S => { 15 | abstract class PaddedToken extends constructor { 16 | override getAttribute(key: T): TokenAttribute { 17 | return key === 'padding' ? length as TokenAttribute : super.getAttribute(key); 18 | } 19 | } 20 | mixin(PaddedToken, constructor); 21 | return PaddedToken; 22 | }; 23 | 24 | mixins['padded'] = __filename; 25 | -------------------------------------------------------------------------------- /src/nowiki/hr.ts: -------------------------------------------------------------------------------- 1 | import {NowikiBaseToken} from './base'; 2 | 3 | /* NOT FOR BROWSER */ 4 | 5 | import {classes} from '../../util/constants'; 6 | import {sol} from '../../mixin/sol'; 7 | import {syntax} from '../../mixin/syntax'; 8 | import type {SyntaxBase} from '../../mixin/syntax'; 9 | 10 | export interface HrToken extends SyntaxBase {} 11 | 12 | /* NOT FOR BROWSER END */ 13 | 14 | /** `
` */ 15 | @sol() @syntax(/^-{4,}$/u) 16 | export abstract class HrToken extends NowikiBaseToken { 17 | override get type(): 'hr' { 18 | return 'hr'; 19 | } 20 | 21 | /* NOT FOR BROWSER */ 22 | 23 | /** @private */ 24 | override toHtmlInternal(): string { 25 | return '
'; 26 | } 27 | } 28 | 29 | classes['HrToken'] = __filename; 30 | -------------------------------------------------------------------------------- /src/nowiki/commentLine.ts: -------------------------------------------------------------------------------- 1 | import {NoincludeToken} from './noinclude'; 2 | import type {GalleryToken, ImagemapToken} from '../../internal'; 3 | 4 | /* NOT FOR BROWSER */ 5 | 6 | import {classes} from '../../util/constants'; 7 | import {singleLine} from '../../mixin/singleLine'; 8 | 9 | /* NOT FOR BROWSER END */ 10 | 11 | /** 12 | * ignored line in certain extension tags 13 | * 14 | * 某些扩展标签内被忽略的行 15 | */ 16 | @singleLine 17 | export abstract class CommentLineToken extends NoincludeToken { 18 | abstract override get parentNode(): GalleryToken | ImagemapToken | undefined; 19 | 20 | /* NOT FOR BROWSER */ 21 | 22 | abstract override get parentElement(): GalleryToken | ImagemapToken | undefined; 23 | } 24 | 25 | classes['CommentLineToken'] = __filename; 26 | -------------------------------------------------------------------------------- /test/core/parserFunctionTests.txt: -------------------------------------------------------------------------------- 1 | # This smoketest requires the #ifeq function to be defined 2 | !! options 3 | version=2 4 | requirements=extension:ParserFunctions 5 | !! end 6 | 7 | !! test 8 | T240248: Erroring parser function shouldn't break references 9 | !! wikitext 10 | something 11 | {{#ifeq: {{#expr: string < 5 }} | 1 | true | false or error }} 12 | 13 | !! html 14 |

[1] 15 | false or error 16 |

17 |
    18 |
  1. something 19 |
  2. 20 |
21 | !! end -------------------------------------------------------------------------------- /mixin/cached.ts: -------------------------------------------------------------------------------- 1 | import {cache} from '../util/lint'; 2 | import type {Token} from '../internal'; 3 | 4 | /* NOT FOR BROWSER */ 5 | 6 | import {mixins} from '../util/constants'; 7 | 8 | /* NOT FOR BROWSER END */ 9 | 10 | /** 11 | * 缓存计算结果 12 | * @param force 是否强制缓存 13 | */ 14 | export const cached = (force = true) => 15 | (method: (this: Token, ...args: any[]) => unknown) => { 16 | const stores = new WeakMap(); 17 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 18 | return function(this: Token, ...args: unknown[]): any { 19 | return cache( 20 | stores.get(this), 21 | () => method.apply(this, args), 22 | value => { 23 | stores.set(this, value); 24 | }, 25 | force, 26 | ); 27 | }; 28 | }; 29 | 30 | mixins['cached'] = __filename; 31 | -------------------------------------------------------------------------------- /script/util.ts: -------------------------------------------------------------------------------- 1 | import type * as ParserBase from '../index'; 2 | 3 | const redirects: Record = { 4 | 'File:Redirect_to_foobar.jpg': 'File:Foobar.jpg', 5 | 'Template:Redirect_to_foo': 'Template:Foo', 6 | 'Template:Templateredirect': 'Template:Templatesimple', 7 | }; 8 | 9 | /** 10 | * Prepare the parser for testing 11 | * @param Parser 12 | */ 13 | export const prepare = (Parser: ParserBase): void => { 14 | process.env.TZ = 'UTC'; 15 | Parser.viewOnly = true; 16 | Parser.warning = false; 17 | Parser.now = new Date(Date.UTC(1970, 0, 1, 0, 2, 3)); 18 | Parser.templateDir = './test/templates'; 19 | Parser.redirects = Object.entries(redirects) as Iterable<[string, string]> as Map; 20 | Parser.getConfig(); 21 | Object.assign(Parser.config, { 22 | testArticlePath: '//example.org/wiki/$1', 23 | testServer: 'http://example.org/wiki/$1', 24 | }); 25 | }; 26 | -------------------------------------------------------------------------------- /typings/event.d.ts: -------------------------------------------------------------------------------- 1 | import type {AstNodes, Token} from '../internal'; 2 | 3 | declare global { 4 | type AstEventType = 'insert' | 'remove' | 'text' | 'replace'; 5 | 6 | interface AstEvent extends Event { 7 | readonly type: AstEventType; 8 | readonly target: EventTarget & AstNodes; 9 | readonly currentTarget: EventTarget & Token; 10 | readonly prevTarget?: Token; 11 | } 12 | 13 | type AstEventData = ({ 14 | readonly type: 'insert'; 15 | readonly position: number; 16 | } | { 17 | readonly type: 'remove'; 18 | readonly position: number; 19 | readonly removed: AstNodes; 20 | } | { 21 | readonly type: 'text'; 22 | readonly oldText: string; 23 | } | { 24 | readonly type: 'replace'; 25 | readonly position: number; 26 | readonly oldToken: Token; 27 | }) & { 28 | oldKey?: string; 29 | newKey?: string; 30 | }; 31 | 32 | type AstListener = (e: AstEvent, data: AstEventData) => void; 33 | } 34 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "addon/**/*", 4 | "render/**/*", 5 | "bin/**/*", 6 | "script/**/*", 7 | "lib/**/*", 8 | "mixin/**/*", 9 | "parser/**/*", 10 | "src/**/*", 11 | "test/**/*", 12 | "typings/**/*", 13 | "util/**/*", 14 | "index.ts", 15 | "base.ts" 16 | ], 17 | "compilerOptions": { 18 | "lib": [ 19 | "es2024" 20 | ], 21 | "target": "es2024", 22 | "module": "NodeNext", 23 | "allowSyntheticDefaultImports": true, 24 | "declaration": true, 25 | "outDir": "dist", 26 | "alwaysStrict": true, 27 | "exactOptionalPropertyTypes": true, 28 | "noImplicitAny": true, 29 | "noImplicitOverride": true, 30 | "noImplicitThis": true, 31 | "noPropertyAccessFromIndexSignature": true, 32 | "noUncheckedIndexedAccess": true, 33 | "strictBindCallApply": true, 34 | "strictFunctionTypes": true, 35 | "strictNullChecks": true, 36 | "useUnknownInCatchVariables": true, 37 | "skipLibCheck": true 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /mixin/singleLine.ts: -------------------------------------------------------------------------------- 1 | import {mixin} from '../util/debug'; 2 | import {mixins} from '../util/constants'; 3 | 4 | /** 5 | * 不可包含换行符的类 6 | * @ignore 7 | */ 8 | export const singleLine = (constructor: T): T => { 9 | abstract class SingleLineToken extends constructor { 10 | override toString(skip?: boolean): string { 11 | if (this.parentNode?.name === 'inputbox') { 12 | return this.childNodes.map(child => { 13 | const str = child.toString(skip), 14 | {type} = child; 15 | return type === 'comment' || type === 'include' || type === 'ext' 16 | ? str 17 | : str.replaceAll('\n', ' '); 18 | }).join(''); 19 | } 20 | return super.toString(skip).replaceAll('\n', ' '); 21 | } 22 | 23 | override text(): string { 24 | return super.text().replaceAll('\n', ' '); 25 | } 26 | } 27 | mixin(SingleLineToken, constructor); 28 | return SingleLineToken; 29 | }; 30 | 31 | mixins['singleLine'] = __filename; 32 | -------------------------------------------------------------------------------- /mixin/fixed.ts: -------------------------------------------------------------------------------- 1 | import {Shadow, mixin} from '../util/debug'; 2 | import {mixins} from '../util/constants'; 3 | import type {AstNodes, AstText} from '../internal'; 4 | 5 | /** 6 | * 不可增删子节点的类 7 | * @ignore 8 | */ 9 | export const fixedToken = (constructor: S): S => { 10 | abstract class FixedToken extends constructor { 11 | get fixed(): true { 12 | return true; 13 | } 14 | 15 | removeAt(): never { 16 | this.constructorError('cannot remove child nodes'); 17 | } 18 | 19 | override insertAt(token: string, i?: number): AstText; 20 | override insertAt(token: T, i?: number): T; 21 | override insertAt(token: T | string, i?: number): T | AstText { 22 | return Shadow.running 23 | ? super.insertAt(token, i) as T | AstText 24 | : this.constructorError('cannot insert child nodes'); 25 | } 26 | } 27 | mixin(FixedToken, constructor); 28 | return FixedToken; 29 | }; 30 | 31 | mixins['fixedToken'] = __filename; 32 | -------------------------------------------------------------------------------- /lib/rect.ts: -------------------------------------------------------------------------------- 1 | import type {AstNodes, Position} from './node'; 2 | 3 | /* NOT FOR BROWSER */ 4 | 5 | import {classes} from '../util/constants'; 6 | 7 | /* NOT FOR BROWSER END */ 8 | 9 | /** 节点位置 */ 10 | export class BoundingRect { 11 | readonly #token: AstNodes; 12 | readonly #start: number; 13 | #pos: Position | undefined; 14 | 15 | /** 起点 */ 16 | get start(): number { 17 | return this.#start; 18 | } 19 | 20 | /** 起点行 */ 21 | get top(): number { 22 | return this.#getPosition().top; 23 | } 24 | 25 | /** 起点列 */ 26 | get left(): number { 27 | return this.#getPosition().left; 28 | } 29 | 30 | /** 31 | * @param token 节点 32 | * @param start 起点 33 | */ 34 | constructor(token: AstNodes, start: number) { 35 | this.#token = token; 36 | this.#start = start; 37 | } 38 | 39 | /** 计算位置 */ 40 | #getPosition(): Position { 41 | this.#pos ??= this.#token.getRootNode().posFromIndex(this.#start)!; 42 | return this.#pos; 43 | } 44 | } 45 | 46 | classes['BoundingRect'] = __filename; 47 | -------------------------------------------------------------------------------- /lib/redirectMap.ts: -------------------------------------------------------------------------------- 1 | import {classes} from '../util/constants'; 2 | 3 | /** 4 | * 快速规范化页面标题 5 | * @param title 标题 6 | */ 7 | const normalizeTitle = (title: string): string => { 8 | const Parser: typeof import('../index') = require('../index'); 9 | return String(Parser.normalizeTitle( 10 | title, 11 | 0, 12 | false, 13 | undefined, 14 | {temporary: true, page: ''}, 15 | )); 16 | }; 17 | 18 | /** 重定向列表 */ 19 | export class RedirectMap extends Map { 20 | #redirect; 21 | 22 | /** @ignore */ 23 | constructor(entries?: Iterable<[string, string]> | Record, redirect = true) { 24 | super(); 25 | this.#redirect = redirect; 26 | if (entries) { 27 | for (const [k, v] of Symbol.iterator in entries ? entries : Object.entries(entries)) { 28 | this.set(k, v); 29 | } 30 | } 31 | } 32 | 33 | override set(key: string, value: string): this { 34 | return super.set(normalizeTitle(key), this.#redirect ? normalizeTitle(value) : value); 35 | } 36 | } 37 | 38 | classes['RedirectMap'] = __filename; 39 | -------------------------------------------------------------------------------- /script/coverage.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | 3 | declare interface Summary { 4 | total: number; 5 | covered: number; 6 | skipped: number; 7 | pct: number; 8 | } 9 | declare interface Coverage { 10 | total: { 11 | lines: Summary; 12 | functions: Summary; 13 | statements: Summary; 14 | branches: Summary; 15 | }; 16 | } 17 | 18 | // eslint-disable-next-line n/no-missing-require 19 | const {total: {statements: {pct}}}: Coverage = require('../../coverage/coverage-summary.json'); 20 | const colors = ['#4c1', '#dfb317', '#e05d44'] as const, 21 | value = String(Math.round(pct)); 22 | /#4c1|#dfb317|#e05d44|\b\d{2}(?=%)/gu; // eslint-disable-line @typescript-eslint/no-unused-expressions 23 | const re = new RegExp(String.raw`${colors.join('|')}|\b\d{2}(?=%)`, 'gu'); 24 | let color: string; 25 | if (pct >= 80) { 26 | [color] = colors; 27 | } else if (pct >= 60) { 28 | [, color] = colors; 29 | } else { 30 | [,, color] = colors; 31 | } 32 | const svg = fs.readFileSync('coverage/badge.svg', 'utf8') 33 | .replace(re, m => m.startsWith('#') ? color : value); 34 | fs.writeFileSync('coverage/badge.svg', svg); 35 | -------------------------------------------------------------------------------- /test/core/stringFunctionTests.txt: -------------------------------------------------------------------------------- 1 | !! Version 2 2 | # Force the test runner to ensure the extension is loaded 3 | # Use '#switch' to do the detection, since we might not have 4 | # string functions enabled (yet) -- they are force-enabled on a per-test 5 | # basis by the ParserTestGlobals hook. 6 | !! functionhooks 7 | switch 8 | !! endfunctionhooks 9 | 10 | # @todo expand 11 | 12 | !! test 13 | #len 14 | !! wikitext 15 | {{#len:}} 16 | {{#len:0}} 17 | {{#len:test}} 18 | !! html 19 |

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 | Coverage: 88%Coverage88% -------------------------------------------------------------------------------- /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, `
\n\t${ 27 | isEnglish ? 'Table of Contents' : '目录' 28 | }\n\n${toc}\n\n
\n\n# Other Languages\n\n- [${ 29 | isEnglish ? '简体中文' : 'English' 30 | }](./${ 31 | isEnglish ? filename.slice(0, -5) : `${filename}-%28EN%29` 32 | })\n\n${content}`); 33 | -------------------------------------------------------------------------------- /script/declaration.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | import chalk from 'chalk'; 4 | 5 | const regex = /^declare const \w+_base:(?:.+\) &)? typeof (?:\w+\.)?(\w+)\b.+(export [^\n]+ extends )\w+\b/imsu; 6 | 7 | for (const file of fs.readdirSync('dist/src/', {recursive: true}) as string[]) { 8 | if (!file.endsWith('.d.ts')) { 9 | continue; 10 | } 11 | const fullPath = path.join('dist/src', file), 12 | content = fs.readFileSync(fullPath, 'utf8'); 13 | if (/^declare const \w+_base: /mu.test(content)) { 14 | console.log('%s %s', chalk.green('Cleaning declaration:'), file); 15 | fs.writeFileSync( 16 | fullPath, 17 | content.replace(regex, (_, base: string, exp: string): string => { 18 | /import \{\s*Token\b.+?;\n/su; // eslint-disable-line @typescript-eslint/no-unused-expressions 19 | const regex2 = new RegExp(String.raw`import \{\s*${base}\b.+?;\n`, 'su'); 20 | return ( 21 | regex2.test(content) 22 | ? '' 23 | : regex2.exec( 24 | fs.readFileSync( 25 | path.join('src', file.replace(/d\.ts$/u, 'ts')), 26 | 'utf8', 27 | ), 28 | )![0] 29 | ) + exp + base; 30 | }), 31 | ); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /mixin/hidden.ts: -------------------------------------------------------------------------------- 1 | import {mixin} from '../util/debug'; 2 | import type {LintError} from '../base'; 3 | 4 | /* NOT FOR BROWSER */ 5 | 6 | import {mixins} from '../util/constants'; 7 | import {cached} from './cached'; 8 | 9 | /* NOT FOR BROWSER END */ 10 | 11 | /** 12 | * 解析后不可见的类 13 | * @param linter 是否覆写 lint 方法 14 | * @param html 是否覆写 toHtml 方法 15 | */ 16 | export const hiddenToken = (linter = true, html = true) => (constructor: T): T => { 17 | abstract class AnyHiddenToken extends constructor { 18 | override text(): string { 19 | return ''; 20 | } 21 | 22 | override lint(start?: number): LintError[] { 23 | // @ts-expect-error private argument 24 | LINT: return linter ? [] : super.lint(start); 25 | } 26 | 27 | /* NOT FOR BROWSER */ 28 | 29 | override dispatchEvent(e: Event, data: unknown): void { 30 | e.stopPropagation(); 31 | super.dispatchEvent(e, data); 32 | } 33 | 34 | @cached() 35 | override toHtmlInternal(opt?: HtmlOpt): string { 36 | return html ? '' : super.toHtmlInternal(opt); 37 | } 38 | } 39 | mixin(AnyHiddenToken, constructor); 40 | return AnyHiddenToken; 41 | }; 42 | 43 | mixins['hiddenToken'] = __filename; 44 | -------------------------------------------------------------------------------- /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs 3 | 4 | name: Node.js CI 5 | 6 | on: 7 | push: 8 | tags: 9 | - 'v[0-9]+.[0-9]+.[0-9]+' 10 | workflow_dispatch: 11 | inputs: 12 | branch: 13 | description: 'The branch to build' 14 | required: true 15 | 16 | jobs: 17 | build: 18 | 19 | runs-on: ubuntu-latest 20 | strategy: 21 | matrix: 22 | node-version: ['20.x', '22.x', '24.x'] 23 | 24 | steps: 25 | - uses: actions/checkout@v4 26 | with: 27 | ref: ${{ github.event.inputs.branch }} 28 | - uses: actions/checkout@v4 29 | with: 30 | repository: bhsd-harry/wikiparser-node.wiki 31 | path: wiki 32 | - name: Use Node.js 33 | uses: actions/setup-node@v4 34 | with: 35 | node-version: ${{ matrix.node-version }} 36 | cache: 'npm' 37 | - run: npm ci 38 | - run: npm run build:core 39 | - run: npm run test:ci 40 | -------------------------------------------------------------------------------- /test/prof.ts: -------------------------------------------------------------------------------- 1 | import {readFileSync} from 'fs'; 2 | import single from './single'; 3 | import type {SimplePage} from '@bhsd/test-util'; 4 | 5 | /* NOT FOR BROWSER ONLY */ 6 | 7 | import {register} from 'module'; 8 | import {pathToFileURL} from 'url'; 9 | import path from 'path'; 10 | import {profile} from '@bhsd/nodejs'; 11 | import lsp from './lsp'; 12 | 13 | register(pathToFileURL(path.join(__dirname, 'hooks.js'))); 14 | 15 | /* NOT FOR BROWSER ONLY END */ 16 | 17 | const content = readFileSync('test/page.wiki', 'utf8'), 18 | {argv: [,, count, method = '']} = process; 19 | 20 | (async () => { 21 | /* NOT FOR BROWSER ONLY */ 22 | 23 | await profile( 24 | async () => { 25 | /* NOT FOR BROWSER ONLY END */ 26 | 27 | for (let i = 0; i < (Number(count) || 10); i++) { 28 | const page: SimplePage = {content, ns: 0, pageid: 0, title: `Pass ${i}`}; 29 | if ( 30 | method !== 'lsp' 31 | ) { 32 | await single(page, method); 33 | } 34 | if (!method || method === 'lsp') { 35 | await lsp(page); 36 | } 37 | console.log(); 38 | } 39 | 40 | /* NOT FOR BROWSER ONLY */ 41 | }, 42 | 43 | 'test', 44 | ); 45 | 46 | /* NOT FOR BROWSER ONLY END */ 47 | })(); 48 | -------------------------------------------------------------------------------- /src/hidden.ts: -------------------------------------------------------------------------------- 1 | import {hiddenToken} from '../mixin/hidden'; 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 type {Config} from '../base'; 10 | 11 | /* NOT FOR BROWSER END */ 12 | 13 | /** 14 | * invisible token 15 | * 16 | * 不可见的节点 17 | */ 18 | @hiddenToken() @noEscape 19 | export class HiddenToken extends Token { 20 | override get type(): 'hidden' { 21 | return 'hidden'; 22 | } 23 | 24 | /* PRINT ONLY */ 25 | 26 | /** @private */ 27 | override getAttribute(key: T): TokenAttribute { 28 | return (key === 'invalid') as TokenAttribute || super.getAttribute(key); 29 | } 30 | 31 | /* PRINT ONLY END */ 32 | 33 | /* NOT FOR BROWSER */ 34 | 35 | /** @class */ 36 | constructor(wikitext?: string, config?: Config, accum?: Token[]) { 37 | super(wikitext, config, accum, { 38 | 'Stage-2': ':', '!HeadingToken': '', 39 | }); 40 | } 41 | 42 | @clone 43 | override cloneNode(): this { 44 | return new HiddenToken(undefined, this.getAttribute('config')) as this; 45 | } 46 | } 47 | 48 | classes['HiddenToken'] = __filename; 49 | -------------------------------------------------------------------------------- /bump.sh: -------------------------------------------------------------------------------- 1 | #!/usr/local/bin/bash 2 | if (( $(ls -1q errors/* | wc -l) > 1 )) 3 | then 4 | echo 'There are remaining error files in the "errors/" directory. Please resolve them before proceeding.' 5 | exit 1 6 | fi 7 | if [[ $2 == 'npm' ]] 8 | then 9 | if [[ $(git tag -l "v$1-b") ]] 10 | then 11 | git checkout browser bundle/bundle{,-lsp}.min.js extensions/dist/*.js extensions/*.css 12 | npm publish --tag "${3-latest}" 13 | if [[ -z $3 ]] 14 | then 15 | npm dist-tag add "wikiparser-node@$1" browser 16 | fi 17 | rm bundle/bundle{,-lsp}.min.js extensions/dist/*.js extensions/*.css 18 | else 19 | echo "Tag v$1-b not found" 20 | exit 1 21 | fi 22 | elif [[ $2 == 'gh' ]] 23 | then 24 | gsed -n "/## v$1/,/##/{/^## .*/d;/./,\$!d;p}" CHANGELOG.md > release-notes.md 25 | gh release create "v$1" --notes-file release-notes.md -t "v$1" --verify-tag --latest="${3-true}" 26 | rm release-notes.md 27 | else 28 | npm run build && npm run lint && npm test && npm run test:real 29 | if [[ $? -eq 0 ]] 30 | then 31 | gsed -i -E "s/\"version\": \".+\"/\"version\": \"$1\"/" package.json 32 | npm i --package-lock-only 33 | git add -A 34 | git commit -m "chore: bump version to v$1" 35 | git push 36 | git tag "v$1" 37 | git push origin "v$1" 38 | fi 39 | fi 40 | -------------------------------------------------------------------------------- /src/multiLine/inputbox.ts: -------------------------------------------------------------------------------- 1 | import {parseCommentAndExt} from '../../parser/commentAndExt'; 2 | import {parseBraces} from '../../parser/braces'; 3 | import {ParamTagToken} from './paramTag'; 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 | 11 | /* NOT FOR BROWSER END */ 12 | 13 | /** `` */ 14 | export abstract class InputboxToken extends ParamTagToken { 15 | /** @param name 扩展标签名 */ 16 | constructor(name: string, include: boolean, wikitext: string | undefined, config: Config, accum: Token[] = []) { 17 | const placeholder = Symbol('InputboxToken'), 18 | newConfig = config.excludes.includes('heading') 19 | ? config 20 | : { 21 | ...config, 22 | excludes: [...config.excludes, 'heading'], 23 | }, 24 | {length} = accum; 25 | accum.push(placeholder as unknown as Token); 26 | wikitext &&= parseCommentAndExt(wikitext, newConfig, accum, include); 27 | wikitext &&= parseBraces(wikitext, newConfig, accum); 28 | accum.splice(length, 1); 29 | super(name, include, wikitext, newConfig, accum, { 30 | ArgToken: ':', TranscludeToken: ':', 31 | }); 32 | } 33 | } 34 | 35 | classes['InputboxToken'] = __filename; 36 | -------------------------------------------------------------------------------- /typings/node.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @stylistic/indent, @stylistic/operator-linebreak */ 2 | import type {Config} from '../base'; 3 | import type {Title} from '../lib/title'; 4 | import type { 5 | AstNodes, 6 | Token, 7 | 8 | /* NOT FOR BROWSER */ 9 | 10 | QuoteToken, 11 | } from '../internal'; 12 | 13 | /* NOT FOR BROWSER */ 14 | 15 | import type {Ranges} from '../lib/ranges'; 16 | 17 | /* NOT FOR BROWSER END */ 18 | 19 | declare global { 20 | type TokenAttribute = 21 | T extends 'stage' | 'padding' | 'aIndex' ? number : 22 | T extends 'config' ? Config : 23 | T extends 'accum' ? Token[] : 24 | T extends 'parentNode' ? Token | undefined : 25 | T extends 'nextSibling' | 'previousSibling' ? AstNodes | undefined : 26 | T extends 'childNodes' ? AstNodes[] : 27 | T extends 'bracket' | 'include' | 'built' | 'invalid' ? boolean : 28 | T extends 'title' ? Title : 29 | 30 | /* NOT FOR BROWSER */ 31 | 32 | T extends 'pattern' ? RegExp : 33 | T extends 'tags' | 'quotes' ? [string, string] : 34 | T extends 'keys' ? Set : 35 | T extends 'protectedChildren' ? Ranges : 36 | T extends 'acceptable' ? WikiParserAcceptable | undefined : 37 | T extends 'bold' | 'italic' ? QuoteToken : 38 | 39 | /* NOT FOR BROWSER END */ 40 | 41 | string; 42 | } 43 | -------------------------------------------------------------------------------- /parser/redirect.ts: -------------------------------------------------------------------------------- 1 | import Parser from '../index'; 2 | import {RedirectToken} from '../src/redirect'; 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 | /** 13 | * 解析重定向 14 | * @param text 15 | * @param config 16 | * @param accum 17 | */ 18 | export const parseRedirect = (text: string, config: Config, accum: Token[]): string | false => { 19 | // eslint-disable-next-line @typescript-eslint/no-unused-expressions 20 | /^(\s*)((?:#redirect|#重定向)\s*(?::\s*)?)\[\[([^\n|\]]+)(\|.*?)?\]\](\s*)/iu; 21 | config.regexRedirect ??= new RegExp(String.raw`^(\s*)((?:${ 22 | config.redirection.join('|') 23 | })\s*(?::\s*)?)\[\[([^\n|\]]+)(\|.*?)?\]\](\s*)`, 'iu'); 24 | const mt = config.regexRedirect.exec(text); 25 | if ( 26 | mt 27 | && Parser.normalizeTitle( 28 | mt[3]!, 29 | 0, 30 | false, 31 | config, 32 | {halfParsed: true, temporary: true, decode: true, page: ''}, 33 | ).valid 34 | ) { 35 | text = `\0${accum.length}o\x7F${text.slice(mt[0].length)}`; 36 | // @ts-expect-error abstract class 37 | new RedirectToken(...mt.slice(1), config, accum); 38 | return text; 39 | } 40 | return false; 41 | }; 42 | 43 | parsers['parseRedirect'] = __filename; 44 | -------------------------------------------------------------------------------- /mixin/sol.ts: -------------------------------------------------------------------------------- 1 | import {mixin} from '../util/debug'; 2 | import {mixins} from '../util/constants'; 3 | import type {Token, ListRangeToken} from '../internal'; 4 | 5 | /** 6 | * 只能位于行首的类 7 | * @param self 是否允许同类节点相邻 8 | */ 9 | export const sol = (self?: boolean) => (constructor: T): T => { 10 | abstract class SolToken extends constructor { 11 | #prependNewLine(): string { 12 | const {previousVisibleSibling, parentNode, type} = this as unknown as Token; 13 | if (previousVisibleSibling) { 14 | return self && previousVisibleSibling.type === type 15 | || previousVisibleSibling.toString().endsWith('\n') 16 | ? '' 17 | : '\n'; 18 | } 19 | return parentNode?.type === 'root' 20 | || type === 'list' && parentNode?.is('list-range') 21 | || parentNode?.type === 'ext-inner' && parentNode.name === 'poem' 22 | ? '' 23 | : '\n'; 24 | } 25 | 26 | override toString(skip?: boolean): string { 27 | return this.#prependNewLine() + super.toString(skip); 28 | } 29 | 30 | override getAttribute(key: S): TokenAttribute { 31 | return key === 'padding' 32 | ? this.#prependNewLine().length + super.getAttribute('padding') as TokenAttribute 33 | : super.getAttribute(key); 34 | } 35 | 36 | override text(): string { 37 | return this.#prependNewLine() + super.text(); 38 | } 39 | } 40 | mixin(SolToken, constructor); 41 | return SolToken; 42 | }; 43 | 44 | mixins['sol'] = __filename; 45 | -------------------------------------------------------------------------------- /mixin/nodeLike.ts: -------------------------------------------------------------------------------- 1 | import {mixin} from '../util/debug'; 2 | import type {Dimension} from '../lib/node'; 3 | import type {AstNodes} from '../internal'; 4 | 5 | /* NOT FOR BROWSER */ 6 | 7 | import {mixins} from '../util/constants'; 8 | 9 | /* NOT FOR BROWSER END */ 10 | 11 | declare type NodeConstructor = abstract new (...args: any[]) => { 12 | readonly childNodes: readonly AstNodes[]; 13 | getDimension(): Dimension; 14 | }; 15 | 16 | export interface NodeLike { 17 | 18 | /** first child node / 首位子节点 */ 19 | readonly firstChild: AstNodes | undefined; 20 | 21 | /** last child node / 末位子节点 */ 22 | readonly lastChild: AstNodes | undefined; 23 | 24 | /** number of lines / 行数 */ 25 | readonly offsetHeight: number; 26 | 27 | /** number of columns of the last line / 最后一行的列数 */ 28 | readonly offsetWidth: number; 29 | } 30 | 31 | /** @ignore */ 32 | export const nodeLike = (constructor: S): S => { 33 | abstract class NodeLike extends constructor implements NodeLike { 34 | get firstChild(): AstNodes | undefined { 35 | return this.childNodes[0]; 36 | } 37 | 38 | get lastChild(): AstNodes | undefined { 39 | return this.childNodes[this.childNodes.length - 1]; 40 | } 41 | 42 | get offsetHeight(): number { 43 | LINT: return this.getDimension().height; 44 | } 45 | 46 | get offsetWidth(): number { 47 | LINT: return this.getDimension().width; 48 | } 49 | } 50 | mixin(NodeLike, constructor); 51 | return NodeLike; 52 | }; 53 | 54 | mixins['nodeLike'] = __filename; 55 | -------------------------------------------------------------------------------- /util/constants.ts: -------------------------------------------------------------------------------- 1 | /* NOT FOR BROWSER */ 2 | 3 | import type {Token, FunctionHook, TagHook} from '../internal'; 4 | 5 | /* NOT FOR BROWSER END */ 6 | 7 | export const MAX_STAGE = 11; 8 | 9 | export enum BuildMethod { 10 | String, 11 | Text, 12 | } 13 | 14 | export const enMsg = /* #__PURE__ */ (() => { 15 | // eslint-disable-next-line n/no-missing-require 16 | LSP: return require('../../i18n/en.json'); 17 | })(); 18 | 19 | export const galleryParams = new Set(['alt', 'link', 'lang', 'page', 'caption']); 20 | 21 | export const extensions = new Set(['tiff', 'tif', 'png', 'gif', 'jpg', 'jpeg', 'webp', 'xcf', 'pdf', 'svg', 'djvu']); 22 | 23 | /* NOT FOR BROWSER ONLY */ 24 | 25 | export const mathTags = new Set(['math', 'chem', 'ce']); 26 | 27 | /* NOT FOR BROWSER ONLY END */ 28 | 29 | /* NOT FOR BROWSER */ 30 | 31 | export const classes: Record = {}, 32 | mixins = classes, 33 | parsers = classes; 34 | 35 | export const aliases = [ 36 | ['AstText'], 37 | ['CommentToken', 'ExtToken', 'IncludeToken', 'NoincludeToken', 'TranslateToken'], 38 | ['ArgToken', 'TranscludeToken', 'HeadingToken'], 39 | ['HtmlToken'], 40 | ['TableToken'], 41 | ['HrToken', 'DoubleUnderscoreToken'], 42 | ['LinkToken', 'FileToken', 'CategoryToken'], 43 | ['QuoteToken'], 44 | ['ExtLinkToken'], 45 | ['MagicLinkToken'], 46 | ['ListToken', 'DdToken'], 47 | ['ConverterToken'], 48 | ]; 49 | 50 | export const states = new WeakMap(); 51 | 52 | export const functionHooks = new Map(); 53 | 54 | export const tagHooks = new Map(); 55 | -------------------------------------------------------------------------------- /util/selector.ts: -------------------------------------------------------------------------------- 1 | import type {AstElement} from '../lib/element'; 2 | import type {Token} from '../internal'; 3 | 4 | // @ts-expect-error unconstrained predicate 5 | export type TokenPredicate = (token: AstElement) => token is T; 6 | declare type BasicCondition = (type: string, name?: string) => boolean; 7 | 8 | /** 9 | * type和name选择器 10 | * @param selector 11 | */ 12 | export const basic = (selector: string): BasicCondition => { 13 | if (selector.includes('#')) { 14 | const i = selector.indexOf('#'), 15 | targetType = selector.slice(0, i), 16 | targetName = selector.slice(i + 1); 17 | return (type, name) => (i === 0 || type === targetType) && name === targetName; 18 | } 19 | return type => type === selector; 20 | }; 21 | 22 | /** 23 | * 将选择器转化为类型谓词 24 | * @param selector 选择器 25 | * @param scope 作用对象 26 | * @param has `:has()`伪选择器 27 | */ 28 | export const getCondition = (selector: string, scope?: AstElement, has?: Token): TokenPredicate => { 29 | selector = selector.trim(); 30 | 31 | /* NOT FOR BROWSER */ 32 | 33 | if (/[^a-z\-,#\s]|(?; 36 | } 37 | 38 | /* NOT FOR BROWSER END */ 39 | 40 | /* istanbul ignore if */ 41 | if (!selector) { 42 | return (() => true) as unknown as TokenPredicate; 43 | } 44 | const parts = selector.split(',').map(str => str.trim()).filter(str => str !== '').map(basic); 45 | return (({type, name}): boolean => parts.some(condition => condition(type, name))) as TokenPredicate; 46 | }; 47 | -------------------------------------------------------------------------------- /data/.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "type": "object", 4 | "additionalProperties": { 5 | "type": "array", 6 | "items": { 7 | "type": "object", 8 | "properties": { 9 | "aliases": { 10 | "description": "An array of aliases for the magic word", 11 | "type": "array", 12 | "items": { 13 | "type": "string", 14 | "pattern": "^[^A-Z]+$" 15 | }, 16 | "minItems": 1, 17 | "uniqueItems": true 18 | }, 19 | "description": { 20 | "description": "A brief description of the magic word", 21 | "type": "string" 22 | }, 23 | "signatures": { 24 | "description": "An array of signatures for the magic word", 25 | "type": "array", 26 | "items": { 27 | "type": "array", 28 | "items": { 29 | "type": "object", 30 | "properties": { 31 | "label": { 32 | "description": "The label for the parameter", 33 | "type": "string" 34 | }, 35 | "const": { 36 | "description": "Whether the parameter is a constant", 37 | "type": "boolean" 38 | }, 39 | "rest": { 40 | "description": "Whether the parameter can repeat indefinitely", 41 | "type": "boolean" 42 | } 43 | }, 44 | "required": [ 45 | "label" 46 | ], 47 | "additionalProperties": false 48 | } 49 | }, 50 | "minItems": 1 51 | } 52 | }, 53 | "required": [ 54 | "aliases", 55 | "description" 56 | ], 57 | "additionalProperties": false 58 | } 59 | }, 60 | "required": [ 61 | "behaviorSwitches", 62 | "parserFunctions" 63 | ] 64 | } 65 | -------------------------------------------------------------------------------- /mixin/syntax.ts: -------------------------------------------------------------------------------- 1 | import {undo, Shadow, mixin} from '../util/debug'; 2 | import {mixins} from '../util/constants'; 3 | import {text} from '../util/string'; 4 | import Parser from '../index'; 5 | import type {AstNodes} from '../internal'; 6 | 7 | export interface SyntaxBase { 8 | /** @private */ 9 | pattern: RegExp; 10 | } 11 | 12 | /** 13 | * 满足特定语法格式的Token 14 | * @param pattern 语法正则 15 | */ 16 | export const syntax = (pattern?: RegExp) => (constructor: S): S => { 17 | abstract class SyntaxToken extends constructor implements SyntaxBase { 18 | declare pattern: RegExp; 19 | 20 | constructor(...args: any[]) { 21 | super(...args); // eslint-disable-line @typescript-eslint/no-unsafe-argument 22 | if (pattern) { 23 | this.pattern = pattern; 24 | } 25 | this.seal('pattern', true); 26 | } 27 | 28 | override afterBuild(): void { 29 | super.afterBuild(); 30 | if (!Parser.internal) { 31 | const /** @implements */ syntaxListener: AstListener = (e, data) => { 32 | if (!Shadow.running && !this.pattern.test(text(this.childNodes))) { 33 | undo(e, data); 34 | this.constructorError('cannot modify the syntax pattern'); 35 | } 36 | }; 37 | this.addEventListener(['remove', 'insert', 'replace', 'text'], syntaxListener); 38 | } 39 | } 40 | 41 | override safeReplaceChildren(elements: readonly (AstNodes | string)[]): void { 42 | if (Shadow.running || this.pattern.test(text(elements))) { 43 | Shadow.run(() => { 44 | super.safeReplaceChildren(elements); 45 | }); 46 | } 47 | } 48 | } 49 | mixin(SyntaxToken, constructor); 50 | return SyntaxToken; 51 | }; 52 | 53 | mixins['syntax'] = __filename; 54 | -------------------------------------------------------------------------------- /src/multiLine/index.ts: -------------------------------------------------------------------------------- 1 | import {Token} from '../index'; 2 | import type {AttributesToken, ExtToken} from '../../internal'; 3 | 4 | /* NOT FOR BROWSER */ 5 | 6 | import {classes} from '../../util/constants'; 7 | import {clone} from '../../mixin/clone'; 8 | 9 | /* NOT FOR BROWSER END */ 10 | 11 | /** 12 | * extension tag that is parsed line by line 13 | * 14 | * 逐行解析的扩展标签 15 | */ 16 | export abstract class MultiLineToken extends Token { 17 | declare readonly name: string; 18 | 19 | abstract override get nextSibling(): undefined; 20 | abstract override get previousSibling(): AttributesToken | undefined; 21 | abstract override get parentNode(): ExtToken | undefined; 22 | 23 | /* NOT FOR BROWSER */ 24 | 25 | abstract override get nextElementSibling(): undefined; 26 | abstract override get previousElementSibling(): AttributesToken | undefined; 27 | abstract override get parentElement(): ExtToken | undefined; 28 | 29 | /* NOT FOR BROWSER END */ 30 | 31 | override get type(): 'ext-inner' { 32 | return 'ext-inner'; 33 | } 34 | 35 | /** @private */ 36 | override toString(skip?: boolean): string { 37 | return super.toString(skip, '\n'); 38 | } 39 | 40 | /** @private */ 41 | override text(): string { 42 | return super.text('\n').replace(/\n\s*\n/gu, '\n'); 43 | } 44 | 45 | /** @private */ 46 | override getGaps(): number { 47 | return 1; 48 | } 49 | 50 | /** @private */ 51 | override print(): string { 52 | PRINT: return super.print({sep: '\n'}); 53 | } 54 | 55 | /* NOT FOR BROWSER */ 56 | 57 | @clone 58 | override cloneNode(): this { 59 | const C = this.constructor as new (...args: any[]) => this; 60 | return new C(undefined, this.getAttribute('config')); 61 | } 62 | } 63 | 64 | classes['MultiLineToken'] = __filename; 65 | -------------------------------------------------------------------------------- /data/ext/ThirdPartyNotices.txt: -------------------------------------------------------------------------------- 1 | THIRD-PARTY SOFTWARE NOTICES AND INFORMATION 2 | Do Not Translate or Localize 3 | 4 | This project incorporates components from the projects listed below. The original copyright notices and the licenses 5 | under which such components were received are set forth below. 6 | 7 | 8 | 9 | %% mediawiki-extensions-Kartographer (https://gerrit.wikimedia.org/g/mediawiki/extensions/Kartographer) 10 | ========================================= 11 | The MIT License (MIT) 12 | 13 | Copyright (c) 2015 Yuri Astrakhan and others, see AUTHORS.txt 14 | 15 | Permission is hereby granted, free of charge, to any person obtaining a copy 16 | of this software and associated documentation files (the "Software"), to deal 17 | in the Software without restriction, including without limitation the rights 18 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 19 | copies of the Software, and to permit persons to whom the Software is 20 | furnished to do so, subject to the following conditions: 21 | 22 | The above copyright notice and this permission notice shall be included in all 23 | copies or substantial portions of the Software. 24 | 25 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 26 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 27 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 28 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 29 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 30 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 31 | SOFTWARE. 32 | ========================================= 33 | END OF mediawiki-extensions-Kartographer NOTICES AND INFORMATION 34 | -------------------------------------------------------------------------------- /test/core/extTags.txt: -------------------------------------------------------------------------------- 1 | !! options 2 | parsoid-compatible=wt2html 3 | version=2 4 | !! end 5 | 6 | # Extension tags and strip markers 7 | 8 | !! test 9 | Extension tags in extension content: 10 | !! wikitext 11 | {{#tag:nowiki|abc}} 12 | !! html 13 |

abc 14 |

15 | !! end 16 | 17 | !! test 18 | Extension tags in extension content:
19 | !! wikitext
20 | {{#tag:pre|abc}}
21 | !! html
22 | 
abc
23 | !! end 24 | 25 | !! test 26 | Extension tags in extension content: 27 | !! wikitext 28 | {{#tag:indicator|abc|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=abc}}
40 | !! html
41 | 
content
42 | !! end 43 | 44 | !! test 45 | Extension tags in extension attributes: 46 | !! wikitext 47 | {{#tag:indicator|content|name=abc}} 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=abc}} 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:noignoredwiki|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]+|?|[^[: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 = `\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 |

`; 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 | [![npm version](https://badge.fury.io/js/wikiparser-node.svg)](https://www.npmjs.com/package/wikiparser-node)
 2 | [![CodeQL](https://github.com/bhsd-harry/wikiparser-node/actions/workflows/codeql.yml/badge.svg)](https://github.com/bhsd-harry/wikiparser-node/actions/workflows/codeql.yml)
 3 | [![CI](https://github.com/bhsd-harry/wikiparser-node/actions/workflows/node.js.yml/badge.svg)](https://github.com/bhsd-harry/wikiparser-node/actions/workflows/node.js.yml)
 4 | [![jsDelivr hits (npm)](https://img.shields.io/jsdelivr/npm/hm/wikiparser-node)](https://www.npmjs.com/package/wikiparser-node)
 5 | [![Codacy Badge](https://app.codacy.com/project/badge/Grade/a2fbe7641031451baca2947ae6d7891f)](https://app.codacy.com/gh/bhsd-harry/wikiparser-node/dashboard)
 6 | ![Istanbul coverage](./coverage/badge.svg)
 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