1use crate::utils::calculate_indentation_width_default;
7use crate::utils::is_definition_list_item;
8use crate::utils::mkdocs_attr_list::{ATTR_LIST_PATTERN, is_standalone_attr_list};
9use crate::utils::mkdocs_snippets::is_snippet_block_delimiter;
10use crate::utils::regex_cache::{
11 DISPLAY_MATH_REGEX, EMAIL_PATTERN, EMOJI_SHORTCODE_REGEX, HTML_ENTITY_REGEX, HTML_TAG_PATTERN,
12 HUGO_SHORTCODE_REGEX, INLINE_MATH_REGEX, WIKI_LINK_REGEX,
13};
14use crate::utils::sentence_utils::{
15 get_abbreviations, is_cjk_char, is_cjk_sentence_ending, is_closing_quote, is_opening_quote,
16 text_ends_with_abbreviation,
17};
18use pulldown_cmark::{BrokenLink, CowStr, Event, LinkType, Options, Parser, Tag, TagEnd};
19use std::collections::HashSet;
20use unicode_width::UnicodeWidthStr;
21
22#[derive(Clone, Copy, Debug, Default, PartialEq)]
24pub enum ReflowLengthMode {
25 Chars,
27 #[default]
29 Visual,
30 Bytes,
32}
33
34fn display_len(s: &str, mode: ReflowLengthMode) -> usize {
36 match mode {
37 ReflowLengthMode::Chars => s.chars().count(),
38 ReflowLengthMode::Visual => s.width(),
39 ReflowLengthMode::Bytes => s.len(),
40 }
41}
42
43#[derive(Clone)]
45pub struct ReflowOptions {
46 pub line_length: usize,
48 pub break_on_sentences: bool,
50 pub preserve_breaks: bool,
52 pub sentence_per_line: bool,
54 pub semantic_line_breaks: bool,
56 pub abbreviations: Option<Vec<String>>,
60 pub length_mode: ReflowLengthMode,
62 pub attr_lists: bool,
65 pub myst_roles: bool,
69 pub require_sentence_capital: bool,
74 pub max_list_continuation_indent: Option<usize>,
78 pub defined_references: Option<HashSet<String>>,
92}
93
94impl Default for ReflowOptions {
95 fn default() -> Self {
96 Self {
97 line_length: 80,
98 break_on_sentences: true,
99 preserve_breaks: false,
100 sentence_per_line: false,
101 semantic_line_breaks: false,
102 abbreviations: None,
103 length_mode: ReflowLengthMode::default(),
104 attr_lists: false,
105 myst_roles: false,
106 require_sentence_capital: true,
107 max_list_continuation_indent: None,
108 defined_references: None,
109 }
110 }
111}
112
113pub fn normalize_reference_label(label: &str) -> String {
120 label.split_whitespace().collect::<Vec<_>>().join(" ").to_lowercase()
121}
122
123fn compute_inline_code_mask(text: &str) -> Vec<bool> {
126 let chars: Vec<char> = text.chars().collect();
127 let len = chars.len();
128 let mut mask = vec![false; len];
129 let mut i = 0;
130
131 while i < len {
132 if chars[i] == '`' {
133 let open_start = i;
135 let mut backtick_count = 0;
136 while i < len && chars[i] == '`' {
137 backtick_count += 1;
138 i += 1;
139 }
140
141 let mut found_close = false;
143 let content_start = i;
144 while i < len {
145 if chars[i] == '`' {
146 let close_start = i;
147 let mut close_count = 0;
148 while i < len && chars[i] == '`' {
149 close_count += 1;
150 i += 1;
151 }
152 if close_count == backtick_count {
153 for item in mask.iter_mut().take(close_start).skip(content_start) {
155 *item = true;
156 }
157 for item in mask.iter_mut().take(content_start).skip(open_start) {
159 *item = true;
160 }
161 for item in mask.iter_mut().take(i).skip(close_start) {
162 *item = true;
163 }
164 found_close = true;
165 break;
166 }
167 } else {
168 i += 1;
169 }
170 }
171
172 if !found_close {
173 i = open_start + backtick_count;
175 }
176 } else {
177 i += 1;
178 }
179 }
180
181 mask
182}
183
184fn is_sentence_boundary(
188 text: &str,
189 chars: &[char],
190 pos: usize,
191 abbreviations: &HashSet<String>,
192 require_sentence_capital: bool,
193) -> bool {
194 if pos + 1 >= chars.len() {
195 return false;
196 }
197
198 let c = chars[pos];
199 let next_char = chars[pos + 1];
200
201 if is_cjk_sentence_ending(c) {
204 let mut after_punct_pos = pos + 1;
206 while after_punct_pos < chars.len()
207 && (chars[after_punct_pos] == '*' || chars[after_punct_pos] == '_' || chars[after_punct_pos] == '~')
208 {
209 after_punct_pos += 1;
210 }
211
212 while after_punct_pos < chars.len() && chars[after_punct_pos].is_whitespace() {
214 after_punct_pos += 1;
215 }
216
217 if after_punct_pos >= chars.len() {
219 return false;
220 }
221
222 while after_punct_pos < chars.len()
224 && (chars[after_punct_pos] == '*' || chars[after_punct_pos] == '_' || chars[after_punct_pos] == '~')
225 {
226 after_punct_pos += 1;
227 }
228
229 if after_punct_pos >= chars.len() {
230 return false;
231 }
232
233 return true;
236 }
237
238 if c != '.' && c != '!' && c != '?' {
240 return false;
241 }
242
243 let (_space_pos, after_space_pos) = if next_char == ' ' {
245 (pos + 1, pos + 2)
247 } else if is_closing_quote(next_char) && pos + 2 < chars.len() {
248 if chars[pos + 2] == ' ' {
250 (pos + 2, pos + 3)
252 } else if (chars[pos + 2] == '*' || chars[pos + 2] == '_') && pos + 3 < chars.len() && chars[pos + 3] == ' ' {
253 (pos + 3, pos + 4)
255 } else if (chars[pos + 2] == '*' || chars[pos + 2] == '_')
256 && pos + 4 < chars.len()
257 && chars[pos + 3] == chars[pos + 2]
258 && chars[pos + 4] == ' '
259 {
260 (pos + 4, pos + 5)
262 } else {
263 return false;
264 }
265 } else if (next_char == '*' || next_char == '_') && pos + 2 < chars.len() && chars[pos + 2] == ' ' {
266 (pos + 2, pos + 3)
268 } else if (next_char == '*' || next_char == '_')
269 && pos + 3 < chars.len()
270 && chars[pos + 2] == next_char
271 && chars[pos + 3] == ' '
272 {
273 (pos + 3, pos + 4)
275 } else if next_char == '~' && pos + 3 < chars.len() && chars[pos + 2] == '~' && chars[pos + 3] == ' ' {
276 (pos + 3, pos + 4)
278 } else {
279 return false;
280 };
281
282 let mut next_char_pos = after_space_pos;
284 while next_char_pos < chars.len() && chars[next_char_pos].is_whitespace() {
285 next_char_pos += 1;
286 }
287
288 if next_char_pos >= chars.len() {
290 return false;
291 }
292
293 let mut first_letter_pos = next_char_pos;
295 while first_letter_pos < chars.len()
296 && (chars[first_letter_pos] == '*'
297 || chars[first_letter_pos] == '_'
298 || chars[first_letter_pos] == '~'
299 || is_opening_quote(chars[first_letter_pos]))
300 {
301 first_letter_pos += 1;
302 }
303
304 if first_letter_pos >= chars.len() {
306 return false;
307 }
308
309 let first_char = chars[first_letter_pos];
310
311 if c == '!' || c == '?' {
313 return true;
314 }
315
316 if pos > 0 {
320 let byte_offset: usize = chars[..=pos].iter().map(|ch| ch.len_utf8()).sum();
322 if text_ends_with_abbreviation(&text[..byte_offset], abbreviations) {
323 return false;
324 }
325
326 if chars[pos - 1].is_numeric() && first_char.is_ascii_digit() {
328 return false;
329 }
330
331 if chars[pos - 1].is_ascii_uppercase() && (pos == 1 || (pos >= 2 && chars[pos - 2].is_whitespace())) {
335 return false;
336 }
337 }
338
339 if require_sentence_capital && !first_char.is_uppercase() && !is_cjk_char(first_char) {
342 return false;
343 }
344
345 true
346}
347
348pub fn split_into_sentences(text: &str) -> Vec<String> {
350 split_into_sentences_custom(text, &None)
351}
352
353pub fn split_into_sentences_custom(text: &str, custom_abbreviations: &Option<Vec<String>>) -> Vec<String> {
355 let abbreviations = get_abbreviations(custom_abbreviations);
356 split_into_sentences_with_set(text, &abbreviations, true)
357}
358
359fn split_into_sentences_with_set(
362 text: &str,
363 abbreviations: &HashSet<String>,
364 require_sentence_capital: bool,
365) -> Vec<String> {
366 let in_code = compute_inline_code_mask(text);
368 let char_vec: Vec<char> = text.chars().collect();
371
372 let mut sentences = Vec::new();
373 let mut current_sentence = String::new();
374 let mut chars = text.chars().peekable();
375 let mut pos = 0;
376
377 while let Some(c) = chars.next() {
378 current_sentence.push(c);
379
380 if !in_code[pos] && is_sentence_boundary(text, &char_vec, pos, abbreviations, require_sentence_capital) {
381 while let Some(&next) = chars.peek() {
383 if next == '*' || next == '_' || next == '~' || is_closing_quote(next) {
384 current_sentence.push(chars.next().unwrap());
385 pos += 1;
386 } else {
387 break;
388 }
389 }
390
391 if chars.peek() == Some(&' ') {
393 chars.next();
394 pos += 1;
395 }
396
397 sentences.push(current_sentence.trim().to_string());
398 current_sentence.clear();
399 }
400
401 pos += 1;
402 }
403
404 if !current_sentence.trim().is_empty() {
406 sentences.push(current_sentence.trim().to_string());
407 }
408 sentences
409}
410
411fn is_horizontal_rule(line: &str) -> bool {
413 if line.len() < 3 {
414 return false;
415 }
416
417 let mut chars = line.chars();
420 let Some(first_char) = chars.next() else {
421 return false;
422 };
423 if first_char != '-' && first_char != '_' && first_char != '*' {
424 return false;
425 }
426
427 let mut non_space_count = 1usize; for c in chars {
429 if c == ' ' {
430 continue;
431 }
432 if c != first_char {
433 return false;
434 }
435 non_space_count += 1;
436 }
437 non_space_count >= 3
438}
439
440fn is_numbered_list_item(line: &str) -> bool {
442 let mut chars = line.chars();
443
444 if !chars.next().is_some_and(char::is_numeric) {
446 return false;
447 }
448
449 while let Some(c) = chars.next() {
451 if c == '.' {
452 return chars.next() == Some(' ');
455 }
456 if !c.is_numeric() {
457 return false;
458 }
459 }
460
461 false
462}
463
464fn is_unordered_list_marker(s: &str) -> bool {
466 matches!(s.as_bytes().first(), Some(b'-' | b'*' | b'+'))
467 && !is_horizontal_rule(s)
468 && (s.len() == 1 || s.as_bytes().get(1) == Some(&b' '))
469}
470
471fn is_block_boundary_core(trimmed: &str) -> bool {
474 trimmed.is_empty()
475 || trimmed.starts_with('#')
476 || trimmed.starts_with("```")
477 || trimmed.starts_with("~~~")
478 || trimmed.starts_with('>')
479 || (trimmed.starts_with('[') && trimmed.contains("]:"))
480 || is_horizontal_rule(trimmed)
481 || is_unordered_list_marker(trimmed)
482 || is_numbered_list_item(trimmed)
483 || is_definition_list_item(trimmed)
484 || trimmed.starts_with(":::")
485}
486
487fn is_block_boundary(trimmed: &str) -> bool {
490 is_block_boundary_core(trimmed) || trimmed.starts_with('|')
491}
492
493fn is_paragraph_boundary(trimmed: &str, line: &str) -> bool {
497 is_block_boundary_core(trimmed)
498 || calculate_indentation_width_default(line) >= 4
499 || crate::utils::table_utils::TableUtils::is_potential_table_row(line)
500}
501
502fn has_hard_break(line: &str) -> bool {
508 let line = line.strip_suffix('\r').unwrap_or(line);
509 line.ends_with(" ") || line.ends_with('\\')
510}
511
512fn ends_with_sentence_punct(text: &str) -> bool {
514 text.ends_with('.') || text.ends_with('!') || text.ends_with('?')
515}
516
517fn trim_preserving_hard_break(s: &str) -> String {
523 let s = s.strip_suffix('\r').unwrap_or(s);
525
526 if s.ends_with('\\') {
528 return s.to_string();
530 }
531
532 if s.ends_with(" ") {
534 let content_end = s.trim_end().len();
536 if content_end == 0 {
537 return String::new();
539 }
540 format!("{} ", &s[..content_end])
542 } else {
543 s.trim_end().to_string()
545 }
546}
547
548fn parse_elements(text: &str, options: &ReflowOptions) -> Vec<Element> {
550 parse_markdown_elements_inner(
551 text,
552 options.attr_lists,
553 options.myst_roles,
554 options.defined_references.as_ref(),
555 )
556}
557
558pub fn reflow_line(line: &str, options: &ReflowOptions) -> Vec<String> {
559 if options.sentence_per_line {
561 let elements = parse_elements(line, options);
562 return reflow_elements_sentence_per_line(&elements, &options.abbreviations, options.require_sentence_capital);
563 }
564
565 if options.semantic_line_breaks {
567 let elements = parse_elements(line, options);
568 return reflow_elements_semantic(&elements, options);
569 }
570
571 if options.line_length == 0 || display_len(line, options.length_mode) <= options.line_length {
574 return vec![line.to_string()];
575 }
576
577 let elements = parse_elements(line, options);
579
580 reflow_elements(&elements, options)
582}
583
584#[derive(Debug, Clone)]
586enum Element {
587 Text(String),
589 Link(String),
591 ReferenceLink(String),
593 EmptyReferenceLink(String),
595 ShortcutReference(String),
597 InlineImage(String),
599 ReferenceImage(String),
601 EmptyReferenceImage(String),
603 LinkedImage(String),
605 FootnoteReference(String),
607 Strikethrough {
609 content: String,
610 double: bool,
612 },
613 WikiLink(String),
615 InlineMath(String),
617 DisplayMath(String),
619 EmojiShortcode(String),
621 Autolink(String),
623 HtmlTag(String),
625 HtmlEntity(String),
627 HugoShortcode(String),
629 AttrList(String),
631 MystRole(String),
635 Code(String),
637 Bold {
639 content: String,
640 underscore: bool,
642 },
643 Italic {
645 content: String,
646 underscore: bool,
648 },
649}
650
651impl std::fmt::Display for Element {
652 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
653 match self {
654 Element::Text(s) => write!(f, "{s}"),
655 Element::Link(s) => write!(f, "{s}"),
656 Element::ReferenceLink(s) => write!(f, "{s}"),
657 Element::EmptyReferenceLink(s) => write!(f, "{s}"),
658 Element::ShortcutReference(s) => write!(f, "{s}"),
659 Element::InlineImage(s) => write!(f, "{s}"),
660 Element::ReferenceImage(s) => write!(f, "{s}"),
661 Element::EmptyReferenceImage(s) => write!(f, "{s}"),
662 Element::LinkedImage(s) => write!(f, "{s}"),
663 Element::FootnoteReference(s) => write!(f, "{s}"),
664 Element::Strikethrough { content, double } => {
665 let marker = if *double { "~~" } else { "~" };
666 write!(f, "{marker}{content}{marker}")
667 }
668 Element::WikiLink(s) => write!(f, "[[{s}]]"),
669 Element::InlineMath(s) => write!(f, "${s}$"),
670 Element::DisplayMath(s) => write!(f, "$${s}$$"),
671 Element::EmojiShortcode(s) => write!(f, ":{s}:"),
672 Element::Autolink(s) => write!(f, "{s}"),
673 Element::HtmlTag(s) => write!(f, "{s}"),
674 Element::HtmlEntity(s) => write!(f, "{s}"),
675 Element::HugoShortcode(s) => write!(f, "{s}"),
676 Element::AttrList(s) => write!(f, "{s}"),
677 Element::MystRole(s) => write!(f, "{s}"),
678 Element::Code(s) => write!(f, "`{s}`"),
679 Element::Bold { content, underscore } => {
680 if *underscore {
681 write!(f, "__{content}__")
682 } else {
683 write!(f, "**{content}**")
684 }
685 }
686 Element::Italic { content, underscore } => {
687 if *underscore {
688 write!(f, "_{content}_")
689 } else {
690 write!(f, "*{content}*")
691 }
692 }
693 }
694 }
695}
696
697#[derive(Debug, Clone)]
699struct EmphasisSpan {
700 start: usize,
702 end: usize,
704 content: String,
706 is_strong: bool,
708 is_strikethrough: bool,
710 uses_underscore: bool,
712 strikethrough_double: bool,
715}
716
717fn extract_emphasis_spans(text: &str) -> Vec<EmphasisSpan> {
727 let mut spans = Vec::new();
728 let mut options = Options::empty();
729 options.insert(Options::ENABLE_STRIKETHROUGH);
730
731 let mut emphasis_stack: Vec<(usize, bool)> = Vec::new(); let mut strong_stack: Vec<(usize, bool)> = Vec::new();
734 let mut strikethrough_stack: Vec<usize> = Vec::new();
735
736 let parser = Parser::new_ext(text, options).into_offset_iter();
737
738 for (event, range) in parser {
739 match event {
740 Event::Start(Tag::Emphasis) => {
741 let uses_underscore = text.get(range.start..range.start + 1) == Some("_");
743 emphasis_stack.push((range.start, uses_underscore));
744 }
745 Event::End(TagEnd::Emphasis) => {
746 if let Some((start_byte, uses_underscore)) = emphasis_stack.pop() {
747 let content_start = start_byte + 1;
749 let content_end = range.end - 1;
750 if content_end > content_start
751 && let Some(content) = text.get(content_start..content_end)
752 {
753 spans.push(EmphasisSpan {
754 start: start_byte,
755 end: range.end,
756 content: content.to_string(),
757 is_strong: false,
758 is_strikethrough: false,
759 uses_underscore,
760 strikethrough_double: false,
761 });
762 }
763 }
764 }
765 Event::Start(Tag::Strong) => {
766 let uses_underscore = text.get(range.start..range.start + 2) == Some("__");
768 strong_stack.push((range.start, uses_underscore));
769 }
770 Event::End(TagEnd::Strong) => {
771 if let Some((start_byte, uses_underscore)) = strong_stack.pop() {
772 let content_start = start_byte + 2;
774 let content_end = range.end - 2;
775 if content_end > content_start
776 && let Some(content) = text.get(content_start..content_end)
777 {
778 spans.push(EmphasisSpan {
779 start: start_byte,
780 end: range.end,
781 content: content.to_string(),
782 is_strong: true,
783 is_strikethrough: false,
784 uses_underscore,
785 strikethrough_double: false,
786 });
787 }
788 }
789 }
790 Event::Start(Tag::Strikethrough) => {
791 strikethrough_stack.push(range.start);
792 }
793 Event::End(TagEnd::Strikethrough) => {
794 if let Some(start_byte) = strikethrough_stack.pop() {
795 let double = text.get(start_byte..start_byte + 2) == Some("~~");
799 let marker_len = if double { 2 } else { 1 };
800 let content_start = start_byte + marker_len;
801 let content_end = range.end - marker_len;
802 if content_end > content_start
803 && let Some(content) = text.get(content_start..content_end)
804 {
805 spans.push(EmphasisSpan {
806 start: start_byte,
807 end: range.end,
808 content: content.to_string(),
809 is_strong: false,
810 is_strikethrough: true,
811 uses_underscore: false,
812 strikethrough_double: double,
813 });
814 }
815 }
816 }
817 _ => {}
818 }
819 }
820
821 spans.sort_by_key(|s| s.start);
823 spans
824}
825
826#[derive(Debug, Clone)]
827struct LinkSpan {
828 start: usize,
829 end: usize,
830 link_type: Option<LinkType>,
831 is_image: bool,
832 is_footnote: bool,
833}
834
835fn extract_link_spans(text: &str, defined_references: Option<&HashSet<String>>) -> Vec<LinkSpan> {
836 let mut spans = Vec::new();
837 let mut options = Options::empty();
838 options.insert(Options::ENABLE_FOOTNOTES);
839
840 let resolve = move |link: BrokenLink<'_>| -> Option<(CowStr<'_>, CowStr<'_>)> {
857 let atomic = match link.link_type {
862 LinkType::Shortcut | LinkType::ShortcutUnknown => match defined_references {
863 Some(defs) => defs.contains(&normalize_reference_label(link.reference.as_ref())),
864 None => true,
865 },
866 _ => true,
867 };
868 atomic.then_some((CowStr::Borrowed(""), CowStr::Borrowed("")))
869 };
870 let parser = Parser::new_with_broken_link_callback(text, options, Some(resolve)).into_offset_iter();
871 let mut stack = Vec::new();
872
873 for (event, range) in parser {
874 match event {
875 Event::Start(Tag::Link { link_type, .. }) => {
876 stack.push((range.start, Some(link_type), false));
877 }
878 Event::Start(Tag::Image { link_type, .. }) => {
879 stack.push((range.start, Some(link_type), true));
880 }
881 Event::End(TagEnd::Link) => {
882 if let Some((start_byte, link_type, is_image)) = stack.pop()
883 && stack.is_empty()
884 {
885 spans.push(LinkSpan {
886 start: start_byte,
887 end: range.end,
888 link_type,
889 is_image,
890 is_footnote: false,
891 });
892 }
893 }
894 Event::End(TagEnd::Image) => {
895 if let Some((start_byte, link_type, is_image)) = stack.pop()
896 && stack.is_empty()
897 {
898 spans.push(LinkSpan {
899 start: start_byte,
900 end: range.end,
901 link_type,
902 is_image,
903 is_footnote: false,
904 });
905 }
906 }
907 Event::FootnoteReference(_) if stack.is_empty() => {
908 spans.push(LinkSpan {
909 start: range.start,
910 end: range.end,
911 link_type: None,
912 is_image: false,
913 is_footnote: true,
914 });
915 }
916 _ => {}
917 }
918 }
919
920 spans.sort_by_key(|s| s.start);
921 spans
922}
923
924fn myst_role_len_at(text: &str) -> Option<usize> {
932 let bytes = text.as_bytes();
933 if bytes.first() != Some(&b'{') {
934 return None;
935 }
936
937 let mut j = 1;
939 match bytes.get(j) {
940 Some(&b) if b.is_ascii_alphabetic() || b == b'_' => {}
941 _ => return None,
942 }
943 while let Some(&b) = bytes.get(j) {
944 if b.is_ascii_alphanumeric() || matches!(b, b'-' | b'_' | b':' | b'.') {
945 j += 1;
946 } else {
947 break;
948 }
949 }
950 if bytes.get(j) != Some(&b'}') {
951 return None;
952 }
953 j += 1; if bytes.get(j) != Some(&b'`') {
957 return None;
958 }
959 let backtick_start = j;
960 while bytes.get(j) == Some(&b'`') {
961 j += 1;
962 }
963 let backtick_count = j - backtick_start;
964
965 while j + backtick_count <= bytes.len() {
967 if bytes[j] == b'`' {
968 let close_count = bytes[j..].iter().take_while(|&&b| b == b'`').count();
969 if close_count == backtick_count {
970 return Some(j + close_count);
971 }
972 j += close_count;
973 } else {
974 j += 1;
975 }
976 }
977
978 None
979}
980
981fn parse_markdown_elements_inner(
992 text: &str,
993 attr_lists: bool,
994 myst_roles: bool,
995 defined_references: Option<&HashSet<String>>,
996) -> Vec<Element> {
997 let mut elements = Vec::new();
998 let mut remaining = text;
999
1000 let emphasis_spans = extract_emphasis_spans(text);
1002 let link_spans = extract_link_spans(text, defined_references);
1003
1004 while !remaining.is_empty() {
1005 let current_offset = text.len() - remaining.len();
1007 let mut earliest_match: Option<(usize, usize, &str)> = None;
1010
1011 let mut next_link: Option<&LinkSpan> = None;
1013 for span in &link_spans {
1014 if span.start >= current_offset {
1015 next_link = Some(span);
1016 break;
1017 }
1018 }
1019
1020 if let Some(span) = next_link {
1021 let pos_in_remaining = span.start - current_offset;
1022 if earliest_match
1023 .as_ref()
1024 .is_none_or(|(start, _, _)| pos_in_remaining < *start)
1025 {
1026 let match_end = span.end - current_offset;
1027 earliest_match = Some((pos_in_remaining, match_end, "link_span"));
1028 }
1029 }
1030
1031 if let Some(m) = WIKI_LINK_REGEX.find(remaining)
1033 && earliest_match.as_ref().is_none_or(|(start, _, _)| m.start() < *start)
1034 {
1035 earliest_match = Some((m.start(), m.end(), "wiki_link"));
1036 }
1037
1038 if let Some(m) = DISPLAY_MATH_REGEX.find(remaining)
1040 && earliest_match.as_ref().is_none_or(|(start, _, _)| m.start() < *start)
1041 {
1042 earliest_match = Some((m.start(), m.end(), "display_math"));
1043 }
1044
1045 if let Ok(Some(m)) = INLINE_MATH_REGEX.find(remaining)
1047 && earliest_match.as_ref().is_none_or(|(start, _, _)| m.start() < *start)
1048 {
1049 earliest_match = Some((m.start(), m.end(), "inline_math"));
1050 }
1051
1052 if let Some(m) = EMOJI_SHORTCODE_REGEX.find(remaining)
1054 && earliest_match.as_ref().is_none_or(|(start, _, _)| m.start() < *start)
1055 {
1056 earliest_match = Some((m.start(), m.end(), "emoji"));
1057 }
1058
1059 if let Some(m) = HTML_ENTITY_REGEX.find(remaining)
1061 && earliest_match.as_ref().is_none_or(|(start, _, _)| m.start() < *start)
1062 {
1063 earliest_match = Some((m.start(), m.end(), "html_entity"));
1064 }
1065
1066 if let Some(m) = HUGO_SHORTCODE_REGEX.find(remaining)
1069 && earliest_match.as_ref().is_none_or(|(start, _, _)| m.start() < *start)
1070 {
1071 earliest_match = Some((m.start(), m.end(), "hugo_shortcode"));
1072 }
1073
1074 if let Some(m) = HTML_TAG_PATTERN.find(remaining)
1077 && earliest_match.as_ref().is_none_or(|(start, _, _)| m.start() < *start)
1078 {
1079 let matched_text = &remaining[m.start()..m.end()];
1081 let is_url_autolink = matched_text.starts_with("<http://")
1082 || matched_text.starts_with("<https://")
1083 || matched_text.starts_with("<mailto:")
1084 || matched_text.starts_with("<ftp://")
1085 || matched_text.starts_with("<ftps://");
1086
1087 let is_email_autolink = {
1090 let content = matched_text.trim_start_matches('<').trim_end_matches('>');
1091 EMAIL_PATTERN.is_match(content)
1092 };
1093
1094 if is_url_autolink || is_email_autolink {
1095 } else {
1097 earliest_match = Some((m.start(), m.end(), "html_tag"));
1098 }
1099 }
1100
1101 let mut next_special = remaining.len();
1103 let mut special_type = "";
1104 let mut pulldown_emphasis: Option<&EmphasisSpan> = None;
1105 let mut attr_list_len: usize = 0;
1106 let mut myst_role_len: usize = 0;
1107
1108 if let Some(pos) = remaining.find('`')
1110 && pos < next_special
1111 {
1112 next_special = pos;
1113 special_type = "code";
1114 }
1115
1116 if myst_roles
1121 && let Some(pos) = remaining.find('{')
1122 && pos < next_special
1123 && let Some(role_len) = myst_role_len_at(&remaining[pos..])
1124 {
1125 next_special = pos;
1126 special_type = "myst_role";
1127 myst_role_len = role_len;
1128 }
1129
1130 if attr_lists
1132 && let Some(pos) = remaining.find('{')
1133 && pos < next_special
1134 && let Some(m) = ATTR_LIST_PATTERN.find(&remaining[pos..])
1135 && m.start() == 0
1136 {
1137 next_special = pos;
1138 special_type = "attr_list";
1139 attr_list_len = m.end();
1140 }
1141
1142 for span in &emphasis_spans {
1145 if span.start >= current_offset && span.start < current_offset + remaining.len() {
1146 let pos_in_remaining = span.start - current_offset;
1147 if pos_in_remaining < next_special {
1148 next_special = pos_in_remaining;
1149 special_type = "pulldown_emphasis";
1150 pulldown_emphasis = Some(span);
1151 }
1152 break; }
1154 }
1155
1156 let should_process_markdown_link = if let Some((pos, _, _)) = earliest_match {
1158 pos < next_special
1159 } else {
1160 false
1161 };
1162
1163 if should_process_markdown_link {
1164 let (pos, match_end, pattern_type) = earliest_match.unwrap();
1165
1166 if pos > 0 {
1168 elements.push(Element::Text(remaining[..pos].to_string()));
1169 }
1170
1171 match pattern_type {
1173 "link_span" => {
1174 let span = next_link.unwrap();
1175 let raw_text = remaining[pos..match_end].to_string();
1176 if span.is_footnote {
1177 elements.push(Element::FootnoteReference(raw_text));
1178 } else if span.is_image {
1179 match span.link_type {
1180 Some(LinkType::Inline) => elements.push(Element::InlineImage(raw_text)),
1181 Some(LinkType::Reference)
1184 | Some(LinkType::ReferenceUnknown)
1185 | Some(LinkType::Shortcut)
1186 | Some(LinkType::ShortcutUnknown) => elements.push(Element::ReferenceImage(raw_text)),
1187 Some(LinkType::Collapsed) | Some(LinkType::CollapsedUnknown) => {
1188 elements.push(Element::EmptyReferenceImage(raw_text))
1189 }
1190 _ => elements.push(Element::InlineImage(raw_text)),
1191 }
1192 } else {
1193 match span.link_type {
1194 Some(LinkType::Inline) => {
1195 if raw_text.starts_with('[') && raw_text.contains("![") {
1196 elements.push(Element::LinkedImage(raw_text));
1197 } else {
1198 elements.push(Element::Link(raw_text));
1199 }
1200 }
1201 Some(LinkType::Reference) | Some(LinkType::ReferenceUnknown) => {
1204 elements.push(Element::ReferenceLink(raw_text))
1205 }
1206 Some(LinkType::Collapsed) | Some(LinkType::CollapsedUnknown) => {
1207 elements.push(Element::EmptyReferenceLink(raw_text))
1208 }
1209 Some(LinkType::Shortcut) | Some(LinkType::ShortcutUnknown) => {
1210 elements.push(Element::ShortcutReference(raw_text))
1211 }
1212 Some(LinkType::Autolink) | Some(LinkType::Email) => {
1213 elements.push(Element::Autolink(raw_text))
1214 }
1215 _ => elements.push(Element::Link(raw_text)),
1216 }
1217 }
1218 remaining = &remaining[match_end..];
1219 }
1220 "wiki_link" => {
1221 if let Some(caps) = WIKI_LINK_REGEX.captures(remaining) {
1222 let content = caps.get(1).map_or("", |m| m.as_str());
1223 elements.push(Element::WikiLink(content.to_string()));
1224 remaining = &remaining[match_end..];
1225 } else {
1226 elements.push(Element::Text("[[".to_string()));
1227 remaining = &remaining[2..];
1228 }
1229 }
1230 "display_math" => {
1231 if let Some(caps) = DISPLAY_MATH_REGEX.captures(remaining) {
1232 let math = caps.get(1).map_or("", |m| m.as_str());
1233 elements.push(Element::DisplayMath(math.to_string()));
1234 remaining = &remaining[match_end..];
1235 } else {
1236 elements.push(Element::Text("$$".to_string()));
1237 remaining = &remaining[2..];
1238 }
1239 }
1240 "inline_math" => {
1241 if let Ok(Some(caps)) = INLINE_MATH_REGEX.captures(remaining) {
1242 let math = caps.get(1).map_or("", |m| m.as_str());
1243 elements.push(Element::InlineMath(math.to_string()));
1244 remaining = &remaining[match_end..];
1245 } else {
1246 elements.push(Element::Text("$".to_string()));
1247 remaining = &remaining[1..];
1248 }
1249 }
1250 "emoji" => {
1251 if let Some(caps) = EMOJI_SHORTCODE_REGEX.captures(remaining) {
1252 let emoji = caps.get(1).map_or("", |m| m.as_str());
1253 elements.push(Element::EmojiShortcode(emoji.to_string()));
1254 remaining = &remaining[match_end..];
1255 } else {
1256 elements.push(Element::Text(":".to_string()));
1257 remaining = &remaining[1..];
1258 }
1259 }
1260 "html_entity" => {
1261 elements.push(Element::HtmlEntity(remaining[pos..match_end].to_string()));
1263 remaining = &remaining[match_end..];
1264 }
1265 "hugo_shortcode" => {
1266 elements.push(Element::HugoShortcode(remaining[pos..match_end].to_string()));
1268 remaining = &remaining[match_end..];
1269 }
1270 "html_tag" => {
1271 elements.push(Element::HtmlTag(remaining[pos..match_end].to_string()));
1273 remaining = &remaining[match_end..];
1274 }
1275 _ => {
1276 elements.push(Element::Text("[".to_string()));
1278 remaining = &remaining[1..];
1279 }
1280 }
1281 } else {
1282 if next_special > 0 && next_special < remaining.len() {
1286 elements.push(Element::Text(remaining[..next_special].to_string()));
1287 remaining = &remaining[next_special..];
1288 }
1289
1290 match special_type {
1292 "code" => {
1293 if let Some(code_end) = remaining[1..].find('`') {
1295 let code = &remaining[1..=code_end];
1296 elements.push(Element::Code(code.to_string()));
1297 remaining = &remaining[1 + code_end + 1..];
1298 } else {
1299 elements.push(Element::Text(remaining.to_string()));
1301 break;
1302 }
1303 }
1304 "attr_list" => {
1305 elements.push(Element::AttrList(remaining[..attr_list_len].to_string()));
1306 remaining = &remaining[attr_list_len..];
1307 }
1308 "myst_role" => {
1309 elements.push(Element::MystRole(remaining[..myst_role_len].to_string()));
1310 remaining = &remaining[myst_role_len..];
1311 }
1312 "pulldown_emphasis" => {
1313 if let Some(span) = pulldown_emphasis {
1315 let span_len = span.end - span.start;
1316 if span.is_strikethrough {
1317 elements.push(Element::Strikethrough {
1318 content: span.content.clone(),
1319 double: span.strikethrough_double,
1320 });
1321 } else if span.is_strong {
1322 elements.push(Element::Bold {
1323 content: span.content.clone(),
1324 underscore: span.uses_underscore,
1325 });
1326 } else {
1327 elements.push(Element::Italic {
1328 content: span.content.clone(),
1329 underscore: span.uses_underscore,
1330 });
1331 }
1332 remaining = &remaining[span_len..];
1333 } else {
1334 elements.push(Element::Text(remaining[..1].to_string()));
1336 remaining = &remaining[1..];
1337 }
1338 }
1339 _ => {
1340 elements.push(Element::Text(remaining.to_string()));
1342 break;
1343 }
1344 }
1345 }
1346 }
1347
1348 elements
1349}
1350
1351fn should_insert_space_before_join(current: &str) -> bool {
1352 !current.is_empty()
1353 && !current.ends_with(' ')
1354 && !current.ends_with('(')
1355 && !current.ends_with('[')
1356 && !current.ends_with('-')
1357}
1358
1359fn reflow_elements_sentence_per_line(
1361 elements: &[Element],
1362 custom_abbreviations: &Option<Vec<String>>,
1363 require_sentence_capital: bool,
1364) -> Vec<String> {
1365 let abbreviations = get_abbreviations(custom_abbreviations);
1366 let mut lines = Vec::new();
1367 let mut current_line = String::new();
1368
1369 for (idx, element) in elements.iter().enumerate() {
1370 let element_str = format!("{element}");
1371
1372 if let Element::Text(text) = element {
1374 let combined = format!("{current_line}{text}");
1376 let sentences = split_into_sentences_with_set(&combined, &abbreviations, require_sentence_capital);
1378
1379 if sentences.len() > 1 {
1380 for (i, sentence) in sentences.iter().enumerate() {
1382 if i == 0 {
1383 let trimmed = sentence.trim();
1386
1387 if text_ends_with_abbreviation(trimmed, &abbreviations) {
1388 current_line.clone_from(sentence);
1390 } else {
1391 lines.push(sentence.clone());
1393 current_line.clear();
1394 }
1395 } else if i == sentences.len() - 1 {
1396 let trimmed = sentence.trim();
1398 let ends_with_sentence_punct = ends_with_sentence_punct(trimmed);
1399
1400 if ends_with_sentence_punct && !text_ends_with_abbreviation(trimmed, &abbreviations) {
1401 lines.push(sentence.clone());
1403 current_line.clear();
1404 } else {
1405 current_line.clone_from(sentence);
1407 }
1408 } else {
1409 lines.push(sentence.clone());
1411 }
1412 }
1413 } else {
1414 let trimmed = combined.trim();
1416
1417 if trimmed.is_empty() {
1421 continue;
1422 }
1423
1424 let ends_with_sentence_punct = ends_with_sentence_punct(trimmed);
1425
1426 if ends_with_sentence_punct && !text_ends_with_abbreviation(trimmed, &abbreviations) {
1427 lines.push(trimmed.to_string());
1429 current_line.clear();
1430 } else {
1431 current_line = combined;
1433 }
1434 }
1435 } else if let Element::Italic { content, underscore } = element {
1436 let marker = if *underscore { "_" } else { "*" };
1438 handle_emphasis_sentence_split(
1439 content,
1440 marker,
1441 &abbreviations,
1442 require_sentence_capital,
1443 &mut current_line,
1444 &mut lines,
1445 );
1446 } else if let Element::Bold { content, underscore } = element {
1447 let marker = if *underscore { "__" } else { "**" };
1449 handle_emphasis_sentence_split(
1450 content,
1451 marker,
1452 &abbreviations,
1453 require_sentence_capital,
1454 &mut current_line,
1455 &mut lines,
1456 );
1457 } else if let Element::Strikethrough { content, double } = element {
1458 handle_emphasis_sentence_split(
1460 content,
1461 if *double { "~~" } else { "~" },
1462 &abbreviations,
1463 require_sentence_capital,
1464 &mut current_line,
1465 &mut lines,
1466 );
1467 } else {
1468 let is_adjacent = if idx > 0 {
1471 match &elements[idx - 1] {
1472 Element::Text(t) => !t.is_empty() && !t.ends_with(char::is_whitespace),
1473 _ => true,
1474 }
1475 } else {
1476 false
1477 };
1478
1479 if !is_adjacent && should_insert_space_before_join(¤t_line) {
1481 current_line.push(' ');
1482 }
1483 current_line.push_str(&element_str);
1484 }
1485 }
1486
1487 if !current_line.is_empty() {
1489 lines.push(current_line.trim().to_string());
1490 }
1491 lines
1492}
1493
1494fn handle_emphasis_sentence_split(
1496 content: &str,
1497 marker: &str,
1498 abbreviations: &HashSet<String>,
1499 require_sentence_capital: bool,
1500 current_line: &mut String,
1501 lines: &mut Vec<String>,
1502) {
1503 let sentences = split_into_sentences_with_set(content, abbreviations, require_sentence_capital);
1505
1506 if sentences.len() <= 1 {
1507 if should_insert_space_before_join(current_line) {
1509 current_line.push(' ');
1510 }
1511 current_line.push_str(marker);
1512 current_line.push_str(content);
1513 current_line.push_str(marker);
1514
1515 let trimmed = content.trim();
1517 let ends_with_punct = ends_with_sentence_punct(trimmed);
1518 if ends_with_punct && !text_ends_with_abbreviation(trimmed, abbreviations) {
1519 lines.push(current_line.clone());
1520 current_line.clear();
1521 }
1522 } else {
1523 for (i, sentence) in sentences.iter().enumerate() {
1525 let trimmed = sentence.trim();
1526 if trimmed.is_empty() {
1527 continue;
1528 }
1529
1530 if i == 0 {
1531 if should_insert_space_before_join(current_line) {
1533 current_line.push(' ');
1534 }
1535 current_line.push_str(marker);
1536 current_line.push_str(trimmed);
1537 current_line.push_str(marker);
1538
1539 let ends_with_punct = ends_with_sentence_punct(trimmed);
1541 if ends_with_punct && !text_ends_with_abbreviation(trimmed, abbreviations) {
1542 lines.push(current_line.clone());
1543 current_line.clear();
1544 }
1545 } else if i == sentences.len() - 1 {
1546 let ends_with_punct = ends_with_sentence_punct(trimmed);
1548
1549 let mut line = String::new();
1550 line.push_str(marker);
1551 line.push_str(trimmed);
1552 line.push_str(marker);
1553
1554 if ends_with_punct && !text_ends_with_abbreviation(trimmed, abbreviations) {
1555 lines.push(line);
1556 } else {
1557 *current_line = line;
1559 }
1560 } else {
1561 let mut line = String::new();
1563 line.push_str(marker);
1564 line.push_str(trimmed);
1565 line.push_str(marker);
1566 lines.push(line);
1567 }
1568 }
1569 }
1570}
1571
1572const BREAK_WORDS: &[&str] = &[
1576 "and",
1577 "or",
1578 "but",
1579 "nor",
1580 "yet",
1581 "so",
1582 "for",
1583 "which",
1584 "that",
1585 "because",
1586 "when",
1587 "if",
1588 "while",
1589 "where",
1590 "although",
1591 "though",
1592 "unless",
1593 "since",
1594 "after",
1595 "before",
1596 "until",
1597 "as",
1598 "once",
1599 "whether",
1600 "however",
1601 "therefore",
1602 "moreover",
1603 "furthermore",
1604 "nevertheless",
1605 "whereas",
1606];
1607
1608fn is_clause_punctuation(c: char) -> bool {
1610 matches!(c, ',' | ';' | ':' | '\u{2014}') }
1612
1613fn clause_break_allowed_after(chars: &[char], i: usize) -> bool {
1621 if chars[i] == '\u{2014}' {
1622 return true;
1623 }
1624 match chars.get(i + 1) {
1625 None => true,
1626 Some(next) => next.is_whitespace(),
1627 }
1628}
1629
1630fn paren_group_end<'a>(slice: &'a str, element_spans: &[(usize, usize)], offset: usize) -> Option<(usize, &'a str)> {
1644 debug_assert!(slice.starts_with('('));
1645 let mut depth: i32 = 0;
1646 for (local_byte, c) in slice.char_indices() {
1647 let global_byte = offset + local_byte;
1648 if depth > 0 && is_inside_element(global_byte, element_spans) {
1653 continue;
1654 }
1655 match c {
1656 '(' => depth += 1,
1657 ')' => {
1658 depth -= 1;
1659 if depth == 0 {
1660 let end = local_byte + 1;
1661 let inner = &slice[1..local_byte];
1662 return Some((end, inner));
1663 }
1664 }
1665 _ => {}
1666 }
1667 }
1668 None
1669}
1670
1671fn split_at_parenthetical(
1688 text: &str,
1689 line_length: usize,
1690 element_spans: &[(usize, usize)],
1691 length_mode: ReflowLengthMode,
1692) -> Option<(String, String)> {
1693 let min_first_len = ((line_length as f64) * MIN_SPLIT_RATIO) as usize;
1694
1695 if text.starts_with('(')
1697 && let Some((end_local, inner)) = paren_group_end(text, element_spans, 0)
1698 && inner.contains(' ')
1699 {
1700 let tail = &text[end_local..];
1704 let attached_len = tail
1705 .char_indices()
1706 .take_while(|(_, c)| is_closing_quote(*c) || is_clause_punctuation(*c))
1707 .last()
1708 .map_or(0, |(idx, c)| idx + c.len_utf8());
1709 let first_end = end_local + attached_len;
1710 let rest_start = first_end;
1711 let first = &text[..first_end];
1712 let first_len = display_len(first, length_mode);
1713 if first_len <= line_length {
1716 let rest = text[rest_start..].trim_start();
1717 if !rest.is_empty() {
1718 return Some((first.to_string(), rest.to_string()));
1719 }
1720 }
1721 }
1722
1723 let mut best_open_byte: Option<usize> = None;
1725 let mut pos = 0usize;
1726 while pos < text.len() {
1727 if text.as_bytes()[pos] != b'(' {
1729 let c = text[pos..].chars().next().unwrap();
1730 pos += c.len_utf8();
1731 continue;
1732 }
1733 if is_inside_element(pos, element_spans) {
1735 pos += 1;
1736 continue;
1737 }
1738 if let Some((end_local, inner)) = paren_group_end(&text[pos..], element_spans, pos) {
1739 let first = text[..pos].trim_end();
1740 let first_len = display_len(first, length_mode);
1741 if !first.is_empty()
1742 && first_len >= min_first_len
1743 && first_len <= line_length
1744 && inner.contains(' ')
1745 && best_open_byte.is_none_or(|prev| pos > prev)
1746 {
1747 best_open_byte = Some(pos);
1748 }
1749 pos += end_local;
1750 } else {
1751 pos += 1;
1752 }
1753 }
1754
1755 let open_byte = best_open_byte?;
1756 let first = text[..open_byte].trim_end().to_string();
1757 let rest = text[open_byte..].to_string();
1758 if first.is_empty() || rest.trim().is_empty() {
1759 return None;
1760 }
1761 Some((first, rest))
1762}
1763
1764fn compute_element_spans(elements: &[Element]) -> Vec<(usize, usize)> {
1768 let mut spans = Vec::new();
1769 let mut offset = 0;
1770 for element in elements {
1771 let rendered = format!("{element}");
1772 let len = rendered.len();
1773 if !matches!(element, Element::Text(_)) {
1774 spans.push((offset, offset + len));
1775 }
1776 offset += len;
1777 }
1778 spans
1779}
1780
1781fn is_inside_element(pos: usize, spans: &[(usize, usize)]) -> bool {
1783 spans.iter().any(|(start, end)| pos > *start && pos < *end)
1784}
1785
1786const MIN_SPLIT_RATIO: f64 = 0.3;
1789
1790fn split_at_clause_punctuation(
1794 text: &str,
1795 line_length: usize,
1796 element_spans: &[(usize, usize)],
1797 length_mode: ReflowLengthMode,
1798) -> Option<(String, String)> {
1799 let chars: Vec<char> = text.chars().collect();
1800 let min_first_len = ((line_length as f64) * MIN_SPLIT_RATIO) as usize;
1801
1802 let mut width_acc = 0;
1804 let mut search_end_char = 0;
1805 for (idx, &c) in chars.iter().enumerate() {
1806 let c_width = display_len(&c.to_string(), length_mode);
1807 if width_acc + c_width > line_length {
1808 break;
1809 }
1810 width_acc += c_width;
1811 search_end_char = idx + 1;
1812 }
1813
1814 let mut paren_depth: i32 = 0;
1821 let mut best_pos = None;
1822 for i in (0..search_end_char).rev() {
1823 let byte_start: usize = chars[..i].iter().map(|c| c.len_utf8()).sum();
1825 let byte_after: usize = byte_start + chars[i].len_utf8();
1827
1828 if !is_inside_element(byte_start, element_spans) {
1829 match chars[i] {
1830 ')' => paren_depth += 1,
1831 '(' => paren_depth = paren_depth.saturating_sub(1),
1832 _ => {}
1833 }
1834 }
1835
1836 if paren_depth == 0
1837 && is_clause_punctuation(chars[i])
1838 && clause_break_allowed_after(&chars, i)
1839 && !is_inside_element(byte_after, element_spans)
1840 {
1841 best_pos = Some(i);
1842 break;
1843 }
1844 }
1845
1846 let pos = best_pos?;
1847
1848 let first: String = chars[..=pos].iter().collect();
1850 let first_display_len = display_len(&first, length_mode);
1851 if first_display_len < min_first_len {
1852 return None;
1853 }
1854
1855 let rest: String = chars[pos + 1..].iter().collect();
1857 let rest = rest.trim_start().to_string();
1858
1859 if rest.is_empty() {
1860 return None;
1861 }
1862
1863 Some((first, rest))
1864}
1865
1866fn paren_depth_map(text: &str, element_spans: &[(usize, usize)]) -> Vec<i32> {
1873 let mut map = vec![0i32; text.len()];
1874 let mut depth = 0i32;
1875 for (byte, c) in text.char_indices() {
1876 if !is_inside_element(byte, element_spans) {
1877 match c {
1878 '(' => depth += 1,
1879 ')' => depth = depth.saturating_sub(1),
1880 _ => {}
1881 }
1882 }
1883 let end = (byte + c.len_utf8()).min(map.len());
1885 for slot in &mut map[byte..end] {
1886 *slot = depth;
1887 }
1888 }
1889 map
1890}
1891
1892fn is_standalone_parenthetical(line: &str) -> bool {
1901 let trimmed = line.trim();
1902 if !trimmed.starts_with('(') {
1903 return false;
1904 }
1905 let core = trimmed.trim_end_matches(|c: char| is_clause_punctuation(c));
1907 if !core.ends_with(')') {
1908 return false;
1909 }
1910 let inner = &core[1..core.len() - 1];
1912 if !inner.contains(' ') {
1913 return false;
1914 }
1915 let mut depth = 0i32;
1917 for c in core.chars() {
1918 match c {
1919 '(' => depth += 1,
1920 ')' => depth -= 1,
1921 _ => {}
1922 }
1923 if depth < 0 {
1924 return false;
1925 }
1926 }
1927 depth == 0
1928}
1929
1930fn split_at_break_word(
1934 text: &str,
1935 line_length: usize,
1936 element_spans: &[(usize, usize)],
1937 length_mode: ReflowLengthMode,
1938) -> Option<(String, String)> {
1939 let lower = text.to_lowercase();
1940 let min_first_len = ((line_length as f64) * MIN_SPLIT_RATIO) as usize;
1941 let mut best_split: Option<(usize, usize)> = None; let depth_map = paren_depth_map(text, element_spans);
1946
1947 for &word in BREAK_WORDS {
1948 let mut search_start = 0;
1949 while let Some(pos) = lower[search_start..].find(word) {
1950 let abs_pos = search_start + pos;
1951
1952 let preceded_by_space = abs_pos == 0 || text.as_bytes().get(abs_pos - 1) == Some(&b' ');
1954 let followed_by_space = text.as_bytes().get(abs_pos + word.len()) == Some(&b' ');
1955
1956 if preceded_by_space && followed_by_space {
1957 let first_part = text[..abs_pos].trim_end();
1959 let first_part_len = display_len(first_part, length_mode);
1960
1961 let inside_paren = depth_map.get(abs_pos).is_some_and(|&d| d > 0);
1963
1964 if first_part_len >= min_first_len
1965 && first_part_len <= line_length
1966 && !is_inside_element(abs_pos, element_spans)
1967 && !inside_paren
1968 {
1969 if best_split.is_none_or(|(prev_pos, _)| abs_pos > prev_pos) {
1971 best_split = Some((abs_pos, word.len()));
1972 }
1973 }
1974 }
1975
1976 search_start = abs_pos + word.len();
1977 }
1978 }
1979
1980 let (byte_start, _word_len) = best_split?;
1981
1982 let first = text[..byte_start].trim_end().to_string();
1983 let rest = text[byte_start..].to_string();
1984
1985 if first.is_empty() || rest.trim().is_empty() {
1986 return None;
1987 }
1988
1989 Some((first, rest))
1990}
1991
1992fn cascade_split_line(
1995 text: &str,
1996 line_length: usize,
1997 abbreviations: &Option<Vec<String>>,
1998 length_mode: ReflowLengthMode,
1999 attr_lists: bool,
2000 myst_roles: bool,
2001 defined_references: Option<&HashSet<String>>,
2002) -> Vec<String> {
2003 if line_length == 0 || display_len(text, length_mode) <= line_length {
2004 return vec![text.to_string()];
2005 }
2006
2007 let elements = parse_markdown_elements_inner(text, attr_lists, myst_roles, defined_references);
2008 let element_spans = compute_element_spans(&elements);
2009
2010 if let Some((first, rest)) = split_at_parenthetical(text, line_length, &element_spans, length_mode) {
2013 let mut result = vec![first];
2014 result.extend(cascade_split_line(
2015 &rest,
2016 line_length,
2017 abbreviations,
2018 length_mode,
2019 attr_lists,
2020 myst_roles,
2021 defined_references,
2022 ));
2023 return result;
2024 }
2025
2026 if let Some((first, rest)) = split_at_clause_punctuation(text, line_length, &element_spans, length_mode) {
2028 let mut result = vec![first];
2029 result.extend(cascade_split_line(
2030 &rest,
2031 line_length,
2032 abbreviations,
2033 length_mode,
2034 attr_lists,
2035 myst_roles,
2036 defined_references,
2037 ));
2038 return result;
2039 }
2040
2041 if let Some((first, rest)) = split_at_break_word(text, line_length, &element_spans, length_mode) {
2043 let mut result = vec![first];
2044 result.extend(cascade_split_line(
2045 &rest,
2046 line_length,
2047 abbreviations,
2048 length_mode,
2049 attr_lists,
2050 myst_roles,
2051 defined_references,
2052 ));
2053 return result;
2054 }
2055
2056 let options = ReflowOptions {
2058 line_length,
2059 break_on_sentences: false,
2060 preserve_breaks: false,
2061 sentence_per_line: false,
2062 semantic_line_breaks: false,
2063 abbreviations: abbreviations.clone(),
2064 length_mode,
2065 attr_lists,
2066 myst_roles,
2067 require_sentence_capital: true,
2068 max_list_continuation_indent: None,
2069 defined_references: None,
2072 };
2073 reflow_elements(&elements, &options)
2074}
2075
2076fn reflow_elements_semantic(elements: &[Element], options: &ReflowOptions) -> Vec<String> {
2080 let sentence_lines =
2082 reflow_elements_sentence_per_line(elements, &options.abbreviations, options.require_sentence_capital);
2083
2084 if options.line_length == 0 {
2087 return sentence_lines;
2088 }
2089
2090 let length_mode = options.length_mode;
2091 let mut result = Vec::new();
2092 for line in sentence_lines {
2093 if display_len(&line, length_mode) <= options.line_length {
2094 result.push(line);
2095 } else {
2096 result.extend(cascade_split_line(
2097 &line,
2098 options.line_length,
2099 &options.abbreviations,
2100 length_mode,
2101 options.attr_lists,
2102 options.myst_roles,
2103 options.defined_references.as_ref(),
2104 ));
2105 }
2106 }
2107
2108 let min_line_len = ((options.line_length as f64) * MIN_SPLIT_RATIO) as usize;
2111 let mut merged: Vec<String> = Vec::with_capacity(result.len());
2112 for line in result {
2113 if !merged.is_empty() && display_len(&line, length_mode) < min_line_len && !line.trim().is_empty() {
2114 if is_standalone_parenthetical(&line) {
2117 merged.push(line);
2118 continue;
2119 }
2120
2121 let prev_ends_at_sentence = {
2123 let trimmed = merged.last().unwrap().trim_end();
2124 trimmed
2125 .chars()
2126 .rev()
2127 .find(|c| !matches!(c, '"' | '\'' | '\u{201D}' | '\u{2019}' | ')' | ']'))
2128 .is_some_and(|c| matches!(c, '.' | '!' | '?'))
2129 };
2130
2131 if !prev_ends_at_sentence {
2132 let prev = merged.last_mut().unwrap();
2133 let combined = format!("{prev} {line}");
2134 if display_len(&combined, length_mode) <= options.line_length {
2136 *prev = combined;
2137 continue;
2138 }
2139 }
2140 }
2141 merged.push(line);
2142 }
2143 merged
2144}
2145
2146fn rfind_safe_space(line: &str, element_spans: &[(usize, usize)]) -> Option<usize> {
2154 line.char_indices()
2155 .rev()
2156 .map(|(pos, _)| pos)
2157 .find(|&pos| line.as_bytes()[pos] == b' ' && !element_spans.iter().any(|(s, e)| pos > *s && pos < *e))
2158}
2159
2160fn reflow_elements(elements: &[Element], options: &ReflowOptions) -> Vec<String> {
2162 let mut lines = Vec::new();
2163 let mut current_line = String::new();
2164 let mut current_length = 0;
2165 let mut current_line_element_spans: Vec<(usize, usize)> = Vec::new();
2167 let length_mode = options.length_mode;
2168
2169 for (idx, element) in elements.iter().enumerate() {
2170 let element_str = format!("{element}");
2173 let element_len = display_len(&element_str, length_mode);
2174
2175 let is_adjacent_to_prev = if idx > 0 {
2181 match (&elements[idx - 1], element) {
2182 (Element::Text(t), _) => !t.is_empty() && !t.ends_with(char::is_whitespace),
2183 (_, Element::Text(t)) => !t.is_empty() && !t.starts_with(char::is_whitespace),
2184 _ => true,
2185 }
2186 } else {
2187 false
2188 };
2189
2190 if let Element::Text(text) = element {
2192 let has_leading_space = text.starts_with(char::is_whitespace);
2194 let words: Vec<&str> = text.split_whitespace().collect();
2196
2197 for (i, word) in words.iter().enumerate() {
2198 let word_len = display_len(word, length_mode);
2199 let is_trailing_punct = word
2201 .chars()
2202 .all(|c| matches!(c, ',' | '.' | ':' | ';' | '!' | '?' | ')' | ']' | '}'));
2203
2204 let is_first_adjacent = i == 0 && is_adjacent_to_prev;
2207
2208 if is_first_adjacent {
2209 if current_length + word_len > options.line_length && current_length > 0 {
2211 if let Some(last_space) = rfind_safe_space(¤t_line, ¤t_line_element_spans) {
2214 let before = current_line[..last_space].trim_end().to_string();
2215 let after = current_line[last_space + 1..].to_string();
2216 lines.push(before);
2217 current_line = format!("{after}{word}");
2218 current_length = display_len(¤t_line, length_mode);
2219 current_line_element_spans.clear();
2220 } else {
2221 current_line.push_str(word);
2222 current_length += word_len;
2223 }
2224 } else {
2225 current_line.push_str(word);
2226 current_length += word_len;
2227 }
2228 } else if current_length > 0
2229 && current_length + 1 + word_len > options.line_length
2230 && !is_trailing_punct
2231 {
2232 lines.push(current_line.trim().to_string());
2234 current_line = word.to_string();
2235 current_length = word_len;
2236 current_line_element_spans.clear();
2237 } else {
2238 let add_space = current_length > 0 && if i == 0 { has_leading_space } else { !is_trailing_punct };
2248 if add_space {
2249 current_line.push(' ');
2250 current_length += 1;
2251 }
2252 current_line.push_str(word);
2253 current_length += word_len;
2254 }
2255 }
2256 } else if matches!(
2257 element,
2258 Element::Italic { .. } | Element::Bold { .. } | Element::Strikethrough { .. }
2259 ) && element_len > options.line_length
2260 {
2261 let (content, marker): (&str, &str) = match element {
2265 Element::Italic { content, underscore } => (content.as_str(), if *underscore { "_" } else { "*" }),
2266 Element::Bold { content, underscore } => (content.as_str(), if *underscore { "__" } else { "**" }),
2267 Element::Strikethrough { content, double } => (content.as_str(), if *double { "~~" } else { "~" }),
2268 _ => unreachable!(),
2269 };
2270
2271 let words: Vec<&str> = content.split_whitespace().collect();
2272 let n = words.len();
2273
2274 if n == 0 {
2275 let full = format!("{marker}{marker}");
2277 let full_len = display_len(&full, length_mode);
2278 if !is_adjacent_to_prev && current_length > 0 {
2279 current_line.push(' ');
2280 current_length += 1;
2281 }
2282 current_line.push_str(&full);
2283 current_length += full_len;
2284 } else {
2285 for (i, word) in words.iter().enumerate() {
2286 let is_first = i == 0;
2287 let is_last = i == n - 1;
2288 let word_str: String = match (is_first, is_last) {
2289 (true, true) => format!("{marker}{word}{marker}"),
2290 (true, false) => format!("{marker}{word}"),
2291 (false, true) => format!("{word}{marker}"),
2292 (false, false) => word.to_string(),
2293 };
2294 let word_len = display_len(&word_str, length_mode);
2295
2296 let needs_space = if is_first {
2297 !is_adjacent_to_prev && current_length > 0
2298 } else {
2299 current_length > 0
2300 };
2301
2302 if needs_space && current_length + 1 + word_len > options.line_length {
2303 lines.push(current_line.trim_end().to_string());
2304 current_line = word_str;
2305 current_length = word_len;
2306 current_line_element_spans.clear();
2307 } else {
2308 if needs_space {
2309 current_line.push(' ');
2310 current_length += 1;
2311 }
2312 current_line.push_str(&word_str);
2313 current_length += word_len;
2314 }
2315 }
2316 }
2317 } else {
2318 if is_adjacent_to_prev {
2322 if current_length + element_len > options.line_length {
2324 if let Some(last_space) = rfind_safe_space(¤t_line, ¤t_line_element_spans) {
2327 let before = current_line[..last_space].trim_end().to_string();
2328 let after = current_line[last_space + 1..].to_string();
2329 lines.push(before);
2330 current_line = format!("{after}{element_str}");
2331 current_length = display_len(¤t_line, length_mode);
2332 current_line_element_spans.clear();
2333 let start = after.len();
2335 current_line_element_spans.push((start, start + element_str.len()));
2336 } else {
2337 let start = current_line.len();
2339 current_line.push_str(&element_str);
2340 current_length += element_len;
2341 current_line_element_spans.push((start, current_line.len()));
2342 }
2343 } else {
2344 let start = current_line.len();
2345 current_line.push_str(&element_str);
2346 current_length += element_len;
2347 current_line_element_spans.push((start, current_line.len()));
2348 }
2349 } else if current_length > 0 && current_length + 1 + element_len > options.line_length {
2350 lines.push(current_line.trim().to_string());
2352 current_line.clone_from(&element_str);
2353 current_length = element_len;
2354 current_line_element_spans.clear();
2355 current_line_element_spans.push((0, element_str.len()));
2356 } else {
2357 let ends_with_opener =
2359 current_line.ends_with('(') || current_line.ends_with('[') || current_line.ends_with('{');
2360 if current_length > 0 && !ends_with_opener {
2361 current_line.push(' ');
2362 current_length += 1;
2363 }
2364 let start = current_line.len();
2365 current_line.push_str(&element_str);
2366 current_length += element_len;
2367 current_line_element_spans.push((start, current_line.len()));
2368 }
2369 }
2370 }
2371
2372 if !current_line.is_empty() {
2374 lines.push(current_line.trim_end().to_string());
2375 }
2376
2377 lines
2378}
2379
2380pub fn reflow_markdown(content: &str, options: &ReflowOptions) -> String {
2382 let lines: Vec<&str> = content.lines().collect();
2383 let mut result = Vec::new();
2384 let mut i = 0;
2385
2386 while i < lines.len() {
2387 let line = lines[i];
2388 let trimmed = line.trim();
2389
2390 if trimmed.is_empty() {
2392 result.push(String::new());
2393 i += 1;
2394 continue;
2395 }
2396
2397 if trimmed.starts_with('#') {
2399 result.push(line.to_string());
2400 i += 1;
2401 continue;
2402 }
2403
2404 if trimmed.starts_with(":::") {
2406 result.push(line.to_string());
2407 i += 1;
2408 continue;
2409 }
2410
2411 if trimmed.starts_with("```") || trimmed.starts_with("~~~") {
2413 result.push(line.to_string());
2414 i += 1;
2415 while i < lines.len() {
2417 result.push(lines[i].to_string());
2418 if lines[i].trim().starts_with("```") || lines[i].trim().starts_with("~~~") {
2419 i += 1;
2420 break;
2421 }
2422 i += 1;
2423 }
2424 continue;
2425 }
2426
2427 if calculate_indentation_width_default(line) >= 4 {
2429 result.push(line.to_string());
2431 i += 1;
2432 while i < lines.len() {
2433 let next_line = lines[i];
2434 if calculate_indentation_width_default(next_line) >= 4 || next_line.trim().is_empty() {
2436 result.push(next_line.to_string());
2437 i += 1;
2438 } else {
2439 break;
2440 }
2441 }
2442 continue;
2443 }
2444
2445 if trimmed.starts_with('>') {
2447 let gt_pos = line.find('>').expect("'>' must exist since trimmed.starts_with('>')");
2450 let quote_prefix = line[0..=gt_pos].to_string();
2451 let quote_content = &line[quote_prefix.len()..].trim_start();
2452
2453 let reflowed = reflow_line(quote_content, options);
2454 for reflowed_line in &reflowed {
2455 result.push(format!("{quote_prefix} {reflowed_line}"));
2456 }
2457 i += 1;
2458 continue;
2459 }
2460
2461 if is_horizontal_rule(trimmed) {
2463 result.push(line.to_string());
2464 i += 1;
2465 continue;
2466 }
2467
2468 if is_unordered_list_marker(trimmed) || is_numbered_list_item(trimmed) {
2470 let indent = line.len() - line.trim_start().len();
2472 let indent_str = " ".repeat(indent);
2473
2474 let mut marker_end = indent;
2477 let mut content_start = indent;
2478
2479 if trimmed.chars().next().is_some_and(char::is_numeric) {
2480 if let Some(period_pos) = line[indent..].find('.') {
2482 marker_end = indent + period_pos + 1; content_start = marker_end;
2484 while content_start < line.len() && line.as_bytes().get(content_start) == Some(&b' ') {
2488 content_start += 1;
2489 }
2490 }
2491 } else {
2492 marker_end = indent + 1; content_start = marker_end;
2495 while content_start < line.len() && line.as_bytes().get(content_start) == Some(&b' ') {
2499 content_start += 1;
2500 }
2501 }
2502
2503 let min_continuation_indent = content_start;
2505
2506 let rest = &line[content_start..];
2509 if rest.starts_with("[ ] ") || rest.starts_with("[x] ") || rest.starts_with("[X] ") {
2510 marker_end = content_start + 3; content_start += 4; }
2513
2514 let marker = &line[indent..marker_end];
2515
2516 let mut list_content = vec![trim_preserving_hard_break(&line[content_start..])];
2519 i += 1;
2520
2521 while i < lines.len() {
2525 let next_line = lines[i];
2526 let next_trimmed = next_line.trim();
2527
2528 if is_block_boundary(next_trimmed) {
2530 break;
2531 }
2532
2533 let next_indent = next_line.len() - next_line.trim_start().len();
2535 if next_indent >= min_continuation_indent {
2536 let trimmed_start = next_line.trim_start();
2539 list_content.push(trim_preserving_hard_break(trimmed_start));
2540 i += 1;
2541 } else {
2542 break;
2544 }
2545 }
2546
2547 let combined_content = if options.preserve_breaks {
2550 list_content[0].clone()
2551 } else {
2552 let has_hard_breaks = list_content.iter().any(|line| has_hard_break(line));
2554 if has_hard_breaks {
2555 list_content.join("\n")
2557 } else {
2558 list_content.join(" ")
2560 }
2561 };
2562
2563 let trimmed_marker = marker;
2565 let continuation_spaces = if let Some(max_indent) = options.max_list_continuation_indent {
2566 indent + (content_start - indent).min(max_indent)
2569 } else {
2570 content_start
2571 };
2572
2573 let prefix_length = indent + trimmed_marker.len() + 1;
2575
2576 let adjusted_options = ReflowOptions {
2578 line_length: options.line_length.saturating_sub(prefix_length),
2579 ..options.clone()
2580 };
2581
2582 let reflowed = reflow_line(&combined_content, &adjusted_options);
2583 for (j, reflowed_line) in reflowed.iter().enumerate() {
2584 if j == 0 {
2585 result.push(format!("{indent_str}{trimmed_marker} {reflowed_line}"));
2586 } else {
2587 let continuation_indent = " ".repeat(continuation_spaces);
2589 result.push(format!("{continuation_indent}{reflowed_line}"));
2590 }
2591 }
2592 continue;
2593 }
2594
2595 if crate::utils::table_utils::TableUtils::is_potential_table_row(line) {
2597 result.push(line.to_string());
2598 i += 1;
2599 continue;
2600 }
2601
2602 if trimmed.starts_with('[') && line.contains("]:") {
2604 result.push(line.to_string());
2605 i += 1;
2606 continue;
2607 }
2608
2609 if is_definition_list_item(trimmed) {
2611 result.push(line.to_string());
2612 i += 1;
2613 continue;
2614 }
2615
2616 let mut is_single_line_paragraph = true;
2618 if i + 1 < lines.len() {
2619 let next_trimmed = lines[i + 1].trim();
2620 if !is_block_boundary(next_trimmed) {
2622 is_single_line_paragraph = false;
2623 }
2624 }
2625
2626 if is_single_line_paragraph && display_len(line, options.length_mode) <= options.line_length {
2628 result.push(line.to_string());
2629 i += 1;
2630 continue;
2631 }
2632
2633 let mut paragraph_parts = Vec::new();
2635 let mut current_part = vec![line];
2636 i += 1;
2637
2638 if options.preserve_breaks {
2640 let hard_break_type = if line.strip_suffix('\r').unwrap_or(line).ends_with('\\') {
2642 Some("\\")
2643 } else if line.ends_with(" ") {
2644 Some(" ")
2645 } else {
2646 None
2647 };
2648 let reflowed = reflow_line(line, options);
2649
2650 if let Some(break_marker) = hard_break_type {
2652 if !reflowed.is_empty() {
2653 let mut reflowed_with_break = reflowed;
2654 let last_idx = reflowed_with_break.len() - 1;
2655 if !has_hard_break(&reflowed_with_break[last_idx]) {
2656 reflowed_with_break[last_idx].push_str(break_marker);
2657 }
2658 result.extend(reflowed_with_break);
2659 }
2660 } else {
2661 result.extend(reflowed);
2662 }
2663 } else {
2664 while i < lines.len() {
2666 let prev_line = if !current_part.is_empty() {
2667 current_part.last().unwrap()
2668 } else {
2669 ""
2670 };
2671 let next_line = lines[i];
2672 let next_trimmed = next_line.trim();
2673
2674 if is_block_boundary(next_trimmed) {
2676 break;
2677 }
2678
2679 let prev_trimmed = prev_line.trim();
2682 let abbreviations = get_abbreviations(&options.abbreviations);
2683 let ends_with_sentence = (prev_trimmed.ends_with('.')
2684 || prev_trimmed.ends_with('!')
2685 || prev_trimmed.ends_with('?')
2686 || prev_trimmed.ends_with(".*")
2687 || prev_trimmed.ends_with("!*")
2688 || prev_trimmed.ends_with("?*")
2689 || prev_trimmed.ends_with("._")
2690 || prev_trimmed.ends_with("!_")
2691 || prev_trimmed.ends_with("?_")
2692 || prev_trimmed.ends_with(".\"")
2694 || prev_trimmed.ends_with("!\"")
2695 || prev_trimmed.ends_with("?\"")
2696 || prev_trimmed.ends_with(".'")
2697 || prev_trimmed.ends_with("!'")
2698 || prev_trimmed.ends_with("?'")
2699 || prev_trimmed.ends_with(".\u{201D}")
2700 || prev_trimmed.ends_with("!\u{201D}")
2701 || prev_trimmed.ends_with("?\u{201D}")
2702 || prev_trimmed.ends_with(".\u{2019}")
2703 || prev_trimmed.ends_with("!\u{2019}")
2704 || prev_trimmed.ends_with("?\u{2019}"))
2705 && !text_ends_with_abbreviation(
2706 prev_trimmed.trim_end_matches(['*', '_', '"', '\'', '\u{201D}', '\u{2019}']),
2707 &abbreviations,
2708 );
2709
2710 if has_hard_break(prev_line) || (options.sentence_per_line && ends_with_sentence) {
2711 paragraph_parts.push(current_part.join(" "));
2713 current_part = vec![next_line];
2714 } else {
2715 current_part.push(next_line);
2716 }
2717 i += 1;
2718 }
2719
2720 if !current_part.is_empty() {
2722 if current_part.len() == 1 {
2723 paragraph_parts.push(current_part[0].to_string());
2725 } else {
2726 paragraph_parts.push(current_part.join(" "));
2727 }
2728 }
2729
2730 for (j, part) in paragraph_parts.iter().enumerate() {
2732 let reflowed = reflow_line(part, options);
2733 result.extend(reflowed);
2734
2735 if j < paragraph_parts.len() - 1 && !result.is_empty() && !options.sentence_per_line {
2739 let last_idx = result.len() - 1;
2740 if !has_hard_break(&result[last_idx]) {
2741 result[last_idx].push_str(" ");
2742 }
2743 }
2744 }
2745 }
2746 }
2747
2748 let result_text = result.join("\n");
2750 if content.ends_with('\n') && !result_text.ends_with('\n') {
2751 format!("{result_text}\n")
2752 } else {
2753 result_text
2754 }
2755}
2756
2757#[derive(Debug, Clone)]
2759pub struct ParagraphReflow {
2760 pub start_byte: usize,
2762 pub end_byte: usize,
2764 pub reflowed_text: String,
2766}
2767
2768#[derive(Debug, Clone)]
2774pub struct BlockquoteLineData {
2775 pub(crate) content: String,
2777 pub(crate) is_explicit: bool,
2779 pub(crate) prefix: Option<String>,
2781}
2782
2783impl BlockquoteLineData {
2784 pub fn explicit(content: String, prefix: String) -> Self {
2786 Self {
2787 content,
2788 is_explicit: true,
2789 prefix: Some(prefix),
2790 }
2791 }
2792
2793 pub fn lazy(content: String) -> Self {
2795 Self {
2796 content,
2797 is_explicit: false,
2798 prefix: None,
2799 }
2800 }
2801}
2802
2803#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2805pub enum BlockquoteContinuationStyle {
2806 Explicit,
2807 Lazy,
2808}
2809
2810pub fn blockquote_continuation_style(lines: &[BlockquoteLineData]) -> BlockquoteContinuationStyle {
2818 let mut explicit_count = 0usize;
2819 let mut lazy_count = 0usize;
2820
2821 for line in lines.iter().skip(1) {
2822 if line.is_explicit {
2823 explicit_count += 1;
2824 } else {
2825 lazy_count += 1;
2826 }
2827 }
2828
2829 if explicit_count > 0 && lazy_count == 0 {
2830 BlockquoteContinuationStyle::Explicit
2831 } else if lazy_count > 0 && explicit_count == 0 {
2832 BlockquoteContinuationStyle::Lazy
2833 } else if explicit_count >= lazy_count {
2834 BlockquoteContinuationStyle::Explicit
2835 } else {
2836 BlockquoteContinuationStyle::Lazy
2837 }
2838}
2839
2840pub fn dominant_blockquote_prefix(lines: &[BlockquoteLineData], fallback: &str) -> String {
2845 let mut counts: std::collections::HashMap<String, (usize, usize)> = std::collections::HashMap::new();
2846
2847 for (idx, line) in lines.iter().enumerate() {
2848 let Some(prefix) = line.prefix.as_ref() else {
2849 continue;
2850 };
2851 counts
2852 .entry(prefix.clone())
2853 .and_modify(|entry| entry.0 += 1)
2854 .or_insert((1, idx));
2855 }
2856
2857 counts
2858 .into_iter()
2859 .max_by(|(_, (count_a, first_idx_a)), (_, (count_b, first_idx_b))| {
2860 count_a.cmp(count_b).then_with(|| first_idx_b.cmp(first_idx_a))
2861 })
2862 .map_or_else(|| fallback.to_string(), |(prefix, _)| prefix)
2863}
2864
2865pub(crate) fn should_force_explicit_blockquote_line(content_line: &str) -> bool {
2870 let trimmed = content_line.trim_start();
2871 trimmed.starts_with('>')
2872 || trimmed.starts_with('#')
2873 || trimmed.starts_with("```")
2874 || trimmed.starts_with("~~~")
2875 || is_unordered_list_marker(trimmed)
2876 || is_numbered_list_item(trimmed)
2877 || is_horizontal_rule(trimmed)
2878 || is_definition_list_item(trimmed)
2879 || (trimmed.starts_with('[') && trimmed.contains("]:"))
2880 || trimmed.starts_with(":::")
2881 || (trimmed.starts_with('<')
2882 && !trimmed.starts_with("<http")
2883 && !trimmed.starts_with("<https")
2884 && !trimmed.starts_with("<mailto:"))
2885}
2886
2887pub fn reflow_blockquote_content(
2896 lines: &[BlockquoteLineData],
2897 explicit_prefix: &str,
2898 continuation_style: BlockquoteContinuationStyle,
2899 options: &ReflowOptions,
2900) -> Vec<String> {
2901 let content_strs: Vec<&str> = lines.iter().map(|l| l.content.as_str()).collect();
2902 let segments = split_into_segments_strs(&content_strs);
2903 let mut reflowed_content_lines: Vec<String> = Vec::new();
2904
2905 for segment in segments {
2906 let hard_break_type = segment.last().and_then(|&line| {
2907 let line = line.strip_suffix('\r').unwrap_or(line);
2908 if line.ends_with('\\') {
2909 Some("\\")
2910 } else if line.ends_with(" ") {
2911 Some(" ")
2912 } else {
2913 None
2914 }
2915 });
2916
2917 let pieces: Vec<&str> = segment
2918 .iter()
2919 .map(|&line| {
2920 if let Some(l) = line.strip_suffix('\\') {
2921 l.trim_end()
2922 } else if let Some(l) = line.strip_suffix(" ") {
2923 l.trim_end()
2924 } else {
2925 line.trim_end()
2926 }
2927 })
2928 .collect();
2929
2930 let segment_text = pieces.join(" ");
2931 let segment_text = segment_text.trim();
2932 if segment_text.is_empty() {
2933 continue;
2934 }
2935
2936 let mut reflowed = reflow_line(segment_text, options);
2937 if let Some(break_marker) = hard_break_type
2938 && !reflowed.is_empty()
2939 {
2940 let last_idx = reflowed.len() - 1;
2941 if !has_hard_break(&reflowed[last_idx]) {
2942 reflowed[last_idx].push_str(break_marker);
2943 }
2944 }
2945 reflowed_content_lines.extend(reflowed);
2946 }
2947
2948 let mut styled_lines: Vec<String> = Vec::new();
2949 for (idx, line) in reflowed_content_lines.iter().enumerate() {
2950 let force_explicit = idx == 0
2951 || continuation_style == BlockquoteContinuationStyle::Explicit
2952 || should_force_explicit_blockquote_line(line);
2953 if force_explicit {
2954 styled_lines.push(format!("{explicit_prefix}{line}"));
2955 } else {
2956 styled_lines.push(line.clone());
2957 }
2958 }
2959
2960 styled_lines
2961}
2962
2963fn is_blockquote_content_boundary(content: &str) -> bool {
2964 let trimmed = content.trim();
2965 trimmed.is_empty()
2966 || is_block_boundary(trimmed)
2967 || crate::utils::table_utils::TableUtils::is_potential_table_row(content)
2968 || trimmed.starts_with(":::")
2969 || crate::utils::is_template_directive_only(content)
2970 || is_standalone_attr_list(content)
2971 || is_snippet_block_delimiter(content)
2972}
2973
2974fn split_into_segments_strs<'a>(lines: &[&'a str]) -> Vec<Vec<&'a str>> {
2975 let mut segments = Vec::new();
2976 let mut current = Vec::new();
2977
2978 for &line in lines {
2979 current.push(line);
2980 if has_hard_break(line) {
2981 segments.push(current);
2982 current = Vec::new();
2983 }
2984 }
2985
2986 if !current.is_empty() {
2987 segments.push(current);
2988 }
2989
2990 segments
2991}
2992
2993fn reflow_blockquote_paragraph_at_line(
2994 content: &str,
2995 lines: &[&str],
2996 target_idx: usize,
2997 options: &ReflowOptions,
2998) -> Option<ParagraphReflow> {
2999 let mut anchor_idx = target_idx;
3000 let mut target_level = if let Some(parsed) = crate::utils::blockquote::parse_blockquote_prefix(lines[target_idx]) {
3001 parsed.nesting_level
3002 } else {
3003 let mut found = None;
3004 let mut idx = target_idx;
3005 loop {
3006 if lines[idx].trim().is_empty() {
3007 break;
3008 }
3009 if let Some(parsed) = crate::utils::blockquote::parse_blockquote_prefix(lines[idx]) {
3010 found = Some((idx, parsed.nesting_level));
3011 break;
3012 }
3013 if idx == 0 {
3014 break;
3015 }
3016 idx -= 1;
3017 }
3018 let (idx, level) = found?;
3019 anchor_idx = idx;
3020 level
3021 };
3022
3023 let mut para_start = anchor_idx;
3025 while para_start > 0 {
3026 let prev_idx = para_start - 1;
3027 let prev_line = lines[prev_idx];
3028
3029 if prev_line.trim().is_empty() {
3030 break;
3031 }
3032
3033 if let Some(parsed) = crate::utils::blockquote::parse_blockquote_prefix(prev_line) {
3034 if parsed.nesting_level != target_level || is_blockquote_content_boundary(parsed.content) {
3035 break;
3036 }
3037 para_start = prev_idx;
3038 continue;
3039 }
3040
3041 let prev_lazy = prev_line.trim_start();
3042 if is_blockquote_content_boundary(prev_lazy) {
3043 break;
3044 }
3045 para_start = prev_idx;
3046 }
3047
3048 while para_start < lines.len() {
3050 let Some(parsed) = crate::utils::blockquote::parse_blockquote_prefix(lines[para_start]) else {
3051 para_start += 1;
3052 continue;
3053 };
3054 target_level = parsed.nesting_level;
3055 break;
3056 }
3057
3058 if para_start >= lines.len() || para_start > target_idx {
3059 return None;
3060 }
3061
3062 let mut collected: Vec<(usize, BlockquoteLineData)> = Vec::new();
3065 let mut idx = para_start;
3066 while idx < lines.len() {
3067 if !collected.is_empty() && has_hard_break(&collected[collected.len() - 1].1.content) {
3068 break;
3069 }
3070
3071 let line = lines[idx];
3072 if line.trim().is_empty() {
3073 break;
3074 }
3075
3076 if let Some(parsed) = crate::utils::blockquote::parse_blockquote_prefix(line) {
3077 if parsed.nesting_level != target_level || is_blockquote_content_boundary(parsed.content) {
3078 break;
3079 }
3080 collected.push((
3081 idx,
3082 BlockquoteLineData::explicit(trim_preserving_hard_break(parsed.content), parsed.prefix.to_string()),
3083 ));
3084 idx += 1;
3085 continue;
3086 }
3087
3088 let lazy_content = line.trim_start();
3089 if is_blockquote_content_boundary(lazy_content) {
3090 break;
3091 }
3092
3093 collected.push((idx, BlockquoteLineData::lazy(trim_preserving_hard_break(lazy_content))));
3094 idx += 1;
3095 }
3096
3097 if collected.is_empty() {
3098 return None;
3099 }
3100
3101 let para_end = collected[collected.len() - 1].0;
3102 if target_idx < para_start || target_idx > para_end {
3103 return None;
3104 }
3105
3106 let line_data: Vec<BlockquoteLineData> = collected.iter().map(|(_, d)| d.clone()).collect();
3107
3108 let fallback_prefix = line_data
3109 .iter()
3110 .find_map(|d| d.prefix.clone())
3111 .unwrap_or_else(|| "> ".to_string());
3112 let explicit_prefix = dominant_blockquote_prefix(&line_data, &fallback_prefix);
3113 let continuation_style = blockquote_continuation_style(&line_data);
3114
3115 let adjusted_line_length = options
3116 .line_length
3117 .saturating_sub(display_len(&explicit_prefix, options.length_mode))
3118 .max(1);
3119
3120 let adjusted_options = ReflowOptions {
3121 line_length: adjusted_line_length,
3122 ..options.clone()
3123 };
3124
3125 let styled_lines = reflow_blockquote_content(&line_data, &explicit_prefix, continuation_style, &adjusted_options);
3126
3127 if styled_lines.is_empty() {
3128 return None;
3129 }
3130
3131 let mut start_byte = 0;
3133 for line in lines.iter().take(para_start) {
3134 start_byte += line.len() + 1;
3135 }
3136
3137 let mut end_byte = start_byte;
3138 for line in lines.iter().take(para_end + 1).skip(para_start) {
3139 end_byte += line.len() + 1;
3140 }
3141
3142 let includes_trailing_newline = para_end != lines.len() - 1 || content.ends_with('\n');
3143 if !includes_trailing_newline {
3144 end_byte -= 1;
3145 }
3146
3147 let reflowed_joined = styled_lines.join("\n");
3148 let reflowed_text = if includes_trailing_newline {
3149 if reflowed_joined.ends_with('\n') {
3150 reflowed_joined
3151 } else {
3152 format!("{reflowed_joined}\n")
3153 }
3154 } else if reflowed_joined.ends_with('\n') {
3155 reflowed_joined.trim_end_matches('\n').to_string()
3156 } else {
3157 reflowed_joined
3158 };
3159
3160 Some(ParagraphReflow {
3161 start_byte,
3162 end_byte,
3163 reflowed_text,
3164 })
3165}
3166
3167pub fn reflow_paragraph_at_line(content: &str, line_number: usize, line_length: usize) -> Option<ParagraphReflow> {
3185 reflow_paragraph_at_line_with_mode(content, line_number, line_length, ReflowLengthMode::default())
3186}
3187
3188pub fn reflow_paragraph_at_line_with_mode(
3190 content: &str,
3191 line_number: usize,
3192 line_length: usize,
3193 length_mode: ReflowLengthMode,
3194) -> Option<ParagraphReflow> {
3195 let options = ReflowOptions {
3196 line_length,
3197 length_mode,
3198 ..Default::default()
3199 };
3200 reflow_paragraph_at_line_with_options(content, line_number, &options)
3201}
3202
3203pub fn reflow_paragraph_at_line_with_options(
3214 content: &str,
3215 line_number: usize,
3216 options: &ReflowOptions,
3217) -> Option<ParagraphReflow> {
3218 if line_number == 0 {
3219 return None;
3220 }
3221
3222 let lines: Vec<&str> = content.lines().collect();
3223
3224 if line_number > lines.len() {
3226 return None;
3227 }
3228
3229 let target_idx = line_number - 1; let target_line = lines[target_idx];
3231 let trimmed = target_line.trim();
3232
3233 if let Some(blockquote_reflow) = reflow_blockquote_paragraph_at_line(content, &lines, target_idx, options) {
3236 return Some(blockquote_reflow);
3237 }
3238
3239 if is_paragraph_boundary(trimmed, target_line) {
3241 return None;
3242 }
3243
3244 let mut para_start = target_idx;
3246 while para_start > 0 {
3247 let prev_idx = para_start - 1;
3248 let prev_line = lines[prev_idx];
3249 let prev_trimmed = prev_line.trim();
3250
3251 if is_paragraph_boundary(prev_trimmed, prev_line) {
3253 break;
3254 }
3255
3256 para_start = prev_idx;
3257 }
3258
3259 let mut para_end = target_idx;
3261 while para_end + 1 < lines.len() {
3262 let next_idx = para_end + 1;
3263 let next_line = lines[next_idx];
3264 let next_trimmed = next_line.trim();
3265
3266 if is_paragraph_boundary(next_trimmed, next_line) {
3268 break;
3269 }
3270
3271 para_end = next_idx;
3272 }
3273
3274 let paragraph_lines = &lines[para_start..=para_end];
3276
3277 let mut start_byte = 0;
3279 for line in lines.iter().take(para_start) {
3280 start_byte += line.len() + 1; }
3282
3283 let mut end_byte = start_byte;
3284 for line in paragraph_lines {
3285 end_byte += line.len() + 1; }
3287
3288 let includes_trailing_newline = para_end != lines.len() - 1 || content.ends_with('\n');
3291
3292 if !includes_trailing_newline {
3294 end_byte -= 1;
3295 }
3296
3297 let paragraph_text = paragraph_lines.join("\n");
3299
3300 let reflowed = reflow_markdown(¶graph_text, options);
3302
3303 let reflowed_text = if includes_trailing_newline {
3307 if reflowed.ends_with('\n') {
3309 reflowed
3310 } else {
3311 format!("{reflowed}\n")
3312 }
3313 } else {
3314 if reflowed.ends_with('\n') {
3316 reflowed.trim_end_matches('\n').to_string()
3317 } else {
3318 reflowed
3319 }
3320 };
3321
3322 Some(ParagraphReflow {
3323 start_byte,
3324 end_byte,
3325 reflowed_text,
3326 })
3327}
3328
3329#[cfg(test)]
3330mod tests {
3331 use super::*;
3332
3333 #[test]
3338 fn test_helper_function_text_ends_with_abbreviation() {
3339 let abbreviations = get_abbreviations(&None);
3341
3342 assert!(text_ends_with_abbreviation("Dr.", &abbreviations));
3344 assert!(text_ends_with_abbreviation("word Dr.", &abbreviations));
3345 assert!(text_ends_with_abbreviation("e.g.", &abbreviations));
3346 assert!(text_ends_with_abbreviation("i.e.", &abbreviations));
3347 assert!(text_ends_with_abbreviation("Mr.", &abbreviations));
3348 assert!(text_ends_with_abbreviation("Mrs.", &abbreviations));
3349 assert!(text_ends_with_abbreviation("Ms.", &abbreviations));
3350 assert!(text_ends_with_abbreviation("Prof.", &abbreviations));
3351
3352 assert!(!text_ends_with_abbreviation("etc.", &abbreviations));
3354 assert!(!text_ends_with_abbreviation("paradigms.", &abbreviations));
3355 assert!(!text_ends_with_abbreviation("programs.", &abbreviations));
3356 assert!(!text_ends_with_abbreviation("items.", &abbreviations));
3357 assert!(!text_ends_with_abbreviation("systems.", &abbreviations));
3358 assert!(!text_ends_with_abbreviation("Dr?", &abbreviations)); assert!(!text_ends_with_abbreviation("Mr!", &abbreviations)); assert!(!text_ends_with_abbreviation("paradigms?", &abbreviations)); assert!(!text_ends_with_abbreviation("word", &abbreviations)); assert!(!text_ends_with_abbreviation("", &abbreviations)); }
3364
3365 #[test]
3366 fn test_is_unordered_list_marker() {
3367 assert!(is_unordered_list_marker("- item"));
3369 assert!(is_unordered_list_marker("* item"));
3370 assert!(is_unordered_list_marker("+ item"));
3371 assert!(is_unordered_list_marker("-")); assert!(is_unordered_list_marker("*"));
3373 assert!(is_unordered_list_marker("+"));
3374
3375 assert!(!is_unordered_list_marker("---")); assert!(!is_unordered_list_marker("***")); assert!(!is_unordered_list_marker("- - -")); assert!(!is_unordered_list_marker("* * *")); assert!(!is_unordered_list_marker("*emphasis*")); assert!(!is_unordered_list_marker("-word")); assert!(!is_unordered_list_marker("")); assert!(!is_unordered_list_marker("text")); assert!(!is_unordered_list_marker("# heading")); }
3386
3387 #[test]
3388 fn test_is_block_boundary() {
3389 assert!(is_block_boundary("")); assert!(is_block_boundary("# Heading")); assert!(is_block_boundary("## Level 2")); assert!(is_block_boundary("```rust")); assert!(is_block_boundary("~~~")); assert!(is_block_boundary("> quote")); assert!(is_block_boundary("| cell |")); assert!(is_block_boundary("[link]: http://example.com")); assert!(is_block_boundary("---")); assert!(is_block_boundary("***")); assert!(is_block_boundary("- item")); assert!(is_block_boundary("* item")); assert!(is_block_boundary("+ item")); assert!(is_block_boundary("1. item")); assert!(is_block_boundary("10. item")); assert!(is_block_boundary(": definition")); assert!(is_block_boundary(":::")); assert!(is_block_boundary("::::: {.callout-note}")); assert!(!is_block_boundary("regular text"));
3411 assert!(!is_block_boundary("*emphasis*")); assert!(!is_block_boundary("[link](url)")); assert!(!is_block_boundary("some words here"));
3414 }
3415
3416 #[test]
3417 fn test_definition_list_boundary_in_single_line_paragraph() {
3418 let options = ReflowOptions {
3421 line_length: 80,
3422 ..Default::default()
3423 };
3424 let input = "Term\n: Definition of the term";
3425 let result = reflow_markdown(input, &options);
3426 assert!(
3428 result.contains(": Definition"),
3429 "Definition list item should not be merged into previous line. Got: {result:?}"
3430 );
3431 let lines: Vec<&str> = result.lines().collect();
3432 assert_eq!(lines.len(), 2, "Should remain two separate lines. Got: {lines:?}");
3433 assert_eq!(lines[0], "Term");
3434 assert_eq!(lines[1], ": Definition of the term");
3435 }
3436
3437 #[test]
3438 fn test_is_paragraph_boundary() {
3439 assert!(is_paragraph_boundary("# Heading", "# Heading"));
3441 assert!(is_paragraph_boundary("- item", "- item"));
3442 assert!(is_paragraph_boundary(":::", ":::"));
3443 assert!(is_paragraph_boundary(": definition", ": definition"));
3444
3445 assert!(is_paragraph_boundary("code", " code"));
3447 assert!(is_paragraph_boundary("code", "\tcode"));
3448
3449 assert!(is_paragraph_boundary("| a | b |", "| a | b |"));
3451 assert!(is_paragraph_boundary("a | b", "a | b")); assert!(!is_paragraph_boundary("regular text", "regular text"));
3455 assert!(!is_paragraph_boundary("text", " text")); }
3457
3458 #[test]
3459 fn test_div_marker_boundary_in_reflow_paragraph_at_line() {
3460 let content = "Some paragraph text here.\n\n::: {.callout-note}\nThis is a callout.\n:::\n";
3463 let result = reflow_paragraph_at_line(content, 3, 80);
3465 assert!(result.is_none(), "Div marker line should not be reflowed");
3466 }
3467}