From fee5666971fd3fb6510da9ddee1f1183c5907601 Mon Sep 17 00:00:00 2001 From: sfja Date: Tue, 17 Dec 2024 03:46:12 +0100 Subject: [PATCH] add syntax highlighting --- web/public/src/index.ts | 196 +++++++++++++++++++++++++++++++++++++++- web/public/style.css | 14 ++- 2 files changed, 206 insertions(+), 4 deletions(-) diff --git a/web/public/src/index.ts b/web/public/src/index.ts index 7d2ded7..a5d9cd4 100644 --- a/web/public/src/index.ts +++ b/web/public/src/index.ts @@ -40,6 +40,200 @@ async function checkStatus(): Promise<"running" | "done"> { return "done"; } +function syntaxHighlight(code: string): string { + const colors = { + colorBackground: "#282828", + colorForeground: "#fbf1c7", + colorKeyword: "#fb4934", + colorIdentifier: "#83a598", + colorOperator: "#fe8019", + colorSpecial: "#fe8019", + colorType: "#fabd2f", + colorBoolean: "#d3869b", + colorNumber: "#d3869b", + colorString: "#b8bb26", + colorComment: "#928374", + colorFunction: "#b8bb26", + colorLineNumber: "#7c6f64", + } as const; + + /* + Keyword = { link = "GruvboxRed" }, + Identifier = { link = "GruvboxBlue" }, + Operator = { fg = colors.orange, italic = config.italic.operators }, + Special = { link = "GruvboxOrange" }, + Type = { link = "GruvboxYellow" }, + Boolean = { link = "GruvboxPurple" }, + Number = { link = "GruvboxPurple" }, + String = { fg = colors.green, italic = config.italic.strings }, + Comment = { fg = colors.gray, italic = config.italic.comments }, + Function = { link = "GruvboxGreenBold" }, + */ + + let matches: { + index: number; + length: number; + color: string; + extra: string; + }[] = []; + + function addMatches(color: string, re: RegExp, extra = "") { + for (const match of code.matchAll(re)) { + matches.push({ + index: match.index, + length: match[1].length, + color, + extra, + }); + } + } + function addKeywordMatches(color: string, keywords: string[]) { + addMatches( + color, + new RegExp( + `(? `(?:${kw})`).join("|") + })(?!\\w)`, + "g", + ), + ); + } + + for (let i = 0; i < code.length; ++i) { + if (code[i] !== '"') { + continue; + } + let last = code[i]; + const index = i; + i += 1; + while (i < code.length && !(code[i] === '"' && last !== "\\")) { + last = code[i]; + i += 1; + } + if (i < code.length) { + i += 1; + } + matches.push({ + index, + length: i - index, + color: colors.colorString, + extra: "font-style: italic;", + }); + } + + { + let last = ""; + for (let i = 0; i < code.length; ++i) { + if (last === "/" && code[i] === "/") { + const index = i - 1; + while (i < code.length && code[i] !== "\n") { + i += 1; + } + matches.push({ + index, + length: i - index, + color: colors.colorComment, + extra: "font-style: italic;", + }); + } + last = code[i]; + } + } + + addKeywordMatches( + colors.colorKeyword, + [ + "break", + "return", + "let", + "fn", + "if", + "else", + "struct", + "import", + "or", + "and", + "not", + "while", + "for", + "in", + ], + ); + addKeywordMatches(colors.colorSpecial, ["null"]); + addKeywordMatches(colors.colorType, ["int", "string", "bool"]); + addKeywordMatches(colors.colorBoolean, ["false", "true"]); + addMatches( + colors.colorOperator, + new RegExp( + `(${ + [ + "\\+=", + "\\-=", + "\\+", + "\\->", + "\\-", + "\\*", + "/", + "==", + "!=", + "<=", + ">=", + "=", + "<", + ">", + "\\.", + "::<", + "::", + ":", + ].map((kw) => `(?:${kw})`).join("|") + })`, + "g", + ), + ); + addMatches( + colors.colorNumber, + /(0|(?:[1-9][0-9]*)|(?:0[0-7]+)|(?:0x[0-9a-fA-F]+)|(?:0b[01]+))/g, + ); + addMatches( + colors.colorFunction, + /([a-zA-Z_]\w*(?=\())/g, + "font-weight: 700;", + ); + addMatches(colors.colorIdentifier, /([a-z_]\w*)/g); + addMatches(colors.colorType, /([A-Z_]\w*)/g); + + matches = matches.reduce( + (acc, match) => + acc.find((m) => m.index === match.index) === undefined + ? (acc.push(match), acc) + : acc, + [], + ); + matches.sort((a, b) => a.index - b.index); + + let highlighted = ""; + let i = 0; + for (const match of matches) { + if (match.index < i) { + continue; + } + while (i < match.index) { + highlighted += code[i]; + i += 1; + } + const section = code.slice(match.index, match.index + match.length); + highlighted += + `${section}`; + i += section.length; + } + while (i < code.length) { + highlighted += code[i]; + i += 1; + } + + return highlighted; +} + function sourceCode(view: Element, codeData: string) { const outerContainer = document.createElement("div"); outerContainer.classList.add("code-container"); @@ -51,7 +245,7 @@ function sourceCode(view: Element, codeData: string) { const code = document.createElement("pre"); code.classList.add("code-source"); - code.innerText = codeData; + code.innerHTML = syntaxHighlight(codeData); innerContainer.append(lines, code); outerContainer.append(innerContainer); view.replaceChildren(outerContainer); diff --git a/web/public/style.css b/web/public/style.css index 166f7ba..c2dce17 100644 --- a/web/public/style.css +++ b/web/public/style.css @@ -11,6 +11,10 @@ --white: #ecebe9; --white-transparent: #ecebe9aa; --code-status: var(--white); + + --code-bg: #282828; + --code-fg: #fbf1c7; + --code-linenr: #7c6f64; } * { @@ -99,14 +103,19 @@ main #cover { #view .code-container { max-height: 100%; overflow: scroll; - background-color: rgba(255, 255, 255, 0.1); + background-color: var(--code-bg); padding: 0.5rem; border-radius: 0.5rem; } #view .code-container pre { font-family: "Roboto Mono", monospace; - font-weight: 600; + font-weight: 500; + color: var(--code-fg); +} + +#view .code-container pre.code-lines { + color: var(--code-linenr); } #view .code-container.code-coverage { @@ -121,7 +130,6 @@ main #cover { } #view .code-lines { - color: var(--white-transparent); border-right: 1px solid currentcolor; padding-right: 0.5rem; margin: 0;