Skip to content

Commit af5cd39

Browse files
committed
Reworkd format logic to use the markdown parser instead of the old hacky regex-based approach
1 parent fa5ce87 commit af5cd39

1 file changed

Lines changed: 53 additions & 85 deletions

File tree

src/components/format.ts

Lines changed: 53 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -13,60 +13,19 @@ import { Wheatley } from "../wheatley.js";
1313
import { MINUTE } from "../common.js";
1414
import { MessageContextMenuInteractionBuilder } from "../command-abstractions/context-menu.js";
1515
import { async_exec_file } from "../utils/filesystem.js";
16+
import { markdown_node, MarkdownParser, CodeBlockRule, InlineCodeRule, TextRule } from "dismark";
1617
import { Mutex } from "../utils/containers.js";
1718

1819
const color = 0x7e78fe; //0xA931FF;
1920

21+
const code_only_parser = new MarkdownParser([new CodeBlockRule(), new InlineCodeRule(), new TextRule()]);
22+
2023
const clang_format_path = "/usr/bin/clang-format";
2124

2225
const max_attachment_size = 1024 * 10;
2326

24-
// highlight js accepts all
25-
// prettier-ignore
26-
const languages = [
27-
"1c", "4d", "abnf", "accesslog", "actionscript", "ada", "adoc", "alan", "angelscript", "apache", "apacheconf",
28-
"applescript", "arcade", "arduino", "arm", "armasm", "as", "asc", "asciidoc", "aspectj", "atom", "autohotkey",
29-
"autoit", "avrasm", "awk", "axapta", "bash", "basic", "bat", "bbcode", "bf", "bind", "blade", "bnf", "brainfuck",
30-
"c", "c++", "cal", "capnp", "capnproto", "cc", "chaos", "chapel", "chpl", "cisco", "clj", "clojure", "cls",
31-
"cmake.in", "cmake", "cmd", "coffee", "coffeescript", "console", "coq", "cos", "cpc", "cpp", "cr", "craftcms",
32-
"crm", "crmsh", "crystal", "cs", "csharp", "cshtml", "cson", "csp", "css", "cxx", "cypher", "d", "dart", "delphi",
33-
"dfm", "diff", "django", "dns", "docker", "dockerfile", "dos", "dpr", "dsconfig", "dst", "dts", "dust", "dylan",
34-
"ebnf", "elixir", "elm", "erl", "erlang", "excel", "extempore", "f90", "f95", "fix", "fortran", "freepascal", "fs",
35-
"fsharp", "gams", "gauss", "gawk", "gcode", "gdscript", "gemspec", "gf", "gherkin", "glimmer", "glsl", "gms", "gn",
36-
"gni", "go", "godot", "golang", "golo", "gololang", "gradle", "graph", "groovy", "gss", "gyp", "h", "h++", "haml",
37-
"handlebars", "haskell", "haxe", "hbs", "hbs", "hcl", "hh", "hlsl", "hpp", "hs", "html.handlebars",
38-
"html.handlebars", "html.hbs", "html.hbs", "html", "htmlbars", "http", "https", "hx", "hxx", "hy", "hylang", "i",
39-
"i7", "iced", "iecst", "inform7", "ini", "ino", "instances", "iol", "irb", "irpf90", "java", "javascript", "jinja",
40-
"jolie", "js", "json", "jsp", "jsx", "julia-repl", "julia", "k", "kaos", "kdb", "kotlin", "kt", "lasso",
41-
"lassoscript", "lazarus", "ldif", "leaf", "lean", "less", "lfm", "lisp", "livecodeserver", "livescript", "ln",
42-
"lpr", "ls", "ls", "lua", "mak", "make", "makefile", "markdown", "mathematica", "matlab", "mawk", "maxima", "md",
43-
"mel", "mercury", "mirc", "mizar", "mk", "mkd", "mkdown", "ml", "ml", "mm", "mma", "mojolicious", "monkey", "moon",
44-
"moonscript", "mrc", "n1ql", "nawk", "nc", "never", "nginx", "nginxconf", "nim", "nimrod", "nix", "nsis", "obj-c",
45-
"obj-c++", "objc", "objective-c++", "objectivec", "ocaml", "ocl", "ol", "openscad", "osascript", "oxygene", "p21",
46-
"papyrus", "parser3", "pas", "pascal", "patch", "pcmk", "perl", "pf.conf", "pf", "pgsql", "php", "php3", "php4",
47-
"php5", "php6", "php7", "php8", "pl", "plaintext", "plist", "pm", "podspec", "pony", "postgres", "postgresql",
48-
"powershell", "pp", "processing", "profile", "prolog", "properties", "protobuf", "ps", "ps1", "psc", "puppet", "py",
49-
"pycon", "python-repl", "python", "qml", "qsharp", "r", "razor-cshtml", "razor", "rb", "re", "reasonml", "rebol",
50-
"red-system", "red", "redbol", "rf", "rib", "risc", "riscript", "robot", "rpm-spec", "rpm-specfile", "rpm", "rs",
51-
"rsl", "rss", "ruby", "ruleslanguage", "rust", "sas", "SAS", "sc", "scad", "scala", "scheme", "sci", "scilab",
52-
"scl", "scss", "sh", "shell", "shexc", "smali", "smalltalk", "sml", "sol", "solidity", "spec", "specfile", "spl",
53-
"sql", "st", "stan", "stanfuncs", "stata", "step", "stl", "stp", "structured-text", "styl", "stylus", "subunit",
54-
"supercollider", "svelte", "svg", "swift", "tao", "tap", "tcl", "terraform", "tex", "text", "tf", "thor", "thrift",
55-
"tk", "toml", "tp", "ts", "tsql", "twig", "txt", "typescript", "unicorn-rails-log", "v", "vala", "vb", "vba",
56-
"vbnet", "vbs", "vbscript", "verilog", "vhdl", "vim", "wl", "x++", "x86asm", "xhtml", "xjb", "xl", "xls", "xlsx",
57-
"xml", "xpath", "xq", "xquery", "xsd", "xsl", "xtlang", "xtm", "yaml", "yml", "zenscript", "zep", "zephir", "zone",
58-
"zs", "zsh"
59-
];
60-
6127
const c_cpp_language_codes = new Set(["c", "h", "cpp", "hpp", "cc", "hh", "cxx", "cxx", "c++", "h++"]);
6228

63-
const languages_re = new RegExp(
64-
languages
65-
.sort((a, b) => b.length - a.length)
66-
.map(x => x.replaceAll("+", "\\+"))
67-
.join("|"),
68-
);
69-
7029
const code_begin = [
7130
"//",
7231
"/\\*",
@@ -109,8 +68,6 @@ function word_boundary(regex: string) {
10968

11069
const code_begin_re = new RegExp(`(?:${code_begin.map(word_boundary).join("|")})`);
11170

112-
const code_block_re = new RegExp(`(\`\`\`(?:${languages_re.source}\b)?)(.*?)\`\`\``, "gims");
113-
11471
const default_clang_format_language = "cpp";
11572

11673
const ignore_prefixes = [";compile", ";asm"];
@@ -148,48 +105,55 @@ export async function clang_format_general(text: string) {
148105
return await clang_format(text, [`-style={${clang_format_style.join(", ")}}`]);
149106
}
150107

151-
// https://stackoverflow.com/questions/12568097/how-can-i-replace-a-string-by-range
152-
function replace_range(s: string, start: number, end: number, substitute: string) {
153-
return s.substring(0, start) + substitute + s.substring(end);
154-
}
155-
156-
async function format(replying_to: Discord.Message) {
157-
let content = replying_to.content;
158-
// does the message have code blocks?
159-
const code_blocks: { language: string; content: string }[] = [];
160-
content = content.replaceAll(code_block_re, (_, starter: string, block: string) => {
161-
const language = starter.length > 3 ? starter.substring(3) : "cpp";
162-
code_blocks.push({ language, content: block });
163-
return `<[<[<[<[${code_blocks.length - 1}]>]>]>]>`;
164-
});
165-
// else ...
166-
if (code_blocks.length == 0) {
167-
const start = content.search(code_begin_re);
168-
if (start > -1) {
169-
const end = Math.max(...[...";}"].map(c => content.lastIndexOf(c)));
170-
if (end > start) {
171-
code_blocks.push({
172-
language: default_clang_format_language,
173-
content: content.substring(start, end + 1),
174-
});
175-
content = replace_range(content, start, end + 1, `<[<[<[<[${code_blocks.length - 1}]>]>]>]>`);
108+
async function format_message_content(ast: markdown_node): Promise<{ content: string; found_code: boolean }> {
109+
let found_code = false;
110+
111+
const format_node = async (node: markdown_node): Promise<string> => {
112+
switch (node.type) {
113+
case "doc":
114+
return (await Promise.all(node.content.map(format_node))).join("");
115+
case "code_block": {
116+
found_code = true;
117+
const language = node.language ?? default_clang_format_language;
118+
if (c_cpp_language_codes.has(language)) {
119+
const formatted = await clang_format_general(node.content);
120+
return `\`\`\`${node.language ?? ""}\n${formatted}\n\`\`\``;
121+
} else {
122+
return `\`\`\`${node.language ?? ""}\n${node.content}\n\`\`\``;
123+
}
176124
}
125+
case "inline_code":
126+
return `\`${node.content}\``;
127+
case "plain": {
128+
const code_start = node.content.search(code_begin_re);
129+
if (code_start > -1) {
130+
const end = Math.max(...[...";}"].map(c => node.content.lastIndexOf(c)));
131+
if (end > code_start) {
132+
found_code = true;
133+
const code_content = node.content.substring(code_start, end + 1);
134+
const formatted = await clang_format_general(code_content);
135+
return (
136+
node.content.substring(0, code_start) +
137+
`\`\`\`${default_clang_format_language}\n${formatted}\n\`\`\`` +
138+
node.content.substring(end + 1)
139+
);
140+
}
141+
}
142+
return node.content;
143+
}
144+
default:
145+
throw new Error(`Unknown ast node ${(node as markdown_node).type}`);
177146
}
178-
}
147+
};
179148

180-
for (const [i, block] of code_blocks.entries()) {
181-
if (c_cpp_language_codes.has(block.language)) {
182-
content = content.replace(
183-
`<[<[<[<[${i}]>]>]>]>`,
184-
`\`\`\`${block.language}\n${await clang_format_general(block.content)}\n\`\`\``,
185-
);
186-
} else {
187-
// don't format, just put it back
188-
content = content.replace(`<[<[<[<[${i}]>]>]>]>`, `\`\`\`${block.language}\n${block.content}\n\`\`\``);
189-
}
190-
}
149+
const content = await format_node(ast);
150+
return { content, found_code };
151+
}
152+
153+
async function format(replying_to: Discord.Message) {
154+
const ast = code_only_parser.parse(replying_to.content);
155+
const { content, found_code } = await format_message_content(ast);
191156

192-
// does the message have attachments?
193157
const attachments = await Promise.all(
194158
[...replying_to.attachments.values()]
195159
.filter(attachment => attachment.contentType?.startsWith("text/") ?? false)
@@ -208,7 +172,11 @@ async function format(replying_to: Discord.Message) {
208172
}),
209173
);
210174

211-
return { content, attachments, found_code_blocks: code_blocks.length > 0 };
175+
return {
176+
content,
177+
attachments,
178+
found_code_blocks: found_code,
179+
};
212180
}
213181

214182
function should_replace_original(replying_to: Discord.Message, request_timestamp: Date) {

0 commit comments

Comments
 (0)