Skip to content

Commit 4ea2094

Browse files
author
WebCoder49
committed
Finalise special-chars plugin
1 parent 59908be commit 4ea2094

File tree

20 files changed

+93
-47
lines changed

20 files changed

+93
-47
lines changed

code-input.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -321,6 +321,8 @@ var codeInput = {
321321
set placeholder(val) {
322322
return this.setAttribute("placeholder", val);
323323
}
324+
325+
pluginData = {} // For plugins to store element-specific data under their name, e.g. <code-input>.pluginData.specialChars
324326
},
325327

326328
registerTemplate: function(template_name, template) {

plugins/README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,15 @@ Allows code-input elements to be used with the Prism.js line-numbers plugin, as
3535
Files: [prism-line-numbers.css](./prism-line-numbers.css) (NO JS FILE)
3636

3737
[🚀 *CodePen Demo*](https://codepen.io/WebCoder49/pen/XWPVrWv)
38+
39+
### Special Chars
40+
Render special characters and control characters as a symbol
41+
with their hex code.
42+
43+
Files: [special-chars.js](./special-chars.js) / [special-chars.css](./special-chars.css)
44+
45+
[🚀 *CodePen Demo*](https://codepen.io/WebCoder49/pen/jOeYJbm)
46+
3847
## Using Plugins
3948
Plugins allow you to add extra features to a template, like [automatic indentation](./indent.js) or [support for highlight.js's language autodetection](./autodetect.js). To use them, just:
4049
- Import the plugins' JS/CSS files (there may only be one of these; import all of the files that exist) after you have imported `code-input` and before registering the template.

plugins/special-chars.css

Lines changed: 10 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/**
2-
* Render special characters as a symbol with their escape code.
2+
* Render special characters and control characters as a symbol with their hex code.
33
* Files: special-chars.js, special-chars.css
44
*/
55

@@ -35,19 +35,20 @@
3535
left: 0;
3636
height: 1em;
3737
/* width: set by JS */
38-
3938
overflow: hidden;
40-
4139
text-decoration: none;
4240
text-shadow: none;
43-
4441
vertical-align: middle;
4542
outline: 0.1px solid currentColor;
4643

47-
--hex-0: var(--code-input_special-chars_0);
48-
--hex-1: var(--code-input_special-chars_0);
49-
--hex-2: var(--code-input_special-chars_0);
50-
--hex-3: var(--code-input_special-chars_0);
44+
--hex-0: var(
45+
--code-input_special-chars_0);
46+
--hex-1: var(
47+
--code-input_special-chars_0);
48+
--hex-2: var(
49+
--code-input_special-chars_0);
50+
--hex-3: var(
51+
--code-input_special-chars_0);
5152
}
5253

5354
/* Default - Two bytes - 4 hex chars */
@@ -57,7 +58,7 @@
5758
transform: translate(-50%, 0);
5859
content: " ";
5960

60-
background-color: currentColor;
61+
background-color: var(--code-input_special-char_color, currentColor);
6162
image-rendering: pixelated;
6263
display: inline-block;
6364
width: calc(100%-2px);
@@ -74,14 +75,6 @@
7475
-webkit-mask-position: 10% 10%, min(90%, 0.5em) 10%, 10% 90%, min(90%, 0.5em) 90%;
7576
}
7677

77-
.code-input_special-char.code-input_special-char_narrow::before {
78-
mask-size: 30%, 30%, 30%, 30%;
79-
mask-position: 20% 20%, 80% 20%, 20% 80%, 80% 80%;
80-
81-
-webkit-mask-size: 30%, 30%, 30%, 30%;
82-
-webkit-mask-position: 20% 20%, 80% 20%, 20% 80%, 80% 80%;
83-
}
84-
8578
.code-input_special-char_zero-width {
8679
z-index: 1;
8780
width: 1em;

plugins/special-chars.js

Lines changed: 72 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/**
2-
* Render special characters as a symbol with their hex code.
2+
* Render special characters and control characters as a symbol with their hex code.
33
* Files: special-chars.js, special-chars.css
44
*/
55

@@ -9,30 +9,27 @@ codeInput.plugins.SpecialChars = class extends codeInput.Plugin {
99
specialCharRegExp;
1010

1111
cachedColors; // ascii number > [background color, text color]
12-
cachedWidths; // character > character width
12+
cachedWidths; // font > {character > character width}
1313
canvasContext;
1414

1515
/**
1616
* Create a special characters plugin instance
17-
* @param {RegExp} specialCharRegExp The regular expression which matches special characters
1817
* @param {Boolean} colorInSpecialChars Whether or not to give special characters custom background colors based on their hex code
18+
* @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.
19+
* @param {RegExp} specialCharRegExp The regular expression which matches special characters
1920
*/
20-
constructor(specialCharRegExp = /(?!\n)(?!\t)[\u{0000}-\u{001F}]|[\u{007F}-\u{009F}]|[\u{0200}-\u{FFFF}]/ug, colorInSpecialChars = true) { // By default, covers many non-renderable ASCII characters
21+
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
2122
super();
23+
2224
this.specialCharRegExp = specialCharRegExp;
2325
this.colorInSpecialChars = colorInSpecialChars;
26+
this.inheritTextColor = inheritTextColor;
2427

2528
this.cachedColors = {};
2629
this.cachedWidths = {};
2730

2831
let canvas = document.createElement("canvas");
29-
window.addEventListener("load", () => {
30-
document.body.appendChild(canvas);
31-
32-
});
3332
this.canvasContext = canvas.getContext("2d");
34-
this.canvasContext.fillStyle = "black";
35-
this.canvasContext.font = "20px 'Consolas'"; // TODO: Make dynamic
3633
}
3734

3835
/* Runs before elements are added into a `code-input`; Params: codeInput element) */
@@ -50,10 +47,17 @@ codeInput.plugins.SpecialChars = class extends codeInput.Plugin {
5047
afterHighlight(codeInput) {
5148
let result_element = codeInput.querySelector("pre code");
5249

53-
this.recursivelyReplaceText(result_element);
50+
// Reset data each highlight so can change if font size, etc. changes
51+
codeInput.pluginData.specialChars = {};
52+
codeInput.pluginData.specialChars.textarea = codeInput.getElementsByTagName("textarea")[0];
53+
codeInput.pluginData.specialChars.contrastColor = window.getComputedStyle(result_element).color;
54+
55+
this.recursivelyReplaceText(codeInput, result_element);
56+
57+
this.lastFont = window.getComputedStyle(codeInput.pluginData.specialChars.textarea).font;
5458
}
5559

56-
recursivelyReplaceText(element) {
60+
recursivelyReplaceText(codeInput, element) {
5761
for(let i = 0; i < element.childNodes.length; i++) {
5862

5963
let nextNode = element.childNodes[i];
@@ -73,21 +77,21 @@ codeInput.plugins.SpecialChars = class extends codeInput.Plugin {
7377
}
7478

7579
if(nextNode.textContent != "") {
76-
let replacementElement = this.specialCharReplacer(nextNode.textContent);
80+
let replacementElement = this.specialCharReplacer(codeInput, nextNode.textContent);
7781
nextNode.parentNode.insertBefore(replacementElement, nextNode);
7882
nextNode.textContent = "";
7983
}
8084
}
8185
} else if(nextNode.nodeType == 1) {
8286
if(nextNode.className != "code-input_special-char" && nextNode.nodeValue != "") {
8387
// Element - recurse
84-
this.recursivelyReplaceText(nextNode);
88+
this.recursivelyReplaceText(codeInput, nextNode);
8589
}
8690
}
8791
}
8892
}
8993

90-
specialCharReplacer(match_char) {
94+
specialCharReplacer(codeInput, match_char) {
9195
let hex_code = match_char.codePointAt(0);
9296

9397
let colors;
@@ -97,7 +101,7 @@ codeInput.plugins.SpecialChars = class extends codeInput.Plugin {
97101
hex_code = ("0000" + hex_code).substring(hex_code.length); // So 2 chars with leading 0
98102
hex_code = hex_code.toUpperCase();
99103

100-
let char_width = this.getCharacterWidth(match_char);
104+
let char_width = this.getCharacterWidth(codeInput, match_char);
101105

102106
// Create element with hex code
103107
let result = document.createElement("span");
@@ -106,18 +110,16 @@ codeInput.plugins.SpecialChars = class extends codeInput.Plugin {
106110
result.style.setProperty("--hex-1", "var(--code-input_special-chars_" + hex_code[1] + ")");
107111
result.style.setProperty("--hex-2", "var(--code-input_special-chars_" + hex_code[2] + ")");
108112
result.style.setProperty("--hex-3", "var(--code-input_special-chars_" + hex_code[3] + ")");
109-
110-
if(char_width <= 11) {
111-
result.classList.add("code-input_special-char_narrow");
112-
}
113113

114114
// Handle zero-width chars
115115
if(char_width == 0) result.classList.add("code-input_special-char_zero-width");
116116
else result.style.width = char_width + "px";
117117

118118
if(this.colorInSpecialChars) {
119119
result.style.backgroundColor = "#" + colors[0];
120-
result.style.color = colors[1];
120+
result.style.setProperty("--code-input_special-char_color", colors[1]);
121+
} else if(!this.inheritTextColor) {
122+
result.style.setProperty("--code-input_special-char_color", codeInput.pluginData.specialChars.contrastColor);
121123
}
122124
return result;
123125
}
@@ -135,11 +137,14 @@ codeInput.plugins.SpecialChars = class extends codeInput.Plugin {
135137

136138
// Get most suitable text color - white or black depending on background brightness
137139
let color_brightness = 0;
140+
let luminance_coefficients = [0.299, 0.587, 0.114];
138141
for(let i = 0; i < 6; i += 2) {
139-
color_brightness += parseInt(background_color.substring(i, i+2), 16);
142+
color_brightness += parseInt(background_color.substring(i, i+2), 16) * luminance_coefficients[i/2];
140143
}
141144
// Calculate darkness
142-
text_color = color_brightness < (128*3) ? "white" : "black";
145+
text_color = color_brightness < 128 ? "white" : "black";
146+
147+
// console.log(background_color, color_brightness, text_color);
143148

144149
this.cachedColors[ascii_code] = [background_color, text_color];
145150
return [background_color, text_color];
@@ -148,30 +153,67 @@ codeInput.plugins.SpecialChars = class extends codeInput.Plugin {
148153
}
149154
}
150155

151-
getCharacterWidth(char) {
156+
getCharacterWidth(codeInput, char) { // TODO: Check StackOverflow question
152157
// Force zero-width characters
153158
if(new RegExp("\u00AD|\u02de|[\u0300-\u036F]|[\u0483-\u0489]|\u200b").test(char) ) { return 0 }
154159
// Non-renderable ASCII characters should all be rendered at same size
155160
if(char != "\u0096" && new RegExp("[\u{0000}-\u{001F}]|[\u{007F}-\u{009F}]", "g").test(char)) {
156161
let fallbackWidth = this.getCharacterWidth("\u0096");
157162
return fallbackWidth;
158163
}
164+
165+
let font = window.getComputedStyle(codeInput.pluginData.specialChars.textarea).font;
166+
159167
// Lazy-load - TODO: Get a cleaner way of doing this
160-
if(char in this.cachedWidths) {
161-
return this.cachedWidths[char];
168+
if(this.cachedWidths[font] == undefined) {
169+
this.cachedWidths[font] = {}; // Create new cached widths for this font
162170
}
171+
if(this.cachedWidths[font][char] != undefined) { // Use cached width
172+
return this.cachedWidths[font][char];
173+
}
174+
175+
// Ensure font the same
176+
// console.log(font);
177+
this.canvasContext.font = font;
163178

164-
// Try to get width
179+
// Try to get width from canvas
165180
let width = this.canvasContext.measureText(char).width;
166-
this.canvasContext.fillText(char, 100, 100);
167-
if(width > 20) {
181+
if(width > Number(font.split("px")[0])) {
168182
width /= 2; // Fix double-width-in-canvas Firefox bug
169183
} else if(width == 0 && char != "\u0096") {
170184
let fallbackWidth = this.getCharacterWidth("\u0096");
171185
return fallbackWidth; // In Firefox some control chars don't render, but all control chars are the same width
172186
}
173187

174-
this.cachedWidths[char] = width;
188+
this.cachedWidths[font][char] = width;
189+
190+
// console.log(this.cachedWidths);
175191
return width;
176192
}
193+
194+
// getCharacterWidth(char) { // Doesn't work for now - from StackOverflow suggestion https://stackoverflow.com/a/76146120/21785620
195+
// let textarea = codeInput.pluginData.specialChars.textarea;
196+
197+
// // Create a temporary element to measure the width of the character
198+
// const span = document.createElement('span');
199+
// span.textContent = char;
200+
201+
// // Copy the textarea's font to the temporary element
202+
// span.style.fontSize = window.getComputedStyle(textarea).fontSize;
203+
// span.style.fontFamily = window.getComputedStyle(textarea).fontFamily;
204+
// span.style.fontWeight = window.getComputedStyle(textarea).fontWeight;
205+
// span.style.visibility = 'hidden';
206+
// span.style.position = 'absolute';
207+
208+
// // Add the temporary element to the document so we can measure its width
209+
// document.body.appendChild(span);
210+
211+
// // Get the width of the character in pixels
212+
// const width = span.offsetWidth;
213+
214+
// // Remove the temporary element from the document
215+
// document.body.removeChild(span);
216+
217+
// return width;
218+
// }
177219
}

plugins/special-chars/0.png

-99 Bytes
Binary file not shown.

plugins/special-chars/1.png

-92 Bytes
Binary file not shown.

plugins/special-chars/2.png

-101 Bytes
Binary file not shown.

plugins/special-chars/3.png

-94 Bytes
Binary file not shown.

plugins/special-chars/4.png

-100 Bytes
Binary file not shown.

plugins/special-chars/5.png

-104 Bytes
Binary file not shown.

0 commit comments

Comments
 (0)