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

Make a Markdown file's answer carry it as HTML

The blob answer has `html` for a Markdown file shown as text, its links
pointed from the file's own directory. READMEs use the same test for
Markdown and the same way of naming a directory's pages.
chadtmiller.com committed on Oct 22, 2026 parent 1ac35ce commit 373e638
Showing 1 changed file with 39 additions and 12 deletions
crates/knotless-serve/src/web.rs+39−12
@@ -542,6 +542,15 @@ impl Repo {
)
}
/// Where the directory `path` at `rev` is, as `view` (`blob` or `raw`)
/// shows its files: what links in Markdown found there are made
/// relative to.
fn dir(&self, view: &str, rev: &str, path: &str) -> String {
self.href(&format!("/{view}/{}/{}", url_path(rev), url_path(path)))
.trim_end_matches('/')
.to_string()
}
fn uri(&self) -> String {
forge::repo_uri(&self.did, &self.name)
}
@@ -843,16 +852,13 @@ async fn listing(app: &App, repo: &Repo, rev: &str, path: &str) -> Result<Respon
.blob(rev, &format!("{prefix}{}", readme.name))
.map_err(not_found)?;
let text = String::from_utf8_lossy(&bytes).into_owned();
let dir = |view: &str| {
repo.href(&format!("/{view}/{}/{}", url_path(rev), url_path(path)))
.trim_end_matches('/')
.to_string()
};
let html = readme
.name
.to_lowercase()
.ends_with(".md")
.then(|| markdown(&text, &dir("blob"), &dir("raw")));
let html = is_markdown(&readme.name).then(|| {
markdown(
&text,
&repo.dir("blob", rev, path),
&repo.dir("raw", rev, path),
)
});
Some(Readme {
name: readme.name.clone(),
text: html.is_none().then_some(text),
@@ -880,6 +886,12 @@ async fn listing(app: &App, repo: &Repo, rev: &str, path: &str) -> Result<Respon
}))
}
/// Whether `path` names a Markdown file, by its extension.
fn is_markdown(path: &str) -> bool {
let ext = path.rsplit_once('.').map(|(_, e)| e.to_ascii_lowercase());
matches!(ext.as_deref(), Some("md" | "markdown" | "mdown" | "mkd"))
}
/// Markdown, with raw HTML shown as text and links made relative to the
/// directory it was found in.
fn markdown(text: &str, blobs: &str, raws: &str) -> String {
@@ -917,7 +929,7 @@ fn markdown(text: &str, blobs: &str, raws: &str) -> String {
out
}
/// A link from a README: web and mail links as they are, other schemes
/// A link from Markdown: web and mail links as they are, other schemes
/// nowhere, and paths into the repository.
fn link(dest: &str, base: &str) -> String {
let scheme = dest
@@ -951,7 +963,8 @@ fn is_binary(bytes: &[u8]) -> bool {
}
/// One file at a revision: an image, a binary, too large to show, or text
/// with the ranges its highlighting paints.
/// with the ranges its highlighting paints, and for Markdown, made into
/// HTML too.
#[derive(Serialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export)]
@@ -965,6 +978,9 @@ pub struct Blob {
lines: usize,
text: Option<String>,
ranges: Option<String>,
/// A Markdown file made into HTML, shown in place of its text unless the
/// page is asked for it plain.
html: Option<String>,
}
async fn blob(
@@ -989,6 +1005,14 @@ async fn blob(
.map(|r| r.encode())
.unwrap_or_default()
});
let html = (shown == "text" && is_markdown(path)).then(|| {
let dir = path.rsplit_once('/').map_or("", |(dir, _)| dir);
markdown(
&text,
&repo.dir("blob", &rev, dir),
&repo.dir("raw", &rev, dir),
)
});
Ok(answer(Blob {
repo: repo.head(&app).await,
path: path.to_string(),
@@ -997,6 +1021,7 @@ async fn blob(
lines: text.lines().count().max(1),
text: ranges.is_some().then(|| text.into_owned()),
ranges,
html,
rev,
}))
}
@@ -1593,6 +1618,8 @@ mod tests {
assert_eq!(is_image("a/B.PNG"), Some("image/png"));
assert_eq!(is_image("Makefile"), None);
assert!(is_binary(&[0, 1]) && is_binary(&[0xff, 0xfe]) && !is_binary(b"text"));
assert!(is_markdown("docs/A.MD") && is_markdown("notes.markdown"));
assert!(!is_markdown("README") && !is_markdown("md") && !is_markdown("a.mdx"));
}
#[test]