web_python_editor/editor.js

464 lines
18 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 operator = "/*-+.@%^&><=";
const operators = [
"+", "-", "*", "**", "/", "//",
"%", "@", "<<", ">>", "&", "|",
"^", "~", ":=", "<", ">", "<=",
">=", "==", "!=", "@", "=", "+=",
"-=", "*=", "/=", "/", "/=", "%=",
"@=", "&=", "|=", "^=",
">>=", "<<=", "**="
];
const delimiters = "()[]{},:.;@";
const identifier_not_start =
`0123456789. \n\t\\\`$?"'#()[]{},:.;@+-*/%<>&|^~:=!`;
const identifier_end = ` \n\t\\\`$?"'#()[]{},:.;@+-*/%<>&|^~:=!`;
const keywords = [
"await", "else", "import", "pass", "break", "except", "raise", "class",
"finally", "return", "continue", "for", "lambda", "try", "def", "from",
"nonlocal", "while", "assert", "global", "with", "async", "elif", "if",
"yield",
];
const keywords_soft = [
"case", "match", "_",
];
const keyword_operators = [
"in", "is", "and", "as", "del", "not", "or",
];
const keywords_const = [
"False", "None", "True", "NotImplemented", "__debug__",
];
const builtins = [
"abs", "aiter", "all", "any", "anext", "ascii", "bin", "bool",
"breakpoint", "bytearray", "bytes", "callable", "chr", "classmethod",
"compile", "complex", "delattr", "dict", "dir", "divmod", "enumerate",
"eval", "exec", "filter", "float", "format", "frozenset", "getattr",
"globals", "hasattr", "hash", "help", "hex", "id", "input", "int",
"isinstance", "issubclass", "iter", "len", "list", "locals", "map",
"max", "memoryview", "min", "next", "object", "oct", "open", "ord",
"pow", "print", "property", "range", "repr", "reversed", "round", "set",
"setattr", "slice", "sorted", "staticmethod", "str", "sum", "super",
"tuple", "type", "vars", "zip", "__import__",
];
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 number_after_radix_point = "0123456789eEjJ";
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 == '\\') {
char = text.substr(0, 1); text = text.substr(1);
token.value += char;
char = text.substr(0, 1); text = text.substr(1);
continue;
} else 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);
}
}
// Delimiters
else if ((delimiters.includes(char) ||
"->" == char + text.substr(0, 1)) && !(char == "." &&
number_after_radix_point.includes(text.substr(0, 1)))) {
token.token_type = "punctuation";
token.value += char;
char = text.substr(0, 1); text = text.substr(1);
if (">" == char) {
token.value += char;
char = text.substr(0, 1); text = text.substr(1);
}
}
// Operators
else if (operators.includes(char + text.substr(0, 2))) {
token.token_type = "operator";
token.value += char + text.substr(0, 2);
char = text.substr(2, 1); text = text.substr(3);
} else if (operators.includes(char + text.substr(0, 1))) {
token.token_type = "operator";
token.value += char + text.substr(0, 1);
char = text.substr(1, 1); text = text.substr(2);
} else if (operators.includes(char)) {
token.token_type = "operator";
token.value += char;
char = text.substr(0, 1); text = text.substr(1);
}
// Keywords and Identifiers
else if (!identifier_not_start.includes(char)) {
token.token_type = "identifier";
while (text.length && !identifier_end.includes(char)) {
token.value += char;
char = text.substr(0, 1); text = text.substr(1);
}
if (keywords.includes(token.value))
token.token_type = "keyword";
else if (keywords_soft.includes(token.value))
token.token_type = "keyword_soft";
else if (keyword_operators.includes(token.value))
token.token_type = "keyword_operator";
else if (keywords_const.includes(token.value))
token.token_type = "keywords_const";
else if (builtins.includes(token.value))
token.token_type = "builtin";
}
// 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':'·'});
}
var last = {'token_type': 'token', 'value':''};
var last_not_whitespace = {'token_type': 'token', 'value':''};
for (var i = 0; i < tokens.length; i++) {
var next = {'token_type': 'token', 'value':''};
for (var j = i + 1; j < tokens.length; j++) {
if (tokens[j].token_type != "sep" &&
tokens[j].token_type != "whitespace") {
next = tokens[j];
break;
}
}
if (tokens[i].token_type == "builtin" &&
last_not_whitespace.value == '.')
tokens[i].token_type = "identifier";
else if (tokens[i].token_type == "identifier" &&
last_not_whitespace.value == 'def')
tokens[i].token_type = "function";
else if (tokens[i].token_type == "identifier" &&
last_not_whitespace.value == 'class')
tokens[i].token_type = "class";
else if (tokens[i].token_type == "keyword_soft") {
if (tokens[i].value == "match") {
if (last.value.includes('\n') &&
(next.token_type == "identifier" ||
next.token_type == "punctuation"))
tokens[i].token_type = "keyword";
else
tokens[i].token_type = "identifier";
}
else if (tokens[i].value == "case") {
if (last.value.includes('\n') &&
(next.token_type == "identifier" ||
next.token_type == "punctuation" ||
next.token_type == "keyword_soft"))
tokens[i].token_type = "keyword";
else
tokens[i].token_type = "identifier";
}
else if (tokens[i].value == "_") {
if (last_not_whitespace.value == "case" &&
last_not_whitespace.token_type == "keyword")
tokens[i].token_type = "keyword";
else
tokens[i].token_type = "identifier";
}
}
if (tokens[i].token_type != "sep") {
last = tokens[i];
if (tokens[i].token_type != "whitespace")
last_not_whitespace = tokens[i];
}
}
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 == "punctuation") {
element.classList.add("punctuation");
} else if (token.token_type == "operator") {
element.classList.add("operator");
} else if (token.token_type == "identifier") {
element.classList.add("identifier");
} else if (token.token_type == "function") {
element.classList.add("identifier");
element.classList.add("function");
} else if (token.token_type == "class") {
element.classList.add("identifier");
element.classList.add("class");
}
else if (token.token_type == "keyword") {
element.classList.add("keyword");
} else if (token.token_type == "keyword_operator") {
element.classList.add("keyword");
element.classList.add("operator");
} else if (token.token_type == "keywords_const") {
element.classList.add("keyword");
element.classList.add("constant");
} else if (token.token_type == "builtin") {
element.classList.add("builtin");
}
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));
});