Jump to…
snowinitial commitqoxwzsukwmkx1mo
Matt W1//! Syntax highlighting (spec §8).
Matt W2//!
Matt W3//! > **Syntax highlighting** with `tree-sitter` and a curated grammar set
Matt W4//! > (roughly the top 25 languages), falling back to plain text. […] Skip files
Matt W5//! > over 1 MB or with lines over 5000 characters, and render those as plain
Matt W6//! > text with a notice.
Matt W7//!
Matt W8//! The output is a `Vec<String>` of **per-line HTML fragments**, not one blob of
Matt W9//! markup. The blob view needs a line-number gutter and anchorable line ids, and
Matt W10//! producing lines here is what lets the diff view highlight a single side of a
Matt W11//! hunk without re-parsing.
Matt W12//!
Matt W13//! Two safety properties this module is responsible for:
Matt W14//!
Matt W15//! * **Escaping.** The source is repository content, which is attacker
Matt W16//! controlled. `tree-sitter-highlight`'s `HtmlRenderer` escapes `<`, `&`, `'`
Matt W17//! and `"` in the text it emits, and every attribute this module adds is a
Matt W18//! fixed string chosen from the highlight-name table — no user data ever
Matt W19//! reaches an attribute value. `escaping_is_not_optional` pins that.
Matt W20//! * **Termination.** A pathological file must degrade, not hang the process.
Matt W21//! The size and line-length caps are checked *before* the parser runs, and a
Matt W22//! highlight error returns `None` rather than propagating.
Matt W23
Matt W24use std::collections::HashMap;
Matt W25use std::sync::LazyLock;
Matt W26
Matt W27use tree_sitter_highlight::{HighlightConfiguration, Highlighter, HtmlRenderer};
Matt W28
Matt W29/// Files above this are served as plain text (spec §8).
Matt W30pub const MAX_BYTES: usize = 1024 * 1024;
Matt W31
Matt W32/// A single line this long means the file is generated or minified. Parsing it
Matt W33/// is slow and the result is unreadable either way (spec §8).
Matt W34pub const MAX_LINE_CHARS: usize = 5_000;
Matt W35
Matt W36/// Highlight capture names, in the order the CSS classes are derived from.
Matt W37///
Matt W38/// The index into this array is what `tree-sitter-highlight` hands back, so the
Matt W39/// order is load-bearing: it must match [`CLASSES`] exactly.
Matt W40const HIGHLIGHT_NAMES: &[&str] = &[
Matt W41 "attribute",
Matt W42 "boolean",
Matt W43 "character",
Matt W44 "comment",
Matt W45 "comment.documentation",
Matt W46 "constant",
Matt W47 "constant.builtin",
Matt W48 "constructor",
Matt W49 "embedded",
Matt W50 "escape",
Matt W51 "function",
Matt W52 "function.builtin",
Matt W53 "function.method",
Matt W54 "keyword",
Matt W55 "label",
Matt W56 "module",
Matt W57 "number",
Matt W58 "operator",
Matt W59 "property",
Matt W60 "punctuation",
Matt W61 "punctuation.bracket",
Matt W62 "punctuation.delimiter",
Matt W63 "punctuation.special",
Matt W64 "string",
Matt W65 "string.escape",
Matt W66 "string.special",
Matt W67 "tag",
Matt W68 "type",
Matt W69 "type.builtin",
Matt W70 "variable",
Matt W71 "variable.builtin",
Matt W72 "variable.parameter",
Matt W73];
Matt W74
Matt W75/// The `class="…"` attribute emitted for each highlight index.
Matt W76///
Matt W77/// Precomputed as complete attribute bytes so the render callback is a slice
Matt W78/// copy rather than a format call per token — highlighting a large file emits a
Matt W79/// great many of these.
Matt W80static CLASSES: LazyLock<Vec<Vec<u8>>> = LazyLock::new(|| {
Matt W81 HIGHLIGHT_NAMES
Matt W82 .iter()
Matt W83 .map(|name| {
Matt W84 // `hl-function-builtin` also carries `hl-function`, so the stylesheet
Matt W85 // can theme a whole family and refine one member of it.
Matt W86 let mut classes = String::new();
Matt W87 let mut acc = String::new();
Matt W88 for part in name.split('.') {
Matt W89 if !acc.is_empty() {
Matt W90 acc.push('-');
Matt W91 }
Matt W92 acc.push_str(part);
Matt W93 if !classes.is_empty() {
Matt W94 classes.push(' ');
Matt W95 }
Matt W96 classes.push_str("hl-");
Matt W97 classes.push_str(&acc);
Matt W98 }
Matt W99 format!(r#"class="{classes}""#).into_bytes()
Matt W100 })
Matt W101 .collect()
Matt W102});
Matt W103
Matt W104/// A highlighted file.
Matt W105#[derive(Debug)]
Matt W106pub struct Highlighted {
Matt W107 /// The grammar that was used, for the "detected as Rust" affordance.
Matt W108 pub language: &'static str,
Matt W109 /// One HTML fragment per line, already escaped. No trailing newline.
Matt W110 pub lines: Vec<String>,
Matt W111}
Matt W112
Matt W113/// Why a file was not highlighted.
Matt W114///
Matt W115/// Distinguished from "no grammar" so the view can say *why* — a 4 MB file
Matt W116/// rendering as plain text is a decision the reader should be told about, and a
Matt W117/// `.md` file rendering plain because it is minified is worth explaining.
Matt W118#[derive(Debug, Clone, Copy, PartialEq, Eq)]
Matt W119pub enum Skipped {
Matt W120 /// Over [`MAX_BYTES`].
Matt W121 TooLarge,
Matt W122 /// Contains a line over [`MAX_LINE_CHARS`].
Matt W123 LineTooLong,
Matt W124 /// No grammar for this file type.
Matt W125 NoGrammar,
Matt W126 /// The grammar failed on this input. Rare, and never fatal.
Matt W127 Failed,
Matt W128}
Matt W129
Matt W130/// Highlight `source`, choosing a grammar from `path`.
Matt W131///
Matt W132/// Returns `Err(Skipped)` whenever the file should be shown as plain text —
Matt W133/// there is deliberately no partial result, because half a highlighted file is
Matt W134/// worse than none.
Matt W135pub fn highlight(path: &str, source: &str) -> Result<Highlighted, Skipped> {
Matt W136 if source.len() > MAX_BYTES {
Matt W137 return Err(Skipped::TooLarge);
Matt W138 }
Matt W139 if source.lines().any(|l| l.chars().count() > MAX_LINE_CHARS) {
Matt W140 return Err(Skipped::LineTooLong);
Matt W141 }
Matt W142
Matt W143 let lang = language_for_path(path).ok_or(Skipped::NoGrammar)?;
Matt W144 let config = GRAMMARS.get(lang.grammar).ok_or(Skipped::NoGrammar)?;
Matt W145
Matt W146 let mut highlighter = Highlighter::new();
Matt W147 // The injection callback resolves embedded languages (JS inside HTML, and so
Matt W148 // on). Returning `None` means "render the embedded region with the outer
Matt W149 // grammar", which is correct-but-plain rather than wrong.
Matt W150 let events = highlighter
Matt W151 .highlight(config, source.as_bytes(), None, |name| GRAMMARS.get(name))
Matt W152 .map_err(|_| Skipped::Failed)?;
Matt W153
Matt W154 let mut renderer = HtmlRenderer::new();
Matt W155 renderer
Matt W156 .render(events, source.as_bytes(), &|h, out: &mut Vec<u8>| {
Matt W157 if let Some(class) = CLASSES.get(h.0) {
Matt W158 out.extend_from_slice(class);
Matt W159 }
Matt W160 })
Matt W161 .map_err(|_| Skipped::Failed)?;
Matt W162
Matt W163 // `HtmlRenderer::lines` yields each line with a trailing newline and with
Matt W164 // every span opened on that line also closed on it, so a line is a valid
Matt W165 // standalone fragment.
Matt W166 let lines = renderer
Matt W167 .lines()
Matt W168 .map(|l| l.trim_end_matches('\n').to_owned())
Matt W169 .collect();
Matt W170
Matt W171 Ok(Highlighted { language: lang.display, lines })
Matt W172}
Matt W173
Matt W174/// A grammar we can highlight with.
Matt W175#[derive(Debug, Clone, Copy)]
Matt W176pub struct Language {
Matt W177 /// Key into [`GRAMMARS`]. Also the name injections are looked up by, so it
Matt W178 /// matches tree-sitter's own language names.
Matt W179 pub grammar: &'static str,
Matt W180 /// Human-readable, for the UI.
Matt W181 pub display: &'static str,
Matt W182}
Matt W183
Matt W184/// Pick a grammar for a path.
Matt W185///
Matt W186/// Whole filenames are checked before extensions, so `Dockerfile` and
Matt W187/// `.bashrc` resolve even though they have no useful extension.
Matt W188pub fn language_for_path(path: &str) -> Option<Language> {
Matt W189 let name = path.rsplit('/').next().unwrap_or(path);
Matt W190 let lower = name.to_ascii_lowercase();
Matt W191
Matt W192 let by_name = match lower.as_str() {
Matt W193 "makefile" | "gnumakefile" | "dockerfile" | "containerfile" | "justfile" => Some("bash"),
Matt W194 ".bashrc" | ".bash_profile" | ".profile" | ".zshrc" | ".zprofile" => Some("bash"),
Matt W195 "cargo.lock" | "gemfile.lock" => Some("toml"),
Matt W196 "gemfile" | "rakefile" | "podfile" => Some("ruby"),
Matt W197 "cmakelists.txt" => None,
Matt W198 _ => None,
Matt W199 };
Matt W200 if let Some(g) = by_name {
Matt W201 return GRAMMAR_DISPLAY
Matt W202 .iter()
Matt W203 .find(|(k, _)| *k == g)
Matt W204 .map(|(grammar, display)| Language { grammar, display });
Matt W205 }
Matt W206
Matt W207 // `.tar.gz` must not resolve as `gz`; only the final component matters.
Matt W208 let ext = lower.rsplit_once('.').map(|(_, e)| e)?;
Matt W209
Matt W210 let grammar = match ext {
Matt W211 "rs" => "rust",
Matt W212 "py" | "pyi" | "pyw" => "python",
Matt W213 "js" | "mjs" | "cjs" | "jsx" => "javascript",
Matt W214 "ts" | "mts" | "cts" => "typescript",
Matt W215 "tsx" => "tsx",
Matt W216 "go" => "go",
Matt W217 "c" | "h" => "c",
Matt W218 "cc" | "cpp" | "cxx" | "hpp" | "hh" | "hxx" => "cpp",
Matt W219 "java" => "java",
Matt W220 "rb" | "rake" | "gemspec" => "ruby",
Matt W221 "json" | "jsonc" | "webmanifest" => "json",
Matt W222 "toml" => "toml",
Matt W223 "yaml" | "yml" => "yaml",
Matt W224 "html" | "htm" | "xhtml" => "html",
Matt W225 "css" | "scss" => "css",
Matt W226 "sh" | "bash" | "zsh" | "ksh" => "bash",
Matt W227 "md" | "markdown" | "mdown" => "markdown",
Matt W228 "sql" => "sql",
Matt W229 "php" => "php",
Matt W230 "cs" => "c_sharp",
Matt W231 "scala" | "sbt" | "sc" => "scala",
Matt W232 "hs" => "haskell",
Matt W233 "swift" => "swift",
Matt W234 "ex" | "exs" => "elixir",
Matt W235 "lua" => "lua",
Matt W236 "zig" => "zig",
Matt W237 "nix" => "nix",
Matt W238 _ => return None,
Matt W239 };
Matt W240
Matt W241 GRAMMAR_DISPLAY
Matt W242 .iter()
Matt W243 .find(|(k, _)| *k == grammar)
Matt W244 .map(|(grammar, display)| Language { grammar, display })
Matt W245}
Matt W246
Matt W247/// Grammar key → display name. Also the authoritative list of what is loaded.
Matt W248const GRAMMAR_DISPLAY: &[(&str, &str)] = &[
Matt W249 ("bash", "Shell"),
Matt W250 ("c", "C"),
Matt W251 ("c_sharp", "C#"),
Matt W252 ("cpp", "C++"),
Matt W253 ("css", "CSS"),
Matt W254 ("elixir", "Elixir"),
Matt W255 ("go", "Go"),
Matt W256 ("haskell", "Haskell"),
Matt W257 ("html", "HTML"),
Matt W258 ("java", "Java"),
Matt W259 ("javascript", "JavaScript"),
Matt W260 ("json", "JSON"),
Matt W261 ("lua", "Lua"),
Matt W262 ("markdown", "Markdown"),
Matt W263 ("nix", "Nix"),
Matt W264 ("php", "PHP"),
Matt W265 ("python", "Python"),
Matt W266 ("ruby", "Ruby"),
Matt W267 ("rust", "Rust"),
Matt W268 ("scala", "Scala"),
Matt W269 ("sql", "SQL"),
Matt W270 ("swift", "Swift"),
Matt W271 ("toml", "TOML"),
Matt W272 ("tsx", "TSX"),
Matt W273 ("typescript", "TypeScript"),
Matt W274 ("yaml", "YAML"),
Matt W275 ("zig", "Zig"),
Matt W276];
Matt W277
Matt W278/// Every grammar, parsed and configured once.
Matt W279///
Matt W280/// `HighlightConfiguration::configure` is what binds capture names to the
Matt W281/// indices used by [`CLASSES`], and it mutates the configuration — so it happens
Matt W282/// here, once, rather than per request. Building these costs a few milliseconds
Matt W283/// each and the map is shared across every request thereafter.
Matt W284static GRAMMARS: LazyLock<HashMap<&'static str, HighlightConfiguration>> = LazyLock::new(|| {
Matt W285 let mut m = HashMap::new();
Matt W286
Matt W287 /// Build one configuration, skipping the grammar entirely if its queries do
Matt W288 /// not compile. A broken grammar must degrade to plain text, never panic at
Matt W289 /// the first request that touches it.
Matt W290 fn add(
Matt W291 m: &mut HashMap<&'static str, HighlightConfiguration>,
Matt W292 name: &'static str,
Matt W293 language: tree_sitter::Language,
Matt W294 highlights: &str,
Matt W295 injections: &str,
Matt W296 locals: &str,
Matt W297 ) {
Matt W298 match HighlightConfiguration::new(language, name, highlights, injections, locals) {
Matt W299 Ok(mut c) => {
Matt W300 c.configure(HIGHLIGHT_NAMES);
Matt W301 m.insert(name, c);
Matt W302 }
Matt W303 Err(e) => tracing_warn(name, &e.to_string()),
Matt W304 }
Matt W305 }
Matt W306
Matt W307 add(&mut m, "rust", tree_sitter_rust::LANGUAGE.into(),
Matt W308 tree_sitter_rust::HIGHLIGHTS_QUERY, tree_sitter_rust::INJECTIONS_QUERY, "");
Matt W309 add(&mut m, "python", tree_sitter_python::LANGUAGE.into(),
Matt W310 tree_sitter_python::HIGHLIGHTS_QUERY, "", "");
Matt W311 add(&mut m, "javascript", tree_sitter_javascript::LANGUAGE.into(),
Matt W312 tree_sitter_javascript::HIGHLIGHT_QUERY,
Matt W313 tree_sitter_javascript::INJECTIONS_QUERY,
Matt W314 tree_sitter_javascript::LOCALS_QUERY);
Matt W315 add(&mut m, "typescript", tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into(),
Matt W316 // The TypeScript grammar's queries extend JavaScript's rather than
Matt W317 // repeating them, so both must be supplied or half the file is plain.
Matt W318 &format!("{}\n{}", tree_sitter_javascript::HIGHLIGHT_QUERY, tree_sitter_typescript::HIGHLIGHTS_QUERY),
Matt W319 tree_sitter_javascript::INJECTIONS_QUERY,
Matt W320 tree_sitter_typescript::LOCALS_QUERY);
Matt W321 add(&mut m, "tsx", tree_sitter_typescript::LANGUAGE_TSX.into(),
Matt W322 &format!("{}\n{}", tree_sitter_javascript::HIGHLIGHT_QUERY, tree_sitter_typescript::HIGHLIGHTS_QUERY),
Matt W323 tree_sitter_javascript::INJECTIONS_QUERY,
Matt W324 tree_sitter_typescript::LOCALS_QUERY);
Matt W325 add(&mut m, "go", tree_sitter_go::LANGUAGE.into(),
Matt W326 tree_sitter_go::HIGHLIGHTS_QUERY, "", "");
Matt W327 add(&mut m, "c", tree_sitter_c::LANGUAGE.into(),
Matt W328 tree_sitter_c::HIGHLIGHT_QUERY, "", "");
Matt W329 add(&mut m, "cpp", tree_sitter_cpp::LANGUAGE.into(),
Matt W330 &format!("{}\n{}", tree_sitter_c::HIGHLIGHT_QUERY, tree_sitter_cpp::HIGHLIGHT_QUERY), "", "");
Matt W331 add(&mut m, "java", tree_sitter_java::LANGUAGE.into(),
Matt W332 tree_sitter_java::HIGHLIGHTS_QUERY, "", "");
Matt W333 add(&mut m, "ruby", tree_sitter_ruby::LANGUAGE.into(),
Matt W334 tree_sitter_ruby::HIGHLIGHTS_QUERY, "", tree_sitter_ruby::LOCALS_QUERY);
Matt W335 add(&mut m, "json", tree_sitter_json::LANGUAGE.into(),
Matt W336 tree_sitter_json::HIGHLIGHTS_QUERY, "", "");
Matt W337 add(&mut m, "toml", tree_sitter_toml_ng::LANGUAGE.into(),
Matt W338 tree_sitter_toml_ng::HIGHLIGHTS_QUERY, "", "");
Matt W339 add(&mut m, "yaml", tree_sitter_yaml::LANGUAGE.into(),
Matt W340 tree_sitter_yaml::HIGHLIGHTS_QUERY, "", "");
Matt W341 add(&mut m, "html", tree_sitter_html::LANGUAGE.into(),
Matt W342 tree_sitter_html::HIGHLIGHTS_QUERY, tree_sitter_html::INJECTIONS_QUERY, "");
Matt W343 add(&mut m, "css", tree_sitter_css::LANGUAGE.into(),
Matt W344 tree_sitter_css::HIGHLIGHTS_QUERY, "", "");
Matt W345 add(&mut m, "bash", tree_sitter_bash::LANGUAGE.into(),
Matt W346 tree_sitter_bash::HIGHLIGHT_QUERY, "", "");
Matt W347 add(&mut m, "markdown", tree_sitter_md::LANGUAGE.into(),
Matt W348 tree_sitter_md::HIGHLIGHT_QUERY_BLOCK, tree_sitter_md::INJECTION_QUERY_BLOCK, "");
Matt W349 add(&mut m, "sql", tree_sitter_sequel::LANGUAGE.into(),
Matt W350 tree_sitter_sequel::HIGHLIGHTS_QUERY, "", "");
Matt W351 add(&mut m, "php", tree_sitter_php::LANGUAGE_PHP.into(),
Matt W352 tree_sitter_php::HIGHLIGHTS_QUERY, tree_sitter_php::INJECTIONS_QUERY, "");
Matt W353 add(&mut m, "c_sharp", tree_sitter_c_sharp::LANGUAGE.into(),
Matt W354 tree_sitter_c_sharp::HIGHLIGHTS_QUERY, "", "");
Matt W355 add(&mut m, "scala", tree_sitter_scala::LANGUAGE.into(),
Matt W356 tree_sitter_scala::HIGHLIGHTS_QUERY, "", tree_sitter_scala::LOCALS_QUERY);
Matt W357 add(&mut m, "haskell", tree_sitter_haskell::LANGUAGE.into(),
Matt W358 tree_sitter_haskell::HIGHLIGHTS_QUERY, tree_sitter_haskell::INJECTIONS_QUERY,
Matt W359 tree_sitter_haskell::LOCALS_QUERY);
Matt W360 add(&mut m, "swift", tree_sitter_swift::LANGUAGE.into(),
Matt W361 tree_sitter_swift::HIGHLIGHTS_QUERY, tree_sitter_swift::INJECTIONS_QUERY,
Matt W362 tree_sitter_swift::LOCALS_QUERY);
Matt W363 add(&mut m, "elixir", tree_sitter_elixir::LANGUAGE.into(),
Matt W364 tree_sitter_elixir::HIGHLIGHTS_QUERY, tree_sitter_elixir::INJECTIONS_QUERY, "");
Matt W365 add(&mut m, "lua", tree_sitter_lua::LANGUAGE.into(),
Matt W366 tree_sitter_lua::HIGHLIGHTS_QUERY, tree_sitter_lua::INJECTIONS_QUERY,
Matt W367 tree_sitter_lua::LOCALS_QUERY);
Matt W368 add(&mut m, "zig", tree_sitter_zig::LANGUAGE.into(),
Matt W369 tree_sitter_zig::HIGHLIGHTS_QUERY, tree_sitter_zig::INJECTIONS_QUERY, "");
Matt W370 add(&mut m, "nix", tree_sitter_nix::LANGUAGE.into(),
Matt W371 tree_sitter_nix::HIGHLIGHTS_QUERY, tree_sitter_nix::INJECTIONS_QUERY, "");
Matt W372
Matt W373 m
Matt W374});
Matt W375
Matt W376/// `df-render` deliberately has no `tracing` dependency — it is a pure
Matt W377/// rendering crate — so a grammar that fails to load reports on stderr.
Matt W378/// This only ever runs once per process, at first use.
Matt W379fn tracing_warn(name: &str, message: &str) {
Matt W380 eprintln!("df-render: grammar {name} failed to load, files will render as plain text: {message}");
Matt W381}
Matt W382
Matt W383#[cfg(test)]
Matt W384mod tests {
Matt W385 use super::*;
Matt W386
Matt W387 #[test]
Matt W388 fn highlights_rust_and_splits_by_line() {
Matt W389 let out = highlight("src/main.rs", "fn main() {\n let x = 1;\n}\n").unwrap();
Matt W390 assert_eq!(out.language, "Rust");
Matt W391 assert_eq!(out.lines.len(), 3, "one fragment per source line");
Matt W392 assert!(out.lines[0].contains("hl-keyword"), "`fn` should be a keyword: {:?}", out.lines[0]);
Matt W393 // The Rust grammar captures integer literals as `constant.builtin`, not
Matt W394 // `number`. Asserting the family class rather than the leaf keeps this
Matt W395 // test about "the literal got classified" instead of about upstream's
Matt W396 // choice of capture name.
Matt W397 assert!(
Matt W398 out.lines[1].contains("hl-constant"),
Matt W399 "`1` should be classified: {:?}",
Matt W400 out.lines[1]
Matt W401 );
Matt W402 }
Matt W403
Matt W404 /// The security-critical property: repository content is attacker
Matt W405 /// controlled, and it reaches a page that renders on our origin.
Matt W406 #[test]
Matt W407 fn escaping_is_not_optional() {
Matt W408 let evil = "let s = \"</span><script>alert(1)</script>\";\n";
Matt W409 let out = highlight("x.rs", evil).unwrap();
Matt W410 let joined = out.lines.join("\n");
Matt W411 assert!(
Matt W412 !joined.contains("<script>"),
Matt W413 "raw markup escaped from the highlighter: {joined}"
Matt W414 );
Matt W415 assert!(joined.contains("&lt;script&gt;"), "expected escaped form: {joined}");
Matt W416 }
Matt W417
Matt W418 /// Even with no grammar the caller must be able to tell "plain" from
Matt W419 /// "broken", so every rejection carries a reason.
Matt W420 #[test]
Matt W421 fn unknown_extensions_fall_back_rather_than_erroring() {
Matt W422 assert_eq!(highlight("a.unknownext", "hello").unwrap_err(), Skipped::NoGrammar);
Matt W423 assert_eq!(highlight("noextension", "hello").unwrap_err(), Skipped::NoGrammar);
Matt W424 }
Matt W425
Matt W426 #[test]
Matt W427 fn the_spec_size_limits_are_enforced_before_parsing() {
Matt W428 let big = "a\n".repeat(MAX_BYTES);
Matt W429 assert_eq!(highlight("a.rs", &big).unwrap_err(), Skipped::TooLarge);
Matt W430
Matt W431 let minified = format!("let x = \"{}\";\n", "y".repeat(MAX_LINE_CHARS + 1));
Matt W432 assert_eq!(highlight("a.js", &minified).unwrap_err(), Skipped::LineTooLong);
Matt W433 }
Matt W434
Matt W435 #[test]
Matt W436 fn class_names_carry_the_whole_family() {
Matt W437 let idx = HIGHLIGHT_NAMES.iter().position(|n| *n == "function.builtin").unwrap();
Matt W438 let class = String::from_utf8(CLASSES[idx].clone()).unwrap();
Matt W439 assert_eq!(class, r#"class="hl-function hl-function-builtin""#);
Matt W440 }
Matt W441
Matt W442 #[test]
Matt W443 fn filenames_without_a_useful_extension_still_resolve() {
Matt W444 assert_eq!(language_for_path("Dockerfile").unwrap().display, "Shell");
Matt W445 assert_eq!(language_for_path("a/b/Makefile").unwrap().display, "Shell");
Matt W446 assert_eq!(language_for_path("Cargo.lock").unwrap().display, "TOML");
Matt W447 // Only the final extension counts.
Matt W448 assert!(language_for_path("archive.tar.gz").is_none());
Matt W449 assert_eq!(language_for_path("a.tar.rs").unwrap().display, "Rust");
Matt W450 }
Matt W451
Matt W452 /// Every grammar in the display table must actually have loaded. A typo in
Matt W453 /// a query, or an upstream grammar that stops compiling its queries against
Matt W454 /// the pinned `tree-sitter`, silently turns a language plain — this is the
Matt W455 /// test that notices.
Matt W456 #[test]
Matt W457 fn every_advertised_grammar_loads() {
Matt W458 for (key, display) in GRAMMAR_DISPLAY {
Matt W459 assert!(
Matt W460 GRAMMARS.contains_key(key),
Matt W461 "grammar {key} ({display}) is advertised but did not load"
Matt W462 );
Matt W463 }
Matt W464 assert!(GRAMMARS.len() >= 25, "spec §8 asks for roughly the top 25 languages");
Matt W465 }
Matt W466
Matt W467 /// A file that ends without a trailing newline must not lose its last line.
Matt W468 #[test]
Matt W469 fn a_missing_trailing_newline_does_not_drop_a_line() {
Matt W470 let out = highlight("a.rs", "fn a() {}\nfn b() {}").unwrap();
Matt W471 assert_eq!(out.lines.len(), 2);
Matt W472 assert!(out.lines[1].contains("b"));
Matt W473 }
Matt W474
Matt W475 #[test]
Matt W476 fn an_empty_file_highlights_to_nothing() {
Matt W477 let out = highlight("a.rs", "").unwrap();
Matt W478 assert!(out.lines.is_empty() || out.lines == vec![""]);
Matt W479 }
Matt W480}

480 lines · Rust