Skip to main content

html_outliner/
outline_structure.rs

1use scraper::{ElementRef, Html};
2
3use crate::{heading::*, sectioning_type::SectioningType};
4
5const SECTIONING_ROOTS: [&str; 7] =
6    ["blockquote", "body", "details", "dialog", "fieldset", "figure", "td"];
7
8/// A compact intermediate outline tree built from the parsed HTML tree.
9#[derive(Debug, Clone)]
10pub struct OutlineStructure {
11    /// The kind of HTML structure that created this node.
12    pub sectioning_type:        SectioningType,
13    /// The heading that names this node when one was found.
14    pub heading:                Option<Heading>,
15    /// The nested outline structures found inside this node.
16    pub sub_outline_structures: Vec<OutlineStructure>,
17}
18
19impl OutlineStructure {
20    #[inline]
21    pub(crate) fn new(sectioning_type: SectioningType) -> OutlineStructure {
22        OutlineStructure {
23            sectioning_type,
24            heading: None,
25            sub_outline_structures: Vec::new(),
26        }
27    }
28
29    #[inline]
30    /// Parses HTML and builds the intermediate outline tree.
31    pub fn parse_html<S: AsRef<str>>(html: S, max_depth: usize) -> OutlineStructure {
32        let document = Html::parse_document(html.as_ref());
33        let root_element = document.root_element();
34
35        if let Some(outline_structure) =
36            create_outline_structure_finding_body(root_element, 0, max_depth)
37        {
38            outline_structure
39        } else {
40            create_outline_structure(SectioningType::Root, root_element, 0, max_depth)
41                .unwrap_or_else(|| OutlineStructure::new(SectioningType::Root))
42        }
43    }
44}
45
46pub(crate) fn create_outline_structure(
47    sectioning_type: SectioningType,
48    element: ElementRef<'_>,
49    depth: usize,
50    max_depth: usize,
51) -> Option<OutlineStructure> {
52    if depth > max_depth {
53        return None;
54    }
55
56    let mut outline_structure = OutlineStructure::new(sectioning_type);
57
58    // The first heading names the current section, and later headings become child outline items.
59    let mut find_heading = true;
60
61    for child in element.child_elements() {
62        let local_name = child.value().name();
63
64        // Inner sectioning roots keep their headings out of the current outline.
65        if SECTIONING_ROOTS.binary_search(&local_name).is_ok() {
66            continue;
67        }
68
69        if let Some(sub_sectioning_type) = SectioningType::from_sectioning_content_tag(local_name) {
70            if let Some(sub_outline_structure) =
71                create_outline_structure(sub_sectioning_type, child, depth + 1, max_depth)
72            {
73                outline_structure.sub_outline_structures.push(sub_outline_structure);
74            }
75        } else if let Some(heading) = create_heading(child, depth + 1, max_depth) {
76            if find_heading {
77                outline_structure.heading = Some(heading);
78            } else {
79                let mut sub_outline_structure = OutlineStructure::new(SectioningType::Heading);
80
81                sub_outline_structure.heading = Some(heading);
82
83                outline_structure.sub_outline_structures.push(sub_outline_structure);
84            }
85        } else {
86            if let Some(sub_outline_structure) =
87                create_outline_structure(SectioningType::Root, child, depth + 1, max_depth)
88            {
89                // A normal wrapper can contain the first heading, and that must still close the first-heading phase.
90                let has_sub_outline_structures =
91                    !sub_outline_structure.sub_outline_structures.is_empty();
92                let has_heading = sub_outline_structure.heading.is_some();
93
94                if let Some(heading) = sub_outline_structure.heading {
95                    if find_heading {
96                        outline_structure.heading = Some(heading);
97                    } else {
98                        let mut sub_outline_structure =
99                            OutlineStructure::new(SectioningType::Heading);
100
101                        sub_outline_structure.heading = Some(heading);
102
103                        outline_structure.sub_outline_structures.push(sub_outline_structure);
104                    }
105                }
106
107                for os in sub_outline_structure.sub_outline_structures {
108                    outline_structure.sub_outline_structures.push(os);
109                }
110
111                if has_heading || has_sub_outline_structures {
112                    find_heading = false;
113                }
114            }
115
116            continue;
117        }
118
119        find_heading = false;
120    }
121
122    Some(outline_structure)
123}
124
125pub(crate) fn create_outline_structure_finding_body(
126    element: ElementRef<'_>,
127    depth: usize,
128    max_depth: usize,
129) -> Option<OutlineStructure> {
130    if depth > max_depth {
131        return None;
132    }
133
134    if element.value().name() == "body" {
135        return Some(
136            create_outline_structure(SectioningType::Body, element, depth, max_depth)
137                .unwrap_or_else(|| OutlineStructure::new(SectioningType::Root)),
138        );
139    }
140
141    for child in element.child_elements() {
142        if let Some(outline_structure) =
143            create_outline_structure_finding_body(child, depth + 1, max_depth)
144        {
145            return Some(outline_structure);
146        }
147    }
148
149    None
150}