hsml/parser/tag/
node.rs

1use nom::{
2    bytes::complete::{take_till, take_till1},
3    error::{Error, ErrorKind},
4    IResult,
5};
6
7use crate::parser::{
8    attribute,
9    class::node::{class_node, ClassNode},
10    comment::node::{comment_dev_node, comment_native_node},
11    id::{self, node::IdNode},
12    tag::process::process_tag,
13    text::{self, node::TextNode},
14    HsmlNode, HsmlProcessContext,
15};
16
17#[derive(Debug, PartialEq)]
18pub struct TagNode {
19    pub tag: String,
20    pub id: Option<IdNode>,
21    pub classes: Option<Vec<ClassNode>>,
22    pub attributes: Option<Vec<HsmlNode>>,
23    pub text: Option<TextNode>,
24    pub children: Option<Vec<HsmlNode>>,
25}
26
27pub fn tag_node<'a>(input: &'a str, context: &mut HsmlProcessContext) -> IResult<&'a str, TagNode> {
28    // tag node starts with a tag name or a dot/hash
29    // if it starts with a dot/hash, the tag name is div
30
31    let (mut input, tag_name) = if input.starts_with('.') || input.starts_with('#') {
32        (input, "div")
33    } else {
34        process_tag(input)?
35    };
36
37    // if the next char is a dot, we have a id node
38    // if the next char is a dot, we have a class node
39    // collect id and class nodes until we hit a whitespace, newline, start of attributes or single dot without trailing alphabetical char
40
41    let mut id_node: Option<IdNode> = None;
42    let mut class_nodes: Vec<ClassNode> = vec![];
43    let mut attribute_nodes: Option<Vec<HsmlNode>> = None;
44    let mut text_node: Option<TextNode> = None;
45    let mut child_nodes: Vec<HsmlNode> = vec![];
46
47    loop {
48        let first_char = input.get(..1);
49        let first_two_chars = input.get(..2);
50
51        if first_char == Some("#") {
52            // we hit an id node
53
54            // if there was already an id node, throw an error
55            if id_node.is_some() {
56                // TODO @Shinigami92 2023-05-25: This error could be more specific
57                // Duplicate attribute "id" is not allowed.
58                return Err(nom::Err::Failure(Error::new(input, ErrorKind::Tag)));
59            }
60
61            let (rest, node) = id::node::id_node(input)?;
62            id_node = Some(node);
63            input = rest;
64
65            continue;
66        }
67
68        if first_char == Some(".") {
69            // TODO @Shinigami92 2023-05-18: Maybe we want to support piped text. That would start with a `.\n`
70
71            // we hit a class node
72            let (rest, node) = class_node(input)?;
73            class_nodes.push(node);
74            input = rest;
75
76            continue;
77        }
78
79        if first_char == Some("(") {
80            // we hit the start of attributes
81
82            let (rest, nodes) = attribute::node::attribute_nodes(input, context)?;
83            attribute_nodes = Some(nodes);
84            input = rest;
85
86            continue;
87        }
88
89        if first_char == Some(" ") {
90            // we hit a whitespace and there should be text
91
92            let (rest, node) = text::node::text_node(input)?;
93            text_node = Some(node);
94            input = rest;
95
96            // TODO @Shinigami92 2023-05-22: Theoretically here could also follow a comment
97
98            // there could be child tag nodes, but this will be handled in the next loop iteration by the line ending check
99
100            continue;
101        }
102
103        if first_char == Some("\n") || first_two_chars == Some("\r\n") {
104            // we hit a newline and the tag ended but could have child tag nodes
105
106            // check indentation
107            let (rest, _) = take_till1(|c| c != '\r' && c != '\n')(input)?;
108
109            // check if the next char is a tab or whitespace
110            // if yes, check for indentation level
111            // if no, we have no child tag nodes and can break the loop
112
113            let (remaining, indentation) = take_till(|c: char| !c.is_whitespace())(rest)?;
114
115            if !indentation.is_empty() {
116                // check that the indentation is consistent and does not include tabs and spaces at the same time
117                // if it does, throw an error
118
119                if indentation.contains('\t') && indentation.contains(' ') {
120                    // TODO @Shinigami92 2023-05-18: This error could be more specific
121                    return Err(nom::Err::Error(Error::new(input, ErrorKind::Tag)));
122                }
123
124                // if we never hit an indentation yet, set it
125                // this only happens once
126                if context.indent_string.is_none() {
127                    // println!("set indent string = \"{}\"", indentation);
128                    context.indent_string = Some(indentation.to_string());
129                }
130
131                // persist the indentation level so we can restore it later
132                let indentation_level = context.indent_level;
133
134                context.indent_level += 1;
135
136                // check that we are at the correct indentation level, otherwise break out of the loop
137                let indent_string_len = context.indent_string.as_ref().unwrap().len();
138                let indent_size = indent_string_len * context.indent_level;
139                // dbg!(indent_size, indentation.len());
140                if indent_size != indentation.len() {
141                    // dbg!("break out of loop");
142                    break;
143                }
144
145                // we are at the correct indentation level, so we can continue parsing the child tag nodes
146
147                // there could be a comment (dev or native) node
148                if let Ok((rest, node)) = comment_native_node(remaining) {
149                    child_nodes.push(HsmlNode::Comment(node));
150                    input = rest;
151                } else if let Ok((rest, node)) = comment_dev_node(remaining) {
152                    child_nodes.push(HsmlNode::Comment(node));
153                    input = rest;
154                }
155                // or we have now a child tag node
156                else {
157                    // now we have a child tag node
158                    if let Ok((rest, node)) = tag_node(remaining, context) {
159                        child_nodes.push(HsmlNode::Tag(node));
160                        input = rest;
161                    } else {
162                        return Err(nom::Err::Error(Error::new(input, ErrorKind::Tag)));
163                    }
164                }
165
166                // restore the indentation level
167                context.indent_level = indentation_level;
168
169                continue;
170            }
171
172            // we have no child tag nodes
173            break;
174        }
175
176        break;
177    }
178
179    Ok((
180        input,
181        TagNode {
182            tag: tag_name.to_string(),
183            id: id_node,
184            classes: (!class_nodes.is_empty()).then_some(class_nodes),
185            attributes: attribute_nodes,
186            text: text_node,
187            children: (!child_nodes.is_empty()).then_some(child_nodes),
188        },
189    ))
190}