// 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)); });