Skip to main content

mbr_core/
embedded_hljs.rs

1//! Embedded highlight.js files for syntax highlighting.
2//!
3//! This module embeds all highlight.js core and language files, providing
4//! centralized access to syntax highlighting assets.
5
6/// highlight.js dark theme CSS
7pub const HLJS_DARK_CSS: &[u8] = include_bytes!("../templates/hljs.dark.11.12.0.css");
8/// highlight.js atom-one-dark theme CSS
9pub const HLJS_ATOM_ONE_DARK_CSS: &[u8] =
10    include_bytes!("../templates/hljs.atom-one-dark.11.12.0.css");
11
12/// highlight.js core library
13pub const HLJS_JS: &[u8] = include_bytes!("../templates/hljs.11.12.0.js");
14
15// Language modules
16pub const HLJS_LANG_BASH: &[u8] = include_bytes!("../templates/hljs.lang.bash.11.12.0.js");
17pub const HLJS_LANG_CSS: &[u8] = include_bytes!("../templates/hljs.lang.css.11.12.0.js");
18pub const HLJS_LANG_DOCKERFILE: &[u8] =
19    include_bytes!("../templates/hljs.lang.dockerfile.11.12.0.js");
20pub const HLJS_LANG_GO: &[u8] = include_bytes!("../templates/hljs.lang.go.11.12.0.js");
21pub const HLJS_LANG_JAVA: &[u8] = include_bytes!("../templates/hljs.lang.java.11.12.0.js");
22pub const HLJS_LANG_JAVASCRIPT: &[u8] =
23    include_bytes!("../templates/hljs.lang.javascript.11.12.0.js");
24pub const HLJS_LANG_JSON: &[u8] = include_bytes!("../templates/hljs.lang.json.11.12.0.js");
25pub const HLJS_LANG_MARKDOWN: &[u8] = include_bytes!("../templates/hljs.lang.markdown.11.12.0.js");
26pub const HLJS_LANG_NIX: &[u8] = include_bytes!("../templates/hljs.lang.nix.11.12.0.js");
27pub const HLJS_LANG_PYTHON: &[u8] = include_bytes!("../templates/hljs.lang.python.11.12.0.js");
28pub const HLJS_LANG_RUBY: &[u8] = include_bytes!("../templates/hljs.lang.ruby.11.12.0.js");
29pub const HLJS_LANG_RUST: &[u8] = include_bytes!("../templates/hljs.lang.rust.11.12.0.js");
30pub const HLJS_LANG_SCALA: &[u8] = include_bytes!("../templates/hljs.lang.scala.11.12.0.js");
31pub const HLJS_LANG_SQL: &[u8] = include_bytes!("../templates/hljs.lang.sql.11.12.0.js");
32pub const HLJS_LANG_TYPESCRIPT: &[u8] =
33    include_bytes!("../templates/hljs.lang.typescript.11.12.0.js");
34pub const HLJS_LANG_XML: &[u8] = include_bytes!("../templates/hljs.lang.xml.11.12.0.js");
35pub const HLJS_LANG_YAML: &[u8] = include_bytes!("../templates/hljs.lang.yaml.11.12.0.js");
36
37/// All highlight.js files as (url_path, bytes, mime_type) tuples.
38///
39/// The url_path is the path without version numbers for cleaner URLs.
40pub const HLJS_FILES: &[(&str, &[u8], &str)] = &[
41    (
42        "/hljs.atom-one-dark.css",
43        HLJS_ATOM_ONE_DARK_CSS,
44        "text/css",
45    ),
46    ("/hljs.js", HLJS_JS, "application/javascript"),
47    (
48        "/hljs.lang.bash.js",
49        HLJS_LANG_BASH,
50        "application/javascript",
51    ),
52    ("/hljs.lang.css.js", HLJS_LANG_CSS, "application/javascript"),
53    (
54        "/hljs.lang.dockerfile.js",
55        HLJS_LANG_DOCKERFILE,
56        "application/javascript",
57    ),
58    ("/hljs.lang.go.js", HLJS_LANG_GO, "application/javascript"),
59    (
60        "/hljs.lang.java.js",
61        HLJS_LANG_JAVA,
62        "application/javascript",
63    ),
64    (
65        "/hljs.lang.javascript.js",
66        HLJS_LANG_JAVASCRIPT,
67        "application/javascript",
68    ),
69    (
70        "/hljs.lang.json.js",
71        HLJS_LANG_JSON,
72        "application/javascript",
73    ),
74    (
75        "/hljs.lang.markdown.js",
76        HLJS_LANG_MARKDOWN,
77        "application/javascript",
78    ),
79    ("/hljs.lang.nix.js", HLJS_LANG_NIX, "application/javascript"),
80    (
81        "/hljs.lang.python.js",
82        HLJS_LANG_PYTHON,
83        "application/javascript",
84    ),
85    (
86        "/hljs.lang.ruby.js",
87        HLJS_LANG_RUBY,
88        "application/javascript",
89    ),
90    (
91        "/hljs.lang.rust.js",
92        HLJS_LANG_RUST,
93        "application/javascript",
94    ),
95    (
96        "/hljs.lang.scala.js",
97        HLJS_LANG_SCALA,
98        "application/javascript",
99    ),
100    ("/hljs.lang.sql.js", HLJS_LANG_SQL, "application/javascript"),
101    (
102        "/hljs.lang.typescript.js",
103        HLJS_LANG_TYPESCRIPT,
104        "application/javascript",
105    ),
106    ("/hljs.lang.xml.js", HLJS_LANG_XML, "application/javascript"),
107    (
108        "/hljs.lang.yaml.js",
109        HLJS_LANG_YAML,
110        "application/javascript",
111    ),
112];
113
114/// Maps a filename extension to the highlight.js language that should render
115/// it, or `None` when no *embedded* language module covers that extension.
116///
117/// Deliberately closed over [`HLJS_LANGUAGES`]: the binary only ships those 17
118/// grammars, so returning any other name would emit a `language-*` class that
119/// highlight.js silently ignores. `test_every_mapped_language_is_embedded`
120/// keeps the two in sync.
121///
122/// `extension` is matched case-insensitively and must not include the dot.
123/// Returning `None` is not an error - callers render those files verbatim.
124pub fn language_for_extension(extension: &str) -> Option<&'static str> {
125    // Allocation-free lowercasing for the common (already-lowercase) case.
126    let ext = extension.to_ascii_lowercase();
127    Some(match ext.as_str() {
128        "bash" | "sh" | "zsh" | "ksh" | "bashrc" | "zshrc" => "bash",
129        "css" => "css",
130        "dockerfile" => "dockerfile",
131        "go" => "go",
132        "java" => "java",
133        "js" | "mjs" | "cjs" | "jsx" => "javascript",
134        "json" | "jsonc" | "json5" | "webmanifest" => "json",
135        "md" | "markdown" | "mdown" | "mkd" | "mkdn" => "markdown",
136        "nix" => "nix",
137        "py" | "pyi" | "pyw" => "python",
138        "rb" | "rake" | "gemspec" => "ruby",
139        "rs" => "rust",
140        "scala" | "sbt" | "sc" => "scala",
141        "sql" => "sql",
142        "ts" | "tsx" | "mts" | "cts" => "typescript",
143        "xml" | "html" | "htm" | "xhtml" | "svg" | "plist" | "xsl" | "xslt" => "xml",
144        "yaml" | "yml" => "yaml",
145        _ => return None,
146    })
147}
148
149/// List of supported highlight.js languages.
150pub const HLJS_LANGUAGES: &[&str] = &[
151    "bash",
152    "css",
153    "dockerfile",
154    "go",
155    "java",
156    "javascript",
157    "json",
158    "markdown",
159    "nix",
160    "python",
161    "ruby",
162    "rust",
163    "scala",
164    "sql",
165    "typescript",
166    "xml",
167    "yaml",
168];
169
170#[cfg(test)]
171mod tests {
172    use super::*;
173
174    #[test]
175    fn test_hljs_files_not_empty() {
176        for (path, content, _mime) in HLJS_FILES.iter() {
177            assert!(
178                !content.is_empty(),
179                "HLJS file {} should not be empty",
180                path
181            );
182        }
183    }
184
185    #[test]
186    fn test_hljs_file_count() {
187        // 1 CSS + 1 core JS + 17 language modules = 19 total
188        assert_eq!(HLJS_FILES.len(), 19);
189    }
190
191    #[test]
192    fn test_all_languages_have_files() {
193        for lang in HLJS_LANGUAGES {
194            let expected_path = format!("/hljs.lang.{}.js", lang);
195            assert!(
196                HLJS_FILES.iter().any(|(path, _, _)| *path == expected_path),
197                "Missing HLJS file for language: {}",
198                lang
199            );
200        }
201    }
202
203    #[test]
204    fn test_core_files_present() {
205        assert!(
206            HLJS_FILES.iter().any(|(path, _, _)| *path == "/hljs.js"),
207            "Core hljs.js should be present"
208        );
209        assert!(
210            HLJS_FILES
211                .iter()
212                .any(|(path, _, _)| *path == "/hljs.atom-one-dark.css"),
213            "Dark theme CSS should be present"
214        );
215    }
216
217    #[test]
218    fn test_every_mapped_language_is_embedded() {
219        // A `language-foo` class for a grammar we do not ship is a silent
220        // no-op in the browser, so every mapping must name an embedded module.
221        let mapped = [
222            "sh",
223            "css",
224            "dockerfile",
225            "go",
226            "java",
227            "jsx",
228            "json",
229            "md",
230            "nix",
231            "py",
232            "rb",
233            "rs",
234            "scala",
235            "sql",
236            "tsx",
237            "svg",
238            "yml",
239        ];
240        for ext in mapped {
241            let lang = language_for_extension(ext)
242                .unwrap_or_else(|| panic!("extension {ext} should map to a language"));
243            assert!(
244                HLJS_LANGUAGES.contains(&lang),
245                "{ext} maps to {lang}, which has no embedded hljs module"
246            );
247        }
248    }
249
250    #[test]
251    fn test_language_for_extension_is_case_insensitive() {
252        assert_eq!(language_for_extension("RS"), Some("rust"));
253        assert_eq!(language_for_extension("Json"), Some("json"));
254    }
255
256    #[test]
257    fn test_language_for_extension_unknown_is_none() {
258        // Plain text and types we ship no grammar for must fall through so the
259        // caller renders them verbatim rather than mislabelling them.
260        for ext in ["txt", "log", "toml", "csv", "", "exe"] {
261            assert_eq!(language_for_extension(ext), None, "unexpected match: {ext}");
262        }
263    }
264
265    #[test]
266    fn test_mime_types_correct() {
267        for (path, _, mime) in HLJS_FILES.iter() {
268            if path.ends_with(".css") {
269                assert_eq!(
270                    *mime, "text/css",
271                    "CSS files should have text/css mime type"
272                );
273            } else if path.ends_with(".js") {
274                assert_eq!(
275                    *mime, "application/javascript",
276                    "JS files should have application/javascript mime type"
277                );
278            }
279        }
280    }
281}