pub const MAX_NESTING_DEPTH: usize = 96;
pub fn nesting_depth(text: &str) -> usize {
let mut open_divs = 0usize;
let mut deepest = 0usize;
for line in text.lines() {
let trimmed = line.trim_start();
if let Some(rest) = trimmed.strip_prefix(":::") {
let rest = rest.trim();
if rest.is_empty() || rest.chars().all(|c| c == ':') {
open_divs = open_divs.saturating_sub(1);
} else {
open_divs += 1;
deepest = deepest.max(open_divs);
}
continue;
}
let indent = line.len() - trimmed.len();
let mut quotes = 0usize;
for ch in trimmed.chars() {
match ch {
'>' => quotes += 1,
' ' | '\t' => {}
_ => break,
}
}
deepest = deepest.max(quotes + open_divs + indent / 2);
}
deepest
}
pub fn is_too_deep(text: &str) -> bool {
nesting_depth(text) > MAX_NESTING_DEPTH
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ordinary_prose_is_shallow() {
for text in [
"The ferry was late.\n\nShe waited.\n",
"> He said it plainly.\n>\n> Then he left.\n",
"- one\n - two\n - three\n",
"::: note\nA note.\n:::\n",
"> - a quoted list\n> - nested once\n",
"",
] {
assert!(!is_too_deep(text), "should accept: {text:?}");
}
}
#[test]
fn a_deep_blockquote_run_is_flagged() {
assert!(is_too_deep(&format!("{}deep\n", ">".repeat(2_000))));
}
#[test]
fn deeply_stacked_divs_are_flagged() {
assert!(is_too_deep(&"::: a\n".repeat(500)));
}
#[test]
fn runaway_indentation_is_flagged() {
assert!(is_too_deep(&format!("{}item\n", " ".repeat(1_000))));
}
#[test]
fn sibling_divs_do_not_accumulate() {
assert!(!is_too_deep(&"::: note\nbody\n:::\n".repeat(500)));
}
#[test]
fn a_longer_closing_fence_closes_rather_than_opens() {
let text = ":::: outer\n::: inner\nbody\n:::\n::::\n".repeat(200);
assert!(!is_too_deep(&text), "nested fences must not accumulate");
}
#[test]
fn the_real_parser_survives_input_that_used_to_abort_the_process() {
use crate::parser_tools::content_parser::{ParsedElement, parse_djot};
use crate::parser_tools::djot_options::DjotImportOptions;
let hostile = format!("{}deep\n", ">".repeat(4_000));
let elements = parse_djot(&hostile, &DjotImportOptions::default());
assert_eq!(elements.len(), 1, "degrades to a single block");
let ParsedElement::Block(block) = &elements[0] else {
panic!("expected a plain paragraph");
};
let text: String = block.spans.iter().map(|s| s.text.as_str()).collect();
assert_eq!(
text, hostile,
"the source must come back verbatim — degrading may not lose prose"
);
}
#[test]
fn prose_below_the_ceiling_still_gets_its_structure() {
use crate::parser_tools::content_parser::parse_djot;
use crate::parser_tools::djot_options::DjotImportOptions;
let ok = "> > > a quoted quote\n\nand a paragraph\n";
let elements = parse_djot(ok, &DjotImportOptions::default());
assert!(
elements.len() > 1,
"a two-block document must still parse as two blocks: {elements:#?}"
);
}
}