@bhsd/codemirror-mediawiki 2.19.4 → 2.19.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/i18n/en.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "2.19.4",
2
+ "version": "2.19.6",
3
3
  "lang": "en",
4
4
  "i18n-failed": "Failed to fetch the translation file in $1.",
5
5
  "title": "CodeMirror Addons",
package/i18n/zh-hans.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "2.19.4",
2
+ "version": "2.19.6",
3
3
  "lang": "zh-hans",
4
4
  "i18n-failed": "获取 $1 的语言文件失败。",
5
5
  "title": "CodeMirror插件",
package/i18n/zh-hant.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "2.19.4",
2
+ "version": "2.19.6",
3
3
  "lang": "zh-hant",
4
4
  "i18n-failed": "取得 $1 的語言檔案失敗。",
5
5
  "title": "CodeMirror外掛程式",
package/mediawiki.css CHANGED
@@ -29,17 +29,6 @@
29
29
  .cm-focused .cm-nonmatchingTag {
30
30
  background-color: rgba(187, 85, 85, .27);
31
31
  }
32
- .cm-mw-template-name,
33
- .cm-mw-link-pagename,
34
- .cm-mw-parserfunction.cm-mw-pagename,
35
- .cm-mw-exttag-attribute-value.cm-mw-pagename,
36
- .cm-mw-extlink-protocol,
37
- .cm-mw-extlink,
38
- .cm-mw-free-extlink-protocol,
39
- .cm-mw-free-extlink,
40
- .cm-mw-magic-link {
41
- cursor: var(--codemirror-cursor);
42
- }
43
32
  #cm-preference > .oo-ui-window-frame {
44
33
  height: 100% !important;
45
34
  }
package/mw/config.ts ADDED
@@ -0,0 +1,180 @@
1
+ import {CDN, setObject, getObject} from '@bhsd/common';
2
+ import {getStaticMwConfig} from '../src/static';
3
+ import type {Config} from 'wikiparser-node';
4
+ import type {MwConfig} from '../src/token';
5
+
6
+ declare interface MagicWord {
7
+ name: string;
8
+ aliases: string[];
9
+ 'case-sensitive': boolean;
10
+ }
11
+
12
+ declare type MagicRule = (word: MagicWord) => boolean;
13
+
14
+ // 和本地缓存有关的常数
15
+ const ALL_SETTINGS_CACHE: Record<string, {time: number, config: MwConfig}> =
16
+ getObject('InPageEditMwConfig') ?? {},
17
+ SITE_ID = typeof mw === 'object' ? mw.config.get('wgServerName') + mw.config.get('wgScriptPath') : location.origin,
18
+ SITE_SETTINGS = ALL_SETTINGS_CACHE[SITE_ID],
19
+ VALID = Number(SITE_SETTINGS?.time) > Date.now() - 86_400 * 1000 * 30;
20
+
21
+ /**
22
+ * 将魔术字信息转换为CodeMirror接受的设置
23
+ * @param magicWords 完整魔术字列表
24
+ * @param rule 过滤函数
25
+ * @param flip 是否反向筛选对大小写敏感的魔术字
26
+ */
27
+ const getConfig = (magicWords: MagicWord[], rule: MagicRule, flip?: boolean): Record<string, string> =>
28
+ Object.fromEntries(
29
+ magicWords.filter(rule).filter(({'case-sensitive': i}) => i !== flip)
30
+ .flatMap(({aliases, name, 'case-sensitive': i}) => aliases.map(alias => ({
31
+ alias: (i ? alias : alias.toLowerCase()).replace(/:$/u, ''),
32
+ name,
33
+ })))
34
+ .map(({alias, name}) => [alias, name]),
35
+ );
36
+
37
+ /**
38
+ * 将魔术字信息转换为CodeMirror接受的设置
39
+ * @param magicWords 完整魔术字列表
40
+ * @param rule 过滤函数
41
+ */
42
+ const getConfigPair = (magicWords: MagicWord[], rule: MagicRule): [Record<string, string>, Record<string, string>] =>
43
+ [true, false].map(bool => getConfig(magicWords, rule, bool)) as [Record<string, string>, Record<string, string>];
44
+
45
+ /**
46
+ * 将设置保存到mw.config
47
+ * @param config 设置
48
+ */
49
+ const setConfig = (config: MwConfig): void => {
50
+ mw.config.set('extCodeMirrorConfig', config);
51
+ };
52
+
53
+ /**
54
+ * 加载CodeMirror的mediawiki模块需要的设置
55
+ * @param modes tagModes
56
+ */
57
+ export const getMwConfig = async (modes: Record<string, string>): Promise<MwConfig> => {
58
+ if (mw.loader.getState('ext.CodeMirror') !== null && !VALID) { // 只在localStorage过期时才会重新加载ext.CodeMirror.data
59
+ await mw.loader.using(mw.loader.getState('ext.CodeMirror.data') ? 'ext.CodeMirror.data' : 'ext.CodeMirror');
60
+ }
61
+
62
+ let config = mw.config.get('extCodeMirrorConfig') as MwConfig | null;
63
+ if (!config && VALID) {
64
+ ({config} = SITE_SETTINGS!);
65
+ setConfig(config);
66
+ }
67
+ const isIPE = config && Object.values(config.functionSynonyms[0]).includes(true as unknown as string),
68
+ nsid = mw.config.get('wgNamespaceIds');
69
+ // 情形1:config已更新,可能来自localStorage
70
+ if (config?.img && config.redirection && config.variants && !isIPE) {
71
+ config.urlProtocols = config.urlProtocols.replace(/\\:/gu, ':');
72
+ config.tagModes = modes;
73
+ return {...config, nsid};
74
+ } else if (location.hostname.endsWith('.moegirl.org.cn')) {
75
+ const parserConfig: Config = await (await fetch(
76
+ `${CDN}/npm/wikiparser-node@browser/config/moegirl.json`,
77
+ )).json();
78
+ setObject('wikilintConfig', parserConfig);
79
+ config = getStaticMwConfig(parserConfig, modes);
80
+ } else {
81
+ // 以下情形均需要发送API请求
82
+ // 情形2:localStorage未过期但不包含新设置
83
+ // 情形3:新加载的 ext.CodeMirror.data
84
+ // 情形4:`config === null`
85
+ await mw.loader.using('mediawiki.api');
86
+ const {query: {general: {variants}, magicwords, extensiontags, functionhooks, variables}}: {
87
+ query: {
88
+ general: {variants?: {code: string}[]};
89
+ magicwords: MagicWord[];
90
+ extensiontags: string[];
91
+ functionhooks: string[];
92
+ variables: string[];
93
+ };
94
+ } = await new mw.Api().get({
95
+ meta: 'siteinfo',
96
+ siprop: [
97
+ 'general',
98
+ 'magicwords',
99
+ ...config && !isIPE ? [] : ['extensiontags', 'functionhooks', 'variables'],
100
+ ],
101
+ formatversion: '2',
102
+ }) as any; // eslint-disable-line @typescript-eslint/no-explicit-any
103
+ const others = new Set(['msg', 'raw', 'msgnw', 'subst', 'safesubst']);
104
+
105
+ // 先处理魔术字和状态开关
106
+ if (config && !isIPE) { // 情形2或3
107
+ const {functionSynonyms: [insensitive]} = config;
108
+ if (!('subst' in insensitive)) {
109
+ Object.assign(insensitive, getConfig(magicwords, ({name}) => others.has(name)));
110
+ }
111
+ } else { // 情形4:`config === null`
112
+ const functions = new Set([
113
+ ...functionhooks,
114
+ ...variables,
115
+ ...others,
116
+ ]);
117
+ // @ts-expect-error incomplete properties
118
+ config = {
119
+ tags: Object.fromEntries(extensiontags.map(tag => [tag.slice(1, -1), true])),
120
+ functionSynonyms: getConfigPair(magicwords, ({name}) => functions.has(name)),
121
+ doubleUnderscore: getConfigPair(
122
+ magicwords,
123
+ ({aliases}) => aliases.some(alias => /^__.+__$/u.test(alias)),
124
+ ),
125
+ };
126
+ }
127
+ config!.tagModes = modes;
128
+ config!.img = getConfig(magicwords, ({name}) => name.startsWith('img_'));
129
+ config!.variants = variants ? variants.map(({code}) => code) : [];
130
+ config!.redirection = magicwords.find(({name}) => name === 'redirect')!.aliases;
131
+ config!.urlProtocols = mw.config.get('wgUrlProtocols').replace(/\\:/gu, ':');
132
+ }
133
+ setConfig(config!);
134
+ ALL_SETTINGS_CACHE[SITE_ID] = {config: config!, time: Date.now()};
135
+ setObject('InPageEditMwConfig', ALL_SETTINGS_CACHE);
136
+ return {...config!, nsid};
137
+ };
138
+
139
+ /**
140
+ * 将MwConfig转换为Config
141
+ * @param minConfig 基础Config
142
+ * @param mwConfig
143
+ */
144
+ export const getParserConfig = (minConfig: Config, mwConfig: MwConfig): Config => {
145
+ let config: Config | null = getObject('wikilintConfig');
146
+ if (config) {
147
+ return config;
148
+ }
149
+ config = {
150
+ ...minConfig,
151
+ ext: Object.keys(mwConfig.tags),
152
+ namespaces: mw.config.get('wgFormattedNamespaces'),
153
+ nsid: mwConfig.nsid,
154
+ doubleUnderscore: mwConfig.doubleUnderscore.map(
155
+ obj => Object.keys(obj).map(s => s.slice(2, -2)),
156
+ ) as [string[], string[]],
157
+ variants: mwConfig.variants!,
158
+ protocol: mwConfig.urlProtocols.replace(/\|\\?\/\\?\//u, ''),
159
+ redirection: mwConfig.redirection ?? minConfig.redirection,
160
+ };
161
+ if (location.hostname.endsWith('.moegirl.org.cn')) {
162
+ config.html[2].push('img');
163
+ }
164
+ [config.parserFunction[0]] = mwConfig.functionSynonyms;
165
+ if (mw.loader.getState('ext.CodeMirror') === null) {
166
+ for (const [key, val] of Object.entries(mwConfig.functionSynonyms[0])) {
167
+ if (!key.startsWith('#')) {
168
+ config.parserFunction[0][`#${key}`] = val;
169
+ }
170
+ }
171
+ }
172
+ config.parserFunction[1] = [
173
+ ...Object.keys(mwConfig.functionSynonyms[1]),
174
+ '=',
175
+ ];
176
+ for (const [key, val] of Object.entries(mwConfig.img!)) {
177
+ config.img[key] = val.slice(4);
178
+ }
179
+ return config;
180
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bhsd/codemirror-mediawiki",
3
- "version": "2.19.4",
3
+ "version": "2.19.6",
4
4
  "description": "Modified CodeMirror mode based on wikimedia/mediawiki-extensions-CodeMirror",
5
5
  "keywords": [
6
6
  "mediawiki",
@@ -16,8 +16,12 @@
16
16
  "/i18n/",
17
17
  "/dist/*.js",
18
18
  "/dist/*.d.ts",
19
+ "/dist/*.mjs",
19
20
  "!/dist/*-page.d.ts",
20
21
  "!/dist/parser.*",
22
+ "/src/linter.ts",
23
+ "/src/static.ts",
24
+ "/mw/config.ts",
21
25
  "/mediawiki.css"
22
26
  ],
23
27
  "browser": "dist/main.min.js",
@@ -29,8 +33,8 @@
29
33
  },
30
34
  "scripts": {
31
35
  "prepublishOnly": "npm run build",
32
- "build:core": "esbuild ./src/codemirror.ts --charset=utf8 --bundle --minify --target=es2019 --format=esm --sourcemap --outfile=dist/main.min.js && tsc --emitDeclarationOnly",
33
- "build:mw": "esbuild ./mw/base.ts --charset=utf8 --bundle --minify --target=es2019 --format=esm --sourcemap --outfile=dist/mw.min.js",
36
+ "build:core": "esbuild ./src/codemirror.ts --charset=utf8 --bundle --minify --target=es2019 --format=esm --sourcemap --outfile=dist/main.min.js && esbuild ./src/linter.ts --charset=utf8 --target=es2019 --format=esm --outfile=dist/linter.mjs && tsc --emitDeclarationOnly",
37
+ "build:mw": "esbuild ./mw/base.ts --charset=utf8 --bundle --minify --target=es2019 --format=esm --sourcemap --outfile=dist/mw.min.js && esbuild ./mw/config.ts --charset=utf8 --bundle --target=es2019 --format=esm --outfile=dist/mwConfig.mjs",
34
38
  "build:wiki": "esbuild ./mw/base.ts --charset=utf8 --bundle --minify --target=es2019 --format=iife --sourcemap --outfile=dist/wiki.min.js",
35
39
  "build:gh-page": "bash build.sh",
36
40
  "build:test": "tsc --project test/tsconfig.json && node test/dist/test/test.js",
@@ -45,6 +49,7 @@
45
49
  "test:real": "node test/dist/test/real.js"
46
50
  },
47
51
  "dependencies": {
52
+ "@bhsd/common": "^0.5.0",
48
53
  "@codemirror/language": "^6.10.6",
49
54
  "@codemirror/lint": "^6.8.4",
50
55
  "@codemirror/state": "^6.4.1",
@@ -54,7 +59,6 @@
54
59
  "wikiparser-node": "^1.14.0"
55
60
  },
56
61
  "devDependencies": {
57
- "@bhsd/common": "^0.4.6",
58
62
  "@codemirror/autocomplete": "^6.18.3",
59
63
  "@codemirror/commands": "^6.7.1",
60
64
  "@codemirror/lang-css": "^6.3.1",
@@ -66,7 +70,9 @@
66
70
  "@replit/codemirror-css-color-picker": "^6.3.0",
67
71
  "@stylistic/eslint-plugin": "^2.11.0",
68
72
  "@stylistic/stylelint-plugin": "^3.1.1",
73
+ "@types/eslint": "^8.56.10",
69
74
  "@types/jquery": "^3.5.32",
75
+ "@types/node": "^22.10.1",
70
76
  "@types/oojs-ui": "^0.49.0",
71
77
  "@typescript-eslint/eslint-plugin": "^8.16.0",
72
78
  "@typescript-eslint/parser": "^8.16.0",
package/src/linter.ts ADDED
@@ -0,0 +1,149 @@
1
+ import {CDN, loadScript} from '@bhsd/common';
2
+ import type {LinterBase} from 'wikiparser-node/extensions/typings';
3
+ import type {Linter} from 'eslint';
4
+ import type {Warning} from 'stylelint';
5
+ import type {Diagnostic} from 'luacheck-browserify';
6
+
7
+ declare type getLinter<T> = (opt?: Record<string, unknown>) => T;
8
+ declare type getAsyncLinter<T> = (opt?: Record<string, unknown>) => Promise<T>;
9
+
10
+ /**
11
+ * 获取 WikiLint
12
+ * @param opt 选项
13
+ */
14
+ export const getWikiLinter: getAsyncLinter<LinterBase> = async opt => {
15
+ const REPO = 'npm/wikiparser-node@browser',
16
+ DIR = `${REPO}/extensions/dist`,
17
+ lang = opt?.['i18n'];
18
+ await loadScript(`${DIR}/base.min.js`, 'wikiparse');
19
+ await loadScript(`${DIR}/lint.min.js`, 'wikiparse.Linter');
20
+ if (typeof lang === 'string') {
21
+ try {
22
+ const i18n: Record<string, string> =
23
+ await (await fetch(`${CDN}/${REPO}/i18n/${lang.toLowerCase()}.json`)).json();
24
+ wikiparse.setI18N(i18n);
25
+ } catch {}
26
+ }
27
+ return new wikiparse.Linter!(opt?.['include'] as boolean | undefined);
28
+ };
29
+
30
+ /**
31
+ * 获取 ESLint
32
+ * @param opt 选项
33
+ */
34
+ export const getJsLinter: getAsyncLinter<(text: string) => Linter.LintMessage[]> = async opt => {
35
+ await loadScript('npm/eslint-linter-browserify@8.57.0/linter.min.js', 'eslint', true);
36
+ /** @see https://www.npmjs.com/package/@codemirror/lang-javascript */
37
+ const esLinter = new eslint.Linter(),
38
+ conf: Linter.Config = {
39
+ env: {browser: true, es2024: true},
40
+ parserOptions: {ecmaVersion: 15, sourceType: 'module'},
41
+ rules: {},
42
+ ...opt,
43
+ };
44
+ for (const [name, {meta}] of esLinter.getRules()) {
45
+ if (meta?.docs?.recommended) {
46
+ conf.rules![name] ??= 2;
47
+ }
48
+ }
49
+ return text => esLinter.verify(text, conf);
50
+ };
51
+
52
+ /**
53
+ * 获取 Stylelint
54
+ * @param opt 选项
55
+ */
56
+ export const getCssLinter: getAsyncLinter<(text: string) => Promise<Warning[]>> = async opt => {
57
+ await loadScript('npm/stylelint-bundle', 'stylelint');
58
+ /** @see https://www.npmjs.com/package/stylelint-config-recommended */
59
+ const config = {
60
+ rules: {
61
+ 'annotation-no-unknown': true,
62
+ 'at-rule-no-unknown': true,
63
+ 'block-no-empty': true,
64
+ 'color-no-invalid-hex': true,
65
+ 'comment-no-empty': true,
66
+ 'custom-property-no-missing-var-function': true,
67
+ 'declaration-block-no-duplicate-custom-properties': true,
68
+ 'declaration-block-no-duplicate-properties': [
69
+ true,
70
+ {
71
+ ignore: ['consecutive-duplicates-with-different-syntaxes'],
72
+ },
73
+ ],
74
+ 'declaration-block-no-shorthand-property-overrides': true,
75
+ 'font-family-no-duplicate-names': true,
76
+ 'font-family-no-missing-generic-family-keyword': true,
77
+ 'function-calc-no-unspaced-operator': true,
78
+ 'function-linear-gradient-no-nonstandard-direction': true,
79
+ 'function-no-unknown': true,
80
+ 'keyframe-block-no-duplicate-selectors': true,
81
+ 'keyframe-declaration-no-important': true,
82
+ 'media-feature-name-no-unknown': true,
83
+ 'media-query-no-invalid': true,
84
+ 'named-grid-areas-no-invalid': true,
85
+ 'no-descending-specificity': true,
86
+ 'no-duplicate-at-import-rules': true,
87
+ 'no-duplicate-selectors': true,
88
+ 'no-empty-source': true,
89
+ 'no-invalid-double-slash-comments': true,
90
+ 'no-invalid-position-at-import-rule': true,
91
+ 'no-irregular-whitespace': true,
92
+ 'property-no-unknown': true,
93
+ 'selector-anb-no-unmatchable': true,
94
+ 'selector-pseudo-class-no-unknown': true,
95
+ 'selector-pseudo-element-no-unknown': true,
96
+ 'selector-type-no-unknown': [
97
+ true,
98
+ {
99
+ ignore: ['custom-elements'],
100
+ },
101
+ ],
102
+ 'string-no-newline': true,
103
+ 'unit-no-unknown': true,
104
+ ...opt?.['rules'] as Record<string, unknown>,
105
+ },
106
+ };
107
+ return async code => (await stylelint.lint({code, config})).results.flatMap(({warnings}) => warnings);
108
+ };
109
+
110
+ /** 获取 Luacheck */
111
+ export const getLuaLinter: getAsyncLinter<(text: string) => Promise<Diagnostic[]>> = async () => {
112
+ await loadScript('npm/luacheck-browserify/dist/index.min.js', 'luacheck');
113
+ const luachecker = await luacheck(undefined as unknown as string);
114
+ return async text => (await luachecker.queue(text)).filter(({severity}) => severity);
115
+ };
116
+
117
+ declare interface JsonError {
118
+ message: string;
119
+ severity: 'error';
120
+ line: string | undefined;
121
+ column: string | undefined;
122
+ position: string | undefined;
123
+ }
124
+
125
+ /** JSON.parse */
126
+ export const getJsonLinter: getLinter<(text: string) => JsonError[]> = () => str => {
127
+ try {
128
+ if (str.trim()) {
129
+ JSON.parse(str);
130
+ }
131
+ } catch (e) {
132
+ if (e instanceof SyntaxError) {
133
+ const {message} = e,
134
+ line = /\bline (\d+)/u.exec(message)?.[1],
135
+ column = /\bcolumn (\d+)/u.exec(message)?.[1],
136
+ position = /\bposition (\d+)/u.exec(message)?.[1];
137
+ return [
138
+ {
139
+ message,
140
+ severity: 'error',
141
+ line,
142
+ column,
143
+ position,
144
+ },
145
+ ];
146
+ }
147
+ }
148
+ return [];
149
+ };
package/src/static.ts ADDED
@@ -0,0 +1,66 @@
1
+ import type {Config} from 'wikiparser-node';
2
+ import type {MwConfig} from './token';
3
+
4
+ export const tagModes = {
5
+ onlyinclude: 'mediawiki',
6
+ includeonly: 'mediawiki',
7
+ noinclude: 'mediawiki',
8
+ pre: 'text/pre',
9
+ nowiki: 'text/nowiki',
10
+ indicator: 'mediawiki',
11
+ poem: 'mediawiki',
12
+ ref: 'mediawiki',
13
+ references: 'text/references',
14
+ gallery: 'text/gallery',
15
+ poll: 'mediawiki',
16
+ tabs: 'mediawiki',
17
+ tab: 'mediawiki',
18
+ choose: 'text/choose',
19
+ option: 'mediawiki',
20
+ combobox: 'text/combobox',
21
+ combooption: 'mediawiki',
22
+ inputbox: 'text/inputbox',
23
+ templatedata: 'json',
24
+ mapframe: 'json',
25
+ maplink: 'json',
26
+ graph: 'json',
27
+ };
28
+
29
+ /**
30
+ * Object.fromEntries polyfill
31
+ * @param entries
32
+ * @param obj
33
+ * @param string 是否为字符串
34
+ */
35
+ const fromEntries = (entries: readonly string[], obj: Record<string, unknown>, string?: boolean): void => {
36
+ for (const entry of entries) {
37
+ obj[entry] = string ? entry : true;
38
+ }
39
+ };
40
+
41
+ export const getStaticMwConfig = (
42
+ {parserFunction, protocol, nsid, variants, redirection, ext, doubleUnderscore, img}: Config,
43
+ modes: Record<string, string>,
44
+ ): MwConfig => {
45
+ const mwConfig: MwConfig = {
46
+ tags: {},
47
+ tagModes: modes,
48
+ doubleUnderscore: [{}, {}],
49
+ functionSynonyms: [parserFunction[0], {}],
50
+ urlProtocols: `${protocol}|//`,
51
+ nsid,
52
+ img: Object.fromEntries(Object.entries(img).map(([key, val]) => [key, `img_${val}`])),
53
+ variants,
54
+ redirection,
55
+ },
56
+ [insensitive,, obj] = doubleUnderscore;
57
+ fromEntries(ext, mwConfig.tags);
58
+ fromEntries(
59
+ (obj && insensitive.length === 0 ? Object.keys(obj) : insensitive).map(s => `__${s}__`),
60
+ mwConfig.doubleUnderscore[0],
61
+ );
62
+ fromEntries(doubleUnderscore[1].map(s => `__${s}__`), mwConfig.doubleUnderscore[1]);
63
+ fromEntries((parserFunction.slice(2) as string[][]).flat(), mwConfig.functionSynonyms[0], true);
64
+ fromEntries(parserFunction[1], mwConfig.functionSynonyms[1]);
65
+ return mwConfig;
66
+ };