-
Notifications
You must be signed in to change notification settings - Fork 22
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #37 from WebCoder49/display-special-chars
Display special chars
- Loading branch information
Showing
4 changed files
with
330 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
// } | ||
} |