#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum CodeRules {
#[default]
CommonMark,
SkillExport,
}
fn fence_run(line: &[u8]) -> Option<(u8, usize, usize)> {
let at = leading_spaces(line, 3);
let character = *line.get(at)?;
if character != b'`' && character != b'~' {
return None;
}
let length = line[at..].iter().take_while(|b| **b == character).count();
(length >= 3).then_some((character, length, at + length))
}
fn opens_fence(line: &[u8], rules: CodeRules) -> Option<(u8, usize)> {
let (character, length, end) = fence_run(line)?;
let backtick_in_info = character == b'`' && line[end..].contains(&b'`');
if rules == CodeRules::CommonMark && backtick_in_info {
return None;
}
Some((character, length))
}
fn closes_fence(line: &[u8], (open_char, open_len): (u8, usize), rules: CodeRules) -> bool {
let Some((character, length, end)) = fence_run(line) else {
return false;
};
character == open_char
&& length >= open_len
&& (rules == CodeRules::SkillExport || is_blank(&line[end..]))
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FenceLine {
Marker,
Inside,
Outside,
}
#[derive(Debug, Clone, Copy, Default)]
pub struct Fences {
rules: CodeRules,
open: Option<(u8, usize)>,
}
impl Fences {
pub fn new(rules: CodeRules) -> Self {
Self { rules, open: None }
}
pub fn line(&mut self, line: &[u8]) -> FenceLine {
match self.open {
None => match opens_fence(line, self.rules) {
Some(fence) => {
self.open = Some(fence);
FenceLine::Marker
}
None => FenceLine::Outside,
},
Some(fence) => {
if closes_fence(line, fence, self.rules) {
self.open = None;
FenceLine::Marker
} else if self.rules == CodeRules::SkillExport && fence_run(line).is_some() {
FenceLine::Marker
} else {
FenceLine::Inside
}
}
}
}
}
#[derive(Debug, Clone)]
pub struct Spans<'a> {
bytes: &'a [u8],
rules: CodeRules,
one_line: bool,
no_close: Option<BlockRuns>,
}
#[derive(Debug, Clone)]
struct BlockRuns {
rules: CodeRules,
start: usize,
end: usize,
last_close: Vec<(usize, usize)>,
}
impl<'a> Spans<'a> {
pub fn new(rules: CodeRules, bytes: &'a [u8]) -> Self {
Self {
bytes,
rules,
one_line: false,
no_close: None,
}
}
pub fn line(&mut self, line: &[u8]) {
self.one_line = self.rules == CodeRules::CommonMark && is_one_line_block(line);
}
pub fn close(&mut self, start: usize) -> (usize, Option<usize>) {
debug_assert_eq!(self.bytes.get(start), Some(&b'`'));
let length = run_length(self.bytes, start);
let from = start + length;
if let Some(runs) = &self.no_close
&& runs.start <= from
&& from < runs.end
&& !runs.has_close(length, from)
{
return (length, None);
}
match self.search(length, from) {
Ok(close) => (length, Some(close)),
Err(end) => {
self.no_close = Some(BlockRuns::new(self.bytes, from, end, self.rules));
(length, None)
}
}
}
fn search(&self, length: usize, from: usize) -> Result<usize, usize> {
let bytes = self.bytes;
let mut at = from;
loop {
while at < bytes.len() && bytes[at] != b'\n' {
if bytes[at] != b'`' {
at += 1;
continue;
}
let run = run_length(bytes, at);
if closes(run, length, self.rules) {
return Ok(at);
}
at += run;
}
let next = at + 1;
if self.one_line || next >= bytes.len() {
return Err(next.min(bytes.len()));
}
if ends_span_block(&bytes[next..line_end(bytes, next)], self.rules) {
return Err(next);
}
at = next;
}
}
}
impl BlockRuns {
fn new(bytes: &[u8], start: usize, end: usize, rules: CodeRules) -> Self {
let mut last = std::collections::BTreeMap::new();
let mut at = start;
while at < end {
if bytes[at] != b'`' {
at += 1;
continue;
}
let run = run_length(&bytes[..end], at);
last.insert(run, at);
at += run;
}
let mut last_close: Vec<(usize, usize)> = last.into_iter().collect();
if rules == CodeRules::SkillExport {
for i in (0..last_close.len().saturating_sub(1)).rev() {
last_close[i].1 = last_close[i].1.max(last_close[i + 1].1);
}
}
Self {
rules,
start,
end,
last_close,
}
}
fn has_close(&self, length: usize, from: usize) -> bool {
let at = self.last_close.partition_point(|(run, _)| *run < length);
self.last_close.get(at).is_some_and(|&(run, start)| {
start >= from && (run == length || self.rules == CodeRules::SkillExport)
})
}
}
fn closes(run: usize, length: usize, rules: CodeRules) -> bool {
match rules {
CodeRules::CommonMark => run == length,
CodeRules::SkillExport => run >= length,
}
}
fn run_length(bytes: &[u8], at: usize) -> usize {
bytes[at..].iter().take_while(|b| **b == b'`').count()
}
fn ends_span_block(line: &[u8], rules: CodeRules) -> bool {
let line = line.strip_suffix(b"\r").unwrap_or(line);
if is_blank(line) {
return true;
}
match rules {
CodeRules::SkillExport => fence_run(line).is_some(),
CodeRules::CommonMark => opens_fence(line, rules).is_some() || starts_block(line),
}
}
fn is_one_line_block(line: &[u8]) -> bool {
let indented = &line[leading_spaces(line, 3)..];
atx_heading(indented) || indented.first() == Some(&b'|')
}
fn atx_heading(line: &[u8]) -> bool {
let hashes = line.iter().take_while(|b| **b == b'#').count();
(1..=6).contains(&hashes) && matches!(line.get(hashes), None | Some(b' ' | b'\t' | b'\r'))
}
fn starts_block(line: &[u8]) -> bool {
let indented = &line[leading_spaces(line, 3)..];
let rest = &line[line
.iter()
.take_while(|b| matches!(b, b' ' | b'\t'))
.count()..];
let ends_marker = |at: usize, text: &[u8]| matches!(text.get(at), None | Some(b' ' | b'\t'));
let equals = indented.iter().take_while(|b| **b == b'=').count();
let setext = equals > 0 && is_blank(&indented[equals..]);
let quote = rest.first() == Some(&b'>');
let bullet = matches!(rest.first(), Some(b'-' | b'*' | b'+')) && ends_marker(1, rest);
let digits = rest.iter().take_while(|b| b.is_ascii_digit()).count();
let ordered = (1..=9).contains(&digits)
&& matches!(rest.get(digits), Some(b'.' | b')'))
&& ends_marker(digits + 1, rest);
is_one_line_block(line) || setext || quote || bullet || ordered || thematic_break(indented)
}
fn thematic_break(line: &[u8]) -> bool {
let Some(&mark @ (b'-' | b'*' | b'_')) = line.first() else {
return false;
};
line.iter()
.all(|b| matches!(*b, b' ' | b'\t') || *b == mark)
&& line.iter().filter(|b| **b == mark).count() >= 3
}
fn leading_spaces(line: &[u8], max: usize) -> usize {
line.iter().take(max).take_while(|b| **b == b' ').count()
}
fn is_blank(text: &[u8]) -> bool {
text.iter().all(|b| matches!(b, b' ' | b'\t' | b'\r'))
}
fn line_end(bytes: &[u8], from: usize) -> usize {
bytes[from..]
.iter()
.position(|b| *b == b'\n')
.map_or(bytes.len(), |i| from + i)
}
pub fn strip_code(text: &str) -> String {
strip_code_with(text, CodeRules::CommonMark)
}
pub fn strip_code_with(text: &str, rules: CodeRules) -> String {
let bytes = text.as_bytes();
let mut out = String::with_capacity(text.len());
let mut fences = Fences::new(rules);
let mut spans = Spans::new(rules, bytes);
let mut written_up_to = 0usize;
let mut blank = |out: &mut String, start: usize, end: usize| {
out.push_str(&text[written_up_to..start]);
out.extend(
text[start..end]
.chars()
.map(|c| if c == '\n' { '\n' } else { ' ' }),
);
written_up_to = end;
};
let mut offset = 0usize;
while offset < bytes.len() {
if offset == 0 || bytes[offset - 1] == b'\n' {
let end = line_end(bytes, offset);
let line = &bytes[offset..end];
if fences.line(line) != FenceLine::Outside {
blank(&mut out, offset, end);
offset = end + 1;
continue;
}
spans.line(line);
}
if bytes[offset] == b'`' {
let (run, close) = spans.close(offset);
offset = match close {
Some(close) => {
blank(&mut out, offset, close + run);
close + run
}
None => offset + run,
};
continue;
}
offset += 1;
}
out.push_str(&text[written_up_to..]);
out
}
#[cfg(test)]
mod tests {
use super::*;
const BOTH: [CodeRules; 2] = [CodeRules::CommonMark, CodeRules::SkillExport];
fn find_span_close(bytes: &[u8], rules: CodeRules) -> Option<usize> {
let mut spans = Spans::new(rules, bytes);
spans.line(&bytes[..line_end(bytes, 0)]);
spans.close(0).1
}
#[test]
fn a_fence_becomes_spaces_and_keeps_its_newlines() {
let out = strip_code("a\n```\nhidden\n```\nb\n");
assert_eq!(out, "a\n \n \n \nb\n");
}
#[test]
fn an_inline_span_becomes_spaces() {
assert_eq!(strip_code("a `x` b"), "a b");
}
#[test]
fn an_unterminated_inline_backtick_is_left_alone() {
assert_eq!(strip_code("a `x b"), "a `x b");
}
#[test]
fn the_char_count_is_preserved_exactly() {
for rules in BOTH {
for src in [
"`é` after",
"```\nsanté\n```\n",
"~~~\nsanté\n",
"``a ` é``",
"plain text",
"a `x` b `y`",
] {
assert_eq!(
strip_code_with(src, rules).chars().count(),
src.chars().count(),
"for {src:?} under {rules:?}"
);
}
}
}
#[test]
fn fence_markers_take_up_to_three_spaces_and_three_or_more_characters() {
assert_eq!(fence_run(b"```"), Some((b'`', 3, 3)));
assert_eq!(fence_run(b" ~~~~ info"), Some((b'~', 4, 7)));
assert_eq!(fence_run(b" ```"), None);
assert_eq!(fence_run(b"``"), None);
assert_eq!(fence_run(b"text ```"), None);
}
#[test]
fn a_fence_closes_only_on_its_own_character_at_its_length_or_more() {
for rules in BOTH {
let mut fences = Fences::new(rules);
let kinds: Vec<FenceLine> = ["````", "```", "~~~~~", "x", "`````", "y"]
.iter()
.map(|line| fences.line(line.as_bytes()))
.collect();
let inner_marker = match rules {
CodeRules::CommonMark => FenceLine::Inside,
CodeRules::SkillExport => FenceLine::Marker,
};
assert_eq!(
kinds,
vec![
FenceLine::Marker,
inner_marker,
inner_marker,
FenceLine::Inside,
FenceLine::Marker,
FenceLine::Outside,
],
"under {rules:?}"
);
}
}
#[test]
fn a_span_does_not_cross_a_blank_line_or_a_fence_line() {
for rules in BOTH {
assert_eq!(find_span_close(b"`a\nb`", rules), Some(4));
assert_eq!(find_span_close(b"`a\n\nb`", rules), None);
assert_eq!(find_span_close(b"`a\n```\nb`", rules), None);
}
}
#[test]
fn block_starts_end_a_commonmark_span_only() {
for next in [
"# h",
"###### h",
"===",
"***",
"_ _ _",
"> q",
"- i",
" * i",
" + nested",
"1. i",
"12) i",
] {
let src = format!("`a\n{next}\nb`");
let bytes = src.as_bytes();
assert_eq!(
find_span_close(bytes, CodeRules::CommonMark),
None,
"{next:?}"
);
assert!(
find_span_close(bytes, CodeRules::SkillExport).is_some(),
"{next:?}"
);
}
for next in [
"#hashtag",
"####### seven",
"-dash",
"1.5 kg",
" # indented",
] {
let src = format!("`a\n{next}\nb`");
assert!(
find_span_close(src.as_bytes(), CodeRules::CommonMark).is_some(),
"{next:?}"
);
}
}
#[test]
fn a_pass_is_linear_on_long_lines_and_unclosed_runs() {
let long_line = "`a` [[x]] ".repeat(200_000);
fn unclosed(lengths: impl Iterator<Item = usize>) -> String {
lengths
.map(|n| format!("x {} text\n", "`".repeat(n)))
.collect()
}
let rising = unclosed(1..=2000);
let falling = unclosed((1..=2000).rev());
for rules in BOTH {
for text in [&long_line, &rising, &falling] {
let started = std::time::Instant::now();
let out = strip_code_with(text, rules);
let elapsed = started.elapsed();
assert_eq!(out.len(), text.len());
assert!(
elapsed < std::time::Duration::from_secs(10),
"{} KB under {rules:?} took {elapsed:?}",
text.len() / 1000
);
}
}
}
#[test]
fn commonmark_closes_a_span_only_on_a_run_of_its_own_length() {
assert_eq!(find_span_close(b"`a``b`", CodeRules::CommonMark), Some(5));
assert_eq!(find_span_close(b"`a``b`", CodeRules::SkillExport), Some(2));
}
}