mirror of
https://github.com/MathMan05/Fermi.git
synced 2026-10-23 10:42:23 +00:00
syntax highlighting
This commit is contained in:
12 files changed
+806
-14
No files matched your search
@@ -0,0 +1,20 @@
|
||||
[
|
||||
{
|
||||
"names":["js","ts","javascript"],
|
||||
"keywords": ["break", "case", "catch", "class", "const", "continue", "debugger", "default", "delete", "do","else", "export", "extends", "finally", "for", "function", "if", "import", "in", "instanceof", "new", "return", "super", "switch", "this", "throw", "try", "typeof", "var", "void", "while", "with", "yield", "let", "await", "true", "false", "null", "undefined"],
|
||||
"firstLineShabang": true,
|
||||
"hashComments": false,
|
||||
"doubleSlashComments": true,
|
||||
"multilineSlashComments": true,
|
||||
"JSLikeTemplateStrings": true
|
||||
},
|
||||
{
|
||||
"names":["h","c"],
|
||||
"keywords":["auto", "break", "case", "char", "continue", "do", "default", "const", "double", "else", "enum", "extern", "for", "if", "goto", "float", "int", "long", "register", "return", "signed", "static", "sizeof", "short", "struct", "switch", "typedef", "union", "void", "while", "volatile", "unsigned"],
|
||||
"firstLineShabang": false,
|
||||
"hashComments": true,
|
||||
"doubleSlashComments": true,
|
||||
"multilineSlashComments": false,
|
||||
"JSLikeTemplateStrings": false
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,105 @@
|
||||
import {hcolors} from "../colors.js";
|
||||
|
||||
interface clikeConf {
|
||||
names: string[];
|
||||
keywords: string[];
|
||||
firstLineShabang: boolean;
|
||||
hashComments: boolean;
|
||||
doubleSlashComments: boolean;
|
||||
multilineSlashComments: boolean;
|
||||
JSLikeTemplateStrings: boolean;
|
||||
}
|
||||
function regex(config: clikeConf): RegExp {
|
||||
const conds = [] as string[];
|
||||
if (config.hashComments) {
|
||||
conds.push("#.*");
|
||||
}
|
||||
if (config.doubleSlashComments) {
|
||||
conds.push("\\/\\/.*");
|
||||
}
|
||||
if (config.multilineSlashComments) {
|
||||
conds.push("\\/\\*([^*]|\\*([^\\/]|$))*(\\*\\/)?");
|
||||
}
|
||||
if (config.multilineSlashComments) {
|
||||
conds.push("\\/\\*([^*]|\\*([^\\/]|$))*(\\*\\/)?");
|
||||
}
|
||||
if (config.JSLikeTemplateStrings) {
|
||||
conds.push("`(\\\\.|[^`])*`?");
|
||||
}
|
||||
conds.push("(.|\n)");
|
||||
return new RegExp(
|
||||
'"(\\\\(.|\\n)|[^"\\n\\\\])*"?|\'(\\\\(.|\\n)|[^"\\n\\\\])*\'?|\\s+|[a-zA-Z][a-zA-Z0-9]*|[0-9][a-zA-Z0-9]*\\.?[a-zA-Z0-9]*|' +
|
||||
conds.join("|"),
|
||||
"gm",
|
||||
);
|
||||
}
|
||||
let j: undefined | clikeConf[] = undefined;
|
||||
export async function canLex(lang: string) {
|
||||
if (!j) {
|
||||
j = await (await fetch("/highlighter/clike/langs.json")).json();
|
||||
}
|
||||
const conf = j!.find((_) => _.names.includes(lang));
|
||||
if (conf) {
|
||||
return (code: string) => {
|
||||
return lex(code, conf);
|
||||
};
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
function* lex(code: string, config: clikeConf) {
|
||||
const r = regex(config);
|
||||
const keywords = new Set(config.keywords);
|
||||
if (config.firstLineShabang) {
|
||||
const m = code.match(/^#!.*\n?/gm);
|
||||
if (m) {
|
||||
code = code.replace(/^#!.*\n?/gm, "");
|
||||
yield {
|
||||
type: hcolors.comment,
|
||||
content: m[0],
|
||||
};
|
||||
}
|
||||
}
|
||||
for (const [lex] of code.matchAll(r)) {
|
||||
if (lex.startsWith("//") && config.doubleSlashComments) {
|
||||
yield {
|
||||
type: hcolors.comment,
|
||||
content: lex,
|
||||
};
|
||||
} else if (lex.startsWith("/*") && config.multilineSlashComments) {
|
||||
yield {
|
||||
type: hcolors.comment,
|
||||
content: lex,
|
||||
};
|
||||
} else if (lex.startsWith("#") && config.hashComments) {
|
||||
yield {
|
||||
type: hcolors.comment,
|
||||
content: lex,
|
||||
};
|
||||
} else if (lex.startsWith("`") && config.JSLikeTemplateStrings) {
|
||||
yield {
|
||||
type: hcolors.string,
|
||||
content: lex,
|
||||
};
|
||||
} else if (lex.match(/^("|')/)) {
|
||||
yield {
|
||||
type: hcolors.string,
|
||||
content: lex,
|
||||
};
|
||||
} else if (lex.match(/^[0-9]/)) {
|
||||
yield {
|
||||
type: hcolors.number,
|
||||
content: lex,
|
||||
};
|
||||
} else if (lex.match(/^[a-zA-Z]/)) {
|
||||
yield {
|
||||
type: keywords.has(lex) ? hcolors.keyword : hcolors.identifier,
|
||||
content: lex,
|
||||
};
|
||||
} else {
|
||||
yield {
|
||||
type: hcolors.symbol,
|
||||
content: lex,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
export const enum hcolors {
|
||||
keyword,
|
||||
number,
|
||||
string,
|
||||
symbol,
|
||||
comment,
|
||||
identifier,
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
import {hcolors} from "./colors";
|
||||
function ifCSSAllows() {
|
||||
if (!("CSS" in globalThis && CSS.highlights && globalThis.Highlight)) return () => {};
|
||||
const strLen = (str: string) => {
|
||||
return [...str].length;
|
||||
};
|
||||
const highkeyword = new Highlight();
|
||||
CSS.highlights.set("highkeyword", highkeyword);
|
||||
|
||||
const highnumber = new Highlight();
|
||||
CSS.highlights.set("highnumber", highnumber);
|
||||
|
||||
const highstring = new Highlight();
|
||||
CSS.highlights.set("highstring", highstring);
|
||||
|
||||
const highsymbol = new Highlight();
|
||||
CSS.highlights.set("highsymbol", highsymbol);
|
||||
const highcomment = new Highlight();
|
||||
CSS.highlights.set("highcomment", highcomment);
|
||||
|
||||
const highidentifier = new Highlight();
|
||||
CSS.highlights.set("highidentifier", highidentifier);
|
||||
const f = new FinalizationRegistry<string>((id) => {
|
||||
eliMap.delete(id);
|
||||
});
|
||||
const eliMap = new Map<string, [Highlight, Range][]>();
|
||||
function addToWeakRange(r: Range, elm: HTMLElement, h: Highlight) {
|
||||
let prop = elm.getAttribute("UUID");
|
||||
if (!prop) {
|
||||
prop = Math.random() + "";
|
||||
elm.setAttribute("UUID", prop);
|
||||
f.register(elm, prop);
|
||||
}
|
||||
let r2: [Highlight, Range][] | undefined = eliMap.get(prop);
|
||||
if (!r2) {
|
||||
r2 = [] as [Highlight, Range][];
|
||||
eliMap.set(prop, r2);
|
||||
}
|
||||
r2.push([h, r]);
|
||||
}
|
||||
function clearHighlights(elm: HTMLElement) {
|
||||
let prop = elm.getAttribute("UUID");
|
||||
if (!prop) return;
|
||||
const m = eliMap.get(prop);
|
||||
if (!m) return;
|
||||
eliMap.delete(prop);
|
||||
for (const [h, r] of m) {
|
||||
h.delete(r);
|
||||
}
|
||||
}
|
||||
|
||||
function highlight(r: Range, color: hcolors, elm: HTMLElement) {
|
||||
switch (color) {
|
||||
case hcolors.keyword:
|
||||
highkeyword.add(r);
|
||||
addToWeakRange(r, elm, highkeyword);
|
||||
break;
|
||||
case hcolors.number:
|
||||
highnumber.add(r);
|
||||
addToWeakRange(r, elm, highnumber);
|
||||
break;
|
||||
case hcolors.comment:
|
||||
highcomment.add(r);
|
||||
addToWeakRange(r, elm, highcomment);
|
||||
break;
|
||||
case hcolors.identifier:
|
||||
highidentifier.add(r);
|
||||
addToWeakRange(r, elm, highidentifier);
|
||||
break;
|
||||
case hcolors.string:
|
||||
highstring.add(r);
|
||||
addToWeakRange(r, elm, highstring);
|
||||
break;
|
||||
case hcolors.symbol:
|
||||
highsymbol.add(r);
|
||||
addToWeakRange(r, elm, highsymbol);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return function highlightFromLex(
|
||||
elm: HTMLElement,
|
||||
lex: Iterable<{
|
||||
type: hcolors;
|
||||
content: string;
|
||||
}>,
|
||||
skipStart: number,
|
||||
) {
|
||||
clearHighlights(elm);
|
||||
const nodes = Array.from(elm.childNodes) as Text[];
|
||||
let r = new Range();
|
||||
const lexed = Array.from(lex);
|
||||
if (skipStart) {
|
||||
lexed.unshift({type: hcolors.symbol, content: " ".repeat(skipStart)});
|
||||
}
|
||||
if (lexed.length === 0) return;
|
||||
let i = 1;
|
||||
let lenToGo = strLen(lexed[0].content);
|
||||
let t = lexed[0].type;
|
||||
r.setStart(nodes[0], 0);
|
||||
function getNextNode() {
|
||||
if (!lexed[i]) {
|
||||
const last = nodes.at(-1) as Text;
|
||||
r.setEnd(last, strLen(last.textContent));
|
||||
highlight(r, t, elm);
|
||||
return -1;
|
||||
} else highlight(r, t, elm);
|
||||
lenToGo = strLen(lexed[i].content);
|
||||
t = lexed[i].type;
|
||||
i++;
|
||||
return 0;
|
||||
}
|
||||
|
||||
for (const node of nodes) {
|
||||
let len = strLen(node.textContent);
|
||||
let start = 0;
|
||||
while (len) {
|
||||
if (len < lenToGo) {
|
||||
lenToGo -= len;
|
||||
break;
|
||||
} else {
|
||||
start += lenToGo;
|
||||
r.setEnd(node, start);
|
||||
len -= lenToGo;
|
||||
if (getNextNode() === -1) return;
|
||||
r = new Range();
|
||||
r.setStart(node, start);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
export const highlightFromLex = ifCSSAllows();
|
||||
@@ -0,0 +1,34 @@
|
||||
import {canLex} from "./clike/lex.js";
|
||||
import {highlightFromLex} from "./highlight.js";
|
||||
import {lex as pylex} from "./python/lex.js";
|
||||
import {lex as xmlex} from "./xml/lex.js";
|
||||
export async function highlight(
|
||||
elm: HTMLElement,
|
||||
lang: string,
|
||||
skipStart: number = 0,
|
||||
skipEnd: number = 0,
|
||||
) {
|
||||
await new Promise((res) => setTimeout(res));
|
||||
lang = lang.trim();
|
||||
let content = elm.textContent;
|
||||
if (skipStart) {
|
||||
content = content.slice(skipStart);
|
||||
}
|
||||
if (skipEnd) {
|
||||
content = content.slice(0, content.length - skipEnd);
|
||||
}
|
||||
if (lang === "py" || lang === "python") {
|
||||
highlightFromLex(elm, pylex(content), skipStart);
|
||||
} else if (
|
||||
lang === "xml" ||
|
||||
lang === "html" ||
|
||||
lang === "htmlx" ||
|
||||
lang === "qml" ||
|
||||
lang === "gmx"
|
||||
) {
|
||||
highlightFromLex(elm, xmlex(content), skipStart);
|
||||
} else {
|
||||
const clike = await canLex(lang);
|
||||
if (clike) highlightFromLex(elm, clike(content), skipStart);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
import {hcolors} from "../colors.js";
|
||||
|
||||
const r =
|
||||
/"""((\\(.|\n)|[^"\\]|"(?!")|""(?!"))*)(""")?|"(\\(.|\n)|[^"\n\\])*"?|'(\\(.|\n)|[^"\n\\])*'?|\s+|#.*|[~()\[\]|:;`=+\-*\/\\&^%$.]|[a-zA-Z][a-zA-Z0-9]*|[0-9][a-zA-Z0-9]*\.?[a-zA-Z0-9]*|./gm;
|
||||
const keywords = new Set([
|
||||
"False",
|
||||
"None",
|
||||
"True",
|
||||
"and",
|
||||
"as",
|
||||
"assert",
|
||||
"async",
|
||||
"await",
|
||||
"break",
|
||||
"class",
|
||||
"continue",
|
||||
"def",
|
||||
"del",
|
||||
"elif",
|
||||
"else",
|
||||
"except",
|
||||
"finally",
|
||||
"for",
|
||||
"from",
|
||||
"global",
|
||||
"if",
|
||||
"import",
|
||||
"in",
|
||||
"is",
|
||||
"lambda",
|
||||
"nonlocal",
|
||||
"not",
|
||||
"or",
|
||||
"pass",
|
||||
"raise",
|
||||
"return",
|
||||
"try",
|
||||
"while",
|
||||
"with",
|
||||
"yield",
|
||||
]);
|
||||
export function* lex(code: string) {
|
||||
for (const [lex] of code.matchAll(r)) {
|
||||
if (lex.startsWith("#")) {
|
||||
yield {
|
||||
type: hcolors.comment,
|
||||
content: lex,
|
||||
};
|
||||
} else if (lex.match(/^("|')/)) {
|
||||
yield {
|
||||
type: hcolors.string,
|
||||
content: lex,
|
||||
};
|
||||
} else if (lex.match(/^[0-9]/)) {
|
||||
yield {
|
||||
type: hcolors.number,
|
||||
content: lex,
|
||||
};
|
||||
} else if (lex.match(/^[a-zA-Z]/)) {
|
||||
yield {
|
||||
type: keywords.has(lex) ? hcolors.keyword : hcolors.identifier,
|
||||
content: lex,
|
||||
};
|
||||
} else {
|
||||
yield {
|
||||
type: hcolors.symbol,
|
||||
content: lex,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import {hcolors} from "../colors.js";
|
||||
|
||||
const r =
|
||||
/"(\\(.|\n)|[^"\n\\])*"?|<!--(([^-])|-[^-]|--[^>])*(-->)?|[a-zA-Z][a-zA-Z0-9]*\s*=|<[\s\/]*[^<> ]+|.|\n/gm;
|
||||
|
||||
export function* lex(code: string) {
|
||||
for (const [lex] of code.matchAll(r)) {
|
||||
if (lex.startsWith("<!--")) {
|
||||
yield {
|
||||
type: hcolors.comment,
|
||||
content: lex,
|
||||
};
|
||||
} else if (lex.startsWith('"')) {
|
||||
yield {
|
||||
type: hcolors.string,
|
||||
content: lex,
|
||||
};
|
||||
} else if (lex.startsWith("<")) {
|
||||
yield {
|
||||
type: hcolors.identifier,
|
||||
content: lex,
|
||||
};
|
||||
} else if (lex.match(/[a-zA-Z][a-zA-Z0-9]*\s*=/)) {
|
||||
yield {
|
||||
type: hcolors.number,
|
||||
content: lex,
|
||||
};
|
||||
} else {
|
||||
yield {
|
||||
type: hcolors.symbol,
|
||||
content: lex,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import {Guild} from "./guild.js";
|
||||
import {I18n} from "./i18n.js";
|
||||
import {Dialog} from "./settings.js";
|
||||
import {Contextmenu} from "./contextmenu.js";
|
||||
import {highlight} from "./highlighter/index.js";
|
||||
const linkMenu = new Contextmenu<string, void>("copyLink", true);
|
||||
linkMenu.addButton(
|
||||
() => I18n.copyRegLink(),
|
||||
@@ -324,6 +325,7 @@ class MarkDown {
|
||||
let find = 0;
|
||||
let j = i + count;
|
||||
let init = true;
|
||||
let lang = "";
|
||||
for (; txt[j] !== undefined && (txt[j] !== "\n" || count === 3) && find !== count; j++) {
|
||||
if (txt[j] === "`") {
|
||||
find++;
|
||||
@@ -336,6 +338,7 @@ class MarkDown {
|
||||
if (txt[j] === " " || txt[j] === "\n") {
|
||||
init = false;
|
||||
}
|
||||
lang += txt[j];
|
||||
if (keep) {
|
||||
build += txt[j];
|
||||
}
|
||||
@@ -367,6 +370,7 @@ class MarkDown {
|
||||
}
|
||||
pre.textContent = build;
|
||||
span.appendChild(pre);
|
||||
if (count === 3) highlight(pre, lang, keep ? 3 + lang.length : 0, keep ? 3 : 0);
|
||||
}
|
||||
i--;
|
||||
continue;
|
||||
|
||||
@@ -3366,6 +3366,25 @@ img.bigembedimg {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
::highlight(highkeyword) {
|
||||
color: yellow;
|
||||
}
|
||||
::highlight(highnumber) {
|
||||
color: orange;
|
||||
}
|
||||
::highlight(highstring) {
|
||||
color: red;
|
||||
}
|
||||
::highlight(highsymbol) {
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
}
|
||||
::highlight(highcomment) {
|
||||
color: green;
|
||||
}
|
||||
::highlight(highidentifier) {
|
||||
color: white;
|
||||
}
|
||||
#memberlisttoggle.svgicon {
|
||||
-moz-appearance: initial;
|
||||
&::before {
|
||||
|
||||
Reference in new issue
Block a user