use std::env;
pub const DEFAULT_CHUNK_THRESHOLD: usize = 28_000;
pub const CHUNK_THRESHOLD_ENV: &str = "MX_WAKE_CHUNK_BYTES";
pub fn chunk_threshold() -> usize {
match env::var(CHUNK_THRESHOLD_ENV) {
Ok(v) => v
.parse::<usize>()
.ok()
.filter(|n| *n > 0)
.unwrap_or(DEFAULT_CHUNK_THRESHOLD),
Err(_) => DEFAULT_CHUNK_THRESHOLD,
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ChunkPlan {
pub total: u16,
pub boundaries: Vec<usize>,
pub oversized: Vec<bool>,
}
impl ChunkPlan {
pub fn chunk<'a>(&self, content: &'a str, idx: u16) -> &'a str {
let (start, end) = self.chunk_range(content, idx);
&content[start..end]
}
pub fn chunk_range(&self, content: &str, idx: u16) -> (usize, usize) {
let idx = idx as usize;
if idx >= self.total as usize {
return (content.len(), content.len());
}
let start = if idx == 0 {
0
} else {
self.boundaries[idx - 1]
};
let end = if idx == self.total as usize - 1 {
content.len()
} else {
self.boundaries[idx]
};
(start, end)
}
pub fn is_oversized(&self, idx: u16) -> bool {
self.oversized.get(idx as usize).copied().unwrap_or(false)
}
pub fn iter<'a>(&'a self, content: &'a str) -> ChunkPlanIter<'a> {
ChunkPlanIter {
plan: self,
content,
idx: 0,
}
}
}
pub struct ChunkPlanIter<'a> {
plan: &'a ChunkPlan,
content: &'a str,
idx: u16,
}
impl<'a> Iterator for ChunkPlanIter<'a> {
type Item = (u16, &'a str, bool);
fn next(&mut self) -> Option<Self::Item> {
if self.idx >= self.plan.total {
return None;
}
let idx = self.idx;
let chunk = self.plan.chunk(self.content, idx);
let flag = self.plan.is_oversized(idx);
self.idx += 1;
Some((idx, chunk, flag))
}
}
pub fn compute_chunks(content: &str, threshold: usize) -> ChunkPlan {
if content.len() <= threshold {
return ChunkPlan {
total: 1,
boundaries: Vec::new(),
oversized: vec![false],
};
}
let fences = find_fence_starts(content);
let filler_start = trailing_filler_start(content);
let mut boundaries: Vec<usize> = Vec::new();
let mut oversized: Vec<bool> = Vec::new();
let mut cursor = 0;
while content.len() - cursor > threshold {
let window_end = cursor + threshold;
match find_break(content, cursor, window_end, &fences, filler_start) {
Some(pos) => {
if pos <= cursor {
let recovery = recover_past_block(content, cursor, window_end, &fences)
.unwrap_or_else(|| safe_utf8_fallback(content, content.len()));
if recovery <= cursor || recovery >= filler_start {
oversized.push(true);
break;
}
let chunk_len = recovery - cursor;
boundaries.push(recovery);
oversized.push(chunk_len > threshold);
cursor = recovery;
continue;
}
let chunk_len = pos - cursor;
boundaries.push(pos);
oversized.push(chunk_len > threshold);
cursor = pos;
}
None => {
let recovery = recover_past_block(content, cursor, window_end, &fences)
.unwrap_or_else(|| safe_utf8_fallback(content, content.len()));
if recovery <= cursor || recovery >= filler_start {
oversized.push(true);
break;
}
let chunk_len = recovery - cursor;
boundaries.push(recovery);
oversized.push(chunk_len > threshold);
cursor = recovery;
}
}
}
if oversized.len() == boundaries.len() {
let tail_start = boundaries.last().copied().unwrap_or(0);
let tail_len = content.len() - tail_start;
oversized.push(tail_len > threshold);
}
let total_u16 = u16::try_from(boundaries.len() + 1).unwrap_or(u16::MAX);
let mut recomputed_oversized = Vec::with_capacity(total_u16 as usize);
for idx in 0..total_u16 {
let i = idx as usize;
let start = if i == 0 { 0 } else { boundaries[i - 1] };
let end = if i == total_u16 as usize - 1 {
content.len()
} else {
boundaries[i]
};
let declared = oversized.get(i).copied().unwrap_or(false);
let actual = end - start > threshold;
recomputed_oversized.push(declared || actual);
}
ChunkPlan {
total: total_u16,
boundaries,
oversized: recomputed_oversized,
}
}
fn find_fence_starts(content: &str) -> Vec<usize> {
let bytes = content.as_bytes();
let mut fences = Vec::new();
let mut i = 0;
while i < bytes.len() {
let at_line_start = i == 0 || bytes[i - 1] == b'\n';
if at_line_start && i + 2 < bytes.len() && &bytes[i..i + 3] == b"```" {
fences.push(i);
match bytes[i..].iter().position(|&b| b == b'\n') {
Some(nl) => i += nl + 1,
None => break,
}
} else {
i += 1;
}
}
fences
}
fn is_inside_fence(fences: &[usize], pos: usize) -> bool {
let count = fences.iter().take_while(|&&f| f < pos).count();
count % 2 == 1
}
fn find_break(
content: &str,
start: usize,
window_end: usize,
fences: &[usize],
filler_start: usize,
) -> Option<usize> {
let window_end = window_end.min(content.len());
if window_end <= start {
return None;
}
let ladder: &[&[u8]] = &[b"\n---\n", b"\n## ", b"\n### ", b"\n\n", b"\n"];
for pat in ladder {
let mut search_end = window_end;
while let Some(pos) = rfind_in_range(content, start, search_end, pat) {
let split = pos + 1; if split <= start
|| split >= window_end
|| is_inside_fence(fences, split)
|| !content.is_char_boundary(split)
{
break;
}
if split >= filler_start {
search_end = pos + pat.len() - 1;
continue;
}
if is_rule_or_blank(&content[start..split]) {
break;
}
return Some(split);
}
}
let prose = content[..filler_start].trim_end();
let last_word = prose.trim_end_matches(|c: char| !c.is_whitespace()).len();
let cap = if window_end < prose.len() || prose.len() <= start + 1 {
window_end
} else if last_word > start.max(prose.rfind('\n').map_or(0, |i| i + 1)) {
last_word
} else {
prose.len() - 1
};
let fallback = safe_utf8_fallback(content, cap);
if fallback > start && !is_inside_fence(fences, fallback) {
return Some(fallback);
}
None
}
fn is_rule_or_blank(text: &str) -> bool {
text.lines().all(|line| {
let mut marks = line.chars().filter(|c| !c.is_whitespace());
match marks.next() {
None => true,
Some(first @ ('-' | '*' | '_')) => {
let rest: Vec<char> = marks.collect();
rest.len() >= 2 && rest.iter().all(|&c| c == first)
}
Some(_) => false,
}
})
}
fn trailing_filler_start(content: &str) -> usize {
let mut start = content.len();
for line in content.split_inclusive('\n').rev() {
if !is_rule_or_blank(line) {
break;
}
start -= line.len();
}
start
}
fn rfind_in_range(content: &str, start: usize, end: usize, needle: &[u8]) -> Option<usize> {
let end = end.min(content.len());
if end <= start || needle.is_empty() || end - start < needle.len() {
return None;
}
let haystack = &content.as_bytes()[start..end];
let n = needle.len();
let mut i = haystack.len().saturating_sub(n);
loop {
if &haystack[i..i + n] == needle {
return Some(start + i);
}
if i == 0 {
return None;
}
i -= 1;
}
}
fn safe_utf8_fallback(content: &str, pos: usize) -> usize {
let mut p = pos.min(content.len());
while p > 0 && !content.is_char_boundary(p) {
p -= 1;
}
p
}
fn recover_past_block(
content: &str,
start: usize,
window_end: usize,
fences: &[usize],
) -> Option<usize> {
let next_fence = *fences.iter().find(|&&f| f > window_end)?;
let bytes = content.as_bytes();
let after_fence_line = match bytes[next_fence..].iter().position(|&b| b == b'\n') {
Some(nl) => next_fence + nl + 1,
None => content.len(),
};
let safe = safe_utf8_fallback(content, after_fence_line);
if safe > start { Some(safe) } else { None }
}
const PHRASE_MAX_CHARS: usize = 120;
const SENTENCE_MAX_CHARS: usize = 100;
const LINE_FALLBACK_MAX_CHARS: usize = 80;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Candidate {
pub phrase: String,
pub text: String,
}
pub fn salient_candidates(content: &str) -> Vec<Candidate> {
let capped = |text: String, max: usize| Candidate {
phrase: cap_chars(text.trim(), max),
text,
};
if let Some(sentences) = body_sentences(content) {
return sentences
.into_iter()
.map(|s| capped(s, SENTENCE_MAX_CHARS))
.collect();
}
if let Some(heading) = first_heading(content).filter(|h| has_letter(h)) {
return vec![capped(heading, PHRASE_MAX_CHARS)];
}
first_non_empty_line(content)
.filter(|line| has_letter(line))
.map(|line| capped(line, LINE_FALLBACK_MAX_CHARS))
.into_iter()
.collect()
}
fn has_letter(text: &str) -> bool {
text.chars().any(char::is_alphabetic)
}
fn first_heading(content: &str) -> Option<String> {
use pulldown_cmark::{Event, Parser, Tag, TagEnd};
let parser = Parser::new(content);
let mut in_heading = false;
let mut heading_text = String::new();
for event in parser {
match event {
Event::Start(Tag::Heading { .. }) => {
in_heading = true;
heading_text.clear();
}
Event::End(TagEnd::Heading(_)) => {
let trimmed = heading_text.trim();
if !trimmed.is_empty() {
return Some(trimmed.to_string());
}
in_heading = false;
}
Event::Text(ref text) | Event::Code(ref text) if in_heading => {
heading_text.push_str(text);
}
_ => {}
}
}
None
}
fn body_sentences(content: &str) -> Option<Vec<String>> {
use pulldown_cmark::{Event, Parser, Tag, TagEnd};
#[derive(Default)]
struct Block {
text: String,
text_outside_strong: bool,
strong_spans_with_text: usize,
}
fn is_inline(tag: &Tag) -> bool {
matches!(
tag,
Tag::Emphasis
| Tag::Strong
| Tag::Strikethrough
| Tag::Superscript
| Tag::Subscript
| Tag::Link { .. }
| Tag::Image { .. }
)
}
fn is_inline_end(tag: &TagEnd) -> bool {
matches!(
tag,
TagEnd::Emphasis
| TagEnd::Strong
| TagEnd::Strikethrough
| TagEnd::Superscript
| TagEnd::Subscript
| TagEnd::Link
| TagEnd::Image
)
}
fn flush(block: &mut Block, out: &mut Vec<String>, saw_body: &mut bool) {
let taken = std::mem::take(block);
let bold_only = !taken.text_outside_strong && taken.strong_spans_with_text == 1;
let mut rest = taken.text.trim();
if bold_only || rest.is_empty() {
return;
}
*saw_body = true;
while let Some(pos) = rest.find(". ") {
let sentence = rest[..=pos].trim();
if has_letter(sentence) {
out.push(sentence.to_string());
}
rest = rest[pos + 2..].trim_start();
}
if has_letter(rest) {
out.push(rest.trim().to_string());
}
}
let mut out = Vec::new();
let mut saw_body = false;
let mut block = Block::default();
let mut body_stack: Vec<bool> = Vec::new();
let mut strong_depth = 0usize;
let mut span_has_text = false;
for event in Parser::new(content) {
let in_body = body_stack.last().copied().unwrap_or(false);
match event {
Event::Start(ref tag) if !is_inline(tag) => {
flush(&mut block, &mut out, &mut saw_body);
body_stack.push(matches!(tag, Tag::Paragraph | Tag::Item));
}
Event::End(ref tag) if !is_inline_end(tag) => {
flush(&mut block, &mut out, &mut saw_body);
body_stack.pop();
}
Event::Start(Tag::Strong) => {
if strong_depth == 0 {
span_has_text = false;
}
strong_depth += 1;
}
Event::End(TagEnd::Strong) => {
strong_depth = strong_depth.saturating_sub(1);
if strong_depth == 0 && span_has_text && in_body {
block.strong_spans_with_text += 1;
}
}
Event::Text(ref text) | Event::Code(ref text) if in_body => {
block.text.push_str(text);
if strong_depth > 0 {
span_has_text |= !text.trim().is_empty();
} else {
block.text_outside_strong |= has_letter(text);
}
}
Event::SoftBreak | Event::HardBreak if in_body => block.text.push(' '),
_ => {}
}
}
flush(&mut block, &mut out, &mut saw_body);
saw_body.then_some(out)
}
fn first_non_empty_line(content: &str) -> Option<String> {
let mut fence_marker: Option<(char, usize)> = None; for line in content.lines() {
let trimmed_start = line.trim_start();
if let Some(marker) = detect_fence_marker(trimmed_start) {
match fence_marker {
None => {
fence_marker = Some(marker);
}
Some((open_ch, open_len)) => {
if marker.0 == open_ch && marker.1 >= open_len {
fence_marker = None;
}
}
}
continue;
}
if fence_marker.is_some() {
continue;
}
let trimmed = line.trim();
if !trimmed.is_empty() {
return Some(trimmed.to_string());
}
}
None
}
fn detect_fence_marker(trimmed_line: &str) -> Option<(char, usize)> {
let first = trimmed_line.chars().next()?;
if first != '`' && first != '~' {
return None;
}
let run_len = trimmed_line.chars().take_while(|&c| c == first).count();
if run_len >= 3 {
Some((first, run_len))
} else {
None
}
}
fn cap_chars(s: &str, max: usize) -> String {
let count = s.chars().count();
if count <= max {
return s.to_string();
}
let head: String = s.chars().take(max).collect();
let cut = match head.rfind(char::is_whitespace) {
Some(i) if i >= max / 2 => &head[..i],
_ => &head[..],
};
format!("{}…", cut.trim_end())
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PhraseMatch {
Exact,
Tolerant,
Mismatch,
}
pub fn compare_phrase(input: &str, target: &str) -> PhraseMatch {
let i = input.trim();
let t = target.trim();
if i == t {
return PhraseMatch::Exact;
}
if normalize_derived(i) == normalize_derived(t) {
PhraseMatch::Tolerant
} else {
PhraseMatch::Mismatch
}
}
pub fn normalize_derived(s: &str) -> String {
let lowered = s.to_lowercase();
let mut out = String::with_capacity(lowered.len());
let mut prev_space = false;
for ch in lowered.chars() {
let c = match ch {
'\u{2018}' | '\u{2019}' | '\u{2032}' => '\'',
'\u{201C}' | '\u{201D}' | '\u{2033}' => '"',
'\u{2013}' | '\u{2014}' => '-',
_ => ch,
};
if c.is_whitespace() {
if !prev_space && !out.is_empty() {
out.push(' ');
prev_space = true;
}
continue;
}
prev_space = false;
out.push(c);
}
while let Some(last) = out.chars().last() {
if matches!(last, '.' | '!' | '?' | '…' | ',' | ';' | ':') {
out.pop();
} else {
break;
}
}
out.trim().to_string()
}
pub fn extract_auto_phrase(content: &str, title: &str) -> String {
if let Some(heading) = first_heading(content) {
return cap_chars(heading.trim(), PHRASE_MAX_CHARS);
}
let sentences = extract_sentences(content);
if !sentences.is_empty() {
let refs: Vec<&str> = sentences.iter().map(|s| s.as_str()).collect();
let idx = select_sentence_index(&refs, content);
let s = sentences[idx].trim();
if !s.is_empty() {
return cap_chars(s, SENTENCE_MAX_CHARS);
}
}
if let Some(line) = first_non_empty_line(content) {
let trimmed = line.trim();
if !trimmed.is_empty() {
return cap_chars(trimmed, LINE_FALLBACK_MAX_CHARS);
}
}
cap_chars(title.trim(), PHRASE_MAX_CHARS)
}
fn extract_sentences(content: &str) -> Vec<String> {
let mut sentences = Vec::new();
let mut current = String::new();
let mut fence_marker: Option<(char, usize)> = None;
for line in content.lines() {
let trimmed_start = line.trim_start();
if let Some(marker) = detect_fence_marker(trimmed_start) {
match fence_marker {
None => {
fence_marker = Some(marker);
}
Some((open_ch, open_len)) => {
if marker.0 == open_ch && marker.1 >= open_len {
fence_marker = None;
}
}
}
continue;
}
if fence_marker.is_some() {
continue;
}
if line.trim().is_empty() {
let trimmed = current.trim().to_string();
if !trimmed.is_empty() {
sentences.push(trimmed);
}
current.clear();
continue;
}
let mut cleaned = line.trim();
if let Some(rest) = cleaned.strip_prefix("- ") {
cleaned = rest;
} else if let Some(rest) = cleaned.strip_prefix("* ") {
cleaned = rest;
}
if !current.is_empty() {
current.push(' ');
}
current.push_str(cleaned);
while let Some(pos) = current.find(". ") {
let sentence = current[..=pos].trim().to_string(); if !sentence.is_empty() {
sentences.push(sentence);
}
current = current[pos + 2..].to_string();
}
}
let trimmed = current.trim().to_string();
if !trimmed.is_empty() {
sentences.push(trimmed);
}
sentences
}
fn select_sentence_index(sentences: &[&str], content: &str) -> usize {
let seed: u64 = content
.bytes()
.fold(0u64, |acc, b| acc.wrapping_mul(31).wrapping_add(b as u64));
seed as usize % sentences.len()
}
#[cfg(test)]
mod tests {
use super::*;
fn candidate_phrases(c: &str) -> Vec<String> {
salient_candidates(c)
.into_iter()
.map(|c| c.phrase)
.collect()
}
fn extract_salient_phrase(c: &str) -> String {
candidate_phrases(c).remove(0)
}
#[test]
fn below_threshold_is_one_chunk() {
let plan = compute_chunks("tiny content", 28_000);
assert_eq!(plan.total, 1);
assert!(plan.boundaries.is_empty());
assert_eq!(plan.chunk("tiny content", 0), "tiny content");
assert!(!plan.is_oversized(0));
}
#[test]
fn exact_threshold_is_one_chunk() {
let content = "a".repeat(100);
let plan = compute_chunks(&content, 100);
assert_eq!(plan.total, 1);
}
#[test]
fn just_over_threshold_chunks_to_size_bound() {
let content = "a".repeat(201);
let plan = compute_chunks(&content, 100);
assert!(plan.total >= 2);
let joined: String = plan
.iter(&content)
.map(|(_, s, _)| s)
.collect::<Vec<_>>()
.join("");
assert_eq!(joined, content);
for (_, chunk, oversized) in plan.iter(&content) {
if !oversized {
assert!(chunk.len() <= 100, "chunk {} over threshold", chunk.len());
}
}
}
#[test]
fn horizontal_rule_preferred_over_paragraph() {
let mut content = String::new();
content.push_str(&"x".repeat(50));
content.push_str("\n---\n");
content.push_str(&"y".repeat(15));
content.push_str("\n\n");
content.push_str(&"z".repeat(80));
let plan = compute_chunks(&content, 100);
assert!(plan.total >= 2);
let chunk0 = plan.chunk(&content, 0);
assert!(
chunk0.ends_with('\n'),
"chunk 0 should end at HR newline: {:?}",
chunk0
);
}
#[test]
fn long_last_line_before_a_trailing_rule_never_splits_the_rule() {
let s = "The tide turns and the boats wait in the harbor. ";
let content = format!("{}\n\n{}\n\n---\n\n", s.repeat(3), s.repeat(4));
let plan = compute_chunks(&content, 200);
assert_every_chunk_phrasable(&plan, &content);
}
#[test]
fn one_unbroken_line_between_leading_and_trailing_filler_keeps_prose_in_the_tail() {
let content = format!(
"{}{}\n{}",
"\n".repeat(50),
"p".repeat(30),
"---\n".repeat(15)
);
assert_no_filler_chunk(&compute_chunks(&content, 100), &content);
}
fn filler(len: usize) -> String {
let mut s = "The tide turns and the boats wait in the harbor. ".repeat(len / 40 + 1);
s.truncate(len - 1);
s.push('.');
s
}
fn chunk_sizes(plan: &ChunkPlan, content: &str) -> Vec<usize> {
plan.iter(content).map(|(_, c, _)| c.len()).collect()
}
fn assert_no_filler_chunk(plan: &ChunkPlan, content: &str) {
for (idx, chunk, _) in plan.iter(content) {
assert!(
!is_rule_or_blank(chunk),
"chunk {idx} is whitespace/rule-only: {chunk:?} (sizes {:?})",
chunk_sizes(plan, content)
);
}
}
fn assert_every_chunk_phrasable(plan: &ChunkPlan, content: &str) {
assert_no_filler_chunk(plan, content);
for (idx, chunk, _) in plan.iter(content) {
assert!(
!salient_candidates(chunk).is_empty(),
"chunk {idx} has no candidate: {chunk:?}"
);
}
}
#[test]
fn rule_before_heading_never_yields_a_rule_only_chunk() {
let content = format!(
"## First\n\n{}\n---\n\n## Second\n\n{}\n\n{}",
filler(161),
filler(169),
filler(174)
);
assert_eq!(content.len(), 533);
let plan = compute_chunks(&content, 200);
assert_every_chunk_phrasable(&plan, &content);
assert_eq!(chunk_sizes(&plan, &content), vec![172, 186, 175]);
assert!(plan.oversized.iter().all(|&o| !o));
}
#[test]
fn trailing_rule_never_yields_a_rule_only_tail() {
let mut content = String::new();
for i in 0..4 {
content.push_str(&format!("line {i} of the closing prose\n"));
}
content.push_str("---\n");
assert_eq!(content.len(), 116);
let plan = compute_chunks(&content, 113);
assert_every_chunk_phrasable(&plan, &content);
assert_eq!(chunk_sizes(&plan, &content), vec![84, 32]);
assert!(plan.oversized.iter().all(|&o| !o));
}
#[test]
fn oversized_fence_never_leaves_a_filler_or_empty_tail() {
let block = format!(
"```text\n{}```\n",
"a line of code inside the fence\n".repeat(8)
);
let closed = format!("{}\n\n{block}\n---\n\n", filler(40));
let plan = compute_chunks(&closed, 100);
assert_no_filler_chunk(&plan, &closed);
let unclosed = format!("{}\n\n```text\n{}", filler(40), "code line\n".repeat(30));
let plan = compute_chunks(&unclosed, 100);
assert_no_filler_chunk(&plan, &unclosed);
}
#[test]
fn paragraph_break_before_long_paragraph_never_yields_a_newline_chunk() {
let content = format!("{}\n\n{}\n\n{}", filler(150), filler(250), filler(90));
let plan = compute_chunks(&content, 200);
assert_every_chunk_phrasable(&plan, &content);
}
#[test]
fn unaffected_documents_keep_their_plans() {
let expected: &[(&str, &[usize], &[bool])] = &[
("paragraphs", &[264, 459, 654], &[false; 4]),
("sections", &[393, 594, 791, 992], &[false; 5]),
(
"fenced",
&[28, 297, 353, 409, 465],
&[false, true, false, false, false, false],
),
(
"multibyte",
&[50, 99, 149, 198, 246, 294, 342, 390, 438],
&[false; 10],
),
("leading_rule", &[74, 266, 461, 656, 851], &[false; 6]),
("rule_then_prose", &[111, 227, 236, 349, 461], &[false; 6]),
(
"lines",
&[75, 150, 225, 302, 380, 458, 536, 614, 692],
&[false; 10],
),
("rule_inside_last_chunk", &[150], &[false; 2]),
];
let docs = pinned_documents();
assert_eq!(docs.len(), expected.len());
for ((name, content, threshold), (want_name, boundaries, oversized)) in
docs.iter().zip(expected)
{
assert_eq!(name, want_name);
let plan = compute_chunks(content, *threshold);
assert_eq!(&plan.boundaries, boundaries, "{name} boundaries");
assert_eq!(&plan.oversized, oversized, "{name} oversized");
assert_eq!(plan.total as usize, boundaries.len() + 1, "{name} total");
}
}
fn pinned_documents() -> Vec<(&'static str, String, usize)> {
let sentence = "The river bends past the mill and the path follows it. ";
let paragraphs = (0..9)
.map(|i| format!("Paragraph {i}. {}", sentence.repeat(1 + i % 2)))
.collect::<Vec<_>>()
.join("\n\n");
let mut sections = String::new();
for i in 0..6 {
sections.push_str(&format!("## Section {i}\n\n{}\n\n", sentence.repeat(2)));
sections.push_str(&format!("### Detail {i}\n\n{}\n", sentence));
if i % 2 == 1 {
sections.push_str("\n---\n\n");
}
}
let fenced = format!(
"Intro line before the code.\n\n```text\n{}```\n{}",
"a line of code inside the fence\n".repeat(8),
format!("{sentence}\n").repeat(4)
);
let multibyte = "\u{00e9}t\u{00e9} \u{1F30A} na\u{00ef}ve caf\u{00e9} ".repeat(20);
let leading_rule = format!(
"---\n\n## Opening\n\n{}\n\n## Second\n\n{}\n\n{}",
sentence,
sentence.repeat(2),
paragraphs
);
let rule_then_prose = format!(
"{}\n---\n{}\n\n## Later\n\n{}",
sentence.repeat(2),
sentence.repeat(2),
format!("{sentence}\n").repeat(5)
);
let lines = (0..30)
.map(|i| format!("- item {i} on its own line"))
.collect::<Vec<_>>()
.join("\n");
let rule_inside_last_chunk = format!(
"{}\n\n{}\n---\n{}\n---\n",
sentence.repeat(3),
sentence,
sentence
);
vec![
("paragraphs", paragraphs, 300),
("sections", sections, 400),
("fenced", fenced, 100),
("multibyte", multibyte, 50),
("leading_rule", leading_rule, 250),
("rule_then_prose", rule_then_prose, 120),
("lines", lines, 90),
("rule_inside_last_chunk", rule_inside_last_chunk, 150),
]
}
#[test]
fn falls_through_to_h2_header() {
let mut content = String::new();
content.push_str(&"x".repeat(50));
content.push_str("\n## Section Two\n");
content.push_str(&"y".repeat(200));
let plan = compute_chunks(&content, 100);
assert!(plan.total >= 2);
let chunk1 = plan.chunk(&content, 1);
assert!(
chunk1.starts_with("## Section Two"),
"chunk 1 starts with H2: {:?}",
&chunk1[..30.min(chunk1.len())]
);
}
#[test]
fn falls_through_to_paragraph_break() {
let mut content = String::new();
content.push_str(&"x".repeat(50));
content.push_str("\n\n");
content.push_str(&"y".repeat(150));
let plan = compute_chunks(&content, 100);
assert!(plan.total >= 2);
}
#[test]
fn no_semantic_breaks_uses_utf8_fallback() {
let content = "A".repeat(250);
let plan = compute_chunks(&content, 100);
assert!(plan.total >= 2);
let joined: String = plan
.iter(&content)
.map(|(_, s, _)| s)
.collect::<Vec<_>>()
.join("");
assert_eq!(joined, content);
}
#[test]
fn utf8_boundary_safety_emoji() {
let prefix = "a".repeat(98);
let mut content = prefix.clone();
content.push('\u{1F41F}'); content.push_str(&"b".repeat(200));
let plan = compute_chunks(&content, 100);
for &b in &plan.boundaries {
assert!(
content.is_char_boundary(b),
"boundary {} not on char boundary",
b
);
}
let joined: String = plan
.iter(&content)
.map(|(_, s, _)| s)
.collect::<Vec<_>>()
.join("");
assert_eq!(joined, content);
}
#[test]
fn code_block_not_split_mid_block() {
let mut content = String::new();
content.push_str(&"x".repeat(20));
content.push_str("\n```rust\n");
content.push_str(&"fn a() {}\n".repeat(20)); content.push_str("```\n");
content.push_str(&"y".repeat(300));
let threshold = 120;
let plan = compute_chunks(&content, threshold);
let fence_open = content.find("```").unwrap();
let fence_close = content.rfind("```").unwrap();
for &b in &plan.boundaries {
assert!(
b <= fence_open || b > fence_close,
"boundary {} landed inside code block [{}, {}]",
b,
fence_open,
fence_close
);
}
}
#[test]
fn oversized_code_block_flagged() {
let mut content = String::new();
content.push_str("intro\n\n");
content.push_str("```\n");
content.push_str(&"A".repeat(500));
content.push_str("\n```\n");
content.push_str(&"tail".repeat(50));
let plan = compute_chunks(&content, 100);
assert!(
plan.oversized.iter().any(|&f| f),
"expected at least one oversized chunk, got {:?}",
plan.oversized
);
}
#[test]
fn chunk_count_beyond_u8_is_not_truncated() {
let threshold = 25;
let content = "a".repeat(15_000);
let plan = compute_chunks(&content, threshold);
assert_eq!(
plan.total as usize,
plan.boundaries.len() + 1,
"total {} does not match boundaries.len() + 1 = {}",
plan.total,
plan.boundaries.len() + 1,
);
assert!(
plan.total > 255,
"expected >255 chunks to exercise the u8-saturation regression, got {}",
plan.total
);
let joined: String = plan
.iter(&content)
.map(|(_, s, _)| s)
.collect::<Vec<_>>()
.join("");
assert_eq!(joined, content);
let last_idx = plan.total - 1;
let last = plan.chunk(&content, last_idx);
assert!(last.len() <= threshold + 8, "last chunk over threshold");
}
#[test]
fn env_var_overrides_threshold() {
let prev = env::var(CHUNK_THRESHOLD_ENV).ok();
unsafe {
env::set_var(CHUNK_THRESHOLD_ENV, "50");
}
assert_eq!(chunk_threshold(), 50);
unsafe {
env::set_var(CHUNK_THRESHOLD_ENV, "not-a-number");
}
assert_eq!(chunk_threshold(), DEFAULT_CHUNK_THRESHOLD);
unsafe {
env::set_var(CHUNK_THRESHOLD_ENV, "0");
}
assert_eq!(chunk_threshold(), DEFAULT_CHUNK_THRESHOLD);
unsafe {
match prev {
Some(v) => env::set_var(CHUNK_THRESHOLD_ENV, v),
None => env::remove_var(CHUNK_THRESHOLD_ENV),
}
}
}
#[test]
fn phrase_from_heading_preferred() {
let content = "\n## Token semantics\n\nThe token signs (session_id, step).";
let p = extract_salient_phrase(content);
assert_eq!(p, "The token signs (session_id, step).");
}
#[test]
fn phrase_heading_strips_all_hash_levels() {
let content = "#### Deep heading\n\nbody";
assert_eq!(extract_salient_phrase(content), "body");
assert_eq!(first_heading(content).unwrap(), "Deep heading");
}
#[test]
fn phrase_from_first_sentence_when_no_heading() {
let content = "The wake ritual walks a cascade. It uses chunks now.";
let p = extract_salient_phrase(content);
assert_eq!(p, "The wake ritual walks a cascade.");
}
#[test]
fn phrase_from_unterminated_paragraph_truncated() {
let content = "word ".repeat(50); let p = extract_salient_phrase(&content);
assert!(
p.chars().count() <= SENTENCE_MAX_CHARS + 1,
"got {} chars",
p.chars().count()
);
assert!(!p.is_empty());
}
#[test]
fn no_candidate_for_empty_input() {
assert_eq!(candidate_phrases(""), Vec::<String>::new());
}
#[test]
fn no_candidate_for_whitespace_only() {
assert_eq!(candidate_phrases(" \n\n\n "), Vec::<String>::new());
}
#[test]
fn every_candidate_has_a_letter() {
let cases = [
"", " ", "\n", "\n\n", "\t\t", "a", ".", "\u{200B}", "---", "1.",
];
for c in cases {
for p in candidate_phrases(c) {
assert!(has_letter(&p), "letterless phrase {p:?} for input {c:?}");
}
}
assert_eq!(candidate_phrases("a"), vec!["a"]);
for c in [".", "\u{200B}", "---", "1."] {
assert_eq!(candidate_phrases(c), Vec::<String>::new(), "input {c:?}");
}
}
#[test]
fn letterless_body_never_falls_back_to_the_heading() {
assert_eq!(
candidate_phrases("\n## Weir\n\n...\n"),
Vec::<String>::new()
);
}
#[test]
fn phrase_abbreviation_splits_at_period_space_known_limitation() {
let content = "See Dr. Smith for details. He prescribes two aspirin.";
let p = extract_salient_phrase(content);
assert_eq!(p, "See Dr.");
}
#[test]
fn phrase_heading_skips_inside_fenced_code_block() {
let content = "Intro paragraph without a heading. More prose here.\n\n\
```markdown\n\
## fake heading in code\n\
more code lines\n\
```\n\
trailing prose line.";
let p = extract_salient_phrase(content);
assert!(
!p.contains("fake heading in code"),
"heading extractor descended into fenced block: {:?}",
p
);
assert!(
p.starts_with("Intro paragraph"),
"expected first-sentence fallback, got {:?}",
p
);
}
#[test]
fn phrase_heading_after_fenced_block_is_picked() {
let content = "Intro prose.\n\n\
```rust\n\
// ## not a heading\n\
fn x() {}\n\
```\n\n\
## Real Heading\n\n\
body text.";
let p = first_heading(content).unwrap();
assert_eq!(p, "Real Heading");
}
#[test]
fn phrase_heading_between_two_fenced_blocks_is_picked() {
let content = "\
```\n\
## fake one\n\
```\n\n\
## Real Heading\n\n\
body\n\n\
```\n\
## fake two\n\
```\n";
let p = first_heading(content).unwrap();
assert_eq!(p, "Real Heading");
}
#[test]
fn phrase_pure_fenced_chunk_has_no_extractable_heading() {
let content = "\
```markdown\n\
## fake heading inside code\n\
more code\n\
```\n";
assert_eq!(candidate_phrases(content), Vec::<String>::new());
}
#[test]
fn heading_quadruple_backtick_fence_ignores_inner_triple() {
let content = "\
````markdown
Here is how you write a fenced block:
```
## This heading is inside the inner fence
```
And that's it.
````
## Real Heading After Quad Fence
Body text.";
let p = first_heading(content).unwrap();
assert_eq!(
p, "Real Heading After Quad Fence",
"should skip heading inside quadruple-backtick fence, got {:?}",
p
);
}
#[test]
fn heading_tilde_fence_ignored() {
let content = "\
~~~
## Heading inside tilde fence
~~~
## Real Tilde Heading
Body.";
let p = first_heading(content).unwrap();
assert_eq!(p, "Real Tilde Heading");
}
#[test]
fn heading_inside_code_block_at_all_levels_ignored() {
let content = "\
```
# H1 fake
## H2 fake
### H3 fake
#### H4 fake
```
### Real H3 Heading
Content.";
let p = first_heading(content).unwrap();
assert_eq!(p, "Real H3 Heading");
}
#[test]
fn no_heading_returns_none_from_first_heading() {
let content = "Just some text without any heading markers.";
assert!(first_heading(content).is_none());
}
#[test]
fn heading_levels_h1_through_h6() {
assert_eq!(first_heading("# H1").unwrap(), "H1");
assert_eq!(first_heading("## H2").unwrap(), "H2");
assert_eq!(first_heading("### H3").unwrap(), "H3");
assert_eq!(first_heading("#### H4").unwrap(), "H4");
assert_eq!(first_heading("##### H5").unwrap(), "H5");
assert_eq!(first_heading("###### H6").unwrap(), "H6");
}
#[test]
fn derived_phrase_skips_atx_heading() {
let content = "\n### Harbor Notes\n\nThe tide turns twice daily. Boats wait.";
assert_eq!(
extract_salient_phrase(content),
"The tide turns twice daily."
);
}
#[test]
fn derived_phrase_skips_setext_heading() {
let h1 = "Lantern Keeping\n===============\n\nWicks are trimmed at dusk. Oil later.";
assert_eq!(extract_salient_phrase(h1), "Wicks are trimmed at dusk.");
let h2 = "Lantern Keeping\n---\n\nWicks are trimmed at dusk.";
assert_eq!(extract_salient_phrase(h2), "Wicks are trimmed at dusk.");
}
#[test]
fn derived_phrase_skips_leading_rule() {
let content = "---\n\n## Ferry Times\n\nThe ferry leaves at noon.";
assert_eq!(extract_salient_phrase(content), "The ferry leaves at noon.");
}
#[test]
fn derived_phrase_skips_bold_only_paragraph() {
let content = "**Status:**\n\nThe greenhouse is closed for repairs.";
assert_eq!(
extract_salient_phrase(content),
"The greenhouse is closed for repairs."
);
}
#[test]
fn derived_phrase_skips_bold_only_list_item() {
let content = "- **Greenhouse**\n- Seedlings need water twice a day.\n";
assert_eq!(
extract_salient_phrase(content),
"Seedlings need water twice a day."
);
let nested = "- **Greenhouse**\n - Seedlings need water twice a day.\n";
assert_eq!(
extract_salient_phrase(nested),
"Seedlings need water twice a day."
);
}
#[test]
fn bold_label_with_colon_outside_is_skipped() {
assert_eq!(
candidate_phrases("**Status**:\n\nThe press runs in October."),
vec!["The press runs in October."]
);
assert_eq!(
candidate_phrases("- **Section**:\n- The press runs in October.\n"),
vec!["The press runs in October."]
);
}
#[test]
fn derived_phrase_skips_a_sentence_without_letters_or_digits() {
assert_eq!(
extract_salient_phrase("\n## Title\n\n... Then the river rose."),
"Then the river rose."
);
assert_eq!(
candidate_phrases("\u{1F30A}. Then the river rose. \u{1F30A}\n"),
vec!["Then the river rose."]
);
}
#[test]
fn heading_only_chunk_derives_the_heading() {
assert_eq!(
extract_salient_phrase("\n## Orchard Ledger\n"),
"Orchard Ledger"
);
}
#[test]
fn code_only_chunk_has_no_candidate() {
let content = "```\nlet pears = 4;\n```\n";
assert_eq!(candidate_phrases(content), Vec::<String>::new());
}
#[test]
fn tight_list_only_chunk_derives_first_item() {
let content = "- Plant the beans after the frost.\n- Water them at dawn.\n";
assert_eq!(
extract_salient_phrase(content),
"Plant the beans after the frost."
);
}
#[test]
fn salient_candidates_lists_every_body_sentence_in_order() {
assert_eq!(
candidate_phrases("## H\n\nOne. Two.\n\n- Three\n"),
vec!["One.", "Two.", "Three"]
);
}
#[test]
fn salient_candidates_never_offer_a_heading_when_the_chunk_has_body_text() {
assert_eq!(
candidate_phrases("## Orchard Ledger\n\nPears ripen late.\n"),
vec!["Pears ripen late."]
);
}
#[test]
fn first_non_empty_line_quadruple_fence() {
let content = "\
````
## fake heading
```
inner triple
```
````
Actual first line.";
let line = first_non_empty_line(content);
assert_eq!(
line.as_deref(),
Some("Actual first line."),
"first_non_empty_line should handle quad fences, got {:?}",
line
);
}
#[test]
fn extract_sentences_quadruple_fence() {
let content = "\
````
Fake sentence inside quad fence. Another fake.
```
More fake inside inner triple.
```
````
Real sentence outside. Another real one.";
let sentences = extract_sentences(content);
for s in &sentences {
assert!(
!s.contains("Fake sentence"),
"extract_sentences should skip quad-fenced content, got {:?}",
s
);
}
assert!(
sentences.iter().any(|s| s.contains("Real sentence")),
"should find real sentence outside fence, got {:?}",
sentences
);
}
#[test]
fn extract_sentences_tilde_fence() {
let content = "\
~~~
Fake sentence inside tilde fence.
~~~
Real tilde sentence.";
let sentences = extract_sentences(content);
for s in &sentences {
assert!(
!s.contains("Fake sentence"),
"should skip tilde-fenced content, got {:?}",
s
);
}
}
#[test]
fn compare_exact_match() {
let r = compare_phrase("Rust is memory-safe", "Rust is memory-safe");
assert_eq!(r, PhraseMatch::Exact);
}
#[test]
fn compare_case_tolerant() {
let r = compare_phrase("Token semantics", "token semantics");
assert_eq!(r, PhraseMatch::Tolerant);
}
#[test]
fn compare_trailing_punct_stripped() {
let r = compare_phrase(
"The wake ritual walks a cascade",
"The wake ritual walks a cascade.",
);
assert_eq!(r, PhraseMatch::Tolerant);
}
#[test]
fn compare_whitespace_collapsed() {
let r = compare_phrase("token semantics", "token semantics");
assert_eq!(r, PhraseMatch::Tolerant);
}
#[test]
fn compare_smart_quotes_normalized() {
let r = compare_phrase("it's \u{201C}alive\u{201D}", "it\u{2019}s \"alive\"");
assert_eq!(r, PhraseMatch::Tolerant);
}
#[test]
fn compare_mismatch_still_mismatches() {
let r = compare_phrase("totally different", "token semantics");
assert_eq!(r, PhraseMatch::Mismatch);
}
use proptest::prelude::*;
fn threshold_strategy() -> impl Strategy<Value = usize> {
prop_oneof![
Just(50usize),
Just(100usize),
Just(256usize),
Just(1024usize),
Just(4096usize),
]
}
proptest! {
#[test]
fn prop_reconstitution(
content in "\\PC{0,8192}", threshold in threshold_strategy(),
) {
let plan = compute_chunks(&content, threshold);
let joined: String = plan.iter(&content).map(|(_, s, _)| s).collect::<Vec<_>>().join("");
prop_assert_eq!(joined, content);
}
#[test]
fn prop_all_boundaries_are_char_boundaries(
content in "\\PC{0,8192}",
threshold in threshold_strategy(),
) {
let plan = compute_chunks(&content, threshold);
for &b in &plan.boundaries {
prop_assert!(content.is_char_boundary(b), "byte {} is not a char boundary", b);
}
}
#[test]
fn prop_boundaries_strictly_increasing(
content in "\\PC{0,8192}",
threshold in threshold_strategy(),
) {
let plan = compute_chunks(&content, threshold);
for pair in plan.boundaries.windows(2) {
prop_assert!(pair[0] < pair[1], "non-monotonic boundaries: {:?}", plan.boundaries);
}
}
#[test]
fn prop_determinism(
content in "\\PC{0,4096}",
threshold in threshold_strategy(),
) {
let a = compute_chunks(&content, threshold);
let b = compute_chunks(&content, threshold);
prop_assert_eq!(a, b);
}
#[test]
fn prop_chunk_size_bound(
content in "\\PC{0,8192}",
threshold in threshold_strategy(),
) {
let plan = compute_chunks(&content, threshold);
for (_, chunk, oversized) in plan.iter(&content) {
if oversized {
prop_assert!(
chunk.len() <= content.len(),
"oversized chunk larger than input: {} vs {}",
chunk.len(), content.len()
);
} else {
prop_assert!(
chunk.len() <= threshold,
"non-oversized chunk {} > threshold {}",
chunk.len(), threshold
);
}
}
}
#[test]
fn prop_candidates_are_never_blank(content in "\\PC{0,4096}") {
for p in candidate_phrases(&content) {
prop_assert!(!p.trim().is_empty(), "blank phrase for content len {}", content.len());
}
}
#[test]
fn prop_phrase_deterministic(content in "\\PC{0,4096}") {
let a = salient_candidates(&content);
let b = salient_candidates(&content);
prop_assert_eq!(a, b);
}
#[test]
fn prop_compare_tolerant_to_case_and_trailing_punct(
word1 in "[a-zA-Z]{2,20}",
word2 in "[a-zA-Z]{2,20}",
) {
let base = format!("{} {}", word1, word2);
let variant = format!("{} {}.", base.to_lowercase(), ""); let variant = variant.trim().to_string();
let r = compare_phrase(&variant, &base);
prop_assert!(matches!(r, PhraseMatch::Exact | PhraseMatch::Tolerant),
"compare rejected trivial variant: {:?} vs {:?}", variant, base);
}
#[test]
fn prop_compare_same_string_is_exact(s in "[\\PC]{1,64}") {
let trimmed = s.trim().to_string();
prop_assume!(!trimmed.is_empty());
prop_assert_eq!(
compare_phrase(&trimmed, &trimmed),
PhraseMatch::Exact
);
}
}
#[test]
fn auto_phrase_from_heading() {
let content = "Some intro text.\n\n## The Spark\n\nBody text here.";
let p = extract_auto_phrase(content, "Fallback Title");
assert_eq!(p, "The Spark");
}
#[test]
fn auto_phrase_from_sentence() {
let content = "The counter stores sample records. Each record holds one measurement.";
let p = extract_auto_phrase(content, "Sample Counter");
assert!(
p.contains("counter stores") || p.contains("record holds"),
"expected a sentence from the content, got {:?}",
p
);
assert!(!p.is_empty());
}
#[test]
fn auto_phrase_from_line() {
let content = "- item one: the first observation\n- item two: something else";
let p = extract_auto_phrase(content, "Fallback");
assert!(
p.contains("item one"),
"expected first line as phrase, got {:?}",
p
);
}
#[test]
fn auto_phrase_from_title_fallback() {
let p = extract_auto_phrase("", "Sample Counter");
assert_eq!(p, "Sample Counter");
}
#[test]
fn auto_phrase_never_empty() {
let cases = ["", " ", "\n", "\n\n", "\t\t"];
for c in cases {
let p = extract_auto_phrase(c, "Title");
assert!(!p.is_empty(), "empty auto-phrase for content {:?}", c);
}
}
#[test]
fn auto_phrase_deterministic() {
let content = "Some paragraph with multiple sentences. Another one here. And a third.";
let a = extract_auto_phrase(content, "Title");
let b = extract_auto_phrase(content, "Title");
assert_eq!(a, b, "auto-phrase must be deterministic");
}
#[test]
fn auto_phrase_skips_fenced_headings() {
let content = "```markdown\n## Fake Heading\n```\n\nReal first sentence here.";
let p = extract_auto_phrase(content, "Title");
assert!(
!p.contains("Fake Heading"),
"auto-phrase picked heading inside fenced block: {:?}",
p
);
}
#[test]
fn auto_phrase_bullet_list_without_headings() {
let content = "- the first observation was recorded\n- the second run finished\n- the third was skipped";
let p = extract_auto_phrase(content, "Sample Counter");
assert!(
p.contains("first observation")
|| p.contains("second run")
|| p.contains("third was skipped"),
"expected a bullet line, got {:?}",
p
);
assert!(!p.is_empty());
}
#[test]
fn auto_phrase_content_hash_varies_across_blooms() {
let content_a = "First sentence here. Second sentence here. Third sentence here.";
let content_b = "Alpha sentence here. Beta sentence here. Gamma sentence here.";
let p_a = extract_auto_phrase(content_a, "A");
let p_b = extract_auto_phrase(content_b, "B");
assert_ne!(
p_a, p_b,
"different content should produce different phrases"
);
}
#[test]
fn extract_sentences_basic() {
let content = "First sentence. Second sentence. Third.";
let sentences = extract_sentences(content);
assert!(
sentences.len() >= 2,
"expected >=2 sentences, got {:?}",
sentences
);
assert!(sentences[0].contains("First sentence."));
}
#[test]
fn extract_sentences_paragraph_break() {
let content = "Paragraph one\n\nParagraph two";
let sentences = extract_sentences(content);
assert_eq!(sentences.len(), 2);
assert_eq!(sentences[0], "Paragraph one");
assert_eq!(sentences[1], "Paragraph two");
}
#[test]
fn extract_sentences_skips_fenced_blocks() {
let content = "Real sentence.\n\n```\nFake sentence inside code.\n```\n\nAnother real one.";
let sentences = extract_sentences(content);
for s in &sentences {
assert!(
!s.contains("Fake sentence"),
"sentence extractor should skip fenced blocks, got {:?}",
s
);
}
}
#[test]
fn select_sentence_index_deterministic() {
let sentences = vec!["a", "b", "c", "d", "e"];
let content = "some content for hashing";
let idx1 = select_sentence_index(&sentences, content);
let idx2 = select_sentence_index(&sentences, content);
assert_eq!(idx1, idx2);
assert!(idx1 < sentences.len());
}
#[test]
fn select_sentence_index_varies_with_content() {
let sentences = vec!["a", "b", "c", "d", "e", "f", "g", "h", "i", "j"];
let idx1 = select_sentence_index(&sentences, "content alpha");
let idx2 = select_sentence_index(&sentences, "content beta");
assert_ne!(
idx1, idx2,
"different content should usually select different indices"
);
}
#[test]
fn first_non_empty_line_skips_fenced_code_block() {
let content = "```rust\nfn main() {}\n```\n\nActual first line.";
let line = first_non_empty_line(content);
assert_eq!(
line.as_deref(),
Some("Actual first line."),
"should skip fenced code block, got {:?}",
line
);
}
#[test]
fn first_non_empty_line_all_fenced_returns_none() {
let content = "```\nonly code here\nmore code\n```";
let line = first_non_empty_line(content);
assert_eq!(line, None, "all-fenced content should return None");
}
#[test]
fn first_non_empty_line_between_fences() {
let content = "```\ncode\n```\n\nSandwiched line\n\n```\nmore code\n```";
let line = first_non_empty_line(content);
assert_eq!(line.as_deref(), Some("Sandwiched line"));
}
#[test]
fn extract_sentences_strips_dash_list_prefix() {
let content = "- first brick line\n- second brick line";
let sentences = extract_sentences(content);
for s in &sentences {
assert!(
!s.starts_with("- "),
"sentence should not keep `- ` prefix: {:?}",
s
);
}
assert!(!sentences.is_empty());
assert!(
sentences[0].contains("first brick line"),
"expected content without prefix, got {:?}",
sentences
);
}
#[test]
fn extract_sentences_strips_star_list_prefix() {
let content = "* item alpha\n* item beta";
let sentences = extract_sentences(content);
for s in &sentences {
assert!(
!s.starts_with("* "),
"sentence should not keep `* ` prefix: {:?}",
s
);
}
assert!(!sentences.is_empty());
assert!(sentences[0].contains("item alpha"));
}
#[test]
fn extract_sentences_list_items_with_paragraph_breaks() {
let content = "- first item\n\n- second item";
let sentences = extract_sentences(content);
assert_eq!(sentences.len(), 2);
assert_eq!(sentences[0], "first item");
assert_eq!(sentences[1], "second item");
}
#[test]
fn poppy_r8_no_letterless_candidate_on_any_tier() {
let mut failures = Vec::new();
for c in [
"...\n",
"\u{2014}\n",
"\u{1F30A}\n",
"\u{1F30A}\n\n```\nx\n```\n",
"\u{200B}\n",
"...\n\n---\n\n...\n",
"> ...\n",
"---\n",
"1.\n",
] {
for p in candidate_phrases(c) {
if !has_letter(&p) {
failures.push(format!("{c:?} -> {p:?}"));
}
}
}
assert!(
failures.is_empty(),
"letterless phrase:\n{}",
failures.join("\n")
);
}
}