1use scraper::{ElementRef, Node};
2
3#[derive(Debug, Clone)]
5pub struct Heading(HeadingKind);
6
7#[derive(Debug, Clone)]
9enum HeadingKind {
10 Header { level: u8, text: String },
12 Group(Vec<Heading>),
14}
15
16impl Heading {
17 #[inline]
19 pub fn get_end_level(&self) -> u8 {
20 match &self.0 {
21 HeadingKind::Header {
22 level, ..
23 } => *level,
24 HeadingKind::Group(headings) => headings[headings.len() - 1].get_end_level(),
25 }
26 }
27
28 #[inline]
30 pub fn get_start_level(&self) -> u8 {
31 match &self.0 {
32 HeadingKind::Header {
33 level, ..
34 } => *level,
35 HeadingKind::Group(headings) => headings[0].get_start_level(),
36 }
37 }
38}
39
40pub(crate) fn create_heading(
41 element: ElementRef<'_>,
42 depth: usize,
43 max_depth: usize,
44) -> Option<Heading> {
45 if depth > max_depth {
46 return None;
47 }
48
49 let local_name = element.value().name();
50 let local_name_length = local_name.len();
51
52 let mut kind = match local_name_length {
53 2 => {
54 let stripped_local_name = local_name.strip_prefix('h')?;
55
56 match stripped_local_name.parse::<u8>() {
57 Ok(level) if (1..=6).contains(&level) => HeadingKind::Header {
58 level,
59 text: String::new(),
60 },
61 _ => return None,
62 }
63 },
64 6 if local_name.eq("hgroup") => HeadingKind::Group(Vec::with_capacity(2)),
65 _ => return None,
66 };
67
68 match &mut kind {
69 HeadingKind::Header {
70 text, ..
71 } => {
72 *text = create_text(element, depth, max_depth);
73 },
74 HeadingKind::Group(headings) => {
75 for child in element.child_elements() {
77 if let Some(heading) = create_heading(child, depth + 1, max_depth) {
78 headings.push(heading);
79 }
80 }
81
82 if headings.is_empty() {
83 return None;
84 }
85 },
86 }
87
88 Some(Heading(kind))
89}
90
91#[inline]
92pub(crate) fn create_text(element: ElementRef<'_>, depth: usize, max_depth: usize) -> String {
93 if depth > max_depth {
94 return String::new();
95 }
96
97 let mut raw_text = String::new();
98 let mut stack = Vec::new();
99
100 for child in element.children().rev() {
102 stack.push((depth + 1, child));
103 }
104
105 while let Some((node_depth, node)) = stack.pop() {
106 if node_depth > max_depth {
107 continue;
108 }
109
110 match node.value() {
111 Node::Text(text) => raw_text.push_str(text),
112 Node::Document | Node::Fragment | Node::Element(_) => {
113 for child in node.children().rev() {
114 stack.push((node_depth + 1, child));
115 }
116 },
117 _ => {},
118 }
119 }
120
121 collapse_whitespace(&raw_text)
122}
123
124impl From<Heading> for String {
125 #[inline]
126 fn from(heading: Heading) -> String {
127 match heading.0 {
128 HeadingKind::Header {
129 text, ..
130 } => text,
131 HeadingKind::Group(headings) => {
132 let mut iter = headings.into_iter();
133
134 let mut text: String = iter.next().unwrap().into();
135
136 for heading in iter {
137 text.push_str(" — ");
138
139 let t: String = heading.into();
140 text.push_str(&t);
141 }
142
143 text
144 },
145 }
146 }
147}
148
149fn collapse_whitespace(raw_text: &str) -> String {
150 let mut text = String::new();
151
152 for word in raw_text.split_whitespace() {
153 if !text.is_empty() {
154 text.push(' ');
155 }
156
157 text.push_str(word);
158 }
159
160 text
161}