chadtmiller.com/knotless

Git hosted in your atproto account, on any stock PDS, fast enough for agents.

git clone knotless://chadtmiller.com/knotless

needs the knotless git helper. set it up

serve: highlight Svelte, match lockfiles and dotfiles by name

Svelte uses tree-sitter-svelte-ng with HTML's highlights under its own,
as its `; inherits: html` asks, and an injections query of our own: the
bundled one leans on Neovim's `inherits` and `#any-of?`. A script's
`lang` attribute names its language, so `<script lang="ts">` is
TypeScript; `{expressions}` are JavaScript, styles CSS. `{#if`, `{:else}`
and `{/if}` read as keywords.

Languages now match whole file names as well as extensions. `.lock` no
longer means TOML: Cargo.lock, uv.lock, poetry.lock and pdm.lock are TOML;
flake.lock, deno.lock, composer.lock and Pipfile.lock are JSON; yarn.lock
is plain. Shell dotfiles (.bashrc, .zshrc, .envrc, ...) are bash, and
jsonc and webmanifest are JSON.

Cargo.lock still needs the tree-sitter-svelte-ng entry: run `cargo build`
before merging, as the Dockerfile builds with --locked.
chadtmiller.com committed on Oct 22, 2026 parent c2f591c commit 138e580
Showing 2 changed files with 126 additions and 6 deletions
crates/knotless-serve/Cargo.toml+1−0
@@ -48,6 +48,7 @@ tree-sitter-html = "0.23.2"
tree-sitter-css = "0.25.0"
tree-sitter-lua = "0.5.0"
tree-sitter-zig = "1.1.2"
tree-sitter-svelte-ng = "1.0.2"
tree-sitter-language = "0.1.7"
ts-rs = "12"
crates/knotless-serve/src/highlight.rs+125−6
@@ -22,7 +22,7 @@ const CODE: &[&str] = &[
/// The captures the grammars' queries use, and the highlight each gives.
/// A capture takes its longest listed prefix: `function.method` is a
/// `function`.
const CAPTURES: [(&str, usize); 23] = [
const CAPTURES: [(&str, usize); 24] = [
("comment", 0),
("string", 1),
("string.special.key", 7),
@@ -43,6 +43,8 @@ const CAPTURES: [(&str, usize); 23] = [
("type.builtin", 5),
("constructor", 5),
("tag", 6),
// Svelte's `#`, `:` and `/` read with the word they open: `{#if`.
("tag.delimiter", 2),
("attribute", 7),
("property.definition", 7),
("label", 2),
@@ -54,8 +56,36 @@ const ROWS: &[&str] = &[
"comment", "string", "keyword", "number", "function", "type", "tag", "attr", "xins", "xdel",
];
/// Svelte's scripts, styles and `{expressions}`. The grammar's own query
/// leans on Neovim's `; inherits:` and `#any-of?`, which tree-sitter-highlight
/// doesn't read. A `<script lang="ts">` names its language by the attribute,
/// resolved by [`injected`]; one without matches only the second pattern.
const SVELTE_INJECTIONS: &str = r#"
((script_element
(start_tag
(attribute
(attribute_name) @_attr
(quoted_attribute_value (attribute_value) @injection.language)))
(raw_text) @injection.content)
(#eq? @_attr "lang"))
((script_element
(start_tag) @_tag
(raw_text) @injection.content)
(#not-match? @_tag "\\slang\\s*=")
(#set! injection.language "javascript"))
((style_element
(raw_text) @injection.content)
(#set! injection.language "css"))
((svelte_raw_text) @injection.content
(#set! injection.language "javascript"))
"#;
struct Language {
name: &'static str,
/// Extensions, and whole file names: `rs`, `Gemfile`, `Cargo.lock`.
extensions: &'static [&'static str],
config: HighlightConfiguration,
}
@@ -162,14 +192,36 @@ static LANGUAGES: LazyLock<Vec<Language>> = LazyLock::new(|| {
),
language(
"bash",
&["sh", "bash", "zsh"],
&[
"sh",
"bash",
"zsh",
".bashrc",
".bash_profile",
".bash_aliases",
".bash_logout",
".profile",
".zshrc",
".zshenv",
".zprofile",
".envrc",
],
tree_sitter_bash::LANGUAGE,
&[tree_sitter_bash::HIGHLIGHT_QUERY],
"",
),
language(
"json",
&["json"],
// Lockfiles by name: not every `.lock` is TOML.
&[
"json",
"jsonc",
"webmanifest",
"flake.lock",
"deno.lock",
"composer.lock",
"Pipfile.lock",
],
tree_sitter_json::LANGUAGE,
// Where patterns overlap the last wins: keys over strings.
&[
@@ -180,7 +232,7 @@ static LANGUAGES: LazyLock<Vec<Language>> = LazyLock::new(|| {
),
language(
"toml",
&["toml", "lock"],
&["toml", "Cargo.lock", "uv.lock", "poetry.lock", "pdm.lock"],
tree_sitter_toml_ng::LANGUAGE,
&[tree_sitter_toml_ng::HIGHLIGHTS_QUERY],
"",
@@ -199,6 +251,17 @@ static LANGUAGES: LazyLock<Vec<Language>> = LazyLock::new(|| {
&[tree_sitter_html::HIGHLIGHTS_QUERY],
tree_sitter_html::INJECTIONS_QUERY,
),
language(
"svelte",
&["svelte"],
tree_sitter_svelte_ng::LANGUAGE,
// Its query builds on HTML's, as Neovim's `; inherits: html` says.
&[
tree_sitter_html::HIGHLIGHTS_QUERY,
tree_sitter_svelte_ng::HIGHLIGHTS_QUERY,
],
SVELTE_INJECTIONS,
),
language(
"css",
&["css"],
@@ -231,11 +294,22 @@ fn by_name(name: &str) -> Option<&'static Language> {
LANGUAGES.iter().find(|l| l.name == name)
}
/// The language an injection names, as a query sets it or as a `lang`
/// attribute spells it.
fn injected(name: &str) -> Option<&'static Language> {
match name {
"ts" => by_name("typescript"),
"js" => by_name("javascript"),
_ => by_name(name),
}
}
/// The language of the file at `path`: by its extension, or its name, or
/// the interpreter its first line names.
fn language_for(path: &str, text: &str) -> Option<&'static Language> {
let name = path.rsplit('/').next().unwrap_or(path);
let ext = name.rsplit_once('.').map_or(name, |(_, ext)| ext);
let whole = LANGUAGES.iter().find(|l| l.extensions.contains(&name));
let by_ext = LANGUAGES.iter().find(|l| l.extensions.contains(&ext));
let shebang = text
.lines()
@@ -250,7 +324,7 @@ fn language_for(path: &str, text: &str) -> Option<&'static Language> {
"ruby" => by_name("ruby"),
_ => None,
};
by_ext.or(by_shebang)
whole.or(by_ext).or(by_shebang)
}
/// Runs of each named highlight, as `[start, end)` in UTF-16 units.
@@ -315,7 +389,7 @@ pub fn code(path: &str, text: &str) -> Option<Ranges> {
HIGHLIGHTER.with_borrow_mut(|highlighter| {
let events = highlighter
.highlight(&language.config, text.as_bytes(), None, None, |name| {
by_name(name).map(|l| &l.config)
injected(name).map(|l| &l.config)
})
.ok()?;
let mut ranges = Ranges::new(CODE);
@@ -481,6 +555,7 @@ mod tests {
fn every_language_parses() {
let samples = [
("a.js", "const f = () => <b x=\"1\">hi</b>; // c\n"),
("a.jsx", "const f = () => <b x=\"1\">hi</b>;\n"),
("a.ts", "let x: number = 1;\n"),
("a.tsx", "const e = <div class=\"x\" />;\n"),
("a.py", "def f():\n return 'x'\n"),
@@ -490,11 +565,16 @@ mod tests {
("A.java", "class A { void f() {} }\n"),
("Gemfile", "gem 'x'\n"),
("a.json", "{\"key\": 1}\n"),
("tsconfig.jsonc", "// c\n{\"key\": 1}\n"),
("Cargo.lock", "[[package]]\nname = \"x\"\n"),
("flake.lock", "{\"nodes\": {}}\n"),
("a.yml", "a: 'b'\n"),
("a.css", "a { color: red; }\n"),
("a.lua", "local x = 'y'\n"),
("a.zig", "const x = 1;\n"),
("a.svelte", "<p class=\"x\">{n}</p>\n"),
("home/.bashrc", "alias ll='ls -l'\n"),
(".envrc", "export A='b'\n"),
("run", "#!/usr/bin/env bash\necho 'hi'\n"),
("tool", "#!/usr/bin/python3.12\nx = 'y'\n"),
("cli", "#!/usr/bin/env node\nlet a = 'b'\n"),
@@ -511,6 +591,17 @@ mod tests {
);
}
#[test]
fn lockfiles_by_name() {
let json = "{\"nodes\": {}}\n";
let r = code("flake.lock", json).unwrap();
assert_eq!(spans(&r, json, "attr"), ["\"nodes\""]);
let toml = "[[package]]\nname = \"x\"\n";
let r = code("Cargo.lock", toml).unwrap();
assert_eq!(spans(&r, toml, "string"), ["\"x\""]);
assert_eq!(code("yarn.lock", "a@1:\n version \"1\"\n"), None);
}
#[test]
fn html_carries_its_scripts_and_styles() {
let html =
@@ -525,6 +616,34 @@ mod tests {
assert!(spans(&r, html, "tag").contains(&"script".to_string()));
}
#[test]
fn svelte() {
let text = "<script lang=\"ts\">\n let n: number = 1;\n</script>\n\n\
{#if n > 0}\n <Count value={n} on:click={() => n++} />\n{:else}\n <p>none</p>\n{/if}\n\n\
<style>\n p { margin: 4px; }\n</style>\n";
let r = code("src/App.svelte", text).unwrap();
let keyword = spans(&r, text, "keyword");
for k in ["let", "#if", ":else", "/if"] {
assert!(keyword.contains(&k.to_string()), "{k} in {keyword:?}");
}
// TypeScript, as the script's `lang` says.
assert!(spans(&r, text, "type").contains(&"number".to_string()));
let number = spans(&r, text, "number");
assert!(number.contains(&"1".to_string()) && number.contains(&"0".to_string()));
let tag = spans(&r, text, "tag");
for t in ["script", "Count", "p", "style"] {
assert!(tag.contains(&t.to_string()), "{t} in {tag:?}");
}
assert!(spans(&r, text, "attr").contains(&"lang".to_string()));
// The style is CSS.
assert!(number.contains(&"4".to_string()), "{number:?}");
let js = "<script>\n let a = 'q';\n</script>\n";
let r = code("a.svelte", js).unwrap();
assert!(spans(&r, js, "string").contains(&"'q'".to_string()));
assert!(spans(&r, js, "keyword").contains(&"let".to_string()));
}
#[test]
fn what_it_leaves_plain() {
assert_eq!(code("notes.unknown-kind", "plain\n"), None);