pub(crate) fn lang_for(path: &str) -> String {
match path
.rsplit_once('.')
.map(|(_, ext)| ext.to_ascii_lowercase())
.as_deref()
{
Some("rs") => "rust".to_owned(),
Some(other) => other.to_owned(),
None => "text".to_owned(),
}
}
pub(crate) use rto_graph::slugify;
pub(crate) use rto_graph::{first_h1, heading_text};
#[must_use]
pub(crate) fn scan_wiki_links(line: &str) -> Vec<String> {
let mut out = Vec::new();
let stripped = strip_code_spans(line);
let mut rest = stripped.as_str();
while let Some(open) = rest.find("[[") {
let after = &rest[open + 2..];
if let Some(close) = after.find("]]") {
let inner = after[..close].trim();
if !inner.is_empty() {
out.push(inner.to_owned());
}
rest = &after[close + 2..];
} else {
break;
}
}
out
}
#[must_use]
pub(crate) fn strip_code_spans(line: &str) -> String {
let bytes = line.as_bytes();
let mut out = String::new();
let mut i = 0;
while i < bytes.len() {
if bytes[i] != b'`' {
let start = i;
while i < bytes.len() && bytes[i] != b'`' {
i += 1;
}
out.push_str(&line[start..i]);
continue;
}
let run_start = i;
while i < bytes.len() && bytes[i] == b'`' {
i += 1;
}
let run = i - run_start;
let mut j = i;
let mut close = None;
while j < bytes.len() {
if bytes[j] == b'`' {
let s = j;
while j < bytes.len() && bytes[j] == b'`' {
j += 1;
}
if j - s == run {
close = Some(j);
break;
}
} else {
j += 1;
}
}
match close {
Some(end) => i = end,
None => out.push_str(&line[run_start..i]),
}
}
out
}
pub(crate) fn trim_blank_lines(span: &str) -> &str {
let blank = |line: &&str| line.trim().is_empty();
let leading: usize = span
.split_inclusive('\n')
.take_while(blank)
.map(str::len)
.sum();
let span = &span[leading..];
let trailing: usize = span
.split_inclusive('\n')
.rev()
.take_while(blank)
.map(str::len)
.sum();
let span = &span[..span.len() - trailing];
span.strip_suffix('\n')
.map_or(span, |s| s.strip_suffix('\r').unwrap_or(s))
}
#[cfg(test)]
mod tests {
use super::{lang_for, strip_code_spans, trim_blank_lines};
#[test]
fn lang_for_lowercases_extension_to_match_the_extractor() {
assert_eq!(lang_for("src/FOO.RS"), "rust", "case-insensitive rust");
assert_eq!(lang_for("a/b.rs"), "rust");
assert_eq!(lang_for("x.PY"), "py", "other extensions lowercased");
assert_eq!(lang_for("README"), "text", "no extension");
}
#[test]
fn removes_single_and_multi_backtick_spans() {
assert_eq!(strip_code_spans("a `code` b"), "a b");
assert_eq!(strip_code_spans("see ``@rto:0001`` here"), "see here");
assert_eq!(strip_code_spans("x ```fenced inline``` y"), "x y");
}
#[test]
fn keeps_unmatched_backticks_and_plain_text() {
assert_eq!(strip_code_spans("no code here"), "no code here");
assert_eq!(strip_code_spans("unmatched ` tick"), "unmatched ` tick");
assert_eq!(strip_code_spans("``open ` mid"), "``open ` mid");
}
#[test]
fn preserves_utf8_outside_spans() {
assert_eq!(strip_code_spans("café `x` — ok"), "café — ok");
}
#[test]
fn trims_surrounding_blank_lines_and_keeps_indentation() {
assert_eq!(
trim_blank_lines("\n\n code;\n\nprose.\n\n"),
" code;\n\nprose."
);
assert_eq!(trim_blank_lines(" \n\t\n\tcode;\n \n"), "\tcode;");
assert_eq!(trim_blank_lines("a\n\nb"), "a\n\nb");
assert_eq!(trim_blank_lines("a \n"), "a ");
assert_eq!(trim_blank_lines("\r\na\r\n\r\n"), "a");
}
#[test]
fn an_all_blank_span_is_empty() {
assert_eq!(trim_blank_lines(""), "");
assert_eq!(trim_blank_lines("\n"), "");
assert_eq!(trim_blank_lines("\n\n"), "");
assert_eq!(trim_blank_lines(" \n\t \n"), "");
}
}