///|
/// Split text into owned lines and drop a trailing CR on Windows files.
fn split_lines(text : String) -> Array[String] {
let lines : Array[String] = []
if text.is_empty() {
return lines
}
for view in text.split("\n") {
lines.push(view.to_owned().trim_end(chars="\r").to_owned())
}
lines
}
///|
/// Clip a line for stable reports.
fn clip(input : String, max : Int) -> String {
let trimmed = input.trim()
if max <= 0 || trimmed.char_length() <= max {
return trimmed.to_owned()
}
let buf = StringBuilder()
let mut count = 0
for ch in trimmed {
if count < max {
buf.write_char(ch)
count += 1
}
}
buf.write_string("...")
buf.to_string()
}
///|
/// Lowercase helper for ASCII-oriented checklist matching.
fn fold_text(input : String) -> String {
input.to_lower()
}
///|
/// Count non-overlapping occurrences of a term.
fn count_occurrences(haystack : String, needle : String) -> Int {
if needle.is_empty() {
return 0
}
let hay = fold_text(haystack)
let low = fold_text(needle)
hay.split(low).to_array().length() - 1
}
///|
/// True when text contains a folded term.
fn contains_folded(haystack : String, needle : String) -> Bool {
fold_text(haystack).contains(fold_text(needle))
}
///|
/// Build concise evidence for any of the supplied terms.
fn collect_evidence(
lines : Array[String],
terms : Array[String],
limit : Int,
) -> Array[Evidence] {
let evidence : Array[Evidence] = []
for i in 0..= limit {
break
}
let lower = fold_text(lines[i])
for term in terms {
if evidence.length() < limit && lower.contains(fold_text(term)) {
evidence.push({
line: i + 1,
snippet: clip(lines[i], 96),
matched: term,
})
}
}
}
evidence
}
///|
/// Evidence for lines over the configured length budget.
fn collect_long_lines(lines : Array[String], max : Int) -> Array[Evidence] {
let evidence : Array[Evidence] = []
for i in 0.. max && evidence.length() < 6 {
evidence.push({
line: i + 1,
snippet: clip(lines[i], 96),
matched: "length=\{len}",
})
}
}
evidence
}
///|
/// Find the first line index at or after `start` that contains `label`.
fn find_line_after(lines : Array[String], label : String, start : Int) -> Int? {
let target = fold_text(label)
let begin = if start < 0 { 0 } else { start }
for i in begin.. Array[LineToken] {
let tokens : Array[LineToken] = []
let lines = split_lines(text.to_owned())
for i in 0.. DocumentShape {
let tokens = tokenize_lines(text)
let mut headings = 0
let mut bullets = 0
let mut code_fences = 0
let mut paragraphs = 0
let mut blanks = 0
for token in tokens {
match token.kind {
Heading => headings += 1
Bullet => bullets += 1
CodeFence => code_fences += 1
Paragraph => paragraphs += 1
Blank => blanks += 1
}
}
{ lines: tokens.length(), headings, bullets, code_fences, paragraphs, blanks }
}