@bhsd/codemirror-mediawiki 2.28.2 → 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.
@@ -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
+ });