use anyhow::{Context, Result, ensure};
use cap_std::{ambient_authority, fs::Dir};
use std::collections::HashSet;
use std::sync::LazyLock;
const DOCUMENT_PATHS: &[&str] = &["README.md", "docs/users-guide.md"];
const MARKER_PREFIX: &str = "<!-- tested-example: ";
const MARKER_SUFFIX: &str = " -->";
static DOCUMENTED_EXAMPLES: LazyLock<Result<Vec<DocumentedExample>, String>> =
LazyLock::new(|| read_documented_examples().map_err(|error| format!("{error:#}")));
#[derive(Clone, Copy)]
struct Cursor {
source: &'static str,
line_index: usize,
}
#[derive(Clone, Copy, Eq, PartialEq)]
struct Fence {
delimiter: u8,
length: usize,
}
impl Cursor {
fn error(self, message: &str) -> String {
format!("{}:{} {message}", self.source, self.line_index + 1)
}
}
#[derive(Debug, Eq, PartialEq)]
pub struct DocumentedExample {
pub id: String,
pub language: String,
pub body: String,
pub source: &'static str,
pub line: usize,
}
pub fn load_documented_examples() -> Result<&'static [DocumentedExample]> {
DOCUMENTED_EXAMPLES
.as_ref()
.map(Vec::as_slice)
.map_err(|message| anyhow::anyhow!(message.clone()))
}
fn read_documented_examples() -> Result<Vec<DocumentedExample>> {
let repository = repository_directory()?;
let mut examples = Vec::new();
for path in DOCUMENT_PATHS {
let contents = repository
.read_to_string(path)
.with_context(|| format!("read {path}"))?;
examples.extend(parse_document(path, &contents)?);
}
let mut ids = HashSet::new();
for example in &examples {
ensure!(
ids.insert(example.id.as_str()),
"duplicate tested-example identifier '{}'",
example.id
);
}
Ok(examples)
}
pub fn documented_example(id: &str) -> Result<&'static DocumentedExample> {
load_documented_examples()?
.iter()
.find(|example| example.id == id)
.with_context(|| format!("documented example '{id}' should exist"))
}
pub(super) fn is_valid_example_id(id: &str) -> bool {
id.as_bytes().first().is_some_and(u8::is_ascii_lowercase)
&& !id.ends_with('-')
&& !id.contains("--")
&& id
.bytes()
.all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit() || byte == b'-')
}
pub(crate) fn parse_document(
source: &'static str,
contents: &str,
) -> Result<Vec<DocumentedExample>> {
let mut lines = contents.lines().enumerate();
let mut examples = Vec::new();
let mut ids = HashSet::new();
while let Some((line_index, line)) = lines.next() {
let cursor = Cursor { source, line_index };
if let Some(id) = parse_marker(line) {
ensure!(
!id.trim().is_empty(),
"{}",
cursor.error("tested-example identifier must not be empty")
);
ensure!(
is_valid_example_id(id),
"{}",
cursor.error(
"tested-example identifier must use lowercase letters, digits, and single hyphens"
)
);
ensure!(ids.insert(id), "duplicate tested-example identifier '{id}'");
examples.push(read_marked_example(&cursor, id, &mut lines)?);
} else {
reject_invalid_example_line(&cursor, line)?;
}
}
Ok(examples)
}
fn repository_directory() -> Result<Dir> {
let repository_root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("..");
Dir::open_ambient_dir(repository_root, ambient_authority()).context("open the repository root")
}
fn reject_invalid_example_line(cursor: &Cursor, line: &str) -> Result<()> {
ensure!(
line != format!("{MARKER_PREFIX}{}", MARKER_SUFFIX.trim_start()),
"{}",
cursor.error("tested-example identifier must not be empty")
);
ensure!(
parse_fence(line).is_none(),
"{}",
cursor.error("fence is missing a tested-example marker")
);
Ok(())
}
fn read_marked_example<'a>(
cursor: &Cursor,
id: &str,
lines: &mut impl Iterator<Item = (usize, &'a str)>,
) -> Result<DocumentedExample> {
let (fence_index, fence) = lines
.next()
.with_context(|| cursor.error("marker has no fence"))?;
let fence_cursor = Cursor {
source: cursor.source,
line_index: fence_index,
};
let (opening_fence, language) = parse_fence(fence)
.with_context(|| fence_cursor.error("expected an opening fence after marker"))?;
ensure!(
!language.is_empty(),
"{}",
fence_cursor.error("fence should declare a language")
);
let body = read_fence_body(cursor.source, fence_index, opening_fence, lines)?;
Ok(DocumentedExample {
id: id.to_owned(),
language: language.to_owned(),
body,
source: cursor.source,
line: fence_index + 1,
})
}
fn parse_marker(line: &str) -> Option<&str> {
line.strip_prefix(MARKER_PREFIX)
.and_then(|value| value.strip_suffix(MARKER_SUFFIX))
}
fn parse_fence(line: &str) -> Option<(Fence, &str)> {
let indentation = line.bytes().take_while(|byte| *byte == b' ').count();
if indentation > 3 {
return None;
}
let remainder = line.get(indentation..)?;
let delimiter = *remainder.as_bytes().first()?;
if !matches!(delimiter, b'`' | b'~') {
return None;
}
let length = remainder
.bytes()
.take_while(|candidate| *candidate == delimiter)
.count();
let language = remainder.get(length..)?;
(length >= 3).then_some((Fence { delimiter, length }, language))
}
fn is_matching_closing_fence(line: &str, opening_fence: Fence) -> bool {
matches!(
parse_fence(line),
Some((closing_fence, suffix))
if closing_fence == opening_fence
&& suffix.bytes().all(|byte| matches!(byte, b' ' | b'\t'))
)
}
fn read_fence_body<'a>(
source: &'static str,
fence_index: usize,
opening_fence: Fence,
lines: &mut impl Iterator<Item = (usize, &'a str)>,
) -> Result<String> {
let mut body = String::new();
for (_, line) in lines {
if is_matching_closing_fence(line, opening_fence) {
return Ok(body);
}
body.push_str(line);
body.push('\n');
}
anyhow::bail!("{source}:{} fence is not terminated", fence_index + 1)
}