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 if let Some(stripped_local_name) = local_name.strip_prefix('h') {
55 match stripped_local_name.parse::<u8>() {
56 Ok(level) if (1..=6).contains(&level) => HeadingKind::Header {
57 level,
58 text: String::new(),
59 },
60 _ => return None,
61 }
62 } else {
63 return None;
64 }
65 },
66 6 if local_name.eq("hgroup") => HeadingKind::Group(Vec::with_capacity(2)),
67 _ => return None,
68 };
69
70 match &mut kind {
71 HeadingKind::Header {
72 text, ..
73 } => {
74 *text = create_text(element, depth, max_depth);
75 },
76 HeadingKind::Group(headings) => {
77 for child in element.child_elements() {
79 if let Some(heading) = create_heading(child, depth + 1, max_depth) {
80 headings.push(heading);
81 }
82 }
83
84 if headings.is_empty() {
85 return None;
86 }
87 },
88 }
89
90 Some(Heading(kind))
91}
92
93#[inline]
94pub(crate) fn create_text(element: ElementRef<'_>, depth: usize, max_depth: usize) -> String {
95 if depth > max_depth {
96 return String::new();
97 }
98
99 let mut raw_text = String::new();
100 let mut stack = Vec::new();
101
102 for child in element.children().rev() {
104 stack.push((depth + 1, child));
105 }
106
107 while let Some((node_depth, node)) = stack.pop() {
108 if node_depth > max_depth {
109 continue;
110 }
111
112 match node.value() {
113 Node::Text(text) => raw_text.push_str(text),
114 Node::Document | Node::Fragment | Node::Element(_) => {
115 for child in node.children().rev() {
116 stack.push((node_depth + 1, child));
117 }
118 },
119 _ => {},
120 }
121 }
122
123 collapse_whitespace(&raw_text)
124}
125
126impl From<Heading> for String {
127 #[inline]
128 fn from(heading: Heading) -> String {
129 match heading.0 {
130 HeadingKind::Header {
131 text, ..
132 } => text,
133 HeadingKind::Group(headings) => {
134 let mut iter = headings.into_iter();
135
136 let mut text: String = iter.next().unwrap().into();
137
138 for heading in iter {
139 text.push_str(" — ");
140
141 let t: String = heading.into();
142 text.push_str(&t);
143 }
144
145 text
146 },
147 }
148 }
149}
150
151fn collapse_whitespace(raw_text: &str) -> String {
152 let mut text = String::new();
153
154 for word in raw_text.split_whitespace() {
155 if !text.is_empty() {
156 text.push(' ');
157 }
158
159 text.push_str(word);
160 }
161
162 text
163}