1use std::fmt::{self, Display, Formatter, Write};
2
3use crate::OutlineStructure;
4
5const EXTRA_INDENT_WIDTH: usize = 1;
6
7type StackEntry = (Option<u8>, Outline);
9
10#[derive(Debug, Clone, Default)]
12pub struct Outline {
13 pub text: Option<String>,
15 pub sub_outlines: Vec<Outline>,
17}
18
19impl Outline {
20 #[inline]
22 pub fn parse_html<S: AsRef<str>>(html: S, max_depth: usize) -> Outline {
23 OutlineStructure::parse_html(html, max_depth).into()
24 }
25}
26
27impl From<OutlineStructure> for Outline {
29 #[inline]
30 fn from(os: OutlineStructure) -> Self {
31 if os.sectioning_type.is_heading() {
32 Outline {
33 text: os.heading.map(|heading| heading.into()),
34 sub_outlines: Vec::new(),
35 }
36 } else {
37 let mut sub_outlines = Vec::new();
38
39 let mut stack: Vec<StackEntry> = vec![];
40
41 for sub_os in os.sub_outline_structures.into_iter().rev() {
42 if sub_os.sectioning_type.is_heading() {
43 let heading = sub_os.heading.unwrap();
44 let heading_level = heading.get_start_level();
45 let heading_text = heading.into();
46
47 push_heading_outline(&mut stack, heading_level, heading_text);
48 } else {
49 stack.push((None, sub_os.into()));
50 }
51 }
52
53 let text = if let Some(heading) = os.heading {
54 let heading_level = heading.get_start_level();
55 let heading_text = heading.into();
56
57 let need_flatten = stack
58 .iter()
59 .any(|(level, _)| level.is_some_and(|level| level >= heading_level));
60
61 if need_flatten {
62 push_heading_outline(&mut stack, heading_level, heading_text);
63
64 None
65 } else {
66 Some(heading_text)
67 }
68 } else if os.sectioning_type.is_sectioning_content_type() {
69 Some(format!("Untitled {}", os.sectioning_type.as_str()))
70 } else {
71 None
72 };
73
74 while let Some(sub_os) = stack.pop() {
75 sub_outlines.push(sub_os.1);
76 }
77
78 Outline {
79 text,
80 sub_outlines,
81 }
82 }
83 }
84}
85
86fn push_heading_outline(stack: &mut Vec<StackEntry>, heading_level: u8, heading_text: String) {
87 let mut outline = Outline {
88 text: Some(heading_text), sub_outlines: Vec::new()
89 };
90
91 while let Some((level, _)) = stack.last() {
93 if level.is_some_and(|level| level <= heading_level) {
94 break;
95 }
96
97 outline.sub_outlines.push(stack.pop().unwrap().1);
98 }
99
100 stack.push((Some(heading_level), outline));
101}
102
103impl Display for Outline {
104 #[inline]
105 fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), fmt::Error> {
106 format(f, self, 1, 0)
107 }
108}
109
110fn format(
111 f: &mut Formatter<'_>,
112 outline: &Outline,
113 number: usize,
114 indent: usize,
115) -> Result<(), fmt::Error> {
116 let new_ident = if let Some(text) = outline.text.as_ref() {
117 if indent > 0 {
118 f.write_char('\n')?;
119
120 for _ in 0..indent {
121 f.write_char(' ')?;
122 }
123 } else if number > 1 {
124 f.write_char('\n')?;
125 }
126
127 f.write_fmt(format_args!("{}. ", number))?;
128 f.write_str(text.as_str())?;
129
130 indent + count_digit(outline.sub_outlines.len()) + 2 + EXTRA_INDENT_WIDTH
131 } else {
132 indent
133 };
134
135 for (i, sub_outline) in outline.sub_outlines.iter().enumerate() {
136 format(f, sub_outline, i + 1, new_ident)?;
137 }
138
139 Ok(())
140}
141
142#[inline]
143fn count_digit(n: usize) -> usize {
144 n.checked_ilog10().unwrap_or(0) as usize + 1
146}