@webcoder49/code-input 2.6.0 → 2.6.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,51 @@
1
+ // NOTICE: This code is @generated from code outside the esm directory. Please do not edit it to contribute!
2
+
3
+ import { Plugin, CodeInput } from "../code-input.d.mts";
4
+ /**
5
+ * Make tokens in the <pre><code> element that are included within the selected text of the <code-input>
6
+ * gain a CSS export default class while selected, or trigger JavaScript callbacks.
7
+ * Files: select-token-callbacks.js
8
+ */
9
+ class SelectTokenCallbacks extends Plugin {
10
+ /**
11
+ * Set up the behaviour of tokens text-selected in the `<code-input>` element, and the exact definition of a token being text-selected.
12
+ *
13
+ * 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.
14
+ *
15
+ * @param {codeInput.plugins.SelectTokenCallbacks.TokenSelectorCallbacks} tokenSelectorCallbacks What to do with text-selected tokens. See docstrings for the TokenSelectorCallbacks class.
16
+ * @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.
17
+ * @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.
18
+ * @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.
19
+ * @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.
20
+ * @param {boolean} partiallySelectedTokensAreSelected Whether tokens for which only some of their text is selected should be treated as selected. Default true.
21
+ * @param {boolean} parentTokensAreSelected Whether all parent tokens of selected tokens should be treated as selected. Default true.
22
+ */
23
+ constructor(tokenSelectorCallbacks?: codeInput.plugins.SelectTokenCallbacks.TokenSelectorCallbacks, onlyCaretNotSelection?: boolean, caretAtStartIsSelected?: boolean, caretAtEndIsSelected?: boolean, createSubTokens?: boolean, partiallySelectedTokensAreSelected?: boolean, parentTokensAreSelected?: boolean);
24
+ }
25
+
26
+ namespace SelectTokenCallbacks {
27
+ /**
28
+ * A data structure specifying what should be done with tokens when they are selected, and also allows for previously selected
29
+ * tokens to be dealt with each time the selection changes. See the constructor and the createClassSynchronisation static method.
30
+ */
31
+ class TokenSelectorCallbacks {
32
+ /**
33
+ * Pass any callbacks you want to customise the behaviour of selected tokens via JavaScript.
34
+ *
35
+ * (If the behaviour you want is just differently styling selected tokens _via CSS_, you should probably use the createClassSynchronisation static method.)
36
+ * @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.
37
+ * @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.
38
+ */
39
+ constructor(tokenSelectedCallback: (token: HTMLElement) => void, selectChangedCallback: (tokenContainer: HTMLElement) => void);
40
+
41
+ /**
42
+ * 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.
43
+ *
44
+ * (If the behaviour you want requires more complex behaviour or JavaScript, you should use TokenSelectorCallbacks' constructor.)
45
+ *
46
+ * @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".
47
+ * @returns {TokenSelectorCallbacks} A new TokenSelectorCallbacks instance that encodes this behaviour.
48
+ */
49
+ static createClassSynchronisation(selectedClass?: string): codeInput.plugins.SelectTokenCallbacks.TokenSelectorCallbacks;
50
+ }
51
+ }
@@ -0,0 +1,296 @@
1
+ // NOTICE: This code is @generated from code outside the esm directory. Please do not edit it to contribute!
2
+
3
+ import { Plugin } from "../code-input.mjs";
4
+ const plugins = {};
5
+ /**
6
+ * Make tokens in the <pre><code> element that are included within the selected text of the <code-input>
7
+ * gain a CSS class while selected, or trigger JavaScript callbacks.
8
+ * Files: select-token-callbacks.js
9
+ */
10
+ "use strict";
11
+
12
+ plugins.SelectTokenCallbacks = class extends Plugin {
13
+ /**
14
+ * Set up the behaviour of tokens text-selected in the `<code-input>` element, and the exact definition of a token being text-selected.
15
+ *
16
+ * 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.
17
+ *
18
+ * @param {plugins.SelectTokenCallbacks.TokenSelectorCallbacks} tokenSelectorCallbacks What to do with text-selected tokens. See docstrings for the TokenSelectorCallbacks class.
19
+ * @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.
20
+ * @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.
21
+ * @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.
22
+ * @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.
23
+ * @param {boolean} partiallySelectedTokensAreSelected Whether tokens for which only some of their text is selected should be treated as selected. Default true.
24
+ * @param {boolean} parentTokensAreSelected Whether all parent tokens of selected tokens should be treated as selected. Default true.
25
+ */
26
+ constructor(tokenSelectorCallbacks = plugins.SelectTokenCallbacks.TokenSelectorCallbacks.createClassSynchronisation(), onlyCaretNotSelection = false, caretAtStartIsSelected = true, caretAtEndIsSelected = true, createSubTokens = false, partiallySelectedTokensAreSelected = true, parentTokensAreSelected = true) {
27
+ super([]); // No observed attributes
28
+
29
+ this.tokenSelectorCallbacks = tokenSelectorCallbacks;
30
+ this.onlyCaretNotSelection = onlyCaretNotSelection;
31
+ this.caretAtStartIsSelected = caretAtStartIsSelected;
32
+ this.caretAtEndIsSelected = caretAtEndIsSelected;
33
+ this.createSubTokens = createSubTokens;
34
+ this.partiallySelectedTokensAreSelected = partiallySelectedTokensAreSelected;
35
+ this.parentTokensAreSelected = parentTokensAreSelected;
36
+ }
37
+ /* Runs after code is highlighted; Params: codeInput element) */
38
+ afterHighlight(codeInputElement) {
39
+ this.syncSelection(codeInputElement);
40
+ }
41
+ /* Runs after elements are added into a `code-input` (useful for adding events to the textarea); Params: codeInput element) */
42
+ afterElementsAdded(codeInputElement) {
43
+ codeInputElement.pluginData.selectTokenCallbacks = {};
44
+ codeInputElement.pluginData.selectTokenCallbacks.lastSelectionStart = codeInputElement.textareaElement.selectionStart;
45
+ codeInputElement.pluginData.selectTokenCallbacks.lastSelectionEnd = codeInputElement.textareaElement.selectionEnd;
46
+ codeInputElement.pluginData.selectTokenCallbacks.selectedTokenState = new plugins.SelectTokenCallbacks.SelectedTokenState(codeInputElement.codeElement, this.tokenSelectorCallbacks, this.onlyCaretNotSelection, this.caretAtStartIsSelected, this.caretAtEndIsSelected, this.createSubTokens, this.partiallySelectedTokensAreSelected, this.parentTokensAreSelected);
47
+ this.syncSelection(codeInputElement);
48
+
49
+ // As of 2024-08, the selectionchange event is only supported on Firefox.
50
+ codeInputElement.textareaElement.addEventListener("selectionchange", () => {
51
+ this.checkSelectionChanged(codeInputElement)
52
+ });
53
+ // When selectionchange has complete support, the listeners below can be deleted.
54
+ codeInputElement.textareaElement.addEventListener("select", () => {
55
+ this.checkSelectionChanged(codeInputElement)
56
+ });
57
+ codeInputElement.textareaElement.addEventListener("keypress", () => {
58
+ this.checkSelectionChanged(codeInputElement)
59
+ });
60
+ codeInputElement.textareaElement.addEventListener("mousedown", () => {
61
+ this.checkSelectionChanged(codeInputElement)
62
+ });
63
+ }
64
+ /* If the text selection has changed, run syncSelection. */
65
+ checkSelectionChanged(codeInputElement) {
66
+ if(
67
+ codeInputElement.textareaElement.selectionStart != codeInputElement.pluginData.selectTokenCallbacks.lastSelectionStart
68
+ || codeInputElement.textareaElement.selectionEnd != codeInputElement.pluginData.selectTokenCallbacks.lastSelectionEnd
69
+ ) {
70
+ this.syncSelection(codeInputElement);
71
+ codeInputElement.pluginData.selectTokenCallbacks.lastSelectionStart = codeInputElement.textareaElement.selectionStart;
72
+ codeInputElement.pluginData.selectTokenCallbacks.lastSelectionEnd = codeInputElement.textareaElement.selectionEnd;
73
+ }
74
+ }
75
+ /* Update which elements have the code-input_selected class. */
76
+ syncSelection(codeInputElement) {
77
+ codeInputElement.pluginData.selectTokenCallbacks.selectedTokenState.updateSelection(codeInputElement.textareaElement.selectionStart, codeInputElement.textareaElement.selectionEnd)
78
+ }
79
+ }
80
+
81
+ /**
82
+ * A data structure specifying what should be done with tokens when they are selected, and also allows for previously selected
83
+ * tokens to be dealt with each time the selection changes. See the constructor and the createClassSynchronisation static method.
84
+ */
85
+ plugins.SelectTokenCallbacks.TokenSelectorCallbacks = class {
86
+ /**
87
+ * Pass any callbacks you want to customise the behaviour of selected tokens via JavaScript.
88
+ *
89
+ * (If the behaviour you want is just differently styling selected tokens _via CSS_, you should probably use the createClassSynchronisation static method.)
90
+ * @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.
91
+ * @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.
92
+ */
93
+ constructor(tokenSelectedCallback, selectChangedCallback) {
94
+ this.tokenSelectedCallback = tokenSelectedCallback;
95
+ this.selectChangedCallback = selectChangedCallback;
96
+ }
97
+
98
+ /**
99
+ * 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.
100
+ *
101
+ * (If the behaviour you want requires more complex behaviour or JavaScript, you should use TokenSelectorCallbacks' constructor.)
102
+ *
103
+ * @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".
104
+ * @returns A new TokenSelectorCallbacks instance that encodes this behaviour.
105
+ */
106
+ static createClassSynchronisation(selectedClass = "code-input_select-token-callbacks_selected") {
107
+ return new plugins.SelectTokenCallbacks.TokenSelectorCallbacks(
108
+ (token) => {
109
+ token.classList.add(selectedClass);
110
+ },
111
+ (tokenContainer) => {
112
+ // Remove selected class
113
+ let selectedClassTokens = tokenContainer.getElementsByClassName(selectedClass);
114
+ // Use it like a queue, because as elements have their class name removed they are live-removed from the collection.
115
+ while(selectedClassTokens.length > 0) {
116
+ selectedClassTokens[0].classList.remove(selectedClass);
117
+ }
118
+ }
119
+ );
120
+ }
121
+ }
122
+
123
+ /* Manages a single <code-input> element's selected tokens, and calling the correct functions on the selected tokens */
124
+ plugins.SelectTokenCallbacks.SelectedTokenState = class {
125
+ constructor(codeElement, tokenSelectorCallbacks, onlyCaretNotSelection, caretAtStartIsSelected, caretAtEndIsSelected, createSubTokens, partiallySelectedTokensAreSelected, parentTokensAreSelected) {
126
+ this.tokenContainer = codeElement;
127
+ this.tokenSelectorCallbacks = tokenSelectorCallbacks;
128
+ this.onlyCaretNotSelection = onlyCaretNotSelection;
129
+ this.caretAtStartIsSelected = caretAtStartIsSelected;
130
+ this.caretAtEndIsSelected = caretAtEndIsSelected;
131
+ this.createSubTokens = createSubTokens;
132
+ this.partiallySelectedTokensAreSelected = partiallySelectedTokensAreSelected;
133
+ this.parentTokensAreSelected = parentTokensAreSelected;
134
+ }
135
+
136
+ /* Change the selected region to a new range from selectionStart to selectionEnd and run
137
+ the callbacks. */
138
+ updateSelection(selectionStart, selectionEnd) {
139
+ this.selectChanged()
140
+ if(!this.onlyCaretNotSelection || selectionStart == selectionEnd) { // Only deal with selected text if onlyCaretNotSelection is false.
141
+ this.updateSelectedTokens(this.tokenContainer, selectionStart, selectionEnd)
142
+ }
143
+ }
144
+ /* Runs when the text selection has changed, before any updateSelectedTokens call. */
145
+ selectChanged() {
146
+ if(this.createSubTokens) {
147
+ // Remove generated spans to hold selected partial tokens
148
+ let tempSpans = this.tokenContainer.getElementsByClassName("code-input_select-token-callbacks_temporary-span");
149
+ while(tempSpans.length > 0) {
150
+ // Replace with textContent as Text node
151
+ // Use it like a queue, because as elements have their class name removed they are live-removed from the collection.
152
+ tempSpans[0].parentElement.replaceChild(new Text(tempSpans[0].textContent), tempSpans[0]);
153
+ }
154
+ }
155
+
156
+ this.tokenSelectorCallbacks.selectChangedCallback(this.tokenContainer);
157
+ }
158
+
159
+ /* Do the desired behaviour for selection to all tokens (elements in the currentElement)
160
+ from startIndex to endIndex in the text. Start from the currentElement as this function is recursive.
161
+ This code is similar to plugins.FindAndReplace.FindMatchState.highlightMatch*/
162
+ updateSelectedTokens(currentElement, startIndex, endIndex) {
163
+ if(endIndex < 0 || endIndex == 0 && !this.caretAtStartIsSelected) {
164
+ return; // Nothing selected
165
+ }
166
+ if(this.parentTokensAreSelected && currentElement !== this.tokenContainer) {
167
+ this.tokenSelectorCallbacks.tokenSelectedCallback(currentElement); // Parent elements also marked with class / have callback called
168
+ }
169
+ for(let i = 0; i < currentElement.childNodes.length; i++) {
170
+ let childElement = currentElement.childNodes[i];
171
+ let childText = childElement.textContent;
172
+
173
+ let noInnerElements = false;
174
+ if(childElement.nodeType == 3) {
175
+ // Text node
176
+ if(this.createSubTokens) {
177
+ // Replace with token
178
+ if(i + 1 < currentElement.childNodes.length && currentElement.childNodes[i+1].nodeType == 3) {
179
+ // Can merge with next text node
180
+ currentElement.childNodes[i+1].textContent = childElement.textContent + currentElement.childNodes[i+1].textContent; // Merge textContent with next node
181
+ currentElement.removeChild(childElement); // Delete this node
182
+ i--; // As an element removed
183
+ continue; // Move to next node
184
+ }
185
+ noInnerElements = true;
186
+
187
+ let replacementElement = document.createElement("span");
188
+ replacementElement.textContent = childText;
189
+ replacementElement.classList.add("code-input_select-token-callbacks_temporary-span"); // Can remove span later
190
+
191
+ currentElement.replaceChild(replacementElement, childElement);
192
+ childElement = replacementElement;
193
+ } else {
194
+ // Skip text node
195
+ // Make indexes skip the element
196
+ startIndex -= childText.length;
197
+ endIndex -= childText.length;
198
+ continue;
199
+ }
200
+ }
201
+
202
+ if(startIndex <= 0) {
203
+ // Started selection
204
+ if(childText.length > endIndex) {
205
+ // Selection ends in childElement
206
+ if(this.partiallySelectedTokensAreSelected) {
207
+ if(noInnerElements) {
208
+ if(this.createSubTokens && startIndex != endIndex) { // Subtoken to create
209
+ // Text node - add selection class to first part
210
+ let startSpan = document.createElement("span");
211
+ this.tokenSelectorCallbacks.tokenSelectedCallback(startSpan); // Selected
212
+ startSpan.classList.add("code-input_select-token-callbacks_temporary-span"); // Can remove span later
213
+ startSpan.textContent = childText.substring(0, endIndex);
214
+
215
+ let endText = childText.substring(endIndex);
216
+ childElement.textContent = endText;
217
+
218
+ childElement.insertAdjacentElement('beforebegin', startSpan);
219
+ i++; // An extra element has been added
220
+ }
221
+ if(this.parentTokensAreSelected || !this.createSubTokens) {
222
+ this.tokenSelectorCallbacks.tokenSelectedCallback(childElement); // Selected
223
+ }
224
+ } else {
225
+ this.updateSelectedTokens(childElement, 0, endIndex);
226
+ }
227
+ }
228
+
229
+ // Match ended - nothing to do after backtracking
230
+ return;
231
+ } else {
232
+ // Match goes through child element
233
+ this.tokenSelectorCallbacks.tokenSelectedCallback(childElement); // Selected
234
+ }
235
+ } else if(this.caretAtEndIsSelected && childText.length >= startIndex || childText.length > startIndex) {
236
+ // Match starts in childElement
237
+ if(this.partiallySelectedTokensAreSelected) {
238
+ if(noInnerElements) {
239
+ if(this.createSubTokens && startIndex != endIndex) { // Subtoken to create
240
+ if(childText.length > endIndex) {
241
+ // Match starts and ends in childElement - selection middle part
242
+ let startSpan = document.createElement("span");
243
+ startSpan.classList.add("code-input_select-token-callbacks_temporary-span"); // Can remove span later
244
+ startSpan.textContent = childText.substring(0, startIndex);
245
+
246
+ let middleText = childText.substring(startIndex, endIndex);
247
+ childElement.textContent = middleText;
248
+ this.tokenSelectorCallbacks.tokenSelectedCallback(childElement); // Selection
249
+
250
+ let endSpan = document.createElement("span");
251
+ endSpan.classList.add("code-input_select-token-callbacks_temporary-span"); // Can remove span later
252
+ endSpan.textContent = childText.substring(endIndex);
253
+
254
+ childElement.insertAdjacentElement('beforebegin', startSpan);
255
+ childElement.insertAdjacentElement('afterend', endSpan);
256
+ i++; // 2 extra elements have been added
257
+ } else {
258
+ // Match starts in element - highlight last part
259
+ let startText = childText.substring(0, startIndex);
260
+ childElement.textContent = startText;
261
+
262
+ let endSpan = document.createElement("span");
263
+ this.tokenSelectorCallbacks.tokenSelectedCallback(endSpan); // Selected
264
+ endSpan.classList.add("code-input_select-token-callbacks_temporary-span"); // Can remove span later
265
+ endSpan.textContent = childText.substring(startIndex);
266
+
267
+ childElement.insertAdjacentElement('afterend', endSpan);
268
+ i++; // An extra element has been added
269
+ }
270
+ }
271
+ if(this.parentTokensAreSelected || !this.createSubTokens) {
272
+ this.tokenSelectorCallbacks.tokenSelectedCallback(childElement); // Selected
273
+ }
274
+ } else {
275
+ this.updateSelectedTokens(childElement, startIndex, endIndex);
276
+ }
277
+ }
278
+
279
+ if(this.caretAtStartIsSelected) {
280
+ if(childText.length > endIndex) {
281
+ // Match completely in childElement - nothing to do after backtracking
282
+ return;
283
+ }
284
+ } else if(childText.length >= endIndex) {
285
+ // Match completely in childElement - nothing to do after backtracking
286
+ return;
287
+ }
288
+ }
289
+
290
+ // Make indexes skip the element
291
+ startIndex -= childText.length;
292
+ endIndex -= childText.length;
293
+ }
294
+ }
295
+ }
296
+ export default plugins.SelectTokenCallbacks;
@@ -0,0 +1,25 @@
1
+ // NOTICE: This code is @generated from code outside the esm directory. Please do not edit it to contribute!
2
+
3
+ import { Plugin, CodeInput } from "../code-input.d.mts";
4
+ /**
5
+ * Render special characters and control characters as a symbol with their hex code.
6
+ * Files: special-chars.js, special-chars.css
7
+ *
8
+ * WARNING:
9
+ *
10
+ * This plugin is currently unstable when used with other plugins,
11
+ * Unicode characters, or highlight.js. I hope to fix much of this by
12
+ * major version 3, and if you could help that would be amazing!
13
+ *
14
+ * See https://github.com/WebCoder49/code-input/issues?q=is%3Aissue%20state%3Aopen%20specialchars
15
+ */
16
+ export default class SpecialChars extends Plugin {
17
+ /**
18
+ * Create a special characters plugin instance.
19
+ * Default = covers many non-renderable ASCII characters.
20
+ * @param {Boolean} colorInSpecialChars Whether or not to give special characters custom background colors based on their hex code. Defaults to false.
21
+ * @param {Boolean} inheritTextColor If true, forces the color of the hex code to inherit from syntax highlighting. If false, the base color of the `pre code` element is used to give contrast to the small characters. Defaults to false.
22
+ * @param {RegExp} specialCharRegExp The regular expression which matches special characters. Defaults to many non-renderable ASCII characters (which characters are renderable depends on the browser and OS).
23
+ */
24
+ constructor(colorInSpecialChars?: boolean, inheritTextColor?: boolean, specialCharRegExp?: RegExp);
25
+ }
@@ -0,0 +1,207 @@
1
+ // NOTICE: This code is @generated from code outside the esm directory. Please do not edit it to contribute!
2
+
3
+ import { Plugin } from "../code-input.mjs";
4
+ const plugins = {};
5
+ /**
6
+ * Render special characters and control characters as a symbol with their hex code.
7
+ * Files: special-chars.js, special-chars.css
8
+ *
9
+ * WARNING:
10
+ *
11
+ * This plugin is currently unstable when used with other plugins,
12
+ * Unicode characters, or highlight.js. I hope to fix much of this by
13
+ * major version 3, and if you could help that would be amazing!
14
+ *
15
+ * See https://github.com/WebCoder49/code-input/issues?q=is%3Aissue%20state%3Aopen%20specialchars
16
+ */
17
+ "use strict";
18
+
19
+ plugins.SpecialChars = class extends Plugin {
20
+ specialCharRegExp;
21
+
22
+ cachedColors; // ascii number > [background color, text color]
23
+ cachedWidths; // character > character width
24
+ canvasContext;
25
+
26
+ /**
27
+ * Create a special characters plugin instance.
28
+ * Default = covers many non-renderable ASCII characters.
29
+ * @param {Boolean} colorInSpecialChars Whether or not to give special characters custom background colors based on their hex code. Defaults to false.
30
+ * @param {Boolean} inheritTextColor If true, forces the color of the hex code to inherit from syntax highlighting. If false, the base color of the `pre code` element is used to give contrast to the small characters. Defaults to false.
31
+ * @param {RegExp} specialCharRegExp The regular expression which matches special characters. Defaults to many non-renderable ASCII characters (which characters are renderable depends on the browser and OS).
32
+ */
33
+ 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
34
+ super([]); // No observed attributes
35
+
36
+ this.specialCharRegExp = specialCharRegExp;
37
+ this.colorInSpecialChars = colorInSpecialChars;
38
+ this.inheritTextColor = inheritTextColor;
39
+
40
+ this.cachedColors = {};
41
+ this.cachedWidths = {};
42
+
43
+ let canvas = document.createElement("canvas");
44
+ this.canvasContext = canvas.getContext("2d");
45
+ }
46
+
47
+ /* Initially render special characters as the highlighting algorithm may automatically highlight and remove them */
48
+ afterElementsAdded(codeInput) {
49
+ setTimeout(() => { codeInput.value = codeInput.value; }, 100);
50
+ }
51
+
52
+ /* After highlighting, render special characters as their stylised hexadecimal equivalents */
53
+ afterHighlight(codeInput) {
54
+ let resultElement = codeInput.codeElement;
55
+
56
+ // Reset data each highlight so can change if font size, etc. changes
57
+ codeInput.pluginData.specialChars = {};
58
+ codeInput.pluginData.specialChars.contrastColor = window.getComputedStyle(resultElement).color;
59
+
60
+ this.recursivelyReplaceText(codeInput, resultElement);
61
+
62
+ this.lastFont = window.getComputedStyle(codeInput.textareaElement).font;
63
+ }
64
+
65
+ /* Search for special characters in an element and replace them with their stylised hexadecimal equivalents */
66
+ recursivelyReplaceText(codeInput, element) {
67
+ for(let i = 0; i < element.childNodes.length; i++) {
68
+
69
+ let nextNode = element.childNodes[i];
70
+ if(nextNode.nodeType == 3) {
71
+ // Text node - Replace in here
72
+ let oldValue = nextNode.nodeValue;
73
+
74
+ this.specialCharRegExp.lastIndex = 0;
75
+ let searchResult = this.specialCharRegExp.exec(oldValue);
76
+ if(searchResult != null) {
77
+ let charIndex = searchResult.index; // Start as returns end
78
+
79
+ nextNode = nextNode.splitText(charIndex+1).previousSibling;
80
+
81
+ if(charIndex > 0) {
82
+ nextNode = nextNode.splitText(charIndex); // Keep characters before the special character in a different span
83
+ }
84
+
85
+ if(nextNode.textContent != "") {
86
+ let replacementElement = this.getStylisedSpecialChar(codeInput, nextNode.textContent);
87
+ // This next node will become the i+1th node so automatically iterated to
88
+ nextNode.parentNode.insertBefore(replacementElement, nextNode);
89
+ nextNode.textContent = "";
90
+ }
91
+ }
92
+ } else if(nextNode.nodeType == 1) {
93
+ if(nextNode.className != "code-input_special-char" && nextNode.nodeValue != "") {
94
+ // Element - recurse
95
+ this.recursivelyReplaceText(codeInput, nextNode);
96
+ }
97
+ }
98
+ }
99
+ }
100
+
101
+ /* Get the stylised hexadecimal representation HTML element for a given special character */
102
+ getStylisedSpecialChar(codeInput, matchChar) {
103
+ let hexCode = matchChar.codePointAt(0);
104
+
105
+ let colors;
106
+ if(this.colorInSpecialChars) colors = this.getCharacterColors(hexCode);
107
+
108
+ hexCode = hexCode.toString(16);
109
+ hexCode = ("0000" + hexCode).substring(hexCode.length); // So 2 chars with leading 0
110
+ hexCode = hexCode.toUpperCase();
111
+
112
+ let charWidth = this.getCharacterWidthEm(codeInput, matchChar);
113
+
114
+ // Create element with hex code
115
+ let result = document.createElement("span");
116
+ result.textContent = matchChar;
117
+ result.classList.add("code-input_special-char");
118
+ result.style.setProperty("--hex-0", "var(--code-input_special-chars_" + hexCode[0] + ")");
119
+ result.style.setProperty("--hex-1", "var(--code-input_special-chars_" + hexCode[1] + ")");
120
+ result.style.setProperty("--hex-2", "var(--code-input_special-chars_" + hexCode[2] + ")");
121
+ result.style.setProperty("--hex-3", "var(--code-input_special-chars_" + hexCode[3] + ")");
122
+
123
+ // Handle zero-width chars
124
+ if(charWidth == 0) result.classList.add("code-input_special-char_zero-width");
125
+ else result.style.width = charWidth + "em";
126
+
127
+ if(this.colorInSpecialChars) {
128
+ result.style.backgroundColor = "#" + colors[0];
129
+ result.style.setProperty("--code-input_special-char_color", colors[1]);
130
+ } else if(!this.inheritTextColor) {
131
+ result.style.setProperty("--code-input_special-char_color", codeInput.pluginData.specialChars.contrastColor);
132
+ }
133
+ return result;
134
+ }
135
+
136
+ /* Get the colors a stylised representation of a given character must be shown in; lazy load and return [background color, text color] */
137
+ getCharacterColors(asciiCode) {
138
+ let textColor;
139
+ if(!(asciiCode in this.cachedColors)) {
140
+ // Get background color
141
+ let asciiHex = asciiCode.toString(16);
142
+ let backgroundColor = "";
143
+ for(let i = 0; i < asciiHex.length; i++) {
144
+ backgroundColor += asciiHex[i] + asciiHex[i];
145
+ }
146
+ backgroundColor = ("000000" + backgroundColor).substring(backgroundColor.length); // So valid HEX color with 6 characters
147
+
148
+ // Get most suitable text color - white or black depending on background brightness
149
+ let colorBrightness = 0;
150
+ const luminanceCoefficients = [0.299, 0.587, 0.114];
151
+ for(let i = 0; i < 6; i += 2) {
152
+ colorBrightness += parseInt(backgroundColor.substring(i, i+2), 16) * luminanceCoefficients[i/2];
153
+ }
154
+ // Calculate darkness
155
+ textColor = colorBrightness < 128 ? "white" : "black";
156
+
157
+ this.cachedColors[asciiCode] = [backgroundColor, textColor];
158
+ return [backgroundColor, textColor];
159
+ } else {
160
+ return this.cachedColors[asciiCode];
161
+ }
162
+ }
163
+
164
+ /* 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 */
165
+ getCharacterWidthEm(codeInput, char) {
166
+ // Force zero-width characters
167
+ if(new RegExp("\u00AD|\u02DE|[\u0300-\u036F]|[\u0483-\u0489]|[\u200B-\u200D]|\uFEFF").test(char) ) { return 0 }
168
+ // Non-renderable ASCII characters should all be rendered at same size
169
+ if(char != "\u0096" && new RegExp("[\u{0000}-\u{001F}]|[\u{007F}-\u{009F}]", "g").test(char)) {
170
+ let fallbackWidth = this.getCharacterWidthEm(codeInput, "\u0096");
171
+ return fallbackWidth;
172
+ }
173
+
174
+ 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
175
+
176
+ // Lazy-load width of each character
177
+ if(this.cachedWidths[font] == undefined) {
178
+ this.cachedWidths[font] = {};
179
+ }
180
+ if(this.cachedWidths[font][char] != undefined) { // Use cached width
181
+ return this.cachedWidths[font][char];
182
+ }
183
+
184
+ // Ensure font the same - 20px font size is where this algorithm works
185
+ this.canvasContext.font = getComputedStyle(codeInput.textareaElement).font.replace(getComputedStyle(codeInput.textareaElement).fontSize, "20px");
186
+
187
+ // Try to get width from canvas
188
+ let width = this.canvasContext.measureText(char).width/20; // From px to em (=proportion of font-size)
189
+ if(width > 1) {
190
+ width /= 2; // Fix double-width-in-canvas Firefox bug
191
+ } else if(width == 0 && char != "\u0096") {
192
+ let fallbackWidth = this.getCharacterWidthEm(codeInput, "\u0096");
193
+ return fallbackWidth; // In Firefox some control chars don't render, but all control chars are the same width
194
+ }
195
+
196
+ // Firefox will never make smaller than size at 20px
197
+ if(navigator.userAgent.includes("Mozilla") && !navigator.userAgent.includes("Chrome") && !navigator.userAgent.includes("Safari")) {
198
+ let fontSize = Number(getComputedStyle(codeInput.textareaElement).fontSize.substring(0, getComputedStyle(codeInput.textareaElement).fontSize.length-2)); // Remove 20, make px
199
+ if(fontSize < 20) width *= 20 / fontSize;
200
+ }
201
+
202
+ this.cachedWidths[font][char] = width;
203
+
204
+ return width;
205
+ }
206
+ }
207
+ export default plugins.SpecialChars;
@@ -0,0 +1,16 @@
1
+ // NOTICE: This code is @generated from code outside the esm directory. Please do not edit it to contribute!
2
+
3
+ import { Plugin, CodeInput } from "../code-input.d.mts";
4
+ /**
5
+ * JavaScript example of a plugin, which brings extra,
6
+ * non-central optional functionality to code-input.
7
+ * Instances of plugins can be passed in in an array
8
+ * to the `plugins` argument when registering a template,
9
+ * for example like this:
10
+ * ```javascript
11
+ * codeInput.registerTemplate("syntax-highlighted", codeInput.templates.hljs(hljs, [new codeInput.plugins.Test()]));
12
+ * ```
13
+ */
14
+ export default class Test extends Plugin {
15
+ constructor();
16
+ }
@@ -0,0 +1,56 @@
1
+ // NOTICE: This code is @generated from code outside the esm directory. Please do not edit it to contribute!
2
+
3
+ import { Plugin } from "../code-input.mjs";
4
+ const plugins = {};
5
+ /**
6
+ * Copy this to create a plugin, which brings extra,
7
+ * non-central optional functionality to code-input.
8
+ * Instances of plugins can be passed in in an array
9
+ * to the `plugins` argument when registering a template,
10
+ * for example like this:
11
+ * ```javascript
12
+ * codeInput.registerTemplate("syntax-highlighted", codeInput.templates.hljs(hljs, [new plugins.Test()]));
13
+ * ```
14
+ */
15
+ "use strict";
16
+
17
+ plugins.Test = class extends Plugin {
18
+ instructions = {
19
+ beforeHighlight: "before highlight",
20
+ afterHighlight: "after highlight",
21
+ beforeElementsAdded: "before elements added",
22
+ afterElementsAdded: "after elements added",
23
+ attributeChanged: (name, oldValue, newValue) => `${name}: '${oldValue}'>'${newValue}'`
24
+ };
25
+
26
+ constructor(instructionTranslations = {}) {
27
+ super(["testattr"]);
28
+ // Array of observed attributes as parameter
29
+
30
+ // instructionTranslations, instructions, and the addTranslations
31
+ // call need not be present if this plugin uses no localisable
32
+ // text.
33
+ this.addTranslations(this.instructions, instructionTranslations);
34
+ }
35
+ /* Runs before code is highlighted; Params: codeInput element) */
36
+ beforeHighlight(codeInput) {
37
+ console.log(codeInput, this.instructions.beforeHighlight);
38
+ }
39
+ /* Runs after code is highlighted; Params: codeInput element) */
40
+ afterHighlight(codeInput) {
41
+ console.log(codeInput, this.instructions.afterHighlight);
42
+ }
43
+ /* Runs before elements are added into a `code-input`; Params: codeInput element) */
44
+ beforeElementsAdded(codeInput) {
45
+ console.log(codeInput, this.instructions.beforeElementsAdded);
46
+ }
47
+ /* Runs after elements are added into a `code-input` (useful for adding events to the textarea); Params: codeInput element) */
48
+ afterElementsAdded(codeInput) {
49
+ console.log(codeInput, this.instructions.afterElementsAdded);
50
+ }
51
+ /* Runs when an observed attribute of a `code-input` is changed (you must add the attribute name in the constructor); Params: codeInput element, name attribute name, oldValue previous value of attribute, newValue changed value of attribute) */
52
+ attributeChanged(codeInput, name, oldValue, newValue) {
53
+ console.log(codeInput, this.instructions.attriibuteChanged(name, oldValue, newValue));
54
+ }
55
+ }
56
+ export default plugins.Test;