Skip to content

Commit

Permalink
Merge pull request #37 from WebCoder49/display-special-chars
Browse files Browse the repository at this point in the history
Display special chars
  • Loading branch information
WebCoder49 committed May 6, 2023
2 parents e6d1201 + 4ea2094 commit 17d695c
Show file tree
Hide file tree
Showing 4 changed files with 330 additions and 2 deletions.
6 changes: 4 additions & 2 deletions code-input.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,6 @@ var codeInput = {
// been run). Thank you to peterprvy for this.
if(this.ignoreValueUpdate) return;

//console.log("Update", text);
this.ignoreValueUpdate = true;
this.value = text; // Change value attribute if necessary.
this.ignoreValueUpdate = false;
Expand All @@ -86,6 +85,7 @@ var codeInput = {
if (text[text.length - 1] == "\n") {
text += " ";
}

// Update code
result_element.innerHTML = this.escape_html(text);
this.plugin_evt("beforeHighlight");
Expand Down Expand Up @@ -216,7 +216,7 @@ var codeInput = {
if(this.template.preElementStyled) this.classList.add("code-input_pre-element-styled");
else this.classList.remove("code-input_pre-element-styled");
// Syntax Highlight
this.update(this.value, true);
this.update(this.value);

break;

Expand Down Expand Up @@ -321,6 +321,8 @@ var codeInput = {
set placeholder(val) {
return this.setAttribute("placeholder", val);
}

pluginData = {} // For plugins to store element-specific data under their name, e.g. <code-input>.pluginData.specialChars
},

registerTemplate: function(template_name, template) {
Expand Down
9 changes: 9 additions & 0 deletions plugins/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,15 @@ Allows code-input elements to be used with the Prism.js line-numbers plugin, as
Files: [prism-line-numbers.css](./prism-line-numbers.css) (NO JS FILE)

[🚀 *CodePen Demo*](https://codepen.io/WebCoder49/pen/XWPVrWv)

### Special Chars
Render special characters and control characters as a symbol
with their hex code.

Files: [special-chars.js](./special-chars.js) / [special-chars.css](./special-chars.css)

[🚀 *CodePen Demo*](https://codepen.io/WebCoder49/pen/jOeYJbm)

## Using Plugins
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:
- 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.
Expand Down
98 changes: 98 additions & 0 deletions plugins/special-chars.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
/**
* Render special characters and control characters as a symbol with their hex code.
* Files: special-chars.js, special-chars.css
*/

/* Main styling */

:root, body { /* Font for hex chars */
--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 { /* pre element */
font-size: 20px;
}

.code-input_special-char {
display: inline-block;
position: relative;
top: 0;
left: 0;
height: 1em;
/* width: set by JS */
overflow: hidden;
text-decoration: none;
text-shadow: none;
vertical-align: middle;
outline: 0.1px solid currentColor;

--hex-0: var(
--code-input_special-chars_0);
--hex-1: var(
--code-input_special-chars_0);
--hex-2: var(
--code-input_special-chars_0);
--hex-3: var(
--code-input_special-chars_0);
}

/* Default - Two bytes - 4 hex chars */

.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%, 0.25em), min(40%, 0.25em), min(40%, 0.25em), min(40%, 0.25em);
-webkit-mask-position: 10% 10%, min(90%, 0.5em) 10%, 10% 90%, min(90%, 0.5em) 90%;
}

.code-input_special-char_zero-width {
z-index: 1;
width: 1em;
margin-left: -0.5em;
margin-right: -0.5em;
position: relative;

opacity: 0.75;
}

/* One byte - 2 hex chars */
.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);
}
219 changes: 219 additions & 0 deletions plugins/special-chars.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
/**
* Render special characters and control characters as a symbol with their hex code.
* Files: special-chars.js, special-chars.css
*/

// INCOMPLETE: TODO Optimise regex - compile at start; Update CSS for character display; clean up + comment

codeInput.plugins.SpecialChars = class extends codeInput.Plugin {
specialCharRegExp;

cachedColors; // ascii number > [background color, text color]
cachedWidths; // font > {character > character width}
canvasContext;

/**
* Create a special characters plugin instance
* @param {Boolean} colorInSpecialChars Whether or not to give special characters custom background colors based on their hex code
* @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.
* @param {RegExp} specialCharRegExp The regular expression which matches special characters
*/
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
super();

this.specialCharRegExp = specialCharRegExp;
this.colorInSpecialChars = colorInSpecialChars;
this.inheritTextColor = inheritTextColor;

this.cachedColors = {};
this.cachedWidths = {};

let canvas = document.createElement("canvas");
this.canvasContext = canvas.getContext("2d");
}

/* Runs before elements are added into a `code-input`; Params: codeInput element) */
beforeElementsAdded(codeInput) {
codeInput.classList.add("code-input_special-char_container");
}

/* Runs after elements are added into a `code-input` (useful for adding events to the textarea); Params: codeInput element) */
afterElementsAdded(codeInput) {
// For some reason, special chars aren't synced the first time - TODO is there a cleaner way to do this?
setTimeout(() => { codeInput.update(codeInput.value); }, 100);
}

/* Runs after code is highlighted; Params: codeInput element) */
afterHighlight(codeInput) {
let result_element = codeInput.querySelector("pre code");

// Reset data each highlight so can change if font size, etc. changes
codeInput.pluginData.specialChars = {};
codeInput.pluginData.specialChars.textarea = codeInput.getElementsByTagName("textarea")[0];
codeInput.pluginData.specialChars.contrastColor = window.getComputedStyle(result_element).color;

this.recursivelyReplaceText(codeInput, result_element);

this.lastFont = window.getComputedStyle(codeInput.pluginData.specialChars.textarea).font;
}

recursivelyReplaceText(codeInput, element) {
for(let i = 0; i < element.childNodes.length; i++) {

let nextNode = element.childNodes[i];
if(nextNode.nodeName == "#text" && nextNode.nodeValue != "") {
// Replace in here
let oldValue = nextNode.nodeValue;

this.specialCharRegExp.lastIndex = 0;
let searchResult = this.specialCharRegExp.exec(oldValue);
if(searchResult != null) {
let charIndex = searchResult.index; // Start as returns end

nextNode = nextNode.splitText(charIndex+1).previousSibling;

if(charIndex > 0) {
nextNode = nextNode.splitText(charIndex); // Keep those before in difft. span
}

if(nextNode.textContent != "") {
let replacementElement = this.specialCharReplacer(codeInput, nextNode.textContent);
nextNode.parentNode.insertBefore(replacementElement, nextNode);
nextNode.textContent = "";
}
}
} else if(nextNode.nodeType == 1) {
if(nextNode.className != "code-input_special-char" && nextNode.nodeValue != "") {
// Element - recurse
this.recursivelyReplaceText(codeInput, nextNode);
}
}
}
}

specialCharReplacer(codeInput, match_char) {
let hex_code = match_char.codePointAt(0);

let colors;
if(this.colorInSpecialChars) colors = this.getCharacterColor(hex_code);

hex_code = hex_code.toString(16);
hex_code = ("0000" + hex_code).substring(hex_code.length); // So 2 chars with leading 0
hex_code = hex_code.toUpperCase();

let char_width = this.getCharacterWidth(codeInput, match_char);

// Create element with hex code
let result = document.createElement("span");
result.classList.add("code-input_special-char");
result.style.setProperty("--hex-0", "var(--code-input_special-chars_" + hex_code[0] + ")");
result.style.setProperty("--hex-1", "var(--code-input_special-chars_" + hex_code[1] + ")");
result.style.setProperty("--hex-2", "var(--code-input_special-chars_" + hex_code[2] + ")");
result.style.setProperty("--hex-3", "var(--code-input_special-chars_" + hex_code[3] + ")");

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

if(this.colorInSpecialChars) {
result.style.backgroundColor = "#" + colors[0];
result.style.setProperty("--code-input_special-char_color", colors[1]);
} else if(!this.inheritTextColor) {
result.style.setProperty("--code-input_special-char_color", codeInput.pluginData.specialChars.contrastColor);
}
return result;
}

getCharacterColor(ascii_code) {
// Choose colors based on character code - lazy load and return [background color, text color]
let background_color;
let text_color;
if(!(ascii_code in this.cachedColors)) {
// Get background color - arbitrary bit manipulation to get a good range of colours
background_color = ascii_code^(ascii_code << 3)^(ascii_code << 7)^(ascii_code << 14)^(ascii_code << 16); // Arbitrary
background_color = background_color^0x1fc627; // Arbitrary
background_color = background_color.toString(16);
background_color = ("000000" + background_color).substring(background_color.length); // So 6 chars with leading 0

// Get most suitable text color - white or black depending on background brightness
let color_brightness = 0;
let luminance_coefficients = [0.299, 0.587, 0.114];
for(let i = 0; i < 6; i += 2) {
color_brightness += parseInt(background_color.substring(i, i+2), 16) * luminance_coefficients[i/2];
}
// Calculate darkness
text_color = color_brightness < 128 ? "white" : "black";

// console.log(background_color, color_brightness, text_color);

this.cachedColors[ascii_code] = [background_color, text_color];
return [background_color, text_color];
} else {
return this.cachedColors[ascii_code];
}
}

getCharacterWidth(codeInput, char) { // TODO: Check StackOverflow question
// Force zero-width characters
if(new RegExp("\u00AD|\u02de|[\u0300-\u036F]|[\u0483-\u0489]|\u200b").test(char) ) { return 0 }
// Non-renderable ASCII characters should all be rendered at same size
if(char != "\u0096" && new RegExp("[\u{0000}-\u{001F}]|[\u{007F}-\u{009F}]", "g").test(char)) {
let fallbackWidth = this.getCharacterWidth("\u0096");
return fallbackWidth;
}

let font = window.getComputedStyle(codeInput.pluginData.specialChars.textarea).font;

// Lazy-load - TODO: Get a cleaner way of doing this
if(this.cachedWidths[font] == undefined) {
this.cachedWidths[font] = {}; // Create new cached widths for this font
}
if(this.cachedWidths[font][char] != undefined) { // Use cached width
return this.cachedWidths[font][char];
}

// Ensure font the same
// console.log(font);
this.canvasContext.font = font;

// Try to get width from canvas
let width = this.canvasContext.measureText(char).width;
if(width > Number(font.split("px")[0])) {
width /= 2; // Fix double-width-in-canvas Firefox bug
} else if(width == 0 && char != "\u0096") {
let fallbackWidth = this.getCharacterWidth("\u0096");
return fallbackWidth; // In Firefox some control chars don't render, but all control chars are the same width
}

this.cachedWidths[font][char] = width;

// console.log(this.cachedWidths);
return width;
}

// getCharacterWidth(char) { // Doesn't work for now - from StackOverflow suggestion https://stackoverflow.com/a/76146120/21785620
// let textarea = codeInput.pluginData.specialChars.textarea;

// // Create a temporary element to measure the width of the character
// const span = document.createElement('span');
// span.textContent = char;

// // Copy the textarea's font to the temporary element
// span.style.fontSize = window.getComputedStyle(textarea).fontSize;
// span.style.fontFamily = window.getComputedStyle(textarea).fontFamily;
// span.style.fontWeight = window.getComputedStyle(textarea).fontWeight;
// span.style.visibility = 'hidden';
// span.style.position = 'absolute';

// // Add the temporary element to the document so we can measure its width
// document.body.appendChild(span);

// // Get the width of the character in pixels
// const width = span.offsetWidth;

// // Remove the temporary element from the document
// document.body.removeChild(span);

// return width;
// }
}

0 comments on commit 17d695c

Please sign in to comment.