wikiplus-highlight 2.7.1

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 ADDED
@@ -0,0 +1,308 @@
1
+ /*
2
+ * CodeMirror, copyright (c) by Marijn Haverbeke and others
3
+ * Distributed under an MIT license: https://codemirror.net/LICENSE
4
+ * Modified for MediaWiki by Bhsd <https://github.com/bhsd-harry>
5
+ */
6
+
7
+ (() => {
8
+ 'use strict';
9
+
10
+ const {Pos, cmpPos} = CodeMirror;
11
+
12
+ const tagStart = /<(\/?)([A-Z_a-z]\w*)/g,
13
+ voidTags = ['br', 'wbr', 'hr', 'img'];
14
+
15
+ class Iter {
16
+ constructor(cm, pos) {
17
+ const {line, ch} = pos,
18
+ {state: {matchTags: {maxScanLines = 1000}}} = cm;
19
+ this.line = line;
20
+ this.ch = ch;
21
+ this.cm = cm;
22
+ this.text = cm.getLine(line);
23
+ this.min = Math.max(line - maxScanLines + 1, cm.firstLine());
24
+ this.max = Math.min(line + maxScanLines - 1, cm.lastLine());
25
+ }
26
+
27
+ isTag() {
28
+ const type = this.cm.getTokenTypeAt(Pos(this.line, this.ch));
29
+ return /\b(?:mw-(?:html|ext)tag|tag\b)/.test(type);
30
+ }
31
+
32
+ bracketAt(ch) {
33
+ const type = this.cm.getTokenTypeAt(Pos(this.line, ch + 1));
34
+ return /\b(?:mw-(?:html|ext)tag-)?bracket\b/.test(type);
35
+ }
36
+
37
+ // Jump to the start of the next line
38
+ nextLine() {
39
+ if (this.line >= this.max) {
40
+ return;
41
+ }
42
+ this.ch = 0;
43
+ this.text = this.cm.getLine(++this.line);
44
+ return true;
45
+ }
46
+
47
+ // Jump to the end of the previous line
48
+ prevLine() {
49
+ if (this.line <= this.min) {
50
+ return;
51
+ }
52
+ this.text = this.cm.getLine(--this.line);
53
+ this.ch = this.text.length;
54
+ return true;
55
+ }
56
+
57
+ // Jump to the letter after a '>' towards the line end
58
+ toTagEnd() {
59
+ for (;;) {
60
+ const gt = this.text.indexOf('>', this.ch);
61
+ if (gt === -1) {
62
+ return;
63
+ }
64
+ if (!this.bracketAt(gt)) {
65
+ this.ch = gt + 1;
66
+ continue;
67
+ }
68
+ const lastSlash = this.text.lastIndexOf('/', gt);
69
+ const selfClose = lastSlash > -1 && !/\S/.test(this.text.slice(lastSlash + 1, gt));
70
+ this.ch = gt + 1;
71
+ return selfClose ? 'selfClose' : 'regular';
72
+ }
73
+ }
74
+
75
+ // Jump to a '<' towards the line start
76
+ toTagStart() {
77
+ for (;;) {
78
+ const lt = this.ch ? this.text.lastIndexOf('<', this.ch - 1) : -1;
79
+ if (lt === -1) {
80
+ return;
81
+ }
82
+ if (!this.bracketAt(lt)) {
83
+ this.ch = lt;
84
+ continue;
85
+ }
86
+ tagStart.lastIndex = lt;
87
+ this.ch = lt;
88
+ const match = tagStart.exec(this.text);
89
+ if (match && match.index === lt) {
90
+ return match;
91
+ }
92
+ }
93
+ }
94
+
95
+ // Jump to the start of the last line, or the letter after a ${tagStart}
96
+ toNextTag() {
97
+ for (;;) {
98
+ tagStart.lastIndex = this.ch;
99
+ const found = tagStart.exec(this.text);
100
+ if (!found) {
101
+ if (this.nextLine()) {
102
+ continue;
103
+ } else {
104
+ return;
105
+ }
106
+ }
107
+ if (!this.bracketAt(found.index)) {
108
+ this.ch = found.index + found[0].length;
109
+ continue;
110
+ }
111
+ this.ch = found.index + found[0].length;
112
+ return found;
113
+ }
114
+ }
115
+
116
+ // Jump to the end of the first line, or a non-bracket '>', or the letter after a tag bracket '>'
117
+ toPrevTag() {
118
+ for (;;) {
119
+ const gt = this.ch ? this.text.lastIndexOf('>', this.ch - 1) : -1;
120
+ if (gt === -1) {
121
+ if (this.prevLine()) {
122
+ continue;
123
+ } else {
124
+ return;
125
+ }
126
+ }
127
+ if (!this.bracketAt(gt)) {
128
+ this.ch = gt;
129
+ continue;
130
+ }
131
+ const lastSlash = this.text.lastIndexOf('/', gt);
132
+ const selfClose = lastSlash > -1 && !/\S/.test(this.text.slice(lastSlash + 1, gt));
133
+ this.ch = gt + 1;
134
+ return selfClose ? 'selfClose' : 'regular';
135
+ }
136
+ }
137
+
138
+ findMatchingClose(tag) {
139
+ const stack = [];
140
+ for (;;) {
141
+ const next = this.toNextTag();
142
+ if (!next) {
143
+ return;
144
+ }
145
+ const start = this.ch - next[0].length,
146
+ end = this.toTagEnd(),
147
+ tagName = next[2].toLowerCase();
148
+ if (!end) {
149
+ return;
150
+ }
151
+ if (end === 'selfClose' || voidTags.includes(tagName)) {
152
+ continue;
153
+ }
154
+ if (next[1]) { // closing tag
155
+ let i = stack.length - 1;
156
+ for (; i >= 0; --i) {
157
+ if (stack[i] === tagName) {
158
+ stack.length = i;
159
+ break;
160
+ }
161
+ }
162
+ if (i < 0 && (!tag || tag === tagName)) {
163
+ return {
164
+ tag: tagName,
165
+ from: Pos(this.line, start),
166
+ to: Pos(this.line, this.ch),
167
+ };
168
+ }
169
+ } else { // opening tag
170
+ stack.push(tagName);
171
+ }
172
+ }
173
+ }
174
+
175
+ findMatchingOpen(tag) {
176
+ const stack = [];
177
+ for (;;) {
178
+ const prev = this.toPrevTag();
179
+ if (!prev) {
180
+ return;
181
+ }
182
+ const end = this.ch,
183
+ start = this.toTagStart();
184
+ if (!start) {
185
+ return;
186
+ }
187
+ const tagName = start[2].toLowerCase();
188
+ if (prev === 'selfClose' || voidTags.includes(tagName)) {
189
+ continue;
190
+ }
191
+ if (start[1]) { // closing tag
192
+ stack.push(tagName);
193
+ } else { // opening tag
194
+ let i = stack.length - 1;
195
+ for (; i >= 0; --i) {
196
+ if (stack[i] === tagName) {
197
+ stack.length = i;
198
+ break;
199
+ }
200
+ }
201
+ if (i < 0 && (!tag || tag === tagName)) {
202
+ return {
203
+ tag: tagName,
204
+ from: Pos(this.line, this.ch),
205
+ to: Pos(this.line, end),
206
+ };
207
+ }
208
+ }
209
+ }
210
+ }
211
+ }
212
+
213
+ CodeMirror.defineExtension('findMatchingTag', function(pos) {
214
+ let iter = new Iter(this, pos);
215
+ if (!iter.isTag()) {
216
+ return;
217
+ }
218
+ const end = iter.toTagEnd(),
219
+ to = end && Pos(iter.line, iter.ch);
220
+ const start = end && iter.toTagStart();
221
+ if (!start || cmpPos(iter, pos) > 0) {
222
+ return;
223
+ }
224
+ const tag = start[2].toLowerCase(),
225
+ here = {from: Pos(iter.line, iter.ch), to, tag};
226
+ if (end === 'selfClose' || voidTags.includes(tag)) {
227
+ return {open: here, close: null, at: 'self'};
228
+ }
229
+
230
+ if (start[1]) { // closing tag
231
+ return {open: iter.findMatchingOpen(tag), close: here, at: 'close'};
232
+ } // opening tag
233
+ iter = new Iter(this, to);
234
+ return {open: here, close: iter.findMatchingClose(tag), at: 'open'};
235
+ });
236
+
237
+ CodeMirror.defineExtension('findEnclosingTag', function(pos, tag) {
238
+ const iter = new Iter(this, pos);
239
+ const open = iter.findMatchingOpen(tag);
240
+ if (!open) {
241
+ return;
242
+ }
243
+ const forward = new Iter(this, pos);
244
+ const close = forward.findMatchingClose(open.tag);
245
+ if (close) {
246
+ return {open, close};
247
+ }
248
+ });
249
+
250
+ // Used by addon/edit/closetag.js
251
+ CodeMirror.scanForClosingTag = function(cm, pos, name) {
252
+ const iter = new Iter(cm, pos);
253
+ return iter.findMatchingClose(name);
254
+ };
255
+
256
+ CodeMirror.defineOption('matchTags', false, (cm, val, old) => {
257
+ if (old && old !== CodeMirror.Init) {
258
+ cm.off('cursorActivity', doMatchTags);
259
+ clear(cm);
260
+ }
261
+ if (val) {
262
+ cm.state.matchTags = typeof val === 'object' ? val : {};
263
+ cm.on('cursorActivity', doMatchTags);
264
+ doMatchTags(cm);
265
+ }
266
+ });
267
+
268
+ function clear(cm) {
269
+ if (cm.state.tagHit) {
270
+ cm.state.tagHit.clear();
271
+ }
272
+ if (cm.state.tagOther) {
273
+ cm.state.tagOther.clear();
274
+ }
275
+ cm.state.tagHit = null;
276
+ cm.state.tagOther = null;
277
+ }
278
+
279
+ function doMatchTags(cm) {
280
+ cm.operation(() => {
281
+ clear(cm);
282
+ if (cm.somethingSelected()) {
283
+ return;
284
+ }
285
+ const match = cm.findMatchingTag(cm.getCursor());
286
+ if (!match) {
287
+ return;
288
+ }
289
+ if (match.at === 'self') {
290
+ cm.state.tagHit = cm.markText(match.open.from, match.open.to, {className: 'cm-matchingtag'});
291
+ return;
292
+ }
293
+ const hit = match.at === 'open' ? match.open : match.close,
294
+ other = match.at === 'close' ? match.open : match.close;
295
+ if (hit) {
296
+ cm.state.tagHit = cm.markText(hit.from, hit.to, {className: `cm-${other ? '' : 'non'}matchingtag`});
297
+ }
298
+ if (other) {
299
+ cm.state.tagOther = cm.markText(other.from, other.to, {className: 'cm-matchingtag'});
300
+ }
301
+ });
302
+ }
303
+
304
+ mw.loader.addStyleTag(
305
+ '.cm-matchingtag{background-color:#c9ffc8}'
306
+ + '.cm-nonmatchingtag{background-color:#fff0a8}',
307
+ );
308
+ })();
package/package.json ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "wikiplus-highlight",
3
+ "version": "2.7.1",
4
+ "description": "A plugin for the MediaWiki front-end add-on \"Wikiplus\"",
5
+ "main": "main.js",
6
+ "scripts": {
7
+ "test": "echo 'Error: no test specified' && exit 1"
8
+ },
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/bhsd-harry/Wikiplus-highlight.git"
12
+ },
13
+ "keywords": [
14
+ "mediawiki",
15
+ "mediawiki-gadget",
16
+ "wikiplus"
17
+ ],
18
+ "author": "Bhsd",
19
+ "license": "GPL-3.0",
20
+ "bugs": {
21
+ "url": "https://github.com/bhsd-harry/Wikiplus-highlight/issues"
22
+ },
23
+ "homepage": "https://github.com/bhsd-harry/Wikiplus-highlight#readme"
24
+ }
package/search.js ADDED
@@ -0,0 +1,187 @@
1
+ /**
2
+ * @author Bhsd <https://github.com/bhsd-harry>
3
+ * @license: GPL-3.0
4
+ */
5
+
6
+ (() => {
7
+ 'use strict';
8
+
9
+ const msg = key => mw.msg(`wphl-${key}`);
10
+
11
+ // Prepare elements
12
+ const $search = $('<input>', {
13
+ id: 'Wikiplus-Quickedit-Search',
14
+ placeholder: msg('search-placeholder'),
15
+ }),
16
+ $searchClose = $('<span>', {
17
+ text: '×',
18
+ id: 'Wikiplus-Quickedit-Search-Close',
19
+ class: 'Wikiplus-Symbol-Btn',
20
+ }),
21
+ $searchNext = $('<span>', {
22
+ text: '▼',
23
+ id: 'Wikiplus-Quickedit-Search-Next',
24
+ class: 'Wikiplus-Symbol-Btn',
25
+ }),
26
+ $searchPrev = $('<span>', {
27
+ text: '▲',
28
+ id: 'Wikiplus-Quickedit-Search-Prev',
29
+ class: 'Wikiplus-Symbol-Btn',
30
+ }),
31
+ $searchContainer = $('<div>', {
32
+ id: 'Wikiplus-Quickedit-Search-Div',
33
+ html: [
34
+ $search,
35
+ $searchNext,
36
+ $searchPrev,
37
+ $searchClose,
38
+ ],
39
+ }),
40
+ $searchBtn = $('<span>', {
41
+ class: 'Wikiplus-Btn',
42
+ text: msg('addon-search'),
43
+ });
44
+
45
+ const escapeRegExp = mw.util.escapeRegExp || mw.RegExp.escape; // @type {(function|undefined)}
46
+ const overlay = {token: () => {}};
47
+
48
+ /**
49
+ * 根据搜索字符串生成高亮
50
+ */
51
+ const token = str => {
52
+ let initial;
53
+ if (typeof str === 'string') {
54
+ initial = RegExp(`[^${escapeRegExp(str[0])}]`, 'i');
55
+ }
56
+ return stream => {
57
+ if (stream.match(str, true, true)) {
58
+ return 'search';
59
+ }
60
+ stream.next();
61
+ if (typeof str === 'string') {
62
+ stream.eatWhile(initial);
63
+ }
64
+ };
65
+ };
66
+
67
+ // input event handler of $search
68
+ const onInput = () => {
69
+ $search.css('background-color', '').off('input', onInput);
70
+ };
71
+
72
+ // keyboard event handler of $search
73
+ let lastPtn, cursor;
74
+ const findNext = (cm, dir) => {
75
+ let ptn = $search.val();
76
+ if (!ptn) {
77
+ return;
78
+ }
79
+
80
+ if (/^\/.+\/i?$/.test(ptn)) {
81
+ ptn = ptn.endsWith('i')
82
+ ? RegExp(ptn.slice(1, -2), 'i')
83
+ : RegExp(ptn.slice(1, -1));
84
+ }
85
+ if (ptn !== lastPtn) {
86
+ cm.removeOverlay(overlay);
87
+ overlay.token = token(ptn);
88
+ cm.addOverlay(overlay);
89
+ lastPtn = ptn;
90
+ cursor = cm.getSearchCursor(ptn, cm.getCursor(), {caseFold: true});
91
+ }
92
+ const method = dir ? 'findNext' : 'findPrevious';
93
+ let result = cursor[method]();
94
+ if (!result) {
95
+ if (dir) {
96
+ cursor = cm.getSearchCursor(ptn, {line: 0, ch: 0}, {caseFold: true});
97
+ } else {
98
+ const lastLine = cm.lastLine(),
99
+ lastCh = cm.getLine(lastLine).length;
100
+ cursor = cm.getSearchCursor(ptn, {line: lastLine, ch: lastCh}, {caseFold: true});
101
+ }
102
+ result = cursor[method]();
103
+ }
104
+ if (result) {
105
+ const from = cursor.from(),
106
+ to = cursor.to();
107
+ cm.setSelection(from, to);
108
+ cm.scrollIntoView({from, to});
109
+ onInput();
110
+ } else {
111
+ $search.css('background-color', 'pink').on('input', onInput);
112
+ }
113
+ };
114
+
115
+ // click event handler of $searchBtn
116
+ const findNew = () => {
117
+ $searchContainer.show();
118
+ $search.select().focus()[0]
119
+ .scrollIntoView({behavior: 'smooth'});
120
+ };
121
+
122
+ // click event handler of $searchClose
123
+ const reset = cm => {
124
+ cm.removeOverlay(overlay);
125
+ $searchContainer.hide();
126
+ lastPtn = '';
127
+ };
128
+
129
+ CodeMirror.commands.findForward = doc => {
130
+ findNext(doc, true);
131
+ };
132
+ CodeMirror.commands.findBackward = doc => {
133
+ findNext(doc, false);
134
+ };
135
+
136
+ const {name} = $.client.profile(),
137
+ focus = name === 'safari'
138
+ ? cm => {
139
+ cm.focus();
140
+ }
141
+ : () => {};
142
+ mw.hook('wiki-codemirror').add(cm => {
143
+ const $textarea = $(cm.getWrapperElement()).prev('#Wikiplus-Quickedit');
144
+ if ($textarea.length === 0) {
145
+ return;
146
+ }
147
+ $searchContainer.hide().insertBefore($textarea);
148
+ $searchBtn.click(findNew).insertAfter('#Wikiplus-Quickedit-Jump');
149
+ $searchClose.click(() => {
150
+ reset(cm);
151
+ });
152
+ $searchNext.click(() => {
153
+ findNext(cm, true);
154
+ focus(cm);
155
+ });
156
+ $searchPrev.click(() => {
157
+ findNext(cm, false);
158
+ focus(cm);
159
+ });
160
+ $search.val('').keydown(e => {
161
+ if (e.key === 'Enter') {
162
+ e.preventDefault();
163
+ findNext(cm, true);
164
+ focus(cm);
165
+ } else if (e.key === 'Escape') {
166
+ e.stopPropagation();
167
+ reset(cm);
168
+ }
169
+ });
170
+ cm.addKeyMap({
171
+ 'Ctrl-F': findNew,
172
+ 'Cmd-F': findNew,
173
+ 'Ctrl-G': 'findForward',
174
+ 'Cmd-G': 'findForward',
175
+ 'Shift-Ctrl-G': 'findBackward',
176
+ 'Shift-Cmd-G': 'findBackward',
177
+ });
178
+ });
179
+
180
+ mw.loader.addStyleTag(
181
+ '.Wikiplus-Btn{line-height:1.4}'
182
+ + '#Wikiplus-Quickedit-Search-Div{margin:7px 0 5px;}'
183
+ + '.Wikiplus-Symbol-Btn{font-size:20px;margin:7px;vertical-align:middle;cursor:pointer;}'
184
+ + '#Wikiplus-Quickedit-Search{width:50%;}'
185
+ + 'span.cm-search{background-color:#ffc0cb83;}',
186
+ );
187
+ })();