@bhsd/codemirror-mediawiki 2.28.0 → 2.29.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -58,8 +58,8 @@ var getParserConfig = (minConfig, mwConfig) => {
58
58
  };
59
59
  };
60
60
  var getVariants = (variants) => {
61
- var _a2;
62
- return (_a2 = variants == null ? void 0 : variants.map(({ code }) => code)) != null ? _a2 : [];
61
+ var _a;
62
+ return (_a = variants == null ? void 0 : variants.map(({ code }) => code)) != null ? _a : [];
63
63
  };
64
64
  var getKeywords = (magicwords, web) => ({
65
65
  img: Object.fromEntries(
@@ -107,18 +107,14 @@ var getStaticMwConfig = ({
107
107
  });
108
108
 
109
109
  // mw/config.ts
110
- var _a;
111
- var ALL_SETTINGS_CACHE = (_a = getObject("InPageEditMwConfig")) != null ? _a : {};
112
- var SITE_ID = typeof mw === "object" ? mw.config.get("wgServerName") + mw.config.get("wgScriptPath") : location.origin;
113
- var SITE_SETTINGS = ALL_SETTINGS_CACHE[SITE_ID];
114
- var VALID = Number(SITE_SETTINGS == null ? void 0 : SITE_SETTINGS.time) > Date.now() - 86400 * 1e3 * 30;
115
110
  var others = /* @__PURE__ */ new Set([...otherParserFunctions, "msgnw"]);
116
111
  var getConfigPair = (magicWords, rule) => [true, false].map((bool) => getConfig(magicWords, rule, bool));
117
112
  var setConfig = (config) => {
118
113
  mw.config.set("extCodeMirrorConfig", config);
119
114
  };
120
115
  var getMwConfig = async (modes) => {
121
- var _a2, _b;
116
+ var _a, _b, _c;
117
+ const ALL_SETTINGS_CACHE = (_a = getObject("InPageEditMwConfig")) != null ? _a : {}, SITE_ID = typeof mw === "object" ? mw.config.get("wgServerName") + mw.config.get("wgScriptPath") : location.origin, SITE_SETTINGS = ALL_SETTINGS_CACHE[SITE_ID], VALID = Number(SITE_SETTINGS == null ? void 0 : SITE_SETTINGS.time) > Date.now() - 86400 * 1e3 * 30;
122
118
  if (mw.loader.getState("ext.CodeMirror") !== null && !VALID) {
123
119
  await mw.loader.using(
124
120
  mw.loader.getState("ext.CodeMirror.data") ? "ext.CodeMirror.data" : "ext.CodeMirror"
@@ -179,8 +175,8 @@ var getMwConfig = async (modes) => {
179
175
  variants: getVariants(variants),
180
176
  urlProtocols: mw.config.get("wgUrlProtocols").replace(/\\:/gu, ":")
181
177
  });
182
- (_a2 = config.variableIDs) != null ? _a2 : config.variableIDs = variables;
183
- (_b = config.functionHooks) != null ? _b : config.functionHooks = functionhooks.map((s) => s.toLowerCase());
178
+ (_b = config.variableIDs) != null ? _b : config.variableIDs = variables;
179
+ (_c = config.functionHooks) != null ? _c : config.functionHooks = functionhooks.map((s) => s.toLowerCase());
184
180
  if (!config.functionHooks.includes("msgnw")) {
185
181
  config.functionHooks.push("msgnw");
186
182
  }
@@ -0,0 +1,97 @@
1
+ import { EditorView } from '@codemirror/view';
2
+ import { ensureSyntaxTree } from '@codemirror/language';
3
+ import { tokens } from './config';
4
+ import { hasTag } from './mediawiki';
5
+ const { vendor, userAgent, maxTouchPoints, platform } = navigator;
6
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
7
+ export const isMac = vendor?.includes('Apple Computer')
8
+ && (userAgent.includes('Mobile/') || maxTouchPoints > 2)
9
+ || platform.includes('Mac');
10
+ const modKey = isMac ? 'metaKey' : 'ctrlKey', key = isMac ? 'Meta' : 'Control', tags = ['extLinkProtocol', 'extLink', 'freeExtLinkProtocol', 'freeExtLink', 'magicLink', 'pageName'], links = ['extlink-protocol', 'extlink', 'free-extlink-protocol', 'free-extlink', 'magic-link'], wikiLinks = [
11
+ 'template-name',
12
+ 'link-pagename',
13
+ 'parserfunction.cm-mw-pagename',
14
+ 'exttag-attribute-value.cm-mw-pagename',
15
+ 'file-text.cm-mw-pagename',
16
+ ];
17
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
18
+ globalThis.document?.addEventListener('keydown', e => {
19
+ if (e.key === key) {
20
+ for (const ele of document.querySelectorAll('.cm-content')) {
21
+ ele.style.setProperty('--codemirror-cursor', 'pointer');
22
+ }
23
+ }
24
+ });
25
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
26
+ globalThis.document?.addEventListener('keyup', e => {
27
+ if (e.key === key) {
28
+ for (const ele of document.querySelectorAll('.cm-content')) {
29
+ ele.style.removeProperty('--codemirror-cursor');
30
+ }
31
+ }
32
+ });
33
+ const wrapURL = (url) => url.startsWith('//') ? location.protocol + url : url;
34
+ export const mouseEventListener = (e, view, langConfig) => {
35
+ if (!e[modKey]
36
+ || !(e.target instanceof Element && getComputedStyle(e.target).textDecorationLine === 'underline')) {
37
+ return undefined;
38
+ }
39
+ const position = view.posAtCoords(e);
40
+ if (!position) {
41
+ return undefined;
42
+ }
43
+ const { state } = view, tree = ensureSyntaxTree(state, position);
44
+ if (!tree) {
45
+ return undefined;
46
+ }
47
+ let node = tree.resolve(position, -1);
48
+ if (node.name.includes(tokens.linkToSection)) {
49
+ node = node.prevSibling;
50
+ }
51
+ else if (!hasTag(new Set(node.name.split('_')), tags)) {
52
+ node = tree.resolve(position, 1);
53
+ }
54
+ const { name, from, to } = node;
55
+ if (name.includes(tokens.pageName) && typeof langConfig?.titleParser === 'function') {
56
+ return langConfig.titleParser(state, node);
57
+ }
58
+ else if (name.includes('-extlink-protocol')) {
59
+ return wrapURL(state.sliceDoc(from, node.nextSibling.to));
60
+ }
61
+ else if (/-extlink(?:_|$)/u.test(name)) {
62
+ return wrapURL(state.sliceDoc(node.prevSibling.from, to));
63
+ }
64
+ else if (name.includes(tokens.magicLink)) {
65
+ const link = state.sliceDoc(from, to);
66
+ if (link.startsWith('RFC')) {
67
+ return `https://datatracker.ietf.org/doc/html/rfc${link.slice(3).trim()}`;
68
+ }
69
+ else if (link.startsWith('PMID')) {
70
+ return `https://pubmed.ncbi.nlm.nih.gov/${link.slice(4).trim()}`;
71
+ }
72
+ else if (typeof langConfig?.isbnParser === 'function') {
73
+ return langConfig.isbnParser(link);
74
+ }
75
+ }
76
+ return undefined;
77
+ };
78
+ export default ({ langConfig }) => [
79
+ EditorView.domEventHandlers({
80
+ mousedown(e, view) {
81
+ if (e.button !== 0) {
82
+ return undefined;
83
+ }
84
+ const url = mouseEventListener(e, view, langConfig);
85
+ if (url) {
86
+ open(url, '_blank');
87
+ return true;
88
+ }
89
+ return undefined;
90
+ },
91
+ }),
92
+ EditorView.theme({
93
+ [[...links, ...langConfig?.titleParser ? wikiLinks : []].map(type => `.cm-mw-${type}`).join()]: {
94
+ cursor: 'var(--codemirror-cursor)',
95
+ },
96
+ }),
97
+ ];
package/dist/ref.js ADDED
@@ -0,0 +1,85 @@
1
+ import { hoverTooltip, EditorView } from '@codemirror/view';
2
+ import { ensureSyntaxTree } from '@codemirror/language';
3
+ import { getLSP } from '@bhsd/common';
4
+ import { getTag } from './matchTag';
5
+ import { tokens } from './config';
6
+ import { indexToPos, posToIndex } from './hover';
7
+ const trees = new WeakMap();
8
+ /**
9
+ * 获取节点内容
10
+ * @param state
11
+ * @param node 语法树节点
12
+ * @param node.from 起始位置
13
+ * @param node.to 结束位置
14
+ */
15
+ const getName = (state, { from, to }) => state.sliceDoc(from, to).trim();
16
+ export default (cm) => [
17
+ hoverTooltip(async (view, pos, side) => {
18
+ const { state } = view, node = ensureSyntaxTree(state, pos)?.resolve(pos, side);
19
+ if (node && /-exttag-(?!bracket)/u.test(node.name)) {
20
+ const tag = getTag(state, node);
21
+ if (!tag) {
22
+ return null;
23
+ }
24
+ const { name, selfClosing, first, last, to } = tag;
25
+ if (name === 'ref' && selfClosing) {
26
+ let prevSibling = last, nextSibling = null;
27
+ while (prevSibling && prevSibling.from > first.to) {
28
+ const key = getName(state, prevSibling);
29
+ if (prevSibling.name.split('_').includes(tokens.extTagAttribute)
30
+ && /(?:^|\s)name(?:$|[\s=])/iu.test(key)) {
31
+ if (/(?:^|\s)name\s*=/iu.test(key)) {
32
+ ({ nextSibling } = prevSibling);
33
+ }
34
+ break;
35
+ }
36
+ ({ prevSibling } = prevSibling);
37
+ }
38
+ if (nextSibling?.name.includes(tokens.extTagAttributeValue)) {
39
+ let target = getName(state, nextSibling);
40
+ const quote = target.charAt(0);
41
+ if (quote === '"' || quote === "'") {
42
+ target = target.slice(1, target.slice(-1) === quote ? -1 : undefined).trim();
43
+ }
44
+ if (target) {
45
+ const { doc } = state, ref = await getLSP(view, false, cm.getWikiConfig)
46
+ ?.provideDefinition(doc.toString(), indexToPos(doc, first.to));
47
+ return {
48
+ pos,
49
+ end: to,
50
+ above: true,
51
+ create() {
52
+ const dom = document.createElement('div');
53
+ dom.className = 'cm-tooltip-ref';
54
+ dom.style.font = getComputedStyle(view.contentDOM).font;
55
+ if (ref) {
56
+ const { range: { start, end } } = ref[0], anchor = posToIndex(doc, start), head = posToIndex(doc, end);
57
+ dom.textContent = state.sliceDoc(anchor, head);
58
+ dom.addEventListener('click', () => {
59
+ view.dispatch({
60
+ selection: { anchor, head },
61
+ scrollIntoView: true,
62
+ });
63
+ });
64
+ }
65
+ else {
66
+ dom.textContent = state.phrase('No definition found');
67
+ }
68
+ return { dom };
69
+ },
70
+ };
71
+ }
72
+ }
73
+ }
74
+ }
75
+ return null;
76
+ }),
77
+ EditorView.updateListener.of(({ view, docChanged }) => {
78
+ if (docChanged) {
79
+ const tree = trees.get(view);
80
+ if (tree) {
81
+ tree.docChanged = true;
82
+ }
83
+ }
84
+ }),
85
+ ];
@@ -0,0 +1,69 @@
1
+ import { EditorView, showTooltip } from '@codemirror/view';
2
+ import { StateField, StateEffect } from '@codemirror/state';
3
+ import { getLSP } from '@bhsd/common';
4
+ import { indexToPos, createTooltipView } from './hover';
5
+ const stateEffect = StateEffect.define(), field = StateField.define({
6
+ create() {
7
+ return undefined;
8
+ },
9
+ update(oldValue, { state: { doc, selection: { main: { head } } }, effects }) {
10
+ const text = doc.toString();
11
+ for (const effect of effects) {
12
+ if (effect.is(stateEffect)) {
13
+ const { value } = effect;
14
+ if (head === value.cursor && text === value.text) {
15
+ return value;
16
+ }
17
+ }
18
+ }
19
+ return oldValue;
20
+ },
21
+ });
22
+ export default (cm) => [
23
+ field,
24
+ EditorView.updateListener.of(({ view, state, docChanged, selectionSet }) => {
25
+ if (docChanged || selectionSet && state.field(field)?.signatureHelp?.signatures.length) {
26
+ const { doc, selection: { main } } = state, { head: cursor } = main, text = doc.toString();
27
+ if (!main.empty) {
28
+ view.dispatch({
29
+ effects: stateEffect.of({ text, cursor }),
30
+ });
31
+ return;
32
+ }
33
+ (async () => {
34
+ view.dispatch({
35
+ effects: stateEffect.of({
36
+ text,
37
+ cursor,
38
+ signatureHelp: await getLSP(view, false, cm.getWikiConfig)
39
+ ?.provideSignatureHelp(text, indexToPos(doc, cursor)),
40
+ }),
41
+ });
42
+ })();
43
+ }
44
+ }),
45
+ showTooltip.from(field, (value) => {
46
+ if (!value) {
47
+ return null;
48
+ }
49
+ const { cursor, signatureHelp } = value;
50
+ if (!signatureHelp || signatureHelp.signatures.length === 0) {
51
+ return null;
52
+ }
53
+ const { signatures, activeParameter: active } = signatureHelp;
54
+ return {
55
+ pos: cursor,
56
+ above: true,
57
+ create(view) {
58
+ return createTooltipView(view, signatures.map(({ label, parameters, activeParameter = active }) => {
59
+ if (activeParameter < 0 || activeParameter >= parameters.length) {
60
+ return label;
61
+ }
62
+ const colon = label.indexOf(':'), parts = label.slice(colon + 1, -2).split('|');
63
+ parts[activeParameter] = `<b>${parts[activeParameter]}</b>`;
64
+ return `${label.slice(0, colon)}:${parts.join('|')}}}`;
65
+ }).join('<br>'));
66
+ },
67
+ };
68
+ }),
69
+ ];
package/dist/static.js ADDED
@@ -0,0 +1,46 @@
1
+ export const tagModes = {
2
+ onlyinclude: 'mediawiki',
3
+ includeonly: 'mediawiki',
4
+ noinclude: 'mediawiki',
5
+ pre: 'text/pre',
6
+ nowiki: 'text/nowiki',
7
+ indicator: 'mediawiki',
8
+ poem: 'mediawiki',
9
+ ref: 'mediawiki',
10
+ references: 'text/references',
11
+ gallery: 'text/gallery',
12
+ poll: 'mediawiki',
13
+ tabs: 'mediawiki',
14
+ tab: 'mediawiki',
15
+ choose: 'text/choose',
16
+ option: 'mediawiki',
17
+ combobox: 'text/combobox',
18
+ combooption: 'mediawiki',
19
+ inputbox: 'text/inputbox',
20
+ templatedata: 'json',
21
+ mapframe: 'json',
22
+ maplink: 'json',
23
+ graph: 'json',
24
+ };
25
+ export const getStaticMwConfig = ({ variable, parserFunction: [p0, p1, ...p2], protocol, nsid, functionHook, variants, redirection, ext, doubleUnderscore: [d0, d1, d2, d3], img, }, modes) => ({
26
+ tags: Object.fromEntries(ext.map(s => [s, true])),
27
+ tagModes: modes,
28
+ doubleUnderscore: [
29
+ Object.fromEntries((d2 && d0.length === 0 ? Object.keys(d2) : d0).map(s => [`__${s}__`, true])),
30
+ Object.fromEntries((d3 && d1.length === 0 ? Object.keys(d3) : d1).map(s => [`__${s}__`, true])),
31
+ ],
32
+ functionHooks: functionHook,
33
+ variableIDs: variable,
34
+ functionSynonyms: [
35
+ {
36
+ ...p0,
37
+ ...Object.fromEntries(p2.flat().map(s => [s, s])),
38
+ },
39
+ Array.isArray(p1) ? Object.fromEntries(p1.map(s => [s, s.toLowerCase()])) : { ...p1 },
40
+ ],
41
+ urlProtocols: `${protocol}|//`,
42
+ nsid,
43
+ img: Object.fromEntries(Object.entries(img).map(([k, v]) => [k, `img_${v}`])),
44
+ variants,
45
+ redirection,
46
+ });
@@ -0,0 +1,138 @@
1
+ import { showPanel } from '@codemirror/view';
2
+ import { nextDiagnostic, setDiagnosticsEffect } from '@codemirror/lint';
3
+ function getLintMarker(view, severity, menu) {
4
+ const marker = document.createElement('div'), icon = document.createElement('div');
5
+ marker.className = `cm-status-${severity}`;
6
+ if (severity === 'fix') {
7
+ icon.className = 'cm-status-fix-disabled';
8
+ marker.title = 'Fix all';
9
+ marker.append(icon);
10
+ if (menu) {
11
+ marker.addEventListener('click', ({ clientX, clientY }) => {
12
+ if (icon.className === 'cm-status-fix-enabled') {
13
+ const { bottom, left } = view.dom.getBoundingClientRect();
14
+ menu.style.bottom = `${bottom - clientY + 5}px`;
15
+ menu.style.left = `${clientX - 20 - left}px`;
16
+ menu.style.display = 'block';
17
+ menu.focus();
18
+ }
19
+ });
20
+ }
21
+ }
22
+ else {
23
+ icon.className = `cm-lint-marker-${severity}`;
24
+ const count = document.createElement('div');
25
+ count.textContent = '0';
26
+ marker.append(icon, count);
27
+ marker.addEventListener('click', () => {
28
+ if (marker.parentElement?.classList.contains('cm-status-worker-enabled')) {
29
+ nextDiagnostic(view);
30
+ view.focus();
31
+ }
32
+ });
33
+ }
34
+ return marker;
35
+ }
36
+ const updateDiagnosticsCount = (diagnostics, s, marker) => {
37
+ marker.lastChild.textContent = String(diagnostics.filter(({ severity }) => severity === s).length);
38
+ };
39
+ const hasFix = (diagnostic) => diagnostic.actions?.some(({ name }) => name === 'fix');
40
+ const updateDiagnosticMessage = (view, allDiagnostics, main, msg, menu) => {
41
+ const diagnostics = allDiagnostics.filter(({ from, to }) => from <= main.to && to >= main.from), diagnostic = diagnostics.find(({ from, to }) => from <= main.head && to >= main.head) ?? diagnostics[0];
42
+ if (diagnostic) {
43
+ msg.textContent = diagnostic.message;
44
+ if (diagnostic.actions) {
45
+ msg.append(...diagnostic.actions.map(({ name, apply }) => {
46
+ const button = document.createElement('button');
47
+ button.type = 'button';
48
+ button.className = 'cm-diagnosticAction';
49
+ button.textContent = name;
50
+ button.addEventListener('click', e => {
51
+ e.preventDefault();
52
+ apply(view, diagnostic.from, diagnostic.to);
53
+ });
54
+ return button;
55
+ }));
56
+ }
57
+ }
58
+ else {
59
+ msg.textContent = '';
60
+ }
61
+ if (menu) {
62
+ menu.replaceChildren(...[
63
+ ...new Set(diagnostics.filter(hasFix)
64
+ .map(({ message }) => / \(([^()]+)\)$/u.exec(message)?.[1])
65
+ .filter(Boolean)),
66
+ ].map(rule => {
67
+ const option = document.createElement('div');
68
+ option.textContent = `Fix all ${rule} problems`;
69
+ option.dataset['rule'] = rule;
70
+ return option;
71
+ }), menu.lastChild);
72
+ }
73
+ };
74
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
75
+ export default (fixer) => showPanel.of(view => {
76
+ let diagnostics = [], menu;
77
+ if (fixer) {
78
+ const optionAll = document.createElement('div');
79
+ optionAll.textContent = 'Fix all auto-fixable problems';
80
+ menu = document.createElement('div');
81
+ menu.className = 'cm-status-fix-menu';
82
+ menu.tabIndex = -1;
83
+ menu.append(optionAll);
84
+ menu.addEventListener('click', ({ target }) => {
85
+ if (target === menu) {
86
+ return;
87
+ }
88
+ (async () => {
89
+ const { doc } = view.state, output = await fixer(doc, target.dataset['rule']);
90
+ if (output !== doc.toString()) {
91
+ view.dispatch({
92
+ changes: { from: 0, to: doc.length, insert: output },
93
+ });
94
+ }
95
+ view.focus();
96
+ })();
97
+ });
98
+ menu.addEventListener('focusout', () => {
99
+ menu.style.display = 'none';
100
+ });
101
+ view.dom.append(menu);
102
+ }
103
+ const dom = document.createElement('div'), worker = document.createElement('div'), message = document.createElement('div'), position = document.createElement('div'), error = getLintMarker(view, 'error'), warning = getLintMarker(view, 'warning'), fix = getLintMarker(view, 'fix', menu);
104
+ worker.className = 'cm-status-worker';
105
+ worker.append(error, warning, fix);
106
+ message.className = 'cm-status-message';
107
+ position.className = 'cm-status-line';
108
+ position.textContent = '0:0';
109
+ dom.className = 'cm-panel cm-panel-status';
110
+ dom.append(worker, message, position);
111
+ return {
112
+ dom,
113
+ update({ state: { selection: { main }, doc }, transactions, docChanged, selectionSet }) {
114
+ for (const tr of transactions) {
115
+ for (const effect of tr.effects) {
116
+ if (effect.is(setDiagnosticsEffect)) {
117
+ diagnostics = effect.value;
118
+ const fixable = Boolean(fixer) && diagnostics.some(hasFix), { classList } = fix.firstChild;
119
+ classList.toggle('cm-status-fix-enabled', fixable);
120
+ classList.toggle('cm-status-fix-disabled', !fixable);
121
+ worker.classList.toggle('cm-status-worker-enabled', diagnostics.length > 0);
122
+ updateDiagnosticsCount(diagnostics, 'error', error);
123
+ updateDiagnosticsCount(diagnostics, 'warning', warning);
124
+ updateDiagnosticMessage(view, diagnostics, main, message, menu);
125
+ }
126
+ }
127
+ }
128
+ if (docChanged || selectionSet) {
129
+ updateDiagnosticMessage(view, diagnostics, main, message, menu);
130
+ const { number, from } = doc.lineAt(main.head);
131
+ position.textContent = `${number}:${main.head - from}`;
132
+ if (!main.empty) {
133
+ position.textContent += ` (${main.to - main.from})`;
134
+ }
135
+ }
136
+ },
137
+ };
138
+ });