/** * Which language a code block is written in — as TELEGRAM understands it. * * Telegram highlights `
` (and the ```X fence, which
* becomes the same thing) with libprisma, its own C++ port of Prism:
* https://github.com/TelegramMessenger/libprisma. `X` is matched against Prism's
* language ids and aliases, and against NOTHING else: an id it does not know is not a
* fallback, it is no highlighting at all.
*
* That is why this file exists rather than a hopeful `path.split(".").pop()`. The
* names are not the ones people habitually type:
*
* - Rust is `rust`. There is no `rs`.
* - C++ is `cpp`. There is no `c++`.
* - Go is `go`. There is no `golang`.
* - But `py`, `ts`, `js`, `rb`, `yml`, `md`, `sh` ARE real aliases.
*
* So we do two things: map a FILE to its language (for the tool cards, which know the
* path but have no fence to read), and normalise a language the MODEL wrote (`rs` →
* `rust`), because an instruction is a request and this is a guarantee.
*/
/**
* Every language id and alias libprisma accepts, verbatim from its README (the
* "Supported Languages" table). Kept whole rather than pruned: it is the definition of
* what Telegram will highlight, and a shorter list would just be a guess about what
* someone might paste.
*/
export const TELEGRAM_CODE_LANGUAGES: ReadonlySet = new Set([
// markup and friends
"markup",
"html",
"xml",
"svg",
"mathml",
"ssml",
"atom",
"rss",
"css",
"clike",
"regex",
"javascript",
"js",
// the long tail, alphabetically as libprisma lists it
"abap",
"abnf",
"actionscript",
"ada",
"agda",
"al",
"antlr4",
"g4",
"apacheconf",
"sql",
"apex",
"apl",
"applescript",
"aql",
"c",
"cpp",
"arduino",
"ino",
"arff",
"armasm",
"arm-asm",
"bash",
"sh",
"shell",
"yaml",
"yml",
"markdown",
"md",
"arturo",
"art",
"asciidoc",
"adoc",
"csharp",
"cs",
"dotnet",
"aspnet",
"asm6502",
"asmatmel",
"autohotkey",
"autoit",
"avisynth",
"avs",
"avro-idl",
"avdl",
"awk",
"gawk",
"basic",
"batch",
"bbcode",
"shortcode",
"bbj",
"bicep",
"birb",
"bison",
"bnf",
"rbnf",
"bqn",
"brainfuck",
"brightscript",
"bro",
"cfscript",
"cfc",
"chaiscript",
"cil",
"cilkc",
"cilk-c",
"cilkcpp",
"cilk-cpp",
"cilk",
"clojure",
"cmake",
"cobol",
"coffeescript",
"coffee",
"concurnas",
"conc",
"csp",
"cooklang",
"ruby",
"rb",
"crystal",
"csv",
"cue",
"cypher",
"d",
"dart",
"dataweave",
"dax",
"dhall",
"diff",
"markup-templating",
"django",
"jinja2",
"dns-zone-file",
"dns-zone",
"docker",
"dockerfile",
"dot",
"gv",
"ebnf",
"editorconfig",
"eiffel",
"ejs",
"eta",
"elixir",
"elm",
"lua",
"etlua",
"erb",
"erlang",
"excel-formula",
"xlsx",
"xls",
"fsharp",
"factor",
"false",
"fift",
"firestore-security-rules",
"flow",
"fortran",
"ftl",
"func",
"gml",
"gamemakerlanguage",
"gap",
"gcode",
"gdscript",
"gedcom",
"gettext",
"po",
"git",
"glsl",
"gn",
"gni",
"linker-script",
"ld",
"go",
"go-module",
"go-mod",
"gradle",
"graphql",
"groovy",
"less",
"scss",
"textile",
"haml",
"handlebars",
"hbs",
"mustache",
"haskell",
"hs",
"haxe",
"hcl",
"hlsl",
"hoon",
"hpkp",
"hsts",
"json",
"webmanifest",
"uri",
"url",
"http",
"ichigojam",
"icon",
"icu-message-format",
"idris",
"idr",
"ignore",
"gitignore",
"hgignore",
"npmignore",
"inform7",
"ini",
"io",
"j",
"java",
"scala",
"php",
"javadoclike",
"javadoc",
"javastacktrace",
"jolie",
"jq",
"typescript",
"ts",
"jsdoc",
"n4js",
"n4jsd",
"json5",
"jsonp",
"jsstacktrace",
"julia",
"keepalived",
"keyman",
"kotlin",
"kt",
"kts",
"kusto",
"latex",
"tex",
"context",
"latte",
"scheme",
"lilypond",
"ly",
"liquid",
"lisp",
"emacs",
"elisp",
"emacs-lisp",
"livescript",
"llvm",
"log",
"lolcode",
"magma",
"makefile",
"mata",
"matlab",
"maxscript",
"mel",
"mermaid",
"metafont",
"mizar",
"mongodb",
"monkey",
"moonscript",
"moon",
"n1ql",
"nand2tetris-hdl",
"naniscript",
"nani",
"nasm",
"neon",
"nevod",
"nginx",
"nim",
"nix",
"nsis",
"objectivec",
"objc",
"ocaml",
"odin",
"opencl",
"openqasm",
"qasm",
"oz",
"parigp",
"parser",
"pascal",
"objectpascal",
"pascaligo",
"psl",
"pcaxis",
"px",
"peoplecode",
"pcode",
"perl",
"phpdoc",
"plant-uml",
"plantuml",
"plsql",
"powerquery",
"pq",
"mscript",
"powershell",
"processing",
"prolog",
"promql",
"properties",
"protobuf",
"stylus",
"twig",
"pug",
"puppet",
"purebasic",
"pbfasm",
"python",
"py",
"qsharp",
"qs",
"q",
"qml",
"qore",
"r",
"racket",
"rkt",
"cshtml",
"razor",
"jsx",
"tsx",
"reason",
"rego",
"renpy",
"rpy",
"rescript",
"res",
"rest",
"rip",
"roboconf",
"robotframework",
"robot",
"rust",
"sas",
"sass",
"shell-session",
"sh-session",
"shellsession",
"smali",
"smalltalk",
"smarty",
"sml",
"smlnj",
"solidity",
"sol",
"solution-file",
"sln",
"soy",
"splunk-spl",
"sqf",
"squirrel",
"stan",
"stata",
"iecst",
"supercollider",
"sclang",
"swift",
"systemd",
"tact",
"t4-templating",
"t4-cs",
"t4",
"vbnet",
"t4-vb",
"tap",
"tcl",
"tt2",
"toml",
"tremor",
"trickle",
"troy",
"tl",
"tlb",
"typoscript",
"tsconfig",
"unrealscript",
"uscript",
"uc",
"uorazor",
"v",
"vala",
"velocity",
"verilog",
"vhdl",
"vim",
"visual-basic",
"vb",
"vba",
"warpscript",
"wasm",
"web-idl",
"webidl",
"wgsl",
"wiki",
"wolfram",
"mathematica",
"nb",
"wl",
"wren",
"xeora",
"xeoracube",
"xojo",
"xquery",
"yang",
"zig",
]);
/**
* The names people (and models) reach for that libprisma does NOT know, and what they
* plainly meant. Every value here is a real id above — this is a correction, never an
* invention.
*/
const CORRECTIONS: Readonly> = {
rs: "rust",
golang: "go",
"c++": "cpp",
cplusplus: "cpp",
cxx: "cpp",
"c#": "csharp",
"objective-c": "objectivec",
python3: "python",
py3: "python",
node: "javascript",
nodejs: "javascript",
shellscript: "bash",
zsh: "bash",
console: "shell-session",
terminal: "shell-session",
htm: "html",
yamlfile: "yaml",
proto: "protobuf",
patch: "diff",
vue: "html",
tf: "hcl",
terraform: "hcl",
};
/** File extension → the language Telegram knows it by. */
const BY_EXTENSION: Readonly> = {
ts: "typescript",
mts: "typescript",
cts: "typescript",
tsx: "tsx",
js: "javascript",
mjs: "javascript",
cjs: "javascript",
jsx: "jsx",
rs: "rust",
py: "python",
pyi: "python",
rb: "ruby",
go: "go",
java: "java",
kt: "kotlin",
kts: "kotlin",
swift: "swift",
c: "c",
h: "c",
cpp: "cpp",
cc: "cpp",
cxx: "cpp",
hpp: "cpp",
hh: "cpp",
cs: "csharp",
php: "php",
sh: "bash",
bash: "bash",
zsh: "bash",
ps1: "powershell",
sql: "sql",
json: "json",
jsonc: "json",
json5: "json5",
yaml: "yaml",
yml: "yaml",
toml: "toml",
xml: "xml",
svg: "svg",
html: "html",
htm: "html",
css: "css",
scss: "scss",
sass: "sass",
less: "less",
md: "markdown",
markdown: "markdown",
lua: "lua",
r: "r",
jl: "julia",
dart: "dart",
ex: "elixir",
exs: "elixir",
erl: "erlang",
hs: "haskell",
scala: "scala",
clj: "clojure",
vim: "vim",
zig: "zig",
nim: "nim",
sol: "solidity",
tf: "hcl",
hcl: "hcl",
gradle: "gradle",
groovy: "groovy",
pl: "perl",
pm: "perl",
ml: "ocaml",
fs: "fsharp",
proto: "protobuf",
graphql: "graphql",
gql: "graphql",
tex: "latex",
diff: "diff",
patch: "diff",
bat: "batch",
cmd: "batch",
ini: "ini",
cfg: "ini",
cmake: "cmake",
log: "log",
csv: "csv",
};
/** Whole file names that name their own language (they have no useful extension). */
const BY_FILENAME: Readonly> = {
dockerfile: "docker",
makefile: "makefile",
"cmakelists.txt": "cmake",
".gitignore": "ignore",
".npmignore": "ignore",
".editorconfig": "editorconfig",
"go.mod": "go-module",
"go.sum": "go-module",
".env": "ini",
};
/**
* The language of the file at `path`, or undefined when we do not know it — in which
* case the block goes out untagged, which is honest: a wrong tag highlights the code
* as the wrong language, which is worse than plain text.
*/
export function languageForPath(path: string | undefined): string | undefined {
if (!path) return undefined;
const name = path.replace(/\\/g, "/").split("/").pop()?.trim().toLowerCase();
if (!name) return undefined;
const byName = BY_FILENAME[name];
if (byName) return byName;
// A dotfile (".gitignore") has no extension; anything else takes the last one.
const dot = name.lastIndexOf(".");
if (dot <= 0) return undefined;
return BY_EXTENSION[name.slice(dot + 1)];
}
/**
* The language a MODEL asked for, as Telegram will actually take it: a known id passes
* through, a common mistake is corrected (`rs` → `rust`), and anything else returns
* undefined so the fence is left untagged rather than tagged with a lie.
*/
export function normalizeLanguage(
name: string | undefined,
): string | undefined {
if (!name) return undefined;
const key = name.trim().toLowerCase();
if (!key) return undefined;
if (TELEGRAM_CODE_LANGUAGES.has(key)) return key;
return CORRECTIONS[key];
}
/** An opening code fence, capturing the info string the model wrote after the backticks. */
const FENCE = /^(\s{0,3})(`{3,}|~{3,})[ \t]*([^\s`]*)[^\n]*$/;
/**
* Rewrite the language tag on every opening fence in a Markdown text so Telegram can
* actually highlight it: `rs` becomes `rust`, `c++` becomes `cpp`, and a tag nobody
* knows is dropped rather than sent to be ignored.
*
* Only the fence LINE is touched — never a byte of the code inside it, and never a
* fence that is closing one. The model is told to tag its code (see the instructions);
* this is what makes the tag land even when it uses the name it is used to.
*/
export function normalizeCodeFences(markdown: string): string {
const lines = markdown.split("\n");
let openFence: string | null = null;
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
const match = FENCE.exec(line);
if (!match) continue;
const [, indent, fence, info] = match;
if (openFence) {
// Inside a block: only a fence of the same kind, at least as long, closes it.
if (
fence[0] === openFence[0] &&
fence.length >= openFence.length &&
!info
) {
openFence = null;
}
continue;
}
openFence = fence;
if (!info) continue;
const language = normalizeLanguage(info);
lines[i] = `${indent}${fence}${language ?? ""}`;
}
return lines.join("\n");
}