@webcoder49/code-input 2.1.0 → 2.5.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.
Files changed (44) hide show
  1. package/CONTRIBUTING.md +11 -1
  2. package/README.md +26 -11
  3. package/code-input.css +126 -29
  4. package/code-input.d.ts +153 -11
  5. package/code-input.js +218 -193
  6. package/code-input.min.css +1 -1
  7. package/code-input.min.js +1 -1
  8. package/package.json +1 -1
  9. package/plugins/README.md +28 -6
  10. package/plugins/auto-close-brackets.js +61 -0
  11. package/plugins/auto-close-brackets.min.js +1 -0
  12. package/plugins/autocomplete.js +21 -12
  13. package/plugins/autocomplete.min.js +1 -1
  14. package/plugins/autodetect.js +4 -4
  15. package/plugins/autodetect.min.js +1 -1
  16. package/plugins/find-and-replace.css +145 -0
  17. package/plugins/find-and-replace.js +746 -0
  18. package/plugins/find-and-replace.min.css +1 -0
  19. package/plugins/find-and-replace.min.js +1 -0
  20. package/plugins/go-to-line.css +77 -0
  21. package/plugins/go-to-line.js +175 -0
  22. package/plugins/go-to-line.min.css +1 -0
  23. package/plugins/go-to-line.min.js +1 -0
  24. package/plugins/indent.js +166 -15
  25. package/plugins/indent.min.js +1 -1
  26. package/plugins/prism-line-numbers.css +10 -9
  27. package/plugins/prism-line-numbers.min.css +1 -1
  28. package/plugins/select-token-callbacks.js +289 -0
  29. package/plugins/select-token-callbacks.min.js +1 -0
  30. package/plugins/special-chars.css +1 -5
  31. package/plugins/special-chars.js +65 -61
  32. package/plugins/special-chars.min.css +2 -2
  33. package/plugins/special-chars.min.js +1 -1
  34. package/plugins/test.js +1 -2
  35. package/plugins/test.min.js +1 -1
  36. package/tests/hljs.html +55 -0
  37. package/tests/i18n.html +197 -0
  38. package/tests/prism-match-braces-compatibility.js +215 -0
  39. package/tests/prism-match-braces-compatibility.min.js +1 -0
  40. package/tests/prism.html +54 -0
  41. package/tests/tester.js +593 -0
  42. package/tests/tester.min.js +21 -0
  43. package/plugins/debounce-update.js +0 -40
  44. package/plugins/debounce-update.min.js +0 -1
@@ -0,0 +1,289 @@
1
+ /**
2
+ * Make tokens in the <pre><code> element that are included within the selected text of the <code-input>
3
+ * gain a CSS class while selected, or trigger JavaScript callbacks.
4
+ * Files: select-token-callbacks.js
5
+ */
6
+ codeInput.plugins.SelectTokenCallbacks = class extends codeInput.Plugin {
7
+ /**
8
+ * Set up the behaviour of tokens text-selected in the `<code-input>` element, and the exact definition of a token being text-selected.
9
+ *
10
+ * All parameters are optional. If you provide no arguments to the constructor, this will dynamically apply the "code-input_select-token-callbacks_selected" class to selected tokens only, for you to style via CSS.
11
+ *
12
+ * @param {codeInput.plugins.SelectTokenCallbacks.TokenSelectorCallbacks} tokenSelectorCallbacks What to do with text-selected tokens. See docstrings for the TokenSelectorCallbacks class.
13
+ * @param {boolean} onlyCaretNotSelection If true, tokens will only be marked as selected when no text is selected but rather the caret is inside them (start of selection == end of selection). Default false.
14
+ * @param {boolean} caretAtStartIsSelected Whether the caret or text selection's end being just before the first character of a token means said token is selected. Default true.
15
+ * @param {boolean} caretAtEndIsSelected Whether the caret or text selection's start being just after the last character of a token means said token is selected. Default true.
16
+ * @param {boolean} createSubTokens Whether temporary `<span>` elements should be created inside partially-selected tokens containing just the selected text and given the selected class. Default false.
17
+ * @param {boolean} partiallySelectedTokensAreSelected Whether tokens for which only some of their text is selected should be treated as selected. Default true.
18
+ * @param {boolean} parentTokensAreSelected Whether all parent tokens of selected tokens should be treated as selected. Default true.
19
+ */
20
+ constructor(tokenSelectorCallbacks = codeInput.plugins.SelectTokenCallbacks.TokenSelectorCallbacks.createClassSynchronisation(), onlyCaretNotSelection = false, caretAtStartIsSelected = true, caretAtEndIsSelected = true, createSubTokens = false, partiallySelectedTokensAreSelected = true, parentTokensAreSelected = true) {
21
+ super([]); // No observed attributes
22
+
23
+ this.tokenSelectorCallbacks = tokenSelectorCallbacks;
24
+ this.onlyCaretNotSelection = onlyCaretNotSelection;
25
+ this.caretAtStartIsSelected = caretAtStartIsSelected;
26
+ this.caretAtEndIsSelected = caretAtEndIsSelected;
27
+ this.createSubTokens = createSubTokens;
28
+ this.partiallySelectedTokensAreSelected = partiallySelectedTokensAreSelected;
29
+ this.parentTokensAreSelected = parentTokensAreSelected;
30
+ }
31
+ /* Runs after code is highlighted; Params: codeInput element) */
32
+ afterHighlight(codeInputElement) {
33
+ this.syncSelection(codeInputElement);
34
+ }
35
+ /* Runs after elements are added into a `code-input` (useful for adding events to the textarea); Params: codeInput element) */
36
+ afterElementsAdded(codeInputElement) {
37
+ codeInputElement.pluginData.selectTokenCallbacks = {};
38
+ codeInputElement.pluginData.selectTokenCallbacks.lastSelectionStart = codeInputElement.textareaElement.selectionStart;
39
+ codeInputElement.pluginData.selectTokenCallbacks.lastSelectionEnd = codeInputElement.textareaElement.selectionEnd;
40
+ codeInputElement.pluginData.selectTokenCallbacks.selectedTokenState = new codeInput.plugins.SelectTokenCallbacks.SelectedTokenState(codeInputElement.codeElement, this.tokenSelectorCallbacks, this.onlyCaretNotSelection, this.caretAtStartIsSelected, this.caretAtEndIsSelected, this.createSubTokens, this.partiallySelectedTokensAreSelected, this.parentTokensAreSelected);
41
+ this.syncSelection(codeInputElement);
42
+
43
+ // As of 2024-08, the selectionchange event is only supported on Firefox.
44
+ codeInputElement.textareaElement.addEventListener("selectionchange", () => {
45
+ this.checkSelectionChanged(codeInputElement)
46
+ });
47
+ // When selectionchange has complete support, the listeners below can be deleted.
48
+ codeInputElement.textareaElement.addEventListener("select", () => {
49
+ this.checkSelectionChanged(codeInputElement)
50
+ });
51
+ codeInputElement.textareaElement.addEventListener("keypress", () => {
52
+ this.checkSelectionChanged(codeInputElement)
53
+ });
54
+ codeInputElement.textareaElement.addEventListener("mousedown", () => {
55
+ this.checkSelectionChanged(codeInputElement)
56
+ });
57
+ }
58
+ /* If the text selection has changed, run syncSelection. */
59
+ checkSelectionChanged(codeInputElement) {
60
+ if(
61
+ codeInputElement.textareaElement.selectionStart != codeInputElement.pluginData.selectTokenCallbacks.lastSelectionStart
62
+ || codeInputElement.textareaElement.selectionEnd != codeInputElement.pluginData.selectTokenCallbacks.lastSelectionEnd
63
+ ) {
64
+ this.syncSelection(codeInputElement);
65
+ codeInputElement.pluginData.selectTokenCallbacks.lastSelectionStart = codeInputElement.textareaElement.selectionStart;
66
+ codeInputElement.pluginData.selectTokenCallbacks.lastSelectionEnd = codeInputElement.textareaElement.selectionEnd;
67
+ }
68
+ }
69
+ /* Update which elements have the code-input_selected class. */
70
+ syncSelection(codeInputElement) {
71
+ codeInputElement.pluginData.selectTokenCallbacks.selectedTokenState.updateSelection(codeInputElement.textareaElement.selectionStart, codeInputElement.textareaElement.selectionEnd)
72
+ }
73
+ }
74
+
75
+ /**
76
+ * A data structure specifying what should be done with tokens when they are selected, and also allows for previously selected
77
+ * tokens to be dealt with each time the selection changes. See the constructor and the createClassSynchronisation static method.
78
+ */
79
+ codeInput.plugins.SelectTokenCallbacks.TokenSelectorCallbacks = class {
80
+ /**
81
+ * Pass any callbacks you want to customise the behaviour of selected tokens via JavaScript.
82
+ *
83
+ * (If the behaviour you want is just differently styling selected tokens _via CSS_, you should probably use the createClassSynchronisation static method.)
84
+ * @param {(token: HTMLElement) => void} tokenSelectedCallback Runs multiple times when the text selection inside the code-input changes, each time inputting a single (part of the highlighted `<pre><code>`) token element that is selected in the new text selection.
85
+ * @param {(tokenContainer: HTMLElement) => void} selectChangedCallback Each time the text selection inside the code-input changes, runs once before any tokenSelectedCallback calls, inputting the highlighted `<pre><code>`'s `<code>` element that contains all token elements.
86
+ */
87
+ constructor(tokenSelectedCallback, selectChangedCallback) {
88
+ this.tokenSelectedCallback = tokenSelectedCallback;
89
+ this.selectChangedCallback = selectChangedCallback;
90
+ }
91
+
92
+ /**
93
+ * Use preset callbacks which ensure all tokens in the selected text range in the `<code-input>`, and only such tokens, are given a certain CSS class.
94
+ *
95
+ * (If the behaviour you want requires more complex behaviour or JavaScript, you should use TokenSelectorCallbacks' constructor.)
96
+ *
97
+ * @param {string} selectedClass The CSS class that will be present on tokens only when they are part of the selected text in the `<code-input>` element. Defaults to "code-input_select-token-callbacks_selected".
98
+ * @returns A new TokenSelectorCallbacks instance that encodes this behaviour.
99
+ */
100
+ static createClassSynchronisation(selectedClass = "code-input_select-token-callbacks_selected") {
101
+ return new codeInput.plugins.SelectTokenCallbacks.TokenSelectorCallbacks(
102
+ (token) => {
103
+ token.classList.add(selectedClass);
104
+ },
105
+ (tokenContainer) => {
106
+ // Remove selected class
107
+ let selectedClassTokens = tokenContainer.getElementsByClassName(selectedClass);
108
+ // Use it like a queue, because as elements have their class name removed they are live-removed from the collection.
109
+ while(selectedClassTokens.length > 0) {
110
+ selectedClassTokens[0].classList.remove(selectedClass);
111
+ }
112
+ }
113
+ );
114
+ }
115
+ }
116
+
117
+ /* Manages a single <code-input> element's selected tokens, and calling the correct functions on the selected tokens */
118
+ codeInput.plugins.SelectTokenCallbacks.SelectedTokenState = class {
119
+ constructor(codeElement, tokenSelectorCallbacks, onlyCaretNotSelection, caretAtStartIsSelected, caretAtEndIsSelected, createSubTokens, partiallySelectedTokensAreSelected, parentTokensAreSelected) {
120
+ this.tokenContainer = codeElement;
121
+ this.tokenSelectorCallbacks = tokenSelectorCallbacks;
122
+ this.onlyCaretNotSelection = onlyCaretNotSelection;
123
+ this.caretAtStartIsSelected = caretAtStartIsSelected;
124
+ this.caretAtEndIsSelected = caretAtEndIsSelected;
125
+ this.createSubTokens = createSubTokens;
126
+ this.partiallySelectedTokensAreSelected = partiallySelectedTokensAreSelected;
127
+ this.parentTokensAreSelected = parentTokensAreSelected;
128
+ }
129
+
130
+ /* Change the selected region to a new range from selectionStart to selectionEnd and run
131
+ the callbacks. */
132
+ updateSelection(selectionStart, selectionEnd) {
133
+ this.selectChanged()
134
+ if(!this.onlyCaretNotSelection || selectionStart == selectionEnd) { // Only deal with selected text if onlyCaretNotSelection is false.
135
+ this.updateSelectedTokens(this.tokenContainer, selectionStart, selectionEnd)
136
+ }
137
+ }
138
+ /* Runs when the text selection has changed, before any updateSelectedTokens call. */
139
+ selectChanged() {
140
+ if(this.createSubTokens) {
141
+ // Remove generated spans to hold selected partial tokens
142
+ let tempSpans = this.tokenContainer.getElementsByClassName("code-input_select-token-callbacks_temporary-span");
143
+ while(tempSpans.length > 0) {
144
+ // Replace with textContent as Text node
145
+ // Use it like a queue, because as elements have their class name removed they are live-removed from the collection.
146
+ tempSpans[0].parentElement.replaceChild(new Text(tempSpans[0].textContent), tempSpans[0]);
147
+ }
148
+ }
149
+
150
+ this.tokenSelectorCallbacks.selectChangedCallback(this.tokenContainer);
151
+ }
152
+
153
+ /* Do the desired behaviour for selection to all tokens (elements in the currentElement)
154
+ from startIndex to endIndex in the text. Start from the currentElement as this function is recursive.
155
+ This code is similar to codeInput.plugins.FindAndReplace.FindMatchState.highlightMatch*/
156
+ updateSelectedTokens(currentElement, startIndex, endIndex) {
157
+ if(endIndex < 0 || endIndex == 0 && !this.caretAtStartIsSelected) {
158
+ return; // Nothing selected
159
+ }
160
+ if(this.parentTokensAreSelected && currentElement !== this.tokenContainer) {
161
+ this.tokenSelectorCallbacks.tokenSelectedCallback(currentElement); // Parent elements also marked with class / have callback called
162
+ }
163
+ for(let i = 0; i < currentElement.childNodes.length; i++) {
164
+ let childElement = currentElement.childNodes[i];
165
+ let childText = childElement.textContent;
166
+
167
+ let noInnerElements = false;
168
+ if(childElement.nodeType == 3) {
169
+ // Text node
170
+ if(this.createSubTokens) {
171
+ // Replace with token
172
+ if(i + 1 < currentElement.childNodes.length && currentElement.childNodes[i+1].nodeType == 3) {
173
+ // Can merge with next text node
174
+ currentElement.childNodes[i+1].textContent = childElement.textContent + currentElement.childNodes[i+1].textContent; // Merge textContent with next node
175
+ currentElement.removeChild(childElement); // Delete this node
176
+ i--; // As an element removed
177
+ continue; // Move to next node
178
+ }
179
+ noInnerElements = true;
180
+
181
+ let replacementElement = document.createElement("span");
182
+ replacementElement.textContent = childText;
183
+ replacementElement.classList.add("code-input_select-token-callbacks_temporary-span"); // Can remove span later
184
+
185
+ currentElement.replaceChild(replacementElement, childElement);
186
+ childElement = replacementElement;
187
+ } else {
188
+ // Skip text node
189
+ // Make indexes skip the element
190
+ startIndex -= childText.length;
191
+ endIndex -= childText.length;
192
+ continue;
193
+ }
194
+ }
195
+
196
+ if(startIndex <= 0) {
197
+ // Started selection
198
+ if(childText.length > endIndex) {
199
+ // Selection ends in childElement
200
+ if(this.partiallySelectedTokensAreSelected) {
201
+ if(noInnerElements) {
202
+ if(this.createSubTokens && startIndex != endIndex) { // Subtoken to create
203
+ // Text node - add selection class to first part
204
+ let startSpan = document.createElement("span");
205
+ this.tokenSelectorCallbacks.tokenSelectedCallback(startSpan); // Selected
206
+ startSpan.classList.add("code-input_select-token-callbacks_temporary-span"); // Can remove span later
207
+ startSpan.textContent = childText.substring(0, endIndex);
208
+
209
+ let endText = childText.substring(endIndex);
210
+ childElement.textContent = endText;
211
+
212
+ childElement.insertAdjacentElement('beforebegin', startSpan);
213
+ i++; // An extra element has been added
214
+ }
215
+ if(this.parentTokensAreSelected || !this.createSubTokens) {
216
+ this.tokenSelectorCallbacks.tokenSelectedCallback(childElement); // Selected
217
+ }
218
+ } else {
219
+ this.updateSelectedTokens(childElement, 0, endIndex);
220
+ }
221
+ }
222
+
223
+ // Match ended - nothing to do after backtracking
224
+ return;
225
+ } else {
226
+ // Match goes through child element
227
+ this.tokenSelectorCallbacks.tokenSelectedCallback(childElement); // Selected
228
+ }
229
+ } else if(this.caretAtEndIsSelected && childText.length >= startIndex || childText.length > startIndex) {
230
+ // Match starts in childElement
231
+ if(this.partiallySelectedTokensAreSelected) {
232
+ if(noInnerElements) {
233
+ if(this.createSubTokens && startIndex != endIndex) { // Subtoken to create
234
+ if(childText.length > endIndex) {
235
+ // Match starts and ends in childElement - selection middle part
236
+ let startSpan = document.createElement("span");
237
+ startSpan.classList.add("code-input_select-token-callbacks_temporary-span"); // Can remove span later
238
+ startSpan.textContent = childText.substring(0, startIndex);
239
+
240
+ let middleText = childText.substring(startIndex, endIndex);
241
+ childElement.textContent = middleText;
242
+ this.tokenSelectorCallbacks.tokenSelectedCallback(childElement); // Selection
243
+
244
+ let endSpan = document.createElement("span");
245
+ endSpan.classList.add("code-input_select-token-callbacks_temporary-span"); // Can remove span later
246
+ endSpan.textContent = childText.substring(endIndex);
247
+
248
+ childElement.insertAdjacentElement('beforebegin', startSpan);
249
+ childElement.insertAdjacentElement('afterend', endSpan);
250
+ i++; // 2 extra elements have been added
251
+ } else {
252
+ // Match starts in element - highlight last part
253
+ let startText = childText.substring(0, startIndex);
254
+ childElement.textContent = startText;
255
+
256
+ let endSpan = document.createElement("span");
257
+ this.tokenSelectorCallbacks.tokenSelectedCallback(endSpan); // Selected
258
+ endSpan.classList.add("code-input_select-token-callbacks_temporary-span"); // Can remove span later
259
+ endSpan.textContent = childText.substring(startIndex);
260
+
261
+ childElement.insertAdjacentElement('afterend', endSpan);
262
+ i++; // An extra element has been added
263
+ }
264
+ }
265
+ if(this.parentTokensAreSelected || !this.createSubTokens) {
266
+ this.tokenSelectorCallbacks.tokenSelectedCallback(childElement); // Selected
267
+ }
268
+ } else {
269
+ this.updateSelectedTokens(childElement, startIndex, endIndex);
270
+ }
271
+ }
272
+
273
+ if(this.caretAtStartIsSelected) {
274
+ if(childText.length > endIndex) {
275
+ // Match completely in childElement - nothing to do after backtracking
276
+ return;
277
+ }
278
+ } else if(childText.length >= endIndex) {
279
+ // Match completely in childElement - nothing to do after backtracking
280
+ return;
281
+ }
282
+ }
283
+
284
+ // Make indexes skip the element
285
+ startIndex -= childText.length;
286
+ endIndex -= childText.length;
287
+ }
288
+ }
289
+ }
@@ -0,0 +1 @@
1
+ codeInput.plugins.SelectTokenCallbacks=class extends codeInput.Plugin{constructor(a=codeInput.plugins.SelectTokenCallbacks.TokenSelectorCallbacks.createClassSynchronisation(),b=!1,c=!0,d=!0,e=!1,f=!0,g=!0){super([]),this.tokenSelectorCallbacks=a,this.onlyCaretNotSelection=b,this.caretAtStartIsSelected=c,this.caretAtEndIsSelected=d,this.createSubTokens=e,this.partiallySelectedTokensAreSelected=f,this.parentTokensAreSelected=g}afterHighlight(a){this.syncSelection(a)}afterElementsAdded(a){a.pluginData.selectTokenCallbacks={},a.pluginData.selectTokenCallbacks.lastSelectionStart=a.textareaElement.selectionStart,a.pluginData.selectTokenCallbacks.lastSelectionEnd=a.textareaElement.selectionEnd,a.pluginData.selectTokenCallbacks.selectedTokenState=new codeInput.plugins.SelectTokenCallbacks.SelectedTokenState(a.codeElement,this.tokenSelectorCallbacks,this.onlyCaretNotSelection,this.caretAtStartIsSelected,this.caretAtEndIsSelected,this.createSubTokens,this.partiallySelectedTokensAreSelected,this.parentTokensAreSelected),this.syncSelection(a),a.textareaElement.addEventListener("selectionchange",()=>{this.checkSelectionChanged(a)}),a.textareaElement.addEventListener("select",()=>{this.checkSelectionChanged(a)}),a.textareaElement.addEventListener("keypress",()=>{this.checkSelectionChanged(a)}),a.textareaElement.addEventListener("mousedown",()=>{this.checkSelectionChanged(a)})}checkSelectionChanged(a){(a.textareaElement.selectionStart!=a.pluginData.selectTokenCallbacks.lastSelectionStart||a.textareaElement.selectionEnd!=a.pluginData.selectTokenCallbacks.lastSelectionEnd)&&(this.syncSelection(a),a.pluginData.selectTokenCallbacks.lastSelectionStart=a.textareaElement.selectionStart,a.pluginData.selectTokenCallbacks.lastSelectionEnd=a.textareaElement.selectionEnd)}syncSelection(a){a.pluginData.selectTokenCallbacks.selectedTokenState.updateSelection(a.textareaElement.selectionStart,a.textareaElement.selectionEnd)}},codeInput.plugins.SelectTokenCallbacks.TokenSelectorCallbacks=class{constructor(a,b){this.tokenSelectedCallback=a,this.selectChangedCallback=b}static createClassSynchronisation(a="code-input_select-token-callbacks_selected"){return new codeInput.plugins.SelectTokenCallbacks.TokenSelectorCallbacks(b=>{b.classList.add(a)},b=>{for(let c=b.getElementsByClassName(a);0<c.length;)c[0].classList.remove(a)})}},codeInput.plugins.SelectTokenCallbacks.SelectedTokenState=class{constructor(a,b,c,d,e,f,g,h){this.tokenContainer=a,this.tokenSelectorCallbacks=b,this.onlyCaretNotSelection=c,this.caretAtStartIsSelected=d,this.caretAtEndIsSelected=e,this.createSubTokens=f,this.partiallySelectedTokensAreSelected=g,this.parentTokensAreSelected=h}updateSelection(a,b){this.selectChanged(),this.onlyCaretNotSelection&&a!=b||this.updateSelectedTokens(this.tokenContainer,a,b)}selectChanged(){if(this.createSubTokens)for(let a=this.tokenContainer.getElementsByClassName("code-input_select-token-callbacks_temporary-span");0<a.length;)a[0].parentElement.replaceChild(new Text(a[0].textContent),a[0]);this.tokenSelectorCallbacks.selectChangedCallback(this.tokenContainer)}updateSelectedTokens(a,b,c){if(!(0>c)&&(0!=c||this.caretAtStartIsSelected)){this.parentTokensAreSelected&&a!==this.tokenContainer&&this.tokenSelectorCallbacks.tokenSelectedCallback(a);for(let d=0;d<a.childNodes.length;d++){let e=a.childNodes[d],f=e.textContent,g=!1;if(3==e.nodeType)if(this.createSubTokens){if(d+1<a.childNodes.length&&3==a.childNodes[d+1].nodeType){a.childNodes[d+1].textContent=e.textContent+a.childNodes[d+1].textContent,a.removeChild(e),d--;continue}g=!0;let b=document.createElement("span");b.textContent=f,b.classList.add("code-input_select-token-callbacks_temporary-span"),a.replaceChild(b,e),e=b}else{b-=f.length,c-=f.length;continue}if(0>=b){if(f.length>c){if(this.partiallySelectedTokensAreSelected)if(g){if(this.createSubTokens&&b!=c){let a=document.createElement("span");this.tokenSelectorCallbacks.tokenSelectedCallback(a),a.classList.add("code-input_select-token-callbacks_temporary-span"),a.textContent=f.substring(0,c);let b=f.substring(c);e.textContent=b,e.insertAdjacentElement("beforebegin",a),d++}(this.parentTokensAreSelected||!this.createSubTokens)&&this.tokenSelectorCallbacks.tokenSelectedCallback(e)}else this.updateSelectedTokens(e,0,c);return}this.tokenSelectorCallbacks.tokenSelectedCallback(e)}else if(this.caretAtEndIsSelected&&f.length>=b||f.length>b){if(this.partiallySelectedTokensAreSelected)if(g){if(this.createSubTokens&&b!=c)if(f.length>c){let a=document.createElement("span");a.classList.add("code-input_select-token-callbacks_temporary-span"),a.textContent=f.substring(0,b);let g=f.substring(b,c);e.textContent=g,this.tokenSelectorCallbacks.tokenSelectedCallback(e);let h=document.createElement("span");h.classList.add("code-input_select-token-callbacks_temporary-span"),h.textContent=f.substring(c),e.insertAdjacentElement("beforebegin",a),e.insertAdjacentElement("afterend",h),d++}else{let a=f.substring(0,b);e.textContent=a;let c=document.createElement("span");this.tokenSelectorCallbacks.tokenSelectedCallback(c),c.classList.add("code-input_select-token-callbacks_temporary-span"),c.textContent=f.substring(b),e.insertAdjacentElement("afterend",c),d++}(this.parentTokensAreSelected||!this.createSubTokens)&&this.tokenSelectorCallbacks.tokenSelectedCallback(e)}else this.updateSelectedTokens(e,b,c);if(this.caretAtStartIsSelected){if(f.length>c)return;}else if(f.length>=c)return}b-=f.length,c-=f.length}}}};
@@ -24,10 +24,6 @@
24
24
  --code-input_special-chars_F: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAAFCAYAAACAcVaiAAAAAXNSR0IArs4c6QAAAB5JREFUGFdj/P///38GKGAEcRgZGRlBfDAHtwxMGQDZZhP+BnB1kwAAAABJRU5ErkJggg==');
25
25
  }
26
26
 
27
- .code-input_special-char_container { /* pre element */
28
- font-size: 20px;
29
- }
30
-
31
27
  .code-input_special-char {
32
28
  display: inline-block;
33
29
  position: relative;
@@ -66,7 +62,7 @@
66
62
 
67
63
  mask-image: var(--hex-0), var(--hex-1), var(--hex-2), var(--hex-3);
68
64
  mask-repeat: no-repeat, no-repeat, no-repeat, no-repeat;
69
- mask-size: 40%, 40%, 40%, 40%;
65
+ mask-size: min(40%, 0.25em), min(40%, 0.25em), min(40%, 0.25em), min(40%, 0.25em);
70
66
  mask-position: 10% 10%, 90% 10%, 10% 90%, 90% 90%;
71
67
 
72
68
  -webkit-mask-image: var(--hex-0), var(--hex-1), var(--hex-2), var(--hex-3);
@@ -7,14 +7,14 @@ codeInput.plugins.SpecialChars = class extends codeInput.Plugin {
7
7
  specialCharRegExp;
8
8
 
9
9
  cachedColors; // ascii number > [background color, text color]
10
- cachedWidths; // font > {character > character width}
10
+ cachedWidths; // character > character width
11
11
  canvasContext;
12
12
 
13
13
  /**
14
14
  * Create a special characters plugin instance.
15
15
  * Default = covers many non-renderable ASCII characters.
16
16
  * @param {Boolean} colorInSpecialChars Whether or not to give special characters custom background colors based on their hex code
17
- * @param {Boolean} inheritTextColor If `colorInSpecialChars` is false, forces the color of the hex code to inherit from syntax highlighting. Otherwise, the base colour of the `pre code` element is used to give contrast to the small characters.
17
+ * @param {Boolean} inheritTextColor If `inheritTextColor` is false, forces the color of the hex code to inherit from syntax highlighting. Otherwise, the base color of the `pre code` element is used to give contrast to the small characters.
18
18
  * @param {RegExp} specialCharRegExp The regular expression which matches special characters
19
19
  */
20
20
  constructor(colorInSpecialChars = false, inheritTextColor = false, specialCharRegExp = /(?!\n)(?!\t)[\u{0000}-\u{001F}]|[\u{007F}-\u{009F}]|[\u{0200}-\u{FFFF}]/ug) { // By default, covers many non-renderable ASCII characters
@@ -31,37 +31,31 @@ codeInput.plugins.SpecialChars = class extends codeInput.Plugin {
31
31
  this.canvasContext = canvas.getContext("2d");
32
32
  }
33
33
 
34
- /* Runs before elements are added into a `code-input`; Params: codeInput element) */
35
- beforeElementsAdded(codeInput) {
36
- codeInput.classList.add("code-input_special-char_container");
37
- }
38
-
39
- /* Runs after elements are added into a `code-input` (useful for adding events to the textarea); Params: codeInput element) */
34
+ /* Initially render special characters as the highlighting algorithm may automatically highlight and remove them */
40
35
  afterElementsAdded(codeInput) {
41
- // For some reason, special chars aren't synced the first time - TODO is there a cleaner way to do this?
42
- setTimeout(() => { codeInput.update(codeInput.value); }, 100);
36
+ setTimeout(() => { codeInput.value = codeInput.value; }, 100);
43
37
  }
44
38
 
45
- /* Runs after code is highlighted; Params: codeInput element) */
39
+ /* After highlighting, render special characters as their stylised hexadecimal equivalents */
46
40
  afterHighlight(codeInput) {
47
- let result_element = codeInput.querySelector("pre code");
41
+ let resultElement = codeInput.codeElement;
48
42
 
49
43
  // Reset data each highlight so can change if font size, etc. changes
50
44
  codeInput.pluginData.specialChars = {};
51
- codeInput.pluginData.specialChars.textarea = codeInput.getElementsByTagName("textarea")[0];
52
- codeInput.pluginData.specialChars.contrastColor = window.getComputedStyle(result_element).color;
45
+ codeInput.pluginData.specialChars.contrastColor = window.getComputedStyle(resultElement).color;
53
46
 
54
- this.recursivelyReplaceText(codeInput, result_element);
47
+ this.recursivelyReplaceText(codeInput, resultElement);
55
48
 
56
- this.lastFont = window.getComputedStyle(codeInput.pluginData.specialChars.textarea).font;
49
+ this.lastFont = window.getComputedStyle(codeInput.textareaElement).font;
57
50
  }
58
51
 
52
+ /* Search for special characters in an element and replace them with their stylised hexadecimal equivalents */
59
53
  recursivelyReplaceText(codeInput, element) {
60
54
  for(let i = 0; i < element.childNodes.length; i++) {
61
55
 
62
56
  let nextNode = element.childNodes[i];
63
- if(nextNode.nodeName == "#text" && nextNode.nodeValue != "") {
64
- // Replace in here
57
+ if(nextNode.nodeType == 3) {
58
+ // Text node - Replace in here
65
59
  let oldValue = nextNode.nodeValue;
66
60
 
67
61
  this.specialCharRegExp.lastIndex = 0;
@@ -72,11 +66,12 @@ codeInput.plugins.SpecialChars = class extends codeInput.Plugin {
72
66
  nextNode = nextNode.splitText(charIndex+1).previousSibling;
73
67
 
74
68
  if(charIndex > 0) {
75
- nextNode = nextNode.splitText(charIndex); // Keep those before in difft. span
69
+ nextNode = nextNode.splitText(charIndex); // Keep characters before the special character in a different span
76
70
  }
77
71
 
78
72
  if(nextNode.textContent != "") {
79
- let replacementElement = this.specialCharReplacer(codeInput, nextNode.textContent);
73
+ let replacementElement = this.getStylisedSpecialChar(codeInput, nextNode.textContent);
74
+ // This next node will become the i+1th node so automatically iterated to
80
75
  nextNode.parentNode.insertBefore(replacementElement, nextNode);
81
76
  nextNode.textContent = "";
82
77
  }
@@ -90,29 +85,30 @@ codeInput.plugins.SpecialChars = class extends codeInput.Plugin {
90
85
  }
91
86
  }
92
87
 
93
- specialCharReplacer(codeInput, match_char) {
94
- let hex_code = match_char.codePointAt(0);
88
+ /* Get the stylised hexadecimal representation HTML element for a given special character */
89
+ getStylisedSpecialChar(codeInput, matchChar) {
90
+ let hexCode = matchChar.codePointAt(0);
95
91
 
96
92
  let colors;
97
- if(this.colorInSpecialChars) colors = this.getCharacterColor(hex_code);
93
+ if(this.colorInSpecialChars) colors = this.getCharacterColors(hexCode);
98
94
 
99
- hex_code = hex_code.toString(16);
100
- hex_code = ("0000" + hex_code).substring(hex_code.length); // So 2 chars with leading 0
101
- hex_code = hex_code.toUpperCase();
95
+ hexCode = hexCode.toString(16);
96
+ hexCode = ("0000" + hexCode).substring(hexCode.length); // So 2 chars with leading 0
97
+ hexCode = hexCode.toUpperCase();
102
98
 
103
- let char_width = this.getCharacterWidth(codeInput, match_char);
99
+ let charWidth = this.getCharacterWidthEm(codeInput, matchChar);
104
100
 
105
101
  // Create element with hex code
106
102
  let result = document.createElement("span");
107
103
  result.classList.add("code-input_special-char");
108
- result.style.setProperty("--hex-0", "var(--code-input_special-chars_" + hex_code[0] + ")");
109
- result.style.setProperty("--hex-1", "var(--code-input_special-chars_" + hex_code[1] + ")");
110
- result.style.setProperty("--hex-2", "var(--code-input_special-chars_" + hex_code[2] + ")");
111
- result.style.setProperty("--hex-3", "var(--code-input_special-chars_" + hex_code[3] + ")");
104
+ result.style.setProperty("--hex-0", "var(--code-input_special-chars_" + hexCode[0] + ")");
105
+ result.style.setProperty("--hex-1", "var(--code-input_special-chars_" + hexCode[1] + ")");
106
+ result.style.setProperty("--hex-2", "var(--code-input_special-chars_" + hexCode[2] + ")");
107
+ result.style.setProperty("--hex-3", "var(--code-input_special-chars_" + hexCode[3] + ")");
112
108
 
113
109
  // Handle zero-width chars
114
- if(char_width == 0) result.classList.add("code-input_special-char_zero-width");
115
- else result.style.width = char_width + "px";
110
+ if(charWidth == 0) result.classList.add("code-input_special-char_zero-width");
111
+ else result.style.width = charWidth + "em";
116
112
 
117
113
  if(this.colorInSpecialChars) {
118
114
  result.style.backgroundColor = "#" + colors[0];
@@ -123,64 +119,72 @@ codeInput.plugins.SpecialChars = class extends codeInput.Plugin {
123
119
  return result;
124
120
  }
125
121
 
126
- getCharacterColor(ascii_code) {
127
- // Choose colors based on character code - lazy load and return [background color, text color]
128
- let background_color;
129
- let text_color;
130
- if(!(ascii_code in this.cachedColors)) {
131
- // Get background color - arbitrary bit manipulation to get a good range of colours
132
- background_color = ascii_code^(ascii_code << 3)^(ascii_code << 7)^(ascii_code << 14)^(ascii_code << 16); // Arbitrary
133
- background_color = background_color^0x1fc627; // Arbitrary
134
- background_color = background_color.toString(16);
135
- background_color = ("000000" + background_color).substring(background_color.length); // So 6 chars with leading 0
122
+ /* Get the colors a stylised representation of a given character must be shown in; lazy load and return [background color, text color] */
123
+ getCharacterColors(asciiCode) {
124
+ let textColor;
125
+ if(!(asciiCode in this.cachedColors)) {
126
+ // Get background color
127
+ let asciiHex = asciiCode.toString(16);
128
+ let backgroundColor = "";
129
+ for(let i = 0; i < asciiHex.length; i++) {
130
+ backgroundColor += asciiHex[i] + asciiHex[i];
131
+ }
132
+ backgroundColor = ("000000" + backgroundColor).substring(backgroundColor.length); // So valid HEX color with 6 characters
136
133
 
137
134
  // Get most suitable text color - white or black depending on background brightness
138
- let color_brightness = 0;
139
- let luminance_coefficients = [0.299, 0.587, 0.114];
135
+ let colorBrightness = 0;
136
+ const luminanceCoefficients = [0.299, 0.587, 0.114];
140
137
  for(let i = 0; i < 6; i += 2) {
141
- color_brightness += parseInt(background_color.substring(i, i+2), 16) * luminance_coefficients[i/2];
138
+ colorBrightness += parseInt(backgroundColor.substring(i, i+2), 16) * luminanceCoefficients[i/2];
142
139
  }
143
140
  // Calculate darkness
144
- text_color = color_brightness < 128 ? "white" : "black";
141
+ textColor = colorBrightness < 128 ? "white" : "black";
145
142
 
146
- this.cachedColors[ascii_code] = [background_color, text_color];
147
- return [background_color, text_color];
143
+ this.cachedColors[asciiCode] = [backgroundColor, textColor];
144
+ return [backgroundColor, textColor];
148
145
  } else {
149
- return this.cachedColors[ascii_code];
146
+ return this.cachedColors[asciiCode];
150
147
  }
151
148
  }
152
149
 
153
- getCharacterWidth(codeInput, char) {
150
+ /* Get the width of a character in em (relative to font size), for use in creation of the stylised hexadecimal representation with the same width */
151
+ getCharacterWidthEm(codeInput, char) {
154
152
  // Force zero-width characters
155
- if(new RegExp("\u00AD|\u02de|[\u0300-\u036F]|[\u0483-\u0489]|\u200b").test(char) ) { return 0 }
153
+ if(new RegExp("\u00AD|\u02DE|[\u0300-\u036F]|[\u0483-\u0489]|[\u200B-\u200D]|\uFEFF").test(char) ) { return 0 }
156
154
  // Non-renderable ASCII characters should all be rendered at same size
157
155
  if(char != "\u0096" && new RegExp("[\u{0000}-\u{001F}]|[\u{007F}-\u{009F}]", "g").test(char)) {
158
- let fallbackWidth = this.getCharacterWidth("\u0096");
156
+ let fallbackWidth = this.getCharacterWidthEm(codeInput, "\u0096");
159
157
  return fallbackWidth;
160
158
  }
161
159
 
162
- let font = window.getComputedStyle(codeInput.pluginData.specialChars.textarea).font;
160
+ let font = getComputedStyle(codeInput.textareaElement).fontFamily + " " + getComputedStyle(codeInput.textareaElement).fontStretch + " " + getComputedStyle(codeInput.textareaElement).fontStyle + " " + getComputedStyle(codeInput.textareaElement).fontVariant + " " + getComputedStyle(codeInput.textareaElement).fontWeight + " " + getComputedStyle(codeInput.textareaElement).lineHeight; // Font without size
163
161
 
164
- // Lazy-load - TODO: Get a cleaner way of doing this
162
+ // Lazy-load width of each character
165
163
  if(this.cachedWidths[font] == undefined) {
166
- this.cachedWidths[font] = {}; // Create new cached widths for this font
164
+ this.cachedWidths[font] = {};
167
165
  }
168
166
  if(this.cachedWidths[font][char] != undefined) { // Use cached width
169
167
  return this.cachedWidths[font][char];
170
168
  }
171
169
 
172
- // Ensure font the same
173
- this.canvasContext.font = font;
170
+ // Ensure font the same - 20px font size is where this algorithm works
171
+ this.canvasContext.font = getComputedStyle(codeInput.textareaElement).font.replace(getComputedStyle(codeInput.textareaElement).fontSize, "20px");
174
172
 
175
173
  // Try to get width from canvas
176
- let width = this.canvasContext.measureText(char).width;
177
- if(width > Number(font.split("px")[0])) {
174
+ let width = this.canvasContext.measureText(char).width/20; // From px to em (=proportion of font-size)
175
+ if(width > 1) {
178
176
  width /= 2; // Fix double-width-in-canvas Firefox bug
179
177
  } else if(width == 0 && char != "\u0096") {
180
- let fallbackWidth = this.getCharacterWidth("\u0096");
178
+ let fallbackWidth = this.getCharacterWidthEm(codeInput, "\u0096");
181
179
  return fallbackWidth; // In Firefox some control chars don't render, but all control chars are the same width
182
180
  }
183
181
 
182
+ // Firefox will never make smaller than size at 20px
183
+ if(navigator.userAgent.includes("Mozilla") && !navigator.userAgent.includes("Chrome") && !navigator.userAgent.includes("Safari")) {
184
+ let fontSize = Number(getComputedStyle(codeInput.textareaElement).fontSize.substring(0, getComputedStyle(codeInput.textareaElement).fontSize.length-2)); // Remove 20, make px
185
+ if(fontSize < 20) width *= 20 / fontSize;
186
+ }
187
+
184
188
  this.cachedWidths[font][char] = width;
185
189
 
186
190
  return width;
@@ -1,5 +1,5 @@
1
- :root,body{--code-input_special-chars_0:url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAAFCAYAAACAcVaiAAAAAXNSR0IArs4c6QAAABtJREFUGFdjZGBgYPj///9/RhCAMcA0bg6yHgAPmh/6BoxTcQAAAABJRU5ErkJgggAA');--code-input_special-chars_1:url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAAFCAYAAACAcVaiAAAAAXNSR0IArs4c6QAAABZJREFUGFdjZGBgYPj///9/RhAggwMAitIUBr9U6sYAAAAASUVORK5CYII=');--code-input_special-chars_2:url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAAFCAYAAACAcVaiAAAAAXNSR0IArs4c6QAAAB9JREFUGFdj/P///38GKGCEMUCCjCgyYBFGRrAKFBkAuLYT9kYcIu0AAAAASUVORK5CYII=');--code-input_special-chars_3:url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAAFCAYAAACAcVaiAAAAAXNSR0IArs4c6QAAABhJREFUGFdj/P///38GKGCEMUCCjMTJAACYiBPyG8sfAgAAAABJRU5ErkJggg==');--code-input_special-chars_4:url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAAFCAYAAACAcVaiAAAAAXNSR0IArs4c6QAAAB5JREFUGFdj/P///39GRkZGMI3BYYACRhgDrAKZAwAYxhvyz0DRIQAAAABJRU5ErkJggg==');--code-input_special-chars_5:url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAAFCAYAAACAcVaiAAAAAXNSR0IArs4c6QAAACJJREFUGFdj/P///38GKGAEcRgZGRlBfDAHLgNjgFUgywAAuR4T9hxJl2YAAAAASUVORK5CYII=');--code-input_special-chars_6:url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAAFCAYAAACAcVaiAAAAAXNSR0IArs4c6QAAACBJREFUGFdj/P///38GKGAEcRgZGRlBfDAHQwasAlkGABcdF/Y4yco2AAAAAElFTkSuQmCC');--code-input_special-chars_7:url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAAFCAYAAACAcVaiAAAAAXNSR0IArs4c6QAAABZJREFUGFdj/P///38GKGCEMUCCRHIAWMgT8kue3bQAAAAASUVORK5CYII=');--code-input_special-chars_8:url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAAFCAYAAACAcVaiAAAAAXNSR0IArs4c6QAAABlJREFUGFdj/P///38GKGAEcRgZGSE0cTIAvHcb8v+mIfAAAAAASUVORK5CYII=');--code-input_special-chars_9:url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAAFCAYAAACAcVaiAAAAAXNSR0IArs4c6QAAAB9JREFUGFdj/P///38GKGAEcRgZGSE0igxMCVgGmQMAPqcX8hWL1K0AAAAASUVORK5CYII=');--code-input_special-chars_A:url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAAFCAYAAACAcVaiAAAAAXNSR0IArs4c6QAAACBJREFUGFdjZGBgYPj///9/RhCAMcA0iADJggCmDEw5ALdxH/aGuYHqAAAAAElFTkSuQmCC');--code-input_special-chars_B:url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAAFCAYAAACAcVaiAAAAAXNSR0IArs4c6QAAABlJREFUGFdj/P///38GBgYGRhAAceA0cTIAvc0b/vRDnVoAAAAASUVORK5CYII=');--code-input_special-chars_C:url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAAFCAYAAACAcVaiAAAAAXNSR0IArs4c6QAAAB5JREFUGFdjZGBgYPj///9/EM0IYjAyMjIS4CDrAQC57hP+uLwvFQAAAABJRU5ErkJggg==');--code-input_special-chars_D:url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAAFCAYAAACAcVaiAAAAAXNSR0IArs4c6QAAABtJREFUGFdj/P///38GBgYGRhAAceA0fg5MDwAveh/6ToN9VwAAAABJRU5ErkJggg==');--code-input_special-chars_E:url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAAFCAYAAACAcVaiAAAAAXNSR0IArs4c6QAAABxJREFUGFdj/P///38GKGAEcRgZGRlBfDCHsAwA2UwT+mVIH1MAAAAASUVORK5CYII=');--code-input_special-chars_F:url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAAFCAYAAACAcVaiAAAAAXNSR0IArs4c6QAAAB5JREFUGFdj/P///38GKGAEcRgZGRlBfDAHtwxMGQDZZhP+BnB1kwAAAABJRU5ErkJggg==')}.code-input_special-char_container{font-size:20px}.code-input_special-char{display:inline-block;position:relative;top:0;left:0;height:1em;overflow:hidden;text-decoration:none;text-shadow:none;vertical-align:middle;outline:.1px solid currentColor;--hex-0:var(
1
+ :root,body{--code-input_special-chars_0:url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAAFCAYAAACAcVaiAAAAAXNSR0IArs4c6QAAABtJREFUGFdjZGBgYPj///9/RhCAMcA0bg6yHgAPmh/6BoxTcQAAAABJRU5ErkJgggAA');--code-input_special-chars_1:url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAAFCAYAAACAcVaiAAAAAXNSR0IArs4c6QAAABZJREFUGFdjZGBgYPj///9/RhAggwMAitIUBr9U6sYAAAAASUVORK5CYII=');--code-input_special-chars_2:url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAAFCAYAAACAcVaiAAAAAXNSR0IArs4c6QAAAB9JREFUGFdj/P///38GKGCEMUCCjCgyYBFGRrAKFBkAuLYT9kYcIu0AAAAASUVORK5CYII=');--code-input_special-chars_3:url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAAFCAYAAACAcVaiAAAAAXNSR0IArs4c6QAAABhJREFUGFdj/P///38GKGCEMUCCjMTJAACYiBPyG8sfAgAAAABJRU5ErkJggg==');--code-input_special-chars_4:url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAAFCAYAAACAcVaiAAAAAXNSR0IArs4c6QAAAB5JREFUGFdj/P///39GRkZGMI3BYYACRhgDrAKZAwAYxhvyz0DRIQAAAABJRU5ErkJggg==');--code-input_special-chars_5:url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAAFCAYAAACAcVaiAAAAAXNSR0IArs4c6QAAACJJREFUGFdj/P///38GKGAEcRgZGRlBfDAHLgNjgFUgywAAuR4T9hxJl2YAAAAASUVORK5CYII=');--code-input_special-chars_6:url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAAFCAYAAACAcVaiAAAAAXNSR0IArs4c6QAAACBJREFUGFdj/P///38GKGAEcRgZGRlBfDAHQwasAlkGABcdF/Y4yco2AAAAAElFTkSuQmCC');--code-input_special-chars_7:url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAAFCAYAAACAcVaiAAAAAXNSR0IArs4c6QAAABZJREFUGFdj/P///38GKGCEMUCCRHIAWMgT8kue3bQAAAAASUVORK5CYII=');--code-input_special-chars_8:url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAAFCAYAAACAcVaiAAAAAXNSR0IArs4c6QAAABlJREFUGFdj/P///38GKGAEcRgZGSE0cTIAvHcb8v+mIfAAAAAASUVORK5CYII=');--code-input_special-chars_9:url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAAFCAYAAACAcVaiAAAAAXNSR0IArs4c6QAAAB9JREFUGFdj/P///38GKGAEcRgZGSE0igxMCVgGmQMAPqcX8hWL1K0AAAAASUVORK5CYII=');--code-input_special-chars_A:url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAAFCAYAAACAcVaiAAAAAXNSR0IArs4c6QAAACBJREFUGFdjZGBgYPj///9/RhCAMcA0iADJggCmDEw5ALdxH/aGuYHqAAAAAElFTkSuQmCC');--code-input_special-chars_B:url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAAFCAYAAACAcVaiAAAAAXNSR0IArs4c6QAAABlJREFUGFdj/P///38GBgYGRhAAceA0cTIAvc0b/vRDnVoAAAAASUVORK5CYII=');--code-input_special-chars_C:url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAAFCAYAAACAcVaiAAAAAXNSR0IArs4c6QAAAB5JREFUGFdjZGBgYPj///9/EM0IYjAyMjIS4CDrAQC57hP+uLwvFQAAAABJRU5ErkJggg==');--code-input_special-chars_D:url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAAFCAYAAACAcVaiAAAAAXNSR0IArs4c6QAAABtJREFUGFdj/P///38GBgYGRhAAceA0fg5MDwAveh/6ToN9VwAAAABJRU5ErkJggg==');--code-input_special-chars_E:url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAAFCAYAAACAcVaiAAAAAXNSR0IArs4c6QAAABxJREFUGFdj/P///38GKGAEcRgZGRlBfDCHsAwA2UwT+mVIH1MAAAAASUVORK5CYII=');--code-input_special-chars_F:url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAAFCAYAAACAcVaiAAAAAXNSR0IArs4c6QAAAB5JREFUGFdj/P///38GKGAEcRgZGRlBfDAHtwxMGQDZZhP+BnB1kwAAAABJRU5ErkJggg==')}.code-input_special-char{display:inline-block;position:relative;top:0;left:0;height:1em;overflow:hidden;text-decoration:none;text-shadow:none;vertical-align:middle;outline:.1px solid currentColor;--hex-0:var(
2
2
  --code-input_special-chars_0);--hex-1:var(
3
3
  --code-input_special-chars_0);--hex-2:var(
4
4
  --code-input_special-chars_0);--hex-3:var(
5
- --code-input_special-chars_0)}.code-input_special-char::before{margin-left:50%;transform:translate(-50%,0);content:" ";background-color:var(--code-input_special-char_color,currentColor);image-rendering:pixelated;display:inline-block;width:calc(100%-2px);height:100%;mask-image:var(--hex-0),var(--hex-1),var(--hex-2),var(--hex-3);mask-repeat:no-repeat,no-repeat,no-repeat,no-repeat;mask-size:40%,40%,40%,40%;mask-position:10% 10%,90% 10%,10% 90%,90% 90%;-webkit-mask-image:var(--hex-0),var(--hex-1),var(--hex-2),var(--hex-3);-webkit-mask-repeat:no-repeat,no-repeat,no-repeat,no-repeat;-webkit-mask-size:min(40%,.25em),min(40%,.25em),min(40%,.25em),min(40%,.25em);-webkit-mask-position:10% 10%,min(90%,.5em) 10%,10% 90%,min(90%,.5em) 90%}.code-input_special-char_zero-width{z-index:1;width:1em;margin-left:-.5em;margin-right:-.5em;position:relative;opacity:.75}.code-input_special-char_one-byte::before{height:1.5em;top:-1em;content:attr(data-hex2)}.code-input_special-char_one-byte::after{height:1.5em;bottom:-1em;content:attr(data-hex3)}
5
+ --code-input_special-chars_0)}.code-input_special-char::before{margin-left:50%;transform:translate(-50%,0);content:" ";background-color:var(--code-input_special-char_color,currentColor);image-rendering:pixelated;display:inline-block;width:calc(100%-2px);height:100%;mask-image:var(--hex-0),var(--hex-1),var(--hex-2),var(--hex-3);mask-repeat:no-repeat,no-repeat,no-repeat,no-repeat;mask-size:min(40%,.25em),min(40%,.25em),min(40%,.25em),min(40%,.25em);mask-position:10% 10%,90% 10%,10% 90%,90% 90%;-webkit-mask-image:var(--hex-0),var(--hex-1),var(--hex-2),var(--hex-3);-webkit-mask-repeat:no-repeat,no-repeat,no-repeat,no-repeat;-webkit-mask-size:min(40%,.25em),min(40%,.25em),min(40%,.25em),min(40%,.25em);-webkit-mask-position:10% 10%,min(90%,.5em) 10%,10% 90%,min(90%,.5em) 90%}.code-input_special-char_zero-width{z-index:1;width:1em;margin-left:-.5em;margin-right:-.5em;position:relative;opacity:.75}.code-input_special-char_one-byte::before{height:1.5em;top:-1em;content:attr(data-hex2)}.code-input_special-char_one-byte::after{height:1.5em;bottom:-1em;content:attr(data-hex3)}
@@ -1 +1 @@
1
- codeInput.plugins.SpecialChars=class extends codeInput.Plugin{specialCharRegExp;cachedColors;cachedWidths;canvasContext;constructor(a=!1,b=!1,c=/(?!\n)(?!\t)[\u{0000}-\u{001F}]|[\u{007F}-\u{009F}]|[\u{0200}-\u{FFFF}]/ug){super([]),this.specialCharRegExp=c,this.colorInSpecialChars=a,this.inheritTextColor=b,this.cachedColors={},this.cachedWidths={};let d=document.createElement("canvas");this.canvasContext=d.getContext("2d")}beforeElementsAdded(a){a.classList.add("code-input_special-char_container")}afterElementsAdded(a){setTimeout(()=>{a.update(a.value)},100)}afterHighlight(a){let b=a.querySelector("pre code");a.pluginData.specialChars={},a.pluginData.specialChars.textarea=a.getElementsByTagName("textarea")[0],a.pluginData.specialChars.contrastColor=window.getComputedStyle(b).color,this.recursivelyReplaceText(a,b),this.lastFont=window.getComputedStyle(a.pluginData.specialChars.textarea).font}recursivelyReplaceText(a,b){for(let c,d=0;d<b.childNodes.length;d++)if(c=b.childNodes[d],"#text"==c.nodeName&&""!=c.nodeValue){let b=c.nodeValue;this.specialCharRegExp.lastIndex=0;let d=this.specialCharRegExp.exec(b);if(null!=d){let b=d.index;if(c=c.splitText(b+1).previousSibling,0<b&&(c=c.splitText(b)),""!=c.textContent){let b=this.specialCharReplacer(a,c.textContent);c.parentNode.insertBefore(b,c),c.textContent=""}}}else 1==c.nodeType&&"code-input_special-char"!=c.className&&""!=c.nodeValue&&this.recursivelyReplaceText(a,c)}specialCharReplacer(a,b){let c,d=b.codePointAt(0);this.colorInSpecialChars&&(c=this.getCharacterColor(d)),d=d.toString(16),d=("0000"+d).substring(d.length),d=d.toUpperCase();let e=this.getCharacterWidth(a,b),f=document.createElement("span");return f.classList.add("code-input_special-char"),f.style.setProperty("--hex-0","var(--code-input_special-chars_"+d[0]+")"),f.style.setProperty("--hex-1","var(--code-input_special-chars_"+d[1]+")"),f.style.setProperty("--hex-2","var(--code-input_special-chars_"+d[2]+")"),f.style.setProperty("--hex-3","var(--code-input_special-chars_"+d[3]+")"),0==e?f.classList.add("code-input_special-char_zero-width"):f.style.width=e+"px",this.colorInSpecialChars?(f.style.backgroundColor="#"+c[0],f.style.setProperty("--code-input_special-char_color",c[1])):!this.inheritTextColor&&f.style.setProperty("--code-input_special-char_color",a.pluginData.specialChars.contrastColor),f}getCharacterColor(a){let b,c;if(!(a in this.cachedColors)){b=a^a<<3^a<<7^a<<14^a<<16,b^=2082343,b=b.toString(16),b=("000000"+b).substring(b.length);let d=0,e=[.299,.587,.114];for(let a=0;6>a;a+=2)d+=parseInt(b.substring(a,a+2),16)*e[a/2];return c=128>d?"white":"black",this.cachedColors[a]=[b,c],[b,c]}return this.cachedColors[a]}getCharacterWidth(a,b){if(/­|˞|[̀-ͯ]|[҃-҉]|​/.test(b))return 0;if("\x96"!=b&&/[\0-]|[-Ÿ]/g.test(b)){let a=this.getCharacterWidth("\x96");return a}let c=window.getComputedStyle(a.pluginData.specialChars.textarea).font;if(null==this.cachedWidths[c]&&(this.cachedWidths[c]={}),null!=this.cachedWidths[c][b])return this.cachedWidths[c][b];this.canvasContext.font=c;let d=this.canvasContext.measureText(b).width;if(d>+c.split("px")[0])d/=2;else if(0==d&&"\x96"!=b){let a=this.getCharacterWidth("\x96");return a}return this.cachedWidths[c][b]=d,d}};
1
+ codeInput.plugins.SpecialChars=class extends codeInput.Plugin{specialCharRegExp;cachedColors;cachedWidths;canvasContext;constructor(a=!1,b=!1,c=/(?!\n)(?!\t)[\u{0000}-\u{001F}]|[\u{007F}-\u{009F}]|[\u{0200}-\u{FFFF}]/ug){super([]),this.specialCharRegExp=c,this.colorInSpecialChars=a,this.inheritTextColor=b,this.cachedColors={},this.cachedWidths={};let d=document.createElement("canvas");this.canvasContext=d.getContext("2d")}afterElementsAdded(a){setTimeout(()=>{a.value=a.value},100)}afterHighlight(a){let b=a.codeElement;a.pluginData.specialChars={},a.pluginData.specialChars.contrastColor=window.getComputedStyle(b).color,this.recursivelyReplaceText(a,b),this.lastFont=window.getComputedStyle(a.textareaElement).font}recursivelyReplaceText(a,b){for(let c,d=0;d<b.childNodes.length;d++)if(c=b.childNodes[d],3==c.nodeType){let b=c.nodeValue;this.specialCharRegExp.lastIndex=0;let d=this.specialCharRegExp.exec(b);if(null!=d){let b=d.index;if(c=c.splitText(b+1).previousSibling,0<b&&(c=c.splitText(b)),""!=c.textContent){let b=this.getStylisedSpecialChar(a,c.textContent);c.parentNode.insertBefore(b,c),c.textContent=""}}}else 1==c.nodeType&&"code-input_special-char"!=c.className&&""!=c.nodeValue&&this.recursivelyReplaceText(a,c)}getStylisedSpecialChar(a,b){let c,d=b.codePointAt(0);this.colorInSpecialChars&&(c=this.getCharacterColors(d)),d=d.toString(16),d=("0000"+d).substring(d.length),d=d.toUpperCase();let e=this.getCharacterWidthEm(a,b),f=document.createElement("span");return f.classList.add("code-input_special-char"),f.style.setProperty("--hex-0","var(--code-input_special-chars_"+d[0]+")"),f.style.setProperty("--hex-1","var(--code-input_special-chars_"+d[1]+")"),f.style.setProperty("--hex-2","var(--code-input_special-chars_"+d[2]+")"),f.style.setProperty("--hex-3","var(--code-input_special-chars_"+d[3]+")"),0==e?f.classList.add("code-input_special-char_zero-width"):f.style.width=e+"em",this.colorInSpecialChars?(f.style.backgroundColor="#"+c[0],f.style.setProperty("--code-input_special-char_color",c[1])):!this.inheritTextColor&&f.style.setProperty("--code-input_special-char_color",a.pluginData.specialChars.contrastColor),f}getCharacterColors(a){let b;if(!(a in this.cachedColors)){let c=a.toString(16),d="";for(let a=0;a<c.length;a++)d+=c[a]+c[a];d=("000000"+d).substring(d.length);let e=0;const f=[.299,.587,.114];for(let a=0;6>a;a+=2)e+=parseInt(d.substring(a,a+2),16)*f[a/2];return b=128>e?"white":"black",this.cachedColors[a]=[d,b],[d,b]}return this.cachedColors[a]}getCharacterWidthEm(a,b){if(/­|˞|[̀-ͯ]|[҃-҉]|[​-‍]|/.test(b))return 0;if("\x96"!=b&&/[\0-]|[-Ÿ]/g.test(b)){let b=this.getCharacterWidthEm(a,"\x96");return b}let c=getComputedStyle(a.textareaElement).fontFamily+" "+getComputedStyle(a.textareaElement).fontStretch+" "+getComputedStyle(a.textareaElement).fontStyle+" "+getComputedStyle(a.textareaElement).fontVariant+" "+getComputedStyle(a.textareaElement).fontWeight+" "+getComputedStyle(a.textareaElement).lineHeight;if(null==this.cachedWidths[c]&&(this.cachedWidths[c]={}),null!=this.cachedWidths[c][b])return this.cachedWidths[c][b];this.canvasContext.font=getComputedStyle(a.textareaElement).font.replace(getComputedStyle(a.textareaElement).fontSize,"20px");let d=this.canvasContext.measureText(b).width/20;if(1<d)d/=2;else if(0==d&&"\x96"!=b){let b=this.getCharacterWidthEm(a,"\x96");return b}if(navigator.userAgent.includes("Mozilla")&&!navigator.userAgent.includes("Chrome")&&!navigator.userAgent.includes("Safari")){let b=+getComputedStyle(a.textareaElement).fontSize.substring(0,getComputedStyle(a.textareaElement).fontSize.length-2);20>b&&(d*=20/b)}return this.cachedWidths[c][b]=d,d}};
package/plugins/test.js CHANGED
@@ -10,9 +10,8 @@
10
10
  */
11
11
  codeInput.plugins.Test = class extends codeInput.Plugin {
12
12
  constructor() {
13
- super(["testattr", "test-*"]);
13
+ super(["testattr"]);
14
14
  // Array of observed attributes as parameter
15
- // Wildcard "*" matches any text
16
15
  }
17
16
  /* Runs before code is highlighted; Params: codeInput element) */
18
17
  beforeHighlight(codeInput) {
@@ -1 +1 @@
1
- codeInput.plugins.Test=class extends codeInput.Plugin{constructor(){super(["testattr","test-*"])}beforeHighlight(a){console.log(a,"before highlight")}afterHighlight(a){console.log(a,"after highlight")}beforeElementsAdded(a){console.log(a,"before elements added")}afterElementsAdded(a){console.log(a,"after elements added")}attributeChanged(a,b,c,d){console.log(a,b,":",c,">",d)}};
1
+ codeInput.plugins.Test=class extends codeInput.Plugin{constructor(){super(["testattr"])}beforeHighlight(a){console.log(a,"before highlight")}afterHighlight(a){console.log(a,"after highlight")}beforeElementsAdded(a){console.log(a,"before elements added")}afterElementsAdded(a){console.log(a,"after elements added")}attributeChanged(a,b,c,d){console.log(a,b,":",c,">",d)}};