chadtmiller.com/knotless
Git hosted in your atproto account, on any stock PDS, fast enough for agents.
git clone knotless://chadtmiller.com/knotlessneeds 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.
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] | ||