From e8c5d3b0e5d5cf7a8a30c20095a87013fd6c7cfb Mon Sep 17 00:00:00 2001 From: Rohit Goswami Date: Wed, 16 Sep 2026 02:59:19 -0500 Subject: [PATCH] fix(rst): leftover include same-line after filename Docutils leftover after .. include:: filename hangs as Prose and still splits. The filename stays Structure. Following flush prose still splits. --- src/parser/rst.rs | 38 +++++++++++++++++++++++++++++++ tests/rst_container_directives.rs | 15 ++++++++++++ 2 files changed, 53 insertions(+) diff --git a/src/parser/rst.rs b/src/parser/rst.rs index b809d85..f40b1a5 100644 --- a/src/parser/rst.rs +++ b/src/parser/rst.rs @@ -330,6 +330,7 @@ fn parse_line_based(input: &str) -> Vec { if let Some(marker_len) = rst_admonition_marker_len(line_text) .or_else(|| rst_substitution_replace_marker_len(line_text)) .or_else(|| rst_table_marker_len(line_text)) + .or_else(|| rst_include_marker_len(line_text)) { if line_text.len() > marker_len && !line_text[marker_len..].trim().is_empty() { in_meta = false; @@ -990,6 +991,43 @@ fn rst_table_marker_len(line: &str) -> Option { Some(colons_at + 2 + pad) } +/// Docutils `include` leftover: `.. include:: filename` plus pad. +/// Same-line leftover after the filename is hung Prose. +fn rst_include_marker_len(line: &str) -> Option { + let indent = line.len() - line.trim_start().len(); + let trimmed = &line[indent..]; + let rest = trimmed.strip_prefix("..")?; + if !rest.starts_with([' ', '\t']) { + return None; + } + let name_off = rest.len() - rest.trim_start().len(); + let after_ws = &rest[name_off..]; + let name_end = after_ws.find("::")?; + let name = after_ws[..name_end].trim().to_ascii_lowercase(); + if name != "include" { + return None; + } + let colons_at = indent + 2 + name_off + name_end; + let after_colons = &line[colons_at + 2..]; + let pad = after_colons.len() - after_colons.trim_start().len(); + let after_pad = &after_colons[pad..]; + if after_pad.is_empty() { + return None; + } + let fname_end = after_pad + .find(|c: char| c.is_whitespace()) + .unwrap_or(after_pad.len()); + if fname_end == 0 { + return None; + } + let after_fname = &after_pad[fname_end..]; + if after_fname.trim().is_empty() { + return None; + } + let fname_pad = after_fname.len() - after_fname.trim_start().len(); + Some(colons_at + 2 + pad + fname_end + fname_pad) +} + pub(crate) fn rst_admonition_marker_len(line: &str) -> Option { let indent = line.len() - line.trim_start().len(); let trimmed = &line[indent..]; diff --git a/tests/rst_container_directives.rs b/tests/rst_container_directives.rs index 00c010f..0ae3069 100644 --- a/tests/rst_container_directives.rs +++ b/tests/rst_container_directives.rs @@ -720,3 +720,18 @@ fn opaque_directives_stay_frozen() { ); } } + +#[test] +fn leftover_include_same_line_after_filename_hangs_and_splits() { + let input = concat!(".. include:: foo.rst leftover. Next.\n", "After. Next.\n",); + let out = format_text(input, &rst_cfg()).unwrap(); + assert!( + !out.contains(".. include:: foo.rst leftover. Next."), + "leftover after include filename must still split, got:\n{out}" + ); + assert!( + out.contains("After.\nNext."), + "following prose must still split, got:\n{out}" + ); + assert_eq!(format_text(&out, &rst_cfg()).unwrap(), out); +}