web_python_editor/editor.js

265 lines
10 KiB
JavaScript

// Kyler Olsen
// November 2023
class PythonEditor{
constructor(ele) {
this.element = ele;
this.element.addEventListener("input", this.textInput.bind(this));
this.element.addEventListener("keydown", this.handleKeyDown.bind(this));
this.view_code = document.createElement("code");
this.edit_code = document.createElement("code");
this.view_pre = document.createElement("pre");
this.edit_pre = document.createElement("pre");
this.edit_code.contentEditable = true;
this.edit_code.spellcheck = false;
this.view_pre.classList.add("view-code");
this.edit_pre.classList.add("edit-code");
this.view_pre.appendChild(this.view_code);
this.edit_pre.appendChild(this.edit_code);
this.element.appendChild(this.view_pre);
this.element.appendChild(this.edit_pre);
}
textInput(e) {
let text = this.edit_code.innerText;
this.view_code.innerHTML = "";
syntax_highlight_html(text).forEach(ele => this.view_code.appendChild(ele));
}
handleKeyDown(e) {
if (e.key === "Tab") {
e.preventDefault();
insertTab(e.target);
}
}
}
function insertTab() {
const selection = window.getSelection();
if (selection.rangeCount > 0) {
const range = selection.getRangeAt(0);
const startOffset = range.startOffset;
const lineStart = range.startContainer.textContent.lastIndexOf('\n', startOffset - 1) + 1;
const spacesToAdd = 4 - ((startOffset - lineStart) % 4);
const tabSpaces = ' '.repeat(spacesToAdd);
const tabNode = document.createTextNode(tabSpaces);
range.deleteContents();
range.insertNode(tabNode);
range.setStartAfter(tabNode);
range.setEndAfter(tabNode);
selection.removeAllRanges();
selection.addRange(range);
}
}
function syntax_highlight(text) {
const whitespace = " \n\t";
const string_start = `'"`;
const string_alt_start = [
`r'`, `r"`, `rf`, `rb`, `rF`, `rB`,
`R'`, `R"`, `Rf`, `Rb`, `RF`, `RB`,
`f'`, `f"`, `fr`, `fR`,
`F'`, `F"`, `Fr`, `FR`,
`b'`, `b"`, `br`, `bR`,
`B'`, `B"`, `Br`, `BR`,
];
const number_start = "0123456789.";
const number_second = "0123456789bBeEjJ0OxX._";
const number_dec_start = "123456789.";
const number_dec_continue = "0123456789._";
const number_bin_start = ["0b", "0B"];
const number_bin_continue = "01_";
const number_oct_start = ["0o", "0O"];
const number_oct_continue = "01234567_";
const number_hex_start = ["0x", "0X"];
const number_hex_continue = "0123456789abcdefABCEDF_";
const number_exp_start = "eE";
const number_exp_second = "+-0123456789";
const number_exp_continue = "0123456789_";
const tokens = [];
var char = text.substr(0, 1); text = text.substr(1);
while (text.length) {
const token = {'token_type': '', 'value':''};
// Whitespace
if (whitespace.includes(char)) {
token.token_type = "whitespace";
while (text.length && whitespace.includes(char)) {
token.value += char;
char = text.substr(0, 1); text = text.substr(1);
}
}
// Comments
else if (char == '#') {
token.token_type = "comment";
while (text.length && char != "\n") {
token.value += char;
char = text.substr(0, 1); text = text.substr(1);
}
}
// String literals
else if (string_start.includes(char) || (string_alt_start.includes(char + text.substr(0, 1)))) {
var depth = 1;
var start;
if (string_start.includes(char)) {
token.token_type = "string";
start = char;
} else {
token.value += char;
char = text.substr(0, 1); text = text.substr(1);
if (!string_start.includes(char)) {
token.value += char;
char = text.substr(0, 1); text = text.substr(1);
}
if (token.value.includes('r') || token.value.includes('R'))
token.token_type += "raw-";
if (token.value.includes('f') || token.value.includes('F'))
token.token_type += "f-";
if (token.value.includes('b') || token.value.includes('B'))
token.token_type += "byte-";
token.token_type += "string";
start = char;
}
if (text.substr(0, 1) == start && text.substr(1, 1) == start) depth = 3;
token.value += char;
char = text.substr(0, 1); text = text.substr(1);
while (text.length && depth && (depth > 1 || char != '\n')) {
token.value += char;
if (char == start && depth == 1) depth--;
else if (char == start && text.substr(0, 1) == start && depth == 2) depth--;
else if (char == start && text.substr(0, 1) == start && text.substr(1, 1) == start && depth == 3) depth--;
char = text.substr(0, 1); text = text.substr(1);
}
}
// Number literals
else if (number_start.includes(char)) {
token.token_type = "number";
if (number_dec_start.includes(char)) {
while (text.length && number_dec_continue.includes(char)) {
token.value += char;
char = text.substr(0, 1); text = text.substr(1);
}
if (number_exp_start.includes(char) && number_exp_second.includes(text.substr(0, 1))) {
token.value += char;
char = text.substr(0, 1); text = text.substr(1);
token.value += char;
char = text.substr(0, 1); text = text.substr(1);
while (text.length && number_exp_continue.includes(char)) {
token.value += char;
char = text.substr(0, 1); text = text.substr(1);
}
}
if ("jJ".includes(char)) {
token.value += char;
char = text.substr(0, 1); text = text.substr(1);
}
} else if (number_bin_start.includes(char + text.substr(0, 1))) {
token.value += char;
char = text.substr(0, 1); text = text.substr(1);
token.value += char;
char = text.substr(0, 1); text = text.substr(1);
while (text.length && number_bin_continue.includes(char)) {
token.value += char;
char = text.substr(0, 1); text = text.substr(1);
}
} else if (number_oct_start.includes(char + text.substr(0, 1))) {
token.value += char;
char = text.substr(0, 1); text = text.substr(1);
token.value += char;
char = text.substr(0, 1); text = text.substr(1);
while (text.length && number_oct_continue.includes(char)) {
token.value += char;
char = text.substr(0, 1); text = text.substr(1);
}
} else if (number_hex_start.includes(char + text.substr(0, 1))) {
token.value += char;
char = text.substr(0, 1); text = text.substr(1);
token.value += char;
char = text.substr(0, 1); text = text.substr(1);
while (text.length && number_hex_continue.includes(char)) {
token.value += char;
char = text.substr(0, 1); text = text.substr(1);
}
} else if (char == 0 && "eE".includes(text.substr(0, 1))) {
token.value += char;
char = text.substr(0, 1); text = text.substr(1);
if (number_exp_start.includes(char) && number_exp_second.includes(text.substr(0, 1))) {
token.value += char;
char = text.substr(0, 1); text = text.substr(1);
token.value += char;
char = text.substr(0, 1); text = text.substr(1);
while (text.length && number_exp_continue.includes(char)) {
token.value += char;
char = text.substr(0, 1); text = text.substr(1);
}
}
if ("jJ".includes(char)) {
token.value += char;
char = text.substr(0, 1); text = text.substr(1);
}
} else if (char == 0 && !number_second.includes(text.substr(0, 1))) {
token.value += char;
char = text.substr(0, 1); text = text.substr(1);
}
}
else {
token.token_type = "token";
token.value = char;
char = text.substr(0, 1); text = text.substr(1);
}
tokens.push(token);
tokens.push({'token_type': 'sep', 'value':'·'});
}
return tokens;
}
function syntax_highlight_html(text) {
const tokens = syntax_highlight(text);
const elements = [];
var last = "whitespace";
tokens.forEach(token => {
const element = document.createElement("span");
if (token.token_type == "whitespace")
element.classList.add("whitespace");
else if (token.token_type == "comment")
element.classList.add("comment");
else if (token.token_type.substr(-6) == "string") {
element.classList.add("literal");
element.classList.add("string");
} else if (token.token_type == "number") {
element.classList.add("literal");
element.classList.add("number");
} else if (token.token_type == "sep")
element.classList.add("token-sep");
else
element.classList.add("token");
element.innerText = token.value;
elements.push(element);
last = token.token_type;
})
return elements;
}
document.addEventListener('DOMContentLoaded', () => {
document.querySelectorAll(".code").forEach(ele => new PythonEditor(ele))
});