Skip to main content

atmos/os_lib/
xml_parser.rs

1//! W3C XML / SVG 構文解析純粋モジュール
2//!
3//! XML 1.0 および W3C DOM Parsing §4.1 に準拠し、
4//! 大文字小文字の完全保持、自己終了タグ(`<rect ... />`)、
5//! タグ整合性検査、および `<parsererror>` 診断を提供する。
6
7extern crate alloc;
8use alloc::collections::BTreeMap;
9use alloc::string::{String, ToString};
10use alloc::vec::Vec;
11use crate::os_lib::dom::{Node, NodeType};
12
13/// DOMParser がサポートする MIME タイプ。
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub enum SupportedMimeType {
16    Html,
17    Xml,
18    Svg,
19    Xhtml,
20    Unsupported,
21}
22
23/// MIME タイプ文字列を判定する(純粋関数)。
24pub fn parse_mime_type(mime: &str) -> SupportedMimeType {
25    let lower = mime.trim().to_ascii_lowercase();
26    match lower.as_str() {
27        "text/html" => SupportedMimeType::Html,
28        "text/xml" | "application/xml" => SupportedMimeType::Xml,
29        "image/svg+xml" => SupportedMimeType::Svg,
30        "application/xhtml+xml" => SupportedMimeType::Xhtml,
31        _ => SupportedMimeType::Unsupported,
32    }
33}
34
35/// XML パースエラー
36#[derive(Debug, Clone, PartialEq, Eq)]
37pub struct XmlError {
38    pub message: String,
39    pub line: usize,
40    pub column: usize,
41}
42
43/// XML 文字列をパースし、case-sensitive な DOM ノードツリーを構築する(純粋関数)。
44pub fn parse_xml(input: &str) -> Result<Node, XmlError> {
45    let chars: Vec<char> = input.chars().collect();
46    let mut pos = 0;
47    let len = chars.len();
48
49    // XML 宣言 <?xml ... ?> をスキップ
50    skip_whitespace_and_misc(&chars, &mut pos, len);
51
52    let mut stack: Vec<Node> = Vec::new();
53    let mut root_node: Option<Node> = None;
54
55    while pos < len {
56        skip_whitespace_chars(&chars, &mut pos, len);
57        if pos >= len {
58            break;
59        }
60
61        if chars[pos] == '<' {
62            if pos + 1 < len && chars[pos + 1] == '/' {
63                // 終了タグ </tag>
64                pos += 2;
65                let tag_name = read_name(&chars, &mut pos, len);
66                skip_whitespace_chars(&chars, &mut pos, len);
67                if pos < len && chars[pos] == '>' {
68                    pos += 1;
69                }
70                let Some(popped) = stack.pop() else {
71                    return Err(XmlError {
72                        message: alloc::format!("Unexpected closing tag </{}> without open tag", tag_name),
73                        line: 1,
74                        column: pos,
75                    });
76                };
77                let popped_tag = match &popped.node_type {
78                    NodeType::Element { tag_name: t, .. } => t.clone(),
79                    _ => String::new(),
80                };
81                if popped_tag != tag_name {
82                    return Err(XmlError {
83                        message: alloc::format!("Mismatched closing tag: expected </{}>, got </{}>", popped_tag, tag_name),
84                        line: 1,
85                        column: pos,
86                    });
87                }
88                if let Some(parent) = stack.last_mut() {
89                    parent.children.push(popped);
90                } else {
91                    if root_node.is_some() {
92                        return Err(XmlError {
93                            message: "Multiple root elements in XML document".to_string(),
94                            line: 1,
95                            column: pos,
96                        });
97                    }
98                    root_node = Some(popped);
99                }
100            } else if pos + 3 < len && chars[pos + 1] == '!' && chars[pos + 2] == '-' && chars[pos + 3] == '-' {
101                // コメント <!-- ... -->
102                pos += 4;
103                while pos + 2 < len {
104                    if chars[pos] == '-' && chars[pos + 1] == '-' && chars[pos + 2] == '>' {
105                        pos += 3;
106                        break;
107                    }
108                    pos += 1;
109                }
110            } else if pos + 8 < len && input_matches_at(&chars, pos, "<![CDATA[") {
111                // CDATA <![CDATA[ ... ]]>
112                pos += 9;
113                let cdata_start = pos;
114                while pos + 2 < len {
115                    if chars[pos] == ']' && chars[pos + 1] == ']' && chars[pos + 2] == '>' {
116                        break;
117                    }
118                    pos += 1;
119                }
120                let cdata_text: String = chars[cdata_start..pos].iter().collect();
121                if pos + 2 < len {
122                    pos += 3;
123                }
124                let text_node = Node::new_text(cdata_text);
125                if let Some(parent) = stack.last_mut() {
126                    parent.children.push(text_node);
127                }
128            } else {
129                // 開始タグ または 自己終了タグ
130                pos += 1;
131                let tag_name = read_name(&chars, &mut pos, len);
132                if tag_name.is_empty() {
133                    return Err(XmlError {
134                        message: "Invalid empty element name".to_string(),
135                        line: 1,
136                        column: pos,
137                    });
138                }
139                let attrs = read_attributes(&chars, &mut pos, len)?;
140                skip_whitespace_chars(&chars, &mut pos, len);
141
142                let is_self_closing = if pos + 1 < len && chars[pos] == '/' && chars[pos + 1] == '>' {
143                    pos += 2;
144                    true
145                } else if pos < len && chars[pos] == '>' {
146                    pos += 1;
147                    false
148                } else {
149                    return Err(XmlError {
150                        message: alloc::format!("Unclosed element <{}>", tag_name),
151                        line: 1,
152                        column: pos,
153                    });
154                };
155
156                let node = Node::new_element_preserve_case(tag_name, attrs);
157                if is_self_closing {
158                    if let Some(parent) = stack.last_mut() {
159                        parent.children.push(node);
160                    } else {
161                        if root_node.is_some() {
162                            return Err(XmlError {
163                                message: "Multiple root elements in XML document".to_string(),
164                                line: 1,
165                                column: pos,
166                            });
167                        }
168                        root_node = Some(node);
169                    }
170                } else {
171                    stack.push(node);
172                }
173            }
174        } else {
175            // テキストノード
176            let text_start = pos;
177            while pos < len && chars[pos] != '<' {
178                pos += 1;
179            }
180            let raw_text: String = chars[text_start..pos].iter().collect();
181            let unescaped = unescape_xml_entities(&raw_text);
182            if !unescaped.trim().is_empty() || !raw_text.is_empty() {
183                let text_node = Node::new_text(unescaped);
184                if let Some(parent) = stack.last_mut() {
185                    parent.children.push(text_node);
186                }
187            }
188        }
189    }
190
191    if let Some(unclosed) = stack.pop() {
192        let tag = match &unclosed.node_type {
193            NodeType::Element { tag_name, .. } => tag_name.clone(),
194            _ => String::new(),
195        };
196        return Err(XmlError {
197            message: alloc::format!("Unclosed tag <{}> at end of document", tag),
198            line: 1,
199            column: pos,
200        });
201    }
202
203    match root_node {
204        Some(r) => Ok(r),
205        None => Err(XmlError {
206            message: "No root element found in XML document".to_string(),
207            line: 1,
208            column: pos,
209        }),
210    }
211}
212
213/// 大文字小文字を維持した Element ノードを生成する。
214impl Node {
215    pub fn new_element_preserve_case(name: String, attrs: BTreeMap<String, String>) -> Self {
216        let id = attrs.get("id").cloned();
217        let classes = match attrs.get("class") {
218            Some(class_str) => class_str.split_whitespace().map(String::from).collect(),
219            None => Vec::new(),
220        };
221
222        Node {
223            node_type: NodeType::Element {
224                tag_name: name,
225                attributes: attrs,
226                classes,
227                id,
228            },
229            children: Vec::new(),
230        }
231    }
232}
233
234/// XML の標準エンティティ(&lt;, &gt;, &amp;, &quot;, &apos;)をアンエスケープする。
235pub fn unescape_xml_entities(s: &str) -> String {
236    let mut out = String::with_capacity(s.len());
237    let chars: Vec<char> = s.chars().collect();
238    let mut i = 0;
239    while i < chars.len() {
240        if chars[i] == '&' {
241            if let Some(end) = chars[i..].iter().position(|&c| c == ';') {
242                let entity: String = chars[i + 1..i + end].iter().collect();
243                match entity.as_str() {
244                    "lt" => out.push('<'),
245                    "gt" => out.push('>'),
246                    "amp" => out.push('&'),
247                    "quot" => out.push('"'),
248                    "apos" => out.push('\''),
249                    _ => {
250                        out.push('&');
251                        out.push_str(&entity);
252                        out.push(';');
253                    }
254                }
255                i += end + 1;
256                continue;
257            }
258        }
259        out.push(chars[i]);
260        i += 1;
261    }
262    out
263}
264
265fn skip_whitespace_chars(chars: &[char], pos: &mut usize, len: usize) {
266    while *pos < len && chars[*pos].is_ascii_whitespace() {
267        *pos += 1;
268    }
269}
270
271fn skip_whitespace_and_misc(chars: &[char], pos: &mut usize, len: usize) {
272    loop {
273        skip_whitespace_chars(chars, pos, len);
274        if *pos + 1 < len && chars[*pos] == '<' && chars[*pos + 1] == '?' {
275            // <?xml ... ?> または <?pi ... ?>
276            *pos += 2;
277            while *pos + 1 < len {
278                if chars[*pos] == '?' && chars[*pos + 1] == '>' {
279                    *pos += 2;
280                    break;
281                }
282                *pos += 1;
283            }
284        } else if *pos + 3 < len && chars[*pos] == '<' && chars[*pos + 1] == '!' && chars[*pos + 2] == '-' && chars[*pos + 3] == '-' {
285            *pos += 4;
286            while *pos + 2 < len {
287                if chars[*pos] == '-' && chars[*pos + 1] == '-' && chars[*pos + 2] == '>' {
288                    *pos += 3;
289                    break;
290                }
291                *pos += 1;
292            }
293        } else {
294            break;
295        }
296    }
297}
298
299fn read_name(chars: &[char], pos: &mut usize, len: usize) -> String {
300    let mut name = String::new();
301    while *pos < len {
302        let c = chars[*pos];
303        if c.is_ascii_alphanumeric() || c == '_' || c == '-' || c == ':' {
304            name.push(c);
305            *pos += 1;
306        } else {
307            break;
308        }
309    }
310    name
311}
312
313fn read_attributes(chars: &[char], pos: &mut usize, len: usize) -> Result<BTreeMap<String, String>, XmlError> {
314    let mut attrs = BTreeMap::new();
315    loop {
316        skip_whitespace_chars(chars, pos, len);
317        if *pos >= len || chars[*pos] == '>' || chars[*pos] == '/' {
318            break;
319        }
320        let attr_name = read_name(chars, pos, len);
321        if attr_name.is_empty() {
322            break;
323        }
324        skip_whitespace_chars(chars, pos, len);
325        let mut attr_val = String::new();
326        if *pos < len && chars[*pos] == '=' {
327            *pos += 1;
328            skip_whitespace_chars(chars, pos, len);
329            if *pos < len && (chars[*pos] == '"' || chars[*pos] == '\'') {
330                let quote = chars[*pos];
331                *pos += 1;
332                let val_start = *pos;
333                while *pos < len && chars[*pos] != quote {
334                    *pos += 1;
335                }
336                attr_val = unescape_xml_entities(&chars[val_start..*pos].iter().collect::<String>());
337                if *pos < len {
338                    *pos += 1;
339                }
340            }
341        }
342        attrs.insert(attr_name, attr_val);
343    }
344    Ok(attrs)
345}
346
347fn input_matches_at(chars: &[char], pos: usize, pat: &str) -> bool {
348    let pat_chars: Vec<char> = pat.chars().collect();
349    if pos + pat_chars.len() > chars.len() {
350        return false;
351    }
352    for (i, &c) in pat_chars.iter().enumerate() {
353        if chars[pos + i] != c {
354            return false;
355        }
356    }
357    true
358}
359
360/// W3C DOM Parsing 仕様準拠の `<parsererror>` 要素ノードを生成する。
361pub fn create_parsererror_node(err: &XmlError) -> Node {
362    let mut attrs = BTreeMap::new();
363    attrs.insert("xmlns".to_string(), "http://www.mozilla.org/newlayout/xml/parsererror.xml".to_string());
364    let mut err_node = Node::new_element_preserve_case("parsererror".to_string(), attrs);
365    err_node.children.push(Node::new_text(alloc::format!("XML Parsing Error: {}", err.message)));
366    err_node
367}