use std::sync::LazyLock;
const README: &str = include_str!("../README.md");
const BLOCKS: usize = 2;
pub static INTRO: LazyLock<Vec<String>> = LazyLock::new(|| blocks(README, BLOCKS));
fn blocks(markdown: &str, count: usize) -> Vec<String> {
let joined = |text: &str| inline(&text.split_whitespace().collect::<Vec<_>>().join(" "));
markdown
.split("\n\n")
.map(str::trim)
.filter(|block| !block.is_empty() && !block.starts_with('#'))
.take(count)
.map(|block| {
let item = |line: &str| {
line.strip_prefix("* ")
.or_else(|| line.strip_prefix("- "))
.map(str::to_string)
};
if block.lines().next().and_then(item).is_some() {
let mut items: Vec<String> = Vec::new();
for line in block.lines().map(str::trim) {
match (item(line), items.last_mut()) {
(Some(text), _) => items.push(text),
(None, Some(last)) => {
last.push(' ');
last.push_str(line);
}
(None, None) => {}
}
}
let items: String = items
.iter()
.map(|text| format!("<li>{}</li>", joined(text)))
.collect();
format!("<ul>{items}</ul>")
} else {
format!("<p>{}</p>", joined(block))
}
})
.collect()
}
fn inline(text: &str) -> String {
let mut out = String::new();
let mut rest = text;
while let Some(c) = rest.chars().next() {
if let Some(after) = rest.strip_prefix("**")
&& let Some((inner, tail)) = after.split_once("**")
{
out.push_str(&format!("<strong>{}</strong>", inline(inner)));
rest = tail;
} else if let Some(after) = rest.strip_prefix('`')
&& let Some((inner, tail)) = after.split_once('`')
{
out.push_str(&format!("<code>{}</code>", escape(inner)));
rest = tail;
} else if let Some((label, url, tail)) = link(rest) {
out.push_str(&format!(
r#"<a href="{}" class="underline" rel="noreferrer">{}</a>"#,
escape(url),
inline(label)
));
rest = tail;
} else {
out.push_str(&escape(&c.to_string()));
rest = &rest[c.len_utf8()..];
}
}
out
}
fn link(text: &str) -> Option<(&str, &str, &str)> {
let (label, after) = text.strip_prefix('[')?.split_once("](")?;
let (url, tail) = after.split_once(')')?;
(url.starts_with("https://") || url.starts_with("http://")).then_some((label, url, tail))
}
fn escape(text: &str) -> String {
text.replace('&', "&")
.replace('<', "<")
.replace('>', ">")
.replace('"', """)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_first_blocks_become_html() {
let markdown = "# defter\n\nA **self-hosted** server: see [Zotero](https://www.zotero.org/)\n\
and `commonmeta sync` <script>:\n\n* updates `one`,\n* fetches\n two.\n\nThird.";
assert_eq!(
blocks(markdown, 2),
[
"<p>A <strong>self-hosted</strong> server: see <a href=\"https://www.zotero.org/\" \
class=\"underline\" rel=\"noreferrer\">Zotero</a> and <code>commonmeta sync</code> \
<script>:</p>",
"<ul><li>updates <code>one</code>,</li><li>fetches two.</li></ul>",
]
);
assert_eq!(inline("[a](javascript:x)"), "[a](javascript:x)");
}
#[test]
fn the_readme_begins_with_a_paragraph_and_a_list() {
assert_eq!(INTRO.len(), 2);
assert!(
INTRO[0].starts_with("<p>") && INTRO[0].contains("Zotero Storage"),
"{}",
INTRO[0]
);
assert!(INTRO[1].starts_with("<ul><li>"), "{}", INTRO[1]);
assert_eq!(INTRO[1].matches("<li>").count(), 3, "{}", INTRO[1]);
}
}