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}