wikiplus-highlight 2.60.1 → 3.0.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/matchtags.js DELETED
@@ -1,324 +0,0 @@
1
- // CodeMirror, copyright (c) by Marijn Haverbeke and others
2
- // Distributed under an MIT license: https://codemirror.net/LICENSE
3
- // Modified for MediaWiki by Bhsd <https://github.com/bhsd-harry>
4
-
5
- (() => {
6
- 'use strict';
7
-
8
- const {Pos, cmpPos, Init} = CodeMirror,
9
- tagStart = /<(\/?)([a-z]\w*)(?=[\s/>]|$)/giu,
10
- voidTags = new Set(['br', 'wbr', 'hr', 'img']),
11
- maxScanLines = 1000;
12
-
13
- /** @ignore */
14
- class Iter {
15
- /**
16
- * @param {CodeMirror.Editor} cm CodeMirror实例
17
- * @param {CodeMirror.Position} pos 当前位置
18
- */
19
- constructor(cm, {line, ch}) {
20
- this.line = line;
21
- this.ch = ch;
22
- this.cm = cm;
23
- this.text = cm.getLine(line);
24
- this.min = Math.max(line - maxScanLines + 1, cm.firstLine());
25
- this.max = Math.min(line + maxScanLines - 1, cm.lastLine());
26
- }
27
-
28
- /** 提取当前位置 */
29
- get pos() {
30
- return Pos(this.line, this.ch);
31
- }
32
-
33
- /** 是否是标签 */
34
- isTag() {
35
- const type = this.cm.getTokenTypeAt(this.pos);
36
- return /\b(?:mw-(?:html|ext)tag|tag\b)/u.test(type);
37
- }
38
-
39
- /**
40
- * 判断是否是`<`或`>`
41
- * @param {number} ch 列号
42
- */
43
- bracketAt(ch) {
44
- const type = this.cm.getTokenTypeAt(Pos(this.line, ch + 1));
45
- return /\b(?:mw-(?:html|ext)tag-)?bracket\b/u.test(type);
46
- }
47
-
48
- /** Jump to the start of the next line */
49
- nextLine() {
50
- if (this.line >= this.max) {
51
- return false;
52
- }
53
- this.ch = 0;
54
- this.text = this.cm.getLine(++this.line);
55
- return true;
56
- }
57
-
58
- /** Jump to the end of the previous line */
59
- prevLine() {
60
- if (this.line <= this.min) {
61
- return false;
62
- }
63
- this.text = this.cm.getLine(--this.line);
64
- this.ch = this.text.length;
65
- return true;
66
- }
67
-
68
- /** Jump to the letter after a `>` towards the line end */
69
- toTagEnd() {
70
- for (;;) {
71
- const gt = this.text.indexOf('>', this.ch);
72
- if (gt === -1) {
73
- if (this.nextLine()) {
74
- continue;
75
- }
76
- return undefined;
77
- }
78
- this.ch = gt + 1;
79
- if (this.bracketAt(gt)) {
80
- return this.text[gt - 1] === '/' ? 'selfClose' : 'regular';
81
- }
82
- }
83
- }
84
-
85
- /** Jump to a `<` towards the line start */
86
- toTagStart() {
87
- for (;;) {
88
- const lt = this.ch > 0 ? this.text.lastIndexOf('<', this.ch - 1) : -1;
89
- if (lt === -1) {
90
- if (this.prevLine()) {
91
- continue;
92
- }
93
- return undefined;
94
- } else if (!this.bracketAt(lt)) {
95
- this.ch = lt;
96
- continue;
97
- }
98
- tagStart.lastIndex = lt;
99
- this.ch = lt;
100
- const match = tagStart.exec(this.text);
101
- if (match && match.index === lt) {
102
- return match;
103
- }
104
- }
105
- }
106
-
107
- /** Jump to the start of the last line, or the letter after a `tagStart` */
108
- toNextTag() {
109
- for (;;) {
110
- tagStart.lastIndex = this.ch;
111
- const found = tagStart.exec(this.text);
112
- if (!found) {
113
- if (this.nextLine()) {
114
- continue;
115
- }
116
- return undefined;
117
- }
118
- this.ch = found.index + found[0].length;
119
- if (this.bracketAt(found.index)) {
120
- return found;
121
- }
122
- }
123
- }
124
-
125
- /** Jump to the end of the first line, or a non-bracket `>`, or the letter after a tag bracket `>` */
126
- toPrevTag() {
127
- for (;;) {
128
- const gt = this.ch > 0 ? this.text.lastIndexOf('>', this.ch - 1) : -1;
129
- if (gt === -1) {
130
- if (this.prevLine()) {
131
- continue;
132
- }
133
- return undefined;
134
- } else if (this.bracketAt(gt)) {
135
- this.ch = gt + 1;
136
- return this.text[gt - 1] === '/' ? 'selfClose' : 'regular';
137
- }
138
- this.ch = gt;
139
- }
140
- }
141
-
142
- // eslint-disable-next-line jsdoc/require-returns-check
143
- /**
144
- * 搜索匹配的闭合标签
145
- * @param {string} tag 标签名
146
- * @returns {CodeMirror.MatchingTag}
147
- */
148
- findMatchingClose(tag) {
149
- const /** @type {string[]} */ stack = [];
150
- for (;;) {
151
- const next = this.toNextTag();
152
- if (!next) {
153
- return undefined;
154
- }
155
- const from = Pos(this.line, this.ch - next[0].length),
156
- end = this.toTagEnd(),
157
- tagName = next[2].toLowerCase();
158
- if (!end) {
159
- return undefined;
160
- } else if (end === 'selfClose' || voidTags.has(tagName)) {
161
- continue;
162
- } else if (next[1]) { // closing tag
163
- let i = stack.length - 1;
164
- for (; i >= 0; --i) {
165
- if (stack[i] === tagName) {
166
- stack.length = i;
167
- break;
168
- }
169
- }
170
- if (i < 0 && (!tag || tag === tagName)) {
171
- return {tag: tagName, from, to: this.pos};
172
- }
173
- } else { // opening tag
174
- stack.push(tagName);
175
- }
176
- }
177
- }
178
-
179
- // eslint-disable-next-line jsdoc/require-returns-check
180
- /**
181
- * 搜索匹配的开启标签
182
- * @param {string|undefined} tag 标签名
183
- * @returns {CodeMirror.MatchingTag}
184
- */
185
- findMatchingOpen(tag) {
186
- const /** @type {string[]} */ stack = [];
187
- for (;;) {
188
- const prev = this.toPrevTag();
189
- if (!prev) {
190
- return undefined;
191
- }
192
- const {pos: to} = this,
193
- start = this.toTagStart();
194
- if (!start) {
195
- return undefined;
196
- }
197
- const tagName = start[2].toLowerCase();
198
- if (prev === 'selfClose' || voidTags.has(tagName)) {
199
- continue;
200
- } else if (start[1]) { // closing tag
201
- stack.push(tagName);
202
- } else { // opening tag
203
- let i = stack.length - 1;
204
- for (; i >= 0; --i) {
205
- if (stack[i] === tagName) {
206
- stack.length = i;
207
- break;
208
- }
209
- }
210
- if (i < 0 && (!tag || tag === tagName)) {
211
- return {tag: tagName, from: this.pos, to};
212
- }
213
- }
214
- }
215
- }
216
- }
217
-
218
- CodeMirror.defineExtension(
219
- 'findMatchingTag',
220
-
221
- /**
222
- * @this {CodeMirror.Editor}
223
- * @param {CodeMirror.Position} pos 当前位置
224
- * @returns {CodeMirror.MatchingTagPair}
225
- */
226
- function(pos) {
227
- const iter = new Iter(this, pos);
228
- if (!iter.isTag()) {
229
- return undefined;
230
- }
231
- const end = iter.toTagEnd(),
232
- to = end && iter.pos,
233
- start = end && iter.toTagStart();
234
- if (!start || cmpPos(iter, pos) > 0) {
235
- return undefined;
236
- }
237
- const tag = start[2].toLowerCase(),
238
- here = {from: iter.pos, to, tag};
239
- if (end === 'selfClose' || voidTags.has(tag)) {
240
- return {open: here, loc: 'self'};
241
- } else if (start[1]) { // closing tag
242
- return {open: iter.findMatchingOpen(tag), close: here, loc: 'close'};
243
- }
244
- // opening tag
245
- return {open: here, close: new Iter(this, to).findMatchingClose(tag), loc: 'open'};
246
- },
247
- );
248
-
249
- CodeMirror.defineExtension(
250
- 'findEnclosingTag',
251
-
252
- /**
253
- * @this {CodeMirror.Editor}
254
- * @param {CodeMirror.Position} pos
255
- * @param {string} tag
256
- * @returns {CodeMirror.MatchingTagPair}
257
- */
258
- function(pos, tag) {
259
- const open = new Iter(this, pos).findMatchingOpen(tag);
260
- if (open) {
261
- const close = new Iter(this, pos).findMatchingClose(open.tag);
262
- return close && {open, close};
263
- }
264
- return undefined;
265
- },
266
- );
267
-
268
- /** Used by addon/edit/closetag.js */
269
- CodeMirror.scanForClosingTag = (cm, pos, tagName) => new Iter(cm, pos).findMatchingClose(tagName);
270
-
271
- CodeMirror.defineOption('matchTags', false, (cm, val, old) => {
272
- if (old && old !== Init) {
273
- cm.off('cursorActivity', doMatchTags);
274
- clear(cm);
275
- }
276
- if (val) {
277
- cm.on('cursorActivity', doMatchTags);
278
- doMatchTags(cm);
279
- }
280
- });
281
-
282
- /**
283
- * 清除高亮
284
- * @param {CodeMirror.EditorWithMatchingTags} cm CodeMirror实例
285
- */
286
- const clear = cm => {
287
- if (cm.state.tagHit) {
288
- cm.state.tagHit.clear();
289
- cm.state.tagHit = undefined;
290
- }
291
- if (cm.state.tagOther) {
292
- cm.state.tagOther.clear();
293
- cm.state.tagOther = undefined;
294
- }
295
- };
296
-
297
- /**
298
- * 搜索并高亮匹配的标签
299
- * @param {CodeMirror.EditorWithMatchingTags} cm CodeMirror实例
300
- */
301
- const doMatchTags = cm => {
302
- cm.operation(() => {
303
- clear(cm);
304
- if (cm.somethingSelected()) {
305
- return;
306
- }
307
- const match = cm.findMatchingTag(cm.getCursor());
308
- if (!match) {
309
- return;
310
- } else if (match.loc === 'self') {
311
- cm.state.tagHit = cm.markText(match.open.from, match.open.to, {className: 'cm-matchingtag'});
312
- return;
313
- }
314
- const hit = match.loc === 'open' ? match.open : match.close,
315
- other = match.loc === 'close' ? match.open : match.close;
316
- if (hit) {
317
- cm.state.tagHit = cm.markText(hit.from, hit.to, {className: `cm-${other ? '' : 'non'}matchingtag`});
318
- if (other) {
319
- cm.state.tagOther = cm.markText(other.from, other.to, {className: 'cm-matchingtag'});
320
- }
321
- }
322
- });
323
- };
324
- })();
package/search.js DELETED
@@ -1,258 +0,0 @@
1
- /**
2
- * @author Bhsd <https://github.com/bhsd-harry>
3
- * @license GPL-3.0
4
- */
5
- /* eslint-disable require-unicode-regexp */
6
- (() => {
7
- 'use strict';
8
- const {Pos} = CodeMirror,
9
- {libs: {wphl: {msg, isPc, addons}}} = mw;
10
-
11
- // Prepare elements
12
- const $search = $('<input>', {class: 'Wikiplus-Quickedit-Search', placeholder: msg('search-placeholder')}),
13
- $searchClose = $('<span>', {text: '×', class: 'Wikiplus-Symbol-Btn'}),
14
- $searchNext = $('<span>', {text: '▼', class: 'Wikiplus-Symbol-Btn'}),
15
- $searchPrev = $('<span>', {text: '▲', class: 'Wikiplus-Symbol-Btn'}),
16
- $searchContainer = $('<div>', {
17
- class: 'Wikiplus-Quickedit-Search-Div',
18
- html: [$search, $searchNext, $searchPrev, $searchClose],
19
- }),
20
- $searchBtn = $('<span>', {class: 'Wikiplus-Btn', html: msg('addon-search')}),
21
- $replace = $('<textarea>', {class: 'Wikiplus-Quickedit-Search', placeholder: msg('replace-placeholder'), rows: 1}),
22
- $replaceClose = $('<span>', {text: '×', class: 'Wikiplus-Symbol-Btn'}),
23
- $replaceNext = $('<span>', {text: '▼', class: 'Wikiplus-Symbol-Btn'}),
24
- $replacePrev = $('<span>', {text: '▲', class: 'Wikiplus-Symbol-Btn'}),
25
- $replaceContainer = $('<div>', {
26
- class: 'Wikiplus-Quickedit-Search-Div',
27
- html: [$replace, $replaceNext, $replacePrev, $replaceClose],
28
- }),
29
- $replaceBtn = $('<span>', {class: 'Wikiplus-Btn', html: msg('search-replace')});
30
-
31
- const escapeRegExp = mw.util.escapeRegExp || mw.RegExp.escape;
32
- const /** @type {CodeMirror.Mode<undefined>} */ overlay = {token: /** @override */ () => {}};
33
-
34
- /**
35
- * 根据搜索字符串生成高亮
36
- * @param {string|RegExp} str 搜索字符串
37
- */
38
- const token = str => {
39
- const initial = typeof str === 'string' && new RegExp(`[^${escapeRegExp(str[0])}]`, 'i');
40
-
41
- /**
42
- * @override
43
- * @param {CodeMirror.StringStream} stream
44
- */
45
- return stream => {
46
- if (stream.match(str, true, true)) {
47
- return 'search';
48
- }
49
- stream.next();
50
- if (initial) {
51
- stream.eatWhile(initial);
52
- }
53
- return undefined;
54
- };
55
- };
56
-
57
- /** input event handler of `$search` */
58
- const onInput = () => {
59
- $search.css('background-color', '').off('input', onInput);
60
- };
61
-
62
- let /** @type {string} */ lastSource,
63
- /** @type {string|RegExp} */ lastPtn,
64
- /** @type {CodeMirror.SearchCursor} */ cursor;
65
-
66
- /**
67
- * 更新搜索字符串
68
- * @param {CodeMirror.Editor} cm CodeMirror实例
69
- */
70
- const updatePtn = cm => {
71
- const /** @type {string} */ source = $search.val();
72
- if (!source) {
73
- return undefined;
74
- } else if (source === lastSource) {
75
- return lastPtn;
76
- }
77
- const caseFold = source.endsWith('i'),
78
- ptn = /^\/.+\/i?$/u.test(source)
79
- ? new RegExp(source.slice(1, caseFold ? -2 : -1), caseFold ? 'im' : 'm')
80
- : source;
81
- cm.removeOverlay(overlay);
82
- overlay.token = token(ptn);
83
- cm.addOverlay(overlay);
84
- lastSource = source;
85
- lastPtn = ptn;
86
- cursor = cm.getSearchCursor(ptn, cm.getCursor(), {caseFold: true});
87
- return ptn;
88
- };
89
-
90
- /**
91
- * keyboard event handler of `$search`
92
- * @param {CodeMirror.Editor} cm CodeMirror实例
93
- * @param {boolean} dir 搜索方向
94
- * @param {boolean} update 是否先更新搜索字符串
95
- */
96
- const findNext = (cm, dir, update = true) => {
97
- if (update && !updatePtn(cm)) {
98
- return;
99
- }
100
- let result = dir ? cursor.findNext() : cursor.findPrevious();
101
- if (!result) {
102
- let pos;
103
- if (dir) {
104
- pos = Pos(0, 0);
105
- } else {
106
- const lastLine = cm.lastLine();
107
- pos = Pos(lastLine, cm.getLine(lastLine).length);
108
- }
109
- cursor.pos = {from: pos, to: pos};
110
- cursor.atOccurrence = false;
111
- result = dir ? cursor.findNext() : cursor.findPrevious();
112
- }
113
- if (result) {
114
- const from = cursor.from(),
115
- to = cursor.to();
116
- cm.setSelection(from, to);
117
- cm.scrollIntoView({from, to});
118
- onInput();
119
- } else {
120
- $search.css('background-color', 'pink').on('input', onInput);
121
- }
122
- };
123
-
124
- /**
125
- * replace one by one
126
- * @param {CodeMirror.Editor} cm CodeMirror实例
127
- * @param {boolean} dir 搜索方向
128
- */
129
- const replaceNext = (cm, dir) => {
130
- const ptn = updatePtn(cm);
131
- if (!ptn) {
132
- return;
133
- } else if (cursor.atOccurrence) {
134
- const replace = $replace.val();
135
- cursor.replace(typeof ptn === 'string' ? replace : cursor.pos.match[0].replace(ptn, replace));
136
- }
137
- findNext(cm, dir, false);
138
- };
139
-
140
- /**
141
- * keyboard event handler of `$replace`
142
- * @param {CodeMirror.Editor} cm CodeMirror实例
143
- */
144
- const replace = async cm => {
145
- const ptn = updatePtn(cm);
146
- if (!ptn) {
147
- return;
148
- }
149
- const replacePtn = typeof ptn === 'string'
150
- ? new RegExp(escapeRegExp(ptn), 'gim')
151
- : new RegExp(ptn, `g${ptn.flags}`),
152
- val = cm.getValue(),
153
- mt = val.match(replacePtn);
154
- if (mt && await OO.ui.confirm(msg('replace-count', mt.length))) {
155
- const {left, top} = cm.getScrollInfo();
156
- cm.setValue(val.replace(replacePtn, $replace.val()));
157
- cm.scrollTo(left, top);
158
- $replaceContainer.hide();
159
- }
160
- };
161
-
162
- /** click event handler of `$searchBtn` */
163
- const findNew = () => {
164
- $searchContainer.show();
165
- $search.select().focus()[0]
166
- .scrollIntoView({behavior: 'smooth'});
167
- };
168
-
169
- /** click event handler of `$replaceBtn` */
170
- const replaceNew = () => {
171
- $replaceContainer.show();
172
- if ($searchContainer.is(':hidden')) {
173
- findNew();
174
- }
175
- };
176
-
177
- /**
178
- * click event handler of `$searchClose`
179
- * @param {CodeMirror.Editor} cm CodeMirror实例
180
- */
181
- const reset = cm => {
182
- cm.removeOverlay(overlay);
183
- $searchContainer.hide();
184
- $replaceContainer.hide();
185
- lastSource = '';
186
- };
187
-
188
- CodeMirror.commands.find = findNew;
189
- CodeMirror.commands.findNext = /** 向后搜索 */ doc => {
190
- findNext(doc, true);
191
- };
192
- CodeMirror.commands.findPrev = /** 向前搜索 */ doc => {
193
- findNext(doc, false);
194
- };
195
- CodeMirror.commands.replace = replaceNew;
196
- CodeMirror.commands.replaceNext = /** 向后替换 */ doc => {
197
- replaceNext(doc, true);
198
- };
199
- CodeMirror.commands.replaceAll = /** 全文替换 */ doc => { // eslint-disable-line es-x/no-string-prototype-replaceall
200
- replace(doc);
201
- };
202
-
203
- mw.hook('wiki-codemirror').add(/** @param {CodeMirror.Editor} cm */ cm => {
204
- if (!cm.getOption('styleSelectedText') || addons.has('wikiEditor')) {
205
- return;
206
- }
207
- const $textarea = $(cm.getWrapperElement()).prev('#Wikiplus-Quickedit');
208
- if ($textarea.length === 0) {
209
- return;
210
- }
211
- $searchContainer.hide().insertBefore($textarea);
212
- $searchBtn.click(findNew).insertAfter('#Wikiplus-Quickedit-Jump');
213
- $searchClose.click(() => {
214
- reset(cm);
215
- });
216
- $searchNext.click(() => {
217
- findNext(cm, true);
218
- });
219
- $searchPrev.click(() => {
220
- findNext(cm, false);
221
- });
222
- $search.val('').keydown(e => {
223
- if (e.key === 'Enter') {
224
- e.preventDefault();
225
- findNext(cm, true);
226
- } else if (e.key === 'Escape') {
227
- e.stopPropagation();
228
- reset(cm);
229
- }
230
- });
231
- $replaceContainer.hide().insertBefore($textarea);
232
- $replaceBtn.click(replaceNew).insertAfter($searchBtn);
233
- $replaceClose.click(() => {
234
- $replaceContainer.hide();
235
- });
236
- $replaceNext.click(() => {
237
- replaceNext(cm, true);
238
- });
239
- $replacePrev.click(() => {
240
- replaceNext(cm, false);
241
- });
242
- $replace.val('').keydown(e => {
243
- if (e.key === 'Enter') {
244
- e.preventDefault();
245
- replace(cm);
246
- } else if (e.key === 'Escape') {
247
- e.stopPropagation();
248
- $replaceContainer.hide();
249
- }
250
- });
251
- const ctrl = isPc(CodeMirror) ? 'Ctrl' : 'Cmd';
252
- cm.addKeyMap({
253
- [`${ctrl}-H`]: 'replace',
254
- [`Shift-${ctrl}-H`]: 'replaceNext',
255
- [`${ctrl}-Alt-Enter`]: 'replaceAll',
256
- });
257
- });
258
- })();