@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.
- package/README.md +15 -2
- package/dist/bidi.js +84 -0
- package/dist/codemirror.js +608 -0
- package/dist/color.js +52 -0
- package/dist/config.js +119 -0
- package/dist/css.js +30 -0
- package/dist/escape.js +35 -0
- package/dist/fold.js +445 -0
- package/dist/hover.js +52 -0
- package/dist/indent.js +50 -0
- package/dist/inlay.js +68 -0
- package/dist/javascript.js +5 -0
- package/dist/keybindings.js +35 -0
- package/dist/keymap.js +36 -0
- package/dist/linter.d.ts +1 -0
- package/dist/linter.js +134 -0
- package/dist/lua.js +428 -0
- package/dist/main.min.js +25 -29
- package/dist/matchBrackets.d.ts +7 -0
- package/dist/matchBrackets.js +58 -0
- package/dist/matchTag.js +139 -0
- package/dist/mediawiki.js +443 -0
- package/dist/mw.min.js +31 -35
- package/dist/openLinks.js +97 -0
- package/dist/ref.js +85 -0
- package/dist/signature.js +69 -0
- package/dist/static.js +46 -0
- package/dist/statusBar.js +138 -0
- package/dist/token.js +1888 -0
- package/dist/wiki.min.js +33 -37
- package/i18n/en.json +1 -1
- package/i18n/zh-hans.json +1 -1
- package/i18n/zh-hant.json +1 -1
- package/package.json +19 -18
- package/dist/keybindings.d.mts +0 -15
- package/dist/keybindings.mjs +0 -34
- package/dist/linter.d.mts +0 -43
- package/dist/linter.mjs +0 -132
- /package/dist/{mwConfig.d.mts → mwConfig.d.ts} +0 -0
- /package/dist/{mwConfig.mjs → mwConfig.js} +0 -0
package/dist/config.js
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Configuration for the MediaWiki highlighting mode for CodeMirror.
|
|
3
|
+
* @author MusikAnimal and others
|
|
4
|
+
* @license GPL-2.0-or-later
|
|
5
|
+
* @see https://gerrit.wikimedia.org/g/mediawiki/extensions/CodeMirror
|
|
6
|
+
*/
|
|
7
|
+
import { tags, Tag } from '@lezer/highlight';
|
|
8
|
+
import { html } from 'wikiparser-node/config/default.json';
|
|
9
|
+
/**
|
|
10
|
+
* All HTML/XML tags permitted in MediaWiki Core.
|
|
11
|
+
*
|
|
12
|
+
* @see https://www.mediawiki.org/wiki/Extension:CodeMirror#Extension_integration
|
|
13
|
+
*/
|
|
14
|
+
export const htmlTags = html.flat(),
|
|
15
|
+
/** HTML tags that are only self-closing. */
|
|
16
|
+
voidHtmlTags = html[2],
|
|
17
|
+
/** HTML tags that can be self-closing. */
|
|
18
|
+
selfClosingTags = html[1],
|
|
19
|
+
/**
|
|
20
|
+
* Mapping of MediaWiki-esque token identifiers to a standardized lezer highlighting tag.
|
|
21
|
+
* Values are one of the default highlighting tags.
|
|
22
|
+
*
|
|
23
|
+
* Once we allow use of other themes, we may want to tweak these values for aesthetic reasons.
|
|
24
|
+
*
|
|
25
|
+
* @see https://lezer.codemirror.net/docs/ref/#highlight.tags
|
|
26
|
+
* @internal
|
|
27
|
+
*/
|
|
28
|
+
tokens = {
|
|
29
|
+
apostrophes: 'mw-apostrophes',
|
|
30
|
+
comment: 'mw-comment',
|
|
31
|
+
convertBracket: 'mw-convert-bracket',
|
|
32
|
+
convertDelimiter: 'mw-convert-delimiter',
|
|
33
|
+
convertFlag: 'mw-convert-flag',
|
|
34
|
+
convertLang: 'mw-convert-lang',
|
|
35
|
+
doubleUnderscore: 'mw-double-underscore',
|
|
36
|
+
em: 'mw-em',
|
|
37
|
+
error: 'mw-error',
|
|
38
|
+
extLink: 'mw-extlink',
|
|
39
|
+
extLinkBracket: 'mw-extlink-bracket',
|
|
40
|
+
extLinkProtocol: 'mw-extlink-protocol',
|
|
41
|
+
extLinkText: 'mw-extlink-text',
|
|
42
|
+
extTag: 'mw-exttag',
|
|
43
|
+
extTagAttribute: 'mw-exttag-attribute',
|
|
44
|
+
extTagAttributeValue: 'mw-exttag-attribute-value',
|
|
45
|
+
extTagBracket: 'mw-exttag-bracket',
|
|
46
|
+
extTagName: 'mw-exttag-name',
|
|
47
|
+
fileDelimiter: 'mw-file-delimiter',
|
|
48
|
+
fileText: 'mw-file-text',
|
|
49
|
+
freeExtLink: 'mw-free-extlink',
|
|
50
|
+
freeExtLinkProtocol: 'mw-free-extlink-protocol',
|
|
51
|
+
hr: 'mw-hr',
|
|
52
|
+
htmlEntity: 'mw-entity',
|
|
53
|
+
htmlTagAttribute: 'mw-htmltag-attribute',
|
|
54
|
+
htmlTagAttributeValue: 'mw-htmltag-attribute-value',
|
|
55
|
+
htmlTagBracket: 'mw-htmltag-bracket',
|
|
56
|
+
htmlTagName: 'mw-htmltag-name',
|
|
57
|
+
imageParameter: 'mw-image-parameter',
|
|
58
|
+
linkBracket: 'mw-link-bracket',
|
|
59
|
+
linkDelimiter: 'mw-link-delimiter',
|
|
60
|
+
linkPageName: 'mw-link-pagename',
|
|
61
|
+
linkText: 'mw-link-text',
|
|
62
|
+
linkToSection: 'mw-link-tosection',
|
|
63
|
+
list: 'mw-list',
|
|
64
|
+
magicLink: 'mw-magic-link',
|
|
65
|
+
pageName: 'mw-pagename',
|
|
66
|
+
parserFunction: 'mw-parserfunction',
|
|
67
|
+
parserFunctionBracket: 'mw-parserfunction-bracket',
|
|
68
|
+
parserFunctionDelimiter: 'mw-parserfunction-delimiter',
|
|
69
|
+
parserFunctionName: 'mw-parserfunction-name',
|
|
70
|
+
redirect: 'mw-redirect',
|
|
71
|
+
section: 'mw-section',
|
|
72
|
+
sectionHeader: 'mw-section-header',
|
|
73
|
+
signature: 'mw-signature',
|
|
74
|
+
skipFormatting: 'mw-skipformatting',
|
|
75
|
+
strong: 'mw-strong',
|
|
76
|
+
tableBracket: 'mw-table-bracket',
|
|
77
|
+
tableCaption: 'mw-table-caption',
|
|
78
|
+
tableDefinition: 'mw-table-definition',
|
|
79
|
+
tableDefinitionValue: 'mw-table-definition-value',
|
|
80
|
+
tableDelimiter: 'mw-table-delimiter',
|
|
81
|
+
tableDelimiter2: 'mw-table-delimiter2',
|
|
82
|
+
tableTd: 'mw-table-td',
|
|
83
|
+
tableTh: 'mw-table-th',
|
|
84
|
+
template: 'mw-template',
|
|
85
|
+
templateArgumentName: 'mw-template-argument-name',
|
|
86
|
+
templateBracket: 'mw-template-bracket',
|
|
87
|
+
templateDelimiter: 'mw-template-delimiter',
|
|
88
|
+
templateName: 'mw-template-name',
|
|
89
|
+
templateVariable: 'mw-templatevariable',
|
|
90
|
+
templateVariableBracket: 'mw-templatevariable-bracket',
|
|
91
|
+
templateVariableDelimiter: 'mw-templatevariable-delimiter',
|
|
92
|
+
templateVariableName: 'mw-templatevariable-name',
|
|
93
|
+
},
|
|
94
|
+
/**
|
|
95
|
+
* These are custom tokens (a.k.a. tags) that aren't mapped to any of the standardized tags.
|
|
96
|
+
*
|
|
97
|
+
* @see https://codemirror.net/docs/ref/#language.StreamParser.tokenTable
|
|
98
|
+
* @see https://lezer.codemirror.net/docs/ref/#highlight.Tag%5Edefine
|
|
99
|
+
*/
|
|
100
|
+
tokenTable = (() => {
|
|
101
|
+
const table = {
|
|
102
|
+
variable: tags.variableName,
|
|
103
|
+
'variable-2': tags.special(tags.variableName),
|
|
104
|
+
'string-2': tags.special(tags.string),
|
|
105
|
+
def: tags.definition(tags.variableName),
|
|
106
|
+
tag: tags.tagName,
|
|
107
|
+
attribute: tags.attributeName,
|
|
108
|
+
type: tags.typeName,
|
|
109
|
+
builtin: tags.standard(tags.variableName),
|
|
110
|
+
qualifier: tags.modifier,
|
|
111
|
+
error: tags.invalid,
|
|
112
|
+
header: tags.heading,
|
|
113
|
+
property: tags.propertyName,
|
|
114
|
+
};
|
|
115
|
+
for (const className of Object.values(tokens)) {
|
|
116
|
+
table[className] = Tag.define();
|
|
117
|
+
}
|
|
118
|
+
return table;
|
|
119
|
+
})();
|
package/dist/css.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { cssLanguage, cssCompletionSource } from '@codemirror/lang-css';
|
|
2
|
+
import { LanguageSupport, syntaxTree } from '@codemirror/language';
|
|
3
|
+
export default (dialect) => new LanguageSupport(cssLanguage, cssLanguage.data.of({
|
|
4
|
+
autocomplete(context) {
|
|
5
|
+
const { state, pos } = context, node = syntaxTree(state).resolveInner(pos, -1), result = cssCompletionSource(context);
|
|
6
|
+
if (result) {
|
|
7
|
+
if (node.name === 'ValueName') {
|
|
8
|
+
const options = [{ label: 'revert', type: 'keyword' }, ...result.options];
|
|
9
|
+
let { prevSibling } = node;
|
|
10
|
+
while (prevSibling && prevSibling.name !== 'PropertyName') {
|
|
11
|
+
({ prevSibling } = prevSibling);
|
|
12
|
+
}
|
|
13
|
+
if (prevSibling) {
|
|
14
|
+
for (let i = 0; i < options.length; i++) {
|
|
15
|
+
const option = options[i];
|
|
16
|
+
if (CSS.supports(state.sliceDoc(prevSibling.from, node.from) + option.label)) {
|
|
17
|
+
options.splice(i, 1, { ...option, boost: 50 });
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
result.options = options;
|
|
22
|
+
}
|
|
23
|
+
else if (dialect === 'sanitized-css') {
|
|
24
|
+
result.options = result.options.filter(({ type, label }) => type !== 'property'
|
|
25
|
+
|| !label.startsWith('-') || label.endsWith('-user-select'));
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return result;
|
|
29
|
+
},
|
|
30
|
+
}));
|
package/dist/escape.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { indentMore, indentLess } from '@codemirror/commands';
|
|
2
|
+
import { CodeMirror6 } from './codemirror';
|
|
3
|
+
const entity = { '"': 'quot', "'": 'apos', '<': 'lt', '>': 'gt', '&': 'amp', ' ': 'nbsp' };
|
|
4
|
+
/**
|
|
5
|
+
* 根据函数转换选中文本
|
|
6
|
+
* @param func 转换函数
|
|
7
|
+
* @param cmd 原命令
|
|
8
|
+
*/
|
|
9
|
+
const convert = (func, cmd) => (view) => {
|
|
10
|
+
if (view.state.selection.ranges.some(range => !range.empty)) {
|
|
11
|
+
CodeMirror6.replaceSelections(view, func);
|
|
12
|
+
return true;
|
|
13
|
+
}
|
|
14
|
+
return cmd(view);
|
|
15
|
+
};
|
|
16
|
+
// eslint-disable-next-line @typescript-eslint/no-misused-spread
|
|
17
|
+
export const escapeHTML = (str) => [...str].map(c => {
|
|
18
|
+
if (c in entity) {
|
|
19
|
+
return `&${entity[c]};`;
|
|
20
|
+
}
|
|
21
|
+
const code = c.codePointAt(0);
|
|
22
|
+
return code < 256 ? `&#${code};` : `&#x${code.toString(16)};`;
|
|
23
|
+
}).join(''), escapeURI = (str) => {
|
|
24
|
+
if (str.includes('%')) {
|
|
25
|
+
try {
|
|
26
|
+
return decodeURIComponent(str);
|
|
27
|
+
}
|
|
28
|
+
catch { }
|
|
29
|
+
}
|
|
30
|
+
return encodeURIComponent(str);
|
|
31
|
+
};
|
|
32
|
+
export default [
|
|
33
|
+
{ key: 'Mod-[', run: convert(escapeHTML, indentLess) },
|
|
34
|
+
{ key: 'Mod-]', run: convert(escapeURI, indentMore) },
|
|
35
|
+
];
|
package/dist/fold.js
ADDED
|
@@ -0,0 +1,445 @@
|
|
|
1
|
+
import { showTooltip, keymap, GutterMarker, gutter, ViewPlugin } from '@codemirror/view';
|
|
2
|
+
import { StateField, RangeSetBuilder, RangeSet } from '@codemirror/state';
|
|
3
|
+
import { syntaxTree, ensureSyntaxTree, foldEffect, unfoldEffect, foldedRanges, unfoldAll, codeFolding, foldGutter, foldKeymap, foldState, language, } from '@codemirror/language';
|
|
4
|
+
import { getRegex } from '@bhsd/common';
|
|
5
|
+
import { tokens } from './config';
|
|
6
|
+
import { matchTag } from './matchTag';
|
|
7
|
+
const getExtRegex = getRegex(tag => new RegExp(`mw-tag-${tag}(?![a-z])`, 'u'));
|
|
8
|
+
const updateSelection = (pos, { to }) => Math.max(pos, to), updateAll = (pos, { from, to }) => from <= pos && to > pos ? to : pos;
|
|
9
|
+
/**
|
|
10
|
+
* Check if a SyntaxNode is among the specified components
|
|
11
|
+
* @param keys The keys of the tokens to check
|
|
12
|
+
*/
|
|
13
|
+
const isComponent = (keys) => ({ name }) => keys.some(key => name.includes(tokens[key])),
|
|
14
|
+
/** Check if a SyntaxNode is a template bracket (`{{` or `}}`) */
|
|
15
|
+
isTemplateBracket = isComponent(['templateBracket', 'parserFunctionBracket']),
|
|
16
|
+
/** Check if a SyntaxNode is a template delimiter (`|` or `:`) */
|
|
17
|
+
isDelimiter = isComponent(['templateDelimiter', 'parserFunctionDelimiter']),
|
|
18
|
+
/**
|
|
19
|
+
* Check if a SyntaxNode is part of a template, except for the brackets
|
|
20
|
+
* @param node 语法树节点
|
|
21
|
+
*/
|
|
22
|
+
isTemplate = (node) => /-(?:template|ext)[a-z\d-]+ground/u.test(node.name) && !isTemplateBracket(node),
|
|
23
|
+
/** Check if a SyntaxNode is an extension tag bracket (`<` or `>`) */
|
|
24
|
+
isExtBracket = isComponent(['extTagBracket']),
|
|
25
|
+
/**
|
|
26
|
+
* Check if a SyntaxNode is part of a extension tag
|
|
27
|
+
* @param node 语法树节点
|
|
28
|
+
*/
|
|
29
|
+
isExt = (node) => node.name.includes('mw-tag-');
|
|
30
|
+
/**
|
|
31
|
+
* Update the stack of opening (+) or closing (-) brackets
|
|
32
|
+
* @param state
|
|
33
|
+
* @param node 语法树节点
|
|
34
|
+
*/
|
|
35
|
+
export const braceStackUpdate = (state, node) => {
|
|
36
|
+
const brackets = state.sliceDoc(node.from, node.to);
|
|
37
|
+
return [brackets.split('{{').length - 1, 1 - brackets.split('}}').length];
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* 寻找可折叠的范围
|
|
41
|
+
* @param state
|
|
42
|
+
* @param posOrNode 字符位置或语法树节点
|
|
43
|
+
* @param tree 语法树
|
|
44
|
+
*/
|
|
45
|
+
export const foldable = (state, posOrNode, tree) => {
|
|
46
|
+
if (typeof posOrNode === 'number') {
|
|
47
|
+
tree = ensureSyntaxTree(state, posOrNode); // eslint-disable-line no-param-reassign
|
|
48
|
+
}
|
|
49
|
+
if (!tree) {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
let node;
|
|
53
|
+
if (typeof posOrNode === 'number') {
|
|
54
|
+
// Find the initial template node on both sides of the position
|
|
55
|
+
const left = tree.resolve(posOrNode, -1);
|
|
56
|
+
if (isTemplate(left)) {
|
|
57
|
+
node = left;
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
const right = tree.resolve(posOrNode, 1);
|
|
61
|
+
node = isExt(left)
|
|
62
|
+
&& left.name.split('mw-tag-').length > right.name.split('mw-tag-').length
|
|
63
|
+
? left
|
|
64
|
+
: right;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
node = posOrNode;
|
|
69
|
+
}
|
|
70
|
+
if (!isTemplate(node)) {
|
|
71
|
+
// Not a template
|
|
72
|
+
if (isExt(node)) {
|
|
73
|
+
const { name } = node, [tag] = /^[a-z]+/u.exec(name.slice(name.lastIndexOf('mw-tag-') + 7)), regex = getExtRegex(tag);
|
|
74
|
+
let { nextSibling } = node;
|
|
75
|
+
while (nextSibling && !(isExtBracket(nextSibling) && !regex.test(nextSibling.name))) {
|
|
76
|
+
({ nextSibling } = nextSibling);
|
|
77
|
+
}
|
|
78
|
+
if (nextSibling) { // The closing bracket of the current extension tag
|
|
79
|
+
return { from: matchTag(state, nextSibling.to).end.to, to: nextSibling.from };
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
let { prevSibling, nextSibling } = node,
|
|
85
|
+
/** The stack of opening (+) or closing (-) brackets */ stack = 1,
|
|
86
|
+
/** The first delimiter */ delimiter = isDelimiter(node) ? node : null,
|
|
87
|
+
/** The start of the closing bracket */ to = 0;
|
|
88
|
+
while (nextSibling) {
|
|
89
|
+
if (isTemplateBracket(nextSibling)) {
|
|
90
|
+
const [lbrace, rbrace] = braceStackUpdate(state, nextSibling);
|
|
91
|
+
stack += rbrace;
|
|
92
|
+
if (stack <= 0) {
|
|
93
|
+
// The closing bracket of the current template
|
|
94
|
+
to = nextSibling.from
|
|
95
|
+
+ state.sliceDoc(nextSibling.from, nextSibling.to)
|
|
96
|
+
.split('}}').slice(0, stack - 1).join('}}').length;
|
|
97
|
+
break;
|
|
98
|
+
}
|
|
99
|
+
stack += lbrace;
|
|
100
|
+
}
|
|
101
|
+
else if (!delimiter && stack === 1 && isDelimiter(nextSibling)) {
|
|
102
|
+
// The first delimiter of the current template so far
|
|
103
|
+
delimiter = nextSibling;
|
|
104
|
+
}
|
|
105
|
+
({ nextSibling } = nextSibling);
|
|
106
|
+
}
|
|
107
|
+
if (!nextSibling) {
|
|
108
|
+
// The closing bracket of the current template is missing
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
stack = -1;
|
|
112
|
+
while (prevSibling) {
|
|
113
|
+
if (isTemplateBracket(prevSibling)) {
|
|
114
|
+
const [lbrace, rbrace] = braceStackUpdate(state, prevSibling);
|
|
115
|
+
stack += lbrace;
|
|
116
|
+
if (stack >= 0) {
|
|
117
|
+
// The opening bracket of the current template
|
|
118
|
+
break;
|
|
119
|
+
}
|
|
120
|
+
stack += rbrace;
|
|
121
|
+
}
|
|
122
|
+
else if (stack === -1 && isDelimiter(prevSibling)) {
|
|
123
|
+
// The first delimiter of the current template so far
|
|
124
|
+
delimiter = prevSibling;
|
|
125
|
+
}
|
|
126
|
+
({ prevSibling } = prevSibling);
|
|
127
|
+
}
|
|
128
|
+
const /** The end of the first delimiter */ from = delimiter?.to;
|
|
129
|
+
return from && from < to ? { from, to } : false;
|
|
130
|
+
};
|
|
131
|
+
/**
|
|
132
|
+
* 创建折叠提示
|
|
133
|
+
* @param state
|
|
134
|
+
*/
|
|
135
|
+
const create = (state) => {
|
|
136
|
+
const { selection: { main: { head } } } = state, range = foldable(state, head);
|
|
137
|
+
if (range) {
|
|
138
|
+
const { from, to } = range;
|
|
139
|
+
let folded = false;
|
|
140
|
+
foldedRanges(state).between(from, to, (i, j) => {
|
|
141
|
+
if (i === from && j === to) {
|
|
142
|
+
folded = true;
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
return folded // eslint-disable-line @typescript-eslint/no-unnecessary-condition
|
|
146
|
+
? null
|
|
147
|
+
: {
|
|
148
|
+
pos: head,
|
|
149
|
+
above: true,
|
|
150
|
+
create() {
|
|
151
|
+
const dom = document.createElement('div');
|
|
152
|
+
dom.className = 'cm-tooltip-fold';
|
|
153
|
+
dom.textContent = '\uff0d';
|
|
154
|
+
dom.title = state.phrase('Fold template or extension tag');
|
|
155
|
+
dom.dataset['from'] = String(from);
|
|
156
|
+
dom.dataset['to'] = String(to);
|
|
157
|
+
return { dom };
|
|
158
|
+
},
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
return null;
|
|
162
|
+
};
|
|
163
|
+
/**
|
|
164
|
+
* 执行折叠
|
|
165
|
+
* @param view
|
|
166
|
+
* @param effects 折叠
|
|
167
|
+
* @param anchor 光标位置
|
|
168
|
+
*/
|
|
169
|
+
const execute = (view, effects, anchor) => {
|
|
170
|
+
if (effects.length > 0) {
|
|
171
|
+
view.dom.querySelector('.cm-tooltip-fold')?.remove();
|
|
172
|
+
// Fold the template(s) and update the cursor position
|
|
173
|
+
view.dispatch({ effects, selection: { anchor } });
|
|
174
|
+
return true;
|
|
175
|
+
}
|
|
176
|
+
return false;
|
|
177
|
+
};
|
|
178
|
+
/**
|
|
179
|
+
* The rightmost position of all selections, to be updated with folding
|
|
180
|
+
* @param state
|
|
181
|
+
*/
|
|
182
|
+
const getAnchor = (state) => Math.max(...state.selection.ranges.map(({ to }) => to));
|
|
183
|
+
/**
|
|
184
|
+
* 折叠所有模板
|
|
185
|
+
* @param state
|
|
186
|
+
* @param tree 语法树
|
|
187
|
+
* @param effects 折叠
|
|
188
|
+
* @param node 语法树节点
|
|
189
|
+
* @param end 终止位置
|
|
190
|
+
* @param anchor 光标位置
|
|
191
|
+
* @param update 更新光标位置
|
|
192
|
+
*/
|
|
193
|
+
const traverse = (state, tree, effects, node, end, anchor, update) => {
|
|
194
|
+
while (node && node.from <= end) {
|
|
195
|
+
/* eslint-disable no-param-reassign */
|
|
196
|
+
const range = foldable(state, node, tree);
|
|
197
|
+
if (range) {
|
|
198
|
+
effects.push(foldEffect.of(range));
|
|
199
|
+
node = tree.resolve(range.to, 1);
|
|
200
|
+
// Update the anchor with the end of the last folded range
|
|
201
|
+
anchor = update(anchor, range);
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
node = node.nextSibling;
|
|
205
|
+
/* eslint-enable no-param-reassign */
|
|
206
|
+
}
|
|
207
|
+
return anchor;
|
|
208
|
+
};
|
|
209
|
+
class FoldMarker extends GutterMarker {
|
|
210
|
+
constructor(open) {
|
|
211
|
+
super();
|
|
212
|
+
this.open = open;
|
|
213
|
+
}
|
|
214
|
+
eq(other) {
|
|
215
|
+
return this.open === other.open;
|
|
216
|
+
}
|
|
217
|
+
toDOM({ state }) {
|
|
218
|
+
const span = document.createElement('span');
|
|
219
|
+
span.textContent = this.open ? '⌄' : '›';
|
|
220
|
+
span.title = state.phrase(this.open ? 'Fold line' : 'Unfold line');
|
|
221
|
+
return span;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
const canFold = new FoldMarker(true), canUnfold = new FoldMarker(false);
|
|
225
|
+
const findFold = ({ state }, line) => {
|
|
226
|
+
let found;
|
|
227
|
+
state.field(foldState, false)?.between(line.from, line.to, (from, to) => {
|
|
228
|
+
if (!found && to === line.to) {
|
|
229
|
+
found = { from, to };
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
return found;
|
|
233
|
+
};
|
|
234
|
+
export const foldableLine = ({ state, viewport: { to: end }, viewportLineBlocks }, { from: f, to: t }) => {
|
|
235
|
+
const tree = syntaxTree(state);
|
|
236
|
+
/**
|
|
237
|
+
* 获取标题层级
|
|
238
|
+
* @param pos 行首位置
|
|
239
|
+
*/
|
|
240
|
+
const getLevel = (pos) => {
|
|
241
|
+
const { name } = tree.resolve(pos, 1);
|
|
242
|
+
return name.includes(tokens.sectionHeader) ? Number(/mw-section--(\d)/u.exec(name)[1]) : 7;
|
|
243
|
+
},
|
|
244
|
+
/**
|
|
245
|
+
* 获取表格语法
|
|
246
|
+
* @param from 行首位置
|
|
247
|
+
* @param to 行尾位置
|
|
248
|
+
*/
|
|
249
|
+
getTable = (from, to) => {
|
|
250
|
+
const line = state.sliceDoc(from, to), bracket = /^\s*(?:(?::+\s*)?\{\||\|\})/u.exec(line)?.[0];
|
|
251
|
+
if (bracket) {
|
|
252
|
+
const { name } = tree.resolve(from + bracket.length, -1);
|
|
253
|
+
if (name.includes(tokens.tableBracket)) {
|
|
254
|
+
return bracket.endsWith('|}') ? -1 : 1;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
return 0;
|
|
258
|
+
};
|
|
259
|
+
const level = getLevel(f);
|
|
260
|
+
if (level < 7) {
|
|
261
|
+
for (const { from } of viewportLineBlocks) {
|
|
262
|
+
if (from > f && getLevel(from) <= level) {
|
|
263
|
+
return t < from - 1 && { from: t, to: from - 1 };
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
return end === state.doc.length && end > t && { from: t, to: end };
|
|
267
|
+
}
|
|
268
|
+
else if (getTable(f, t) === 1) {
|
|
269
|
+
for (const { from, to } of viewportLineBlocks) {
|
|
270
|
+
if (from > f) {
|
|
271
|
+
const bracket = getTable(from, to);
|
|
272
|
+
if (bracket === -1) {
|
|
273
|
+
return t < from - 1 && { from: t, to: from - 1 };
|
|
274
|
+
}
|
|
275
|
+
else if (bracket === 1 || getLevel(from) < 7) {
|
|
276
|
+
break;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
return false;
|
|
282
|
+
};
|
|
283
|
+
const buildMarkers = (view) => {
|
|
284
|
+
const builder = new RangeSetBuilder();
|
|
285
|
+
for (const line of view.viewportLineBlocks) {
|
|
286
|
+
let mark;
|
|
287
|
+
if (findFold(view, line)) {
|
|
288
|
+
mark = canUnfold;
|
|
289
|
+
}
|
|
290
|
+
else if (foldableLine(view, line)) {
|
|
291
|
+
mark = canFold;
|
|
292
|
+
}
|
|
293
|
+
if (mark) {
|
|
294
|
+
builder.add(line.from, line.from, mark);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
return builder.finish();
|
|
298
|
+
};
|
|
299
|
+
const markers = ViewPlugin.fromClass(class {
|
|
300
|
+
constructor(view) {
|
|
301
|
+
this.markers = buildMarkers(view);
|
|
302
|
+
}
|
|
303
|
+
update({ docChanged, viewportChanged, startState, state, view }) {
|
|
304
|
+
if (docChanged
|
|
305
|
+
|| viewportChanged
|
|
306
|
+
|| startState.facet(language) !== state.facet(language)
|
|
307
|
+
|| startState.field(foldState, false) !== state.field(foldState, false)
|
|
308
|
+
|| syntaxTree(startState) !== syntaxTree(state)) {
|
|
309
|
+
this.markers = buildMarkers(view);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
});
|
|
313
|
+
const defaultFoldExtension = [foldGutter(), keymap.of(foldKeymap)];
|
|
314
|
+
export default [
|
|
315
|
+
(e = defaultFoldExtension) => e,
|
|
316
|
+
{
|
|
317
|
+
mediawiki: [
|
|
318
|
+
codeFolding({
|
|
319
|
+
placeholderDOM(view) {
|
|
320
|
+
const element = document.createElement('span');
|
|
321
|
+
element.textContent = '…';
|
|
322
|
+
element.setAttribute('aria-label', 'folded code');
|
|
323
|
+
element.title = view.state.phrase('unfold');
|
|
324
|
+
element.className = 'cm-foldPlaceholder';
|
|
325
|
+
element.addEventListener('click', ({ target }) => {
|
|
326
|
+
const pos = view.posAtDOM(target), { state } = view, { selection } = state;
|
|
327
|
+
foldedRanges(state).between(pos, pos, (from, to) => {
|
|
328
|
+
if (from === pos) {
|
|
329
|
+
// Unfold the template and redraw the selections
|
|
330
|
+
view.dispatch({ effects: unfoldEffect.of({ from, to }), selection });
|
|
331
|
+
}
|
|
332
|
+
});
|
|
333
|
+
});
|
|
334
|
+
return element;
|
|
335
|
+
},
|
|
336
|
+
}),
|
|
337
|
+
/** @see https://codemirror.net/examples/tooltip/ */
|
|
338
|
+
StateField.define({
|
|
339
|
+
create,
|
|
340
|
+
update(tooltip, { state, docChanged, selection }) {
|
|
341
|
+
if (docChanged) {
|
|
342
|
+
return null;
|
|
343
|
+
}
|
|
344
|
+
return selection ? create(state) : tooltip;
|
|
345
|
+
},
|
|
346
|
+
provide(f) {
|
|
347
|
+
return showTooltip.from(f);
|
|
348
|
+
},
|
|
349
|
+
}),
|
|
350
|
+
keymap.of([
|
|
351
|
+
{
|
|
352
|
+
// Fold the template at the selection/cursor
|
|
353
|
+
key: 'Ctrl-Shift-[',
|
|
354
|
+
mac: 'Cmd-Alt-[',
|
|
355
|
+
run(view) {
|
|
356
|
+
const { state } = view, tree = syntaxTree(state), effects = [];
|
|
357
|
+
let anchor = getAnchor(state);
|
|
358
|
+
for (const { from, to, empty } of state.selection.ranges) {
|
|
359
|
+
let node;
|
|
360
|
+
if (empty) {
|
|
361
|
+
// No selection, try both sides of the cursor position
|
|
362
|
+
node = tree.resolve(from, -1);
|
|
363
|
+
}
|
|
364
|
+
if (!node || node.name === 'Document') {
|
|
365
|
+
node = tree.resolve(from, 1);
|
|
366
|
+
}
|
|
367
|
+
anchor = traverse(state, tree, effects, node, to, anchor, updateSelection);
|
|
368
|
+
}
|
|
369
|
+
return execute(view, effects, anchor);
|
|
370
|
+
},
|
|
371
|
+
},
|
|
372
|
+
{
|
|
373
|
+
// Fold all templates in the document
|
|
374
|
+
key: 'Ctrl-Alt-[',
|
|
375
|
+
run(view) {
|
|
376
|
+
const { state } = view, tree = syntaxTree(state), effects = [], anchor = traverse(state, tree, effects, tree.topNode.firstChild, Infinity, getAnchor(state), updateAll);
|
|
377
|
+
return execute(view, effects, anchor);
|
|
378
|
+
},
|
|
379
|
+
},
|
|
380
|
+
{
|
|
381
|
+
// Unfold the template at the selection/cursor
|
|
382
|
+
key: 'Ctrl-Shift-]',
|
|
383
|
+
mac: 'Cmd-Alt-]',
|
|
384
|
+
run(view) {
|
|
385
|
+
const { state } = view, { selection } = state, effects = [], folded = foldedRanges(state);
|
|
386
|
+
for (const { from, to } of selection.ranges) {
|
|
387
|
+
// Unfold any folded range at the selection
|
|
388
|
+
folded.between(from, to, (i, j) => {
|
|
389
|
+
effects.push(unfoldEffect.of({ from: i, to: j }));
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
if (effects.length > 0) {
|
|
393
|
+
// Unfold the template(s) and redraw the selections
|
|
394
|
+
view.dispatch({ effects, selection });
|
|
395
|
+
return true;
|
|
396
|
+
}
|
|
397
|
+
return false;
|
|
398
|
+
},
|
|
399
|
+
},
|
|
400
|
+
{ key: 'Ctrl-Alt-]', run: unfoldAll },
|
|
401
|
+
]),
|
|
402
|
+
markers,
|
|
403
|
+
gutter({
|
|
404
|
+
class: 'cm-foldGutter',
|
|
405
|
+
markers(view) {
|
|
406
|
+
return view.plugin(markers)?.markers ?? RangeSet.empty;
|
|
407
|
+
},
|
|
408
|
+
initialSpacer() {
|
|
409
|
+
return new FoldMarker(false);
|
|
410
|
+
},
|
|
411
|
+
domEventHandlers: {
|
|
412
|
+
click(view, line) {
|
|
413
|
+
const folded = findFold(view, line);
|
|
414
|
+
if (folded) {
|
|
415
|
+
view.dispatch({ effects: unfoldEffect.of(folded) });
|
|
416
|
+
return true;
|
|
417
|
+
}
|
|
418
|
+
const range = foldableLine(view, line);
|
|
419
|
+
if (range) {
|
|
420
|
+
view.dispatch({ effects: foldEffect.of(range) });
|
|
421
|
+
return true;
|
|
422
|
+
}
|
|
423
|
+
return false;
|
|
424
|
+
},
|
|
425
|
+
},
|
|
426
|
+
}),
|
|
427
|
+
],
|
|
428
|
+
},
|
|
429
|
+
];
|
|
430
|
+
/**
|
|
431
|
+
* 点击提示折叠模板参数
|
|
432
|
+
* @param view
|
|
433
|
+
*/
|
|
434
|
+
export const foldHandler = (view) => (e) => {
|
|
435
|
+
const dom = e.target.closest('.cm-tooltip-fold');
|
|
436
|
+
if (dom) {
|
|
437
|
+
e.preventDefault();
|
|
438
|
+
const { dataset } = dom, from = Number(dataset['from']), to = Number(dataset['to']);
|
|
439
|
+
view.dispatch({
|
|
440
|
+
effects: foldEffect.of({ from, to }),
|
|
441
|
+
selection: { anchor: to },
|
|
442
|
+
});
|
|
443
|
+
dom.remove();
|
|
444
|
+
}
|
|
445
|
+
};
|
package/dist/hover.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { hoverTooltip } from '@codemirror/view';
|
|
2
|
+
import { loadScript, getLSP } from '@bhsd/common';
|
|
3
|
+
let md;
|
|
4
|
+
/**
|
|
5
|
+
* 将索引转换为位置
|
|
6
|
+
* @param doc Text 实例
|
|
7
|
+
* @param index 索引
|
|
8
|
+
*/
|
|
9
|
+
export const indexToPos = (doc, index) => {
|
|
10
|
+
const line = doc.lineAt(index);
|
|
11
|
+
return { line: line.number - 1, character: index - line.from };
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* 将位置转换为索引
|
|
15
|
+
* @param doc Text 实例
|
|
16
|
+
* @param pos 位置
|
|
17
|
+
*/
|
|
18
|
+
export const posToIndex = (doc, pos) => {
|
|
19
|
+
const line = doc.line(pos.line + 1);
|
|
20
|
+
return Math.min(line.from + pos.character, line.to);
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* 创建 TooltipView
|
|
24
|
+
* @param view EditorView 实例
|
|
25
|
+
* @param innerHTML 提示内容
|
|
26
|
+
*/
|
|
27
|
+
export const createTooltipView = (view, innerHTML) => {
|
|
28
|
+
const dom = document.createElement('div'), inner = document.createElement('div');
|
|
29
|
+
dom.append(inner);
|
|
30
|
+
dom.className = 'cm-tooltip-hover';
|
|
31
|
+
dom.style.font = getComputedStyle(view.contentDOM).font;
|
|
32
|
+
inner.innerHTML = innerHTML;
|
|
33
|
+
return { dom };
|
|
34
|
+
};
|
|
35
|
+
export default (cm) => hoverTooltip(async (view, pos) => {
|
|
36
|
+
const { state: { doc } } = view, hover = await getLSP(view, false, cm.getWikiConfig)
|
|
37
|
+
?.provideHover(doc.toString(), indexToPos(doc, pos));
|
|
38
|
+
if (hover) {
|
|
39
|
+
await loadScript('npm/markdown-it/dist/markdown-it.min.js', 'markdownit', true);
|
|
40
|
+
md ??= markdownit();
|
|
41
|
+
const { end } = hover.range;
|
|
42
|
+
return {
|
|
43
|
+
pos,
|
|
44
|
+
end: posToIndex(doc, end),
|
|
45
|
+
above: true,
|
|
46
|
+
create() {
|
|
47
|
+
return createTooltipView(view, md.render(hover.contents.value));
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
return null;
|
|
52
|
+
});
|