464 lines
18 KiB
JavaScript
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));
|
|
});
|