Initial commit

This commit is contained in:
Reimar 2023-12-18 23:04:09 +01:00
commit 7327876db2
Signed by: Reimar
GPG Key ID: 93549FA07F0AE268
5 changed files with 230 additions and 0 deletions

BIN
fonts/JetBrainsMono-Regular.woff2 Executable file

Binary file not shown.

BIN
fonts/JetBrainsMonoNL-Regular.ttf Executable file

Binary file not shown.

28
index.html Executable file
View File

@ -0,0 +1,28 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>URL Editor</title>
<link rel="stylesheet" href="style.css">
<script src="main.js"></script>
</head>
<body>
<header>
<h1>URL Editor</h1>
Edit URL encoded strings
</header>
<main>
<label for="raw">Raw</label>
<br>
<div contenteditable spellcheck="false" id="raw" class="textarea" oninput="rawUpdated()"></div>
<label for="formatted">Formatted</label>
<br>
<div contenteditable spellcheck="false" id="formatted" class="textarea" oninput="formattedUpdated()"></div>
</main>
</body>
</html>

144
main.js Executable file
View File

@ -0,0 +1,144 @@
var string = "";
var escapeSequenceRegex = /(%.{2})+/gi;
onload = function() {
document.getElementById("raw").addEventListener("keydown", onKeydown)
document.getElementById("formatted").addEventListener("keydown", onKeydown)
function onKeydown(event) {
if (event.which === 13) {
string += "%0A";
update(event.target);
event.preventDefault();
}
}
}
function rawUpdated() {
string = document.getElementById("raw").innerText;
update(document.getElementById("raw"));
}
function formattedUpdated() {
string = encodeURIComponent(document.getElementById("formatted").innerText);
update(document.getElementById("formatted"));
}
function update(input) {
// Save caret position
var caretPos = getCaretPos(input);
// Create HTML for raw input
document.getElementById("raw").innerHTML =
string
.replace(escapeSequenceRegex, function(str) {
try {
decodeURIComponent(str);
return "<span class='escaped'>" + str + "</span>";
} catch(e) {
return "<span class='invalid'>" + str + "</span>";
}
})
.replace(/%.?$/, "<span class='invalid'>$&</span>"); // Unfinished escape sequence
// Create HTML for formatted input
var formatted = "";
for (var i = 0; i < string.length; i++) {
// URL escape sequence reached
if (string[i] === "%") {
var sequence = string.substr(i).match(escapeSequenceRegex);
// Contains valid characters
if (sequence) {
sequence = sequence[0];
try {
var decoded = decodeURIComponent(sequence)
.replace(/\n/g, "<br>");
formatted += "<span class='escaped'>" + decoded + "</span>";
} catch (e) {
formatted += "<span class='invalid'>" + sequence + "</span>";
}
i += sequence.length - 1;
// Unfinished escape sequence
} else {
formatted += "<span class='invalid'>" + string.substr(i, 3) + "</span>";
i += 2;
}
} else {
formatted += string[i];
}
}
document.getElementById("formatted").innerHTML = formatted;
// Restore caret position
setCaretPos(input, caretPos);
document.querySelectorAll("#raw > span, #formatted > span").forEach(function(escaped) {
escaped.onmouseenter = highlightSegment;
escaped.onmouseleave = unhighlightAll;
});
}
function highlightSegment(event) {
// Get node index (https://stackoverflow.com/questions/5913927/get-child-node-index#comment39914731_5913984)
var node = event.target;
for (var i = 0; (node = node.previousElementSibling); i++);
document.getElementById("formatted").children[i].className += " highlighted";
document.getElementById("raw").children[i].className += " highlighted";
}
function unhighlightAll() {
[].slice.call(document.getElementsByClassName("highlighted")).forEach(function(elem) {
elem.className = elem.className.replace("highlighted", "");
});
}
// why is manipulating the caret in a contenteditable div so painful
// https://stackoverflow.com/a/12500791
function getCaretPos(element) {
var range = window.getSelection().getRangeAt(0);
var selected = range.toString().length;
var preCaretRange = range.cloneRange();
preCaretRange.selectNodeContents(element);
preCaretRange.setEnd(range.endContainer, range.endOffset);
return preCaretRange.toString().length - selected;
}
// https://stackoverflow.com/a/36953852
function setCaretPos(element, pos) {
// Loop through all child nodes
for (var i = 0; i < element.childNodes.length; i++){
var node = element.childNodes[i];
if (node.nodeType === Node.TEXT_NODE) {
if (node.length >= pos){
// finally add our range
var range = document.createRange(),
sel = window.getSelection();
range.setStart(node, pos);
range.collapse(true);
sel.removeAllRanges();
sel.addRange(range);
return -1; // we are done
} else {
pos -= node.length;
}
} else {
pos = setCaretPos(node, pos);
if (pos === -1) {
return -1; // no need to finish the for loop
}
}
}
return pos; // needed because of recursion stuff
}

58
style.css Executable file
View File

@ -0,0 +1,58 @@
@font-face {
font-family: "JetBrains Mono";
src: url("fonts/JetBrainsMono-Regular.woff2") format("woff2"),
url("fonts/JetBrainsMonoNL-Regular.ttf") format("ttf");
}
body {
font-family: Arial, sans-serif;
}
header {
display: block;
text-align: center;
line-height: 0.5;
margin-bottom: 20px;
}
main {
display: block;
max-width: 800px;
margin: auto;
}
label {
font-size: 18px;
}
.textarea {
padding: 5px;
border-radius: 4px;
border: 1px solid black;
font-size: 12px;
font-family: "JetBrains Mono", monospace;
font-variant-ligatures: none;
-webkit-font-variant-ligatures: none;
resize: vertical;
outline: 1px solid white;
margin-bottom: 30px;
line-height: 1.7;
word-break: break-all;
word-wrap: break-word;
}
.textarea:focus {
border-color: #1E88E5;
outline-color: #1E88E5;
}
.textarea > span {
padding: 2px;
}
.text {
color: black;
}
.escaped {
background-color: #FFEE58;
}
.invalid {
background-color: #EF5350;
}
.highlighted {
box-shadow:
inset 2px 2px 0 rgba(0, 0, 0, 0.5),
inset -2px -2px 0 rgba(0, 0, 0, 0.5);
}