Skip to content
File

Blob: src/shared/web/syntax.ts

typescript200 lines
1// Syntax highlighting helpers for Highlight.js
2 
3/**
4 * Infer Highlight.js language slug from a filename/path.
5 * Returns null when unknown.
6 */
7export function inferHljsLang(fileName: string): string | null {
8 const name = fileName || "";
9 const lower = name.toLowerCase();
10 // Special filenames
11 if (lower === "makefile") return "makefile";
12 if (lower === "dockerfile" || lower.endsWith("/dockerfile")) return "dockerfile";
13 
14 const lastDot = lower.lastIndexOf(".");
15 const ext = lastDot >= 0 ? lower.slice(lastDot + 1) : "";
16 
17 switch (ext) {
18 case "ts":
19 case "tsx":
20 return "typescript";
21 case "js":
22 case "mjs":
23 case "cjs":
24 case "jsx":
25 return "javascript";
26 case "json":
27 return "json";
28 case "go":
29 return "go";
30 case "py":
31 return "python";
32 case "rb":
33 return "ruby";
34 case "php":
35 return "php";
36 case "java":
37 return "java";
38 case "c":
39 return "c";
40 case "cc":
41 case "cpp":
42 case "cxx":
43 case "hpp":
44 return "cpp";
45 case "cs":
46 return "csharp";
47 case "rs":
48 return "rust";
49 case "kt":
50 return "kotlin";
51 case "swift":
52 return "swift";
53 case "dart":
54 return "dart";
55 case "sql":
56 return "sql";
57 case "sh":
58 case "bash":
59 case "zsh":
60 return "bash"; // highlight.js alias for sh
61 case "ps1":
62 case "psm1":
63 return "powershell";
64 case "less":
65 return "less";
66 case "lua":
67 return "lua";
68 case "yml":
69 case "yaml":
70 return "yaml";
71 case "toml":
72 return "toml";
73 case "proto":
74 return "protobuf";
75 case "gradle":
76 return "groovy";
77 case "mm":
78 return "objectivec";
79 case "pl":
80 return "perl";
81 case "h":
82 return "cpp";
83 case "ini":
84 case "cfg":
85 case "conf":
86 return "ini";
87 case "md":
88 case "markdown":
89 return "markdown";
90 case "html":
91 case "htm":
92 case "xml":
93 case "svg":
94 return "xml"; // highlight.js uses xml for HTML/XML/SVG
95 case "css":
96 return "css";
97 case "scss":
98 case "sass":
99 return "scss";
100 case "diff":
101 return "diff";
102 default:
103 return null;
104 }
105}
106 
107/**
108 * Default small set of languages useful for README/markdown pages.
109 * Keep this list short to honor "load only when needed" while covering common fences.
110 */
111export function getMarkdownHighlightLangs(): string[] {
112 return ["typescript", "javascript", "json", "bash", "yaml", "diff", "go", "python"];
113}
114 
115/**
116 * Infer language from a shebang line (e.g., #!/usr/bin/env python3)
117 */
118function inferFromShebang(text: string): string | null {
119 const firstLine = (text || "").split(/\r?\n/, 1)[0];
120 if (!firstLine || !firstLine.startsWith("#!")) return null;
121 const lower = firstLine.toLowerCase();
122 if (lower.includes("python")) return "python";
123 if (lower.includes("node") || lower.includes("deno")) return "javascript";
124 if (lower.includes("bash") || lower.includes("sh") || lower.includes("zsh")) return "bash";
125 if (lower.includes("ruby")) return "ruby";
126 if (lower.includes("ts-node")) return "typescript";
127 return null;
128}
129 
130/**
131 * Smarter inference: use file extension first, then shebang fallback.
132 */
133export function inferHljsLangSmart(fileName: string, text?: string): string | null {
134 const byExt = inferHljsLang(fileName);
135 if (byExt) return byExt;
136 if (text) {
137 const byShebang = inferFromShebang(text);
138 if (byShebang) return byShebang;
139 }
140 return null;
141}
142 
143/**
144 * Languages to load for a blob using smart inference.
145 */
146export function getHighlightLangsForBlobSmart(fileName: string, text?: string): string[] {
147 const lang = inferHljsLangSmart(fileName, text);
148 const normalized = normalizeLangForCdn(lang);
149 return normalized ? [normalized] : [];
150}
151 
152// Supported language slugs on our pinned CDN build. Keep this conservative.
153const SUPPORTED_LANGS = new Set<string>([
154 "typescript",
155 "javascript",
156 "json",
157 "bash",
158 "yaml",
159 "diff",
160 "go",
161 "python",
162 "ini",
163 "xml",
164 "css",
165 "scss",
166 "less",
167 "java",
168 "c",
169 "cpp",
170 "csharp",
171 "rust",
172 "kotlin",
173 "swift",
174 "php",
175 "ruby",
176 "sql",
177 "powershell",
178 "dockerfile",
179 "makefile",
180 "nginx",
181 "objectivec",
182 "perl",
183 "protobuf",
184 "groovy",
185 "lua",
186]);
187 
188/**
189 * Normalize an inferred language slug to a CDN-available one.
190 * - Map missing or alias languages to closest available grammar (e.g., toml -> ini)
191 */
192export function normalizeLangForCdn(lang: string | null): string | null {
193 if (!lang) return null;
194 let slug = lang;
195 // Aliases or fallbacks
196 if (slug === "toml") slug = "ini"; // close enough for highlighting
197 // Ensure it's in our allowlist
198 return SUPPORTED_LANGS.has(slug) ? slug : null;
199}