1use core::ffi::c_void;
15use core::ptr;
16use core::slice;
17use std::os::raw::{c_char, c_int};
18
19use crate::abi::allocator::{xmlFree, xmlMalloc, xmlMallocZero, xmlRealloc};
20use crate::abi::constants::XML_DEFAULT_VERSION;
21use crate::abi::structs::*;
22use crate::abi::types::xmlCharEncoding::XML_CHAR_ENCODING_UTF8;
23use crate::abi::types::xmlDocProperties::XML_DOC_WELLFORMED;
24use crate::abi::types::xmlElementType::*;
25use crate::abi::types::*;
26use crate::xml::io;
27use crate::xml::string::*;
28use crate::xml::tree;
29
30const HTML_INLINE: u32 = 0x1;
36const HTML_BLOCK: u32 = 0x2;
37const HTML_EMPTY: u32 = 0x4;
38#[allow(dead_code)]
39const HTML_DEPRECATED: u32 = 0x8;
40const HTML_OL: u32 = 0x10;
41const HTML_DL: u32 = 0x20;
42#[allow(dead_code)]
43const HTML_COMPACT: u32 = 0x40;
44const HTML_HEAD: u32 = 0x80;
45const HTML_BODY: u32 = 0x100;
46#[allow(dead_code)]
47const HTML_HEADSTRUCK: u32 = 0x200;
48const HTML_VALID: u32 = 0x400;
49const HTML_NO_END: u32 = 0x800; const HTML_IMPLIED: u32 = 0x1000; #[derive(Clone, Copy)]
54struct HtmlElementInfo {
55 name: &'static str,
56 flags: u32,
57}
58
59const HTML_ELEMENTS: &[HtmlElementInfo] = &[
62 HtmlElementInfo {
64 name: "br",
65 flags: HTML_INLINE | HTML_EMPTY | HTML_VALID,
66 },
67 HtmlElementInfo {
68 name: "hr",
69 flags: HTML_BLOCK | HTML_EMPTY | HTML_VALID,
70 },
71 HtmlElementInfo {
72 name: "img",
73 flags: HTML_INLINE | HTML_EMPTY | HTML_VALID,
74 },
75 HtmlElementInfo {
76 name: "input",
77 flags: HTML_INLINE | HTML_EMPTY | HTML_VALID,
78 },
79 HtmlElementInfo {
80 name: "meta",
81 flags: HTML_HEAD | HTML_EMPTY | HTML_VALID,
82 },
83 HtmlElementInfo {
84 name: "link",
85 flags: HTML_HEAD | HTML_EMPTY | HTML_VALID,
86 },
87 HtmlElementInfo {
88 name: "base",
89 flags: HTML_HEAD | HTML_EMPTY | HTML_VALID,
90 },
91 HtmlElementInfo {
92 name: "area",
93 flags: HTML_INLINE | HTML_EMPTY | HTML_VALID,
94 },
95 HtmlElementInfo {
96 name: "col",
97 flags: HTML_BLOCK | HTML_EMPTY | HTML_VALID,
98 },
99 HtmlElementInfo {
100 name: "embed",
101 flags: HTML_INLINE | HTML_EMPTY | HTML_VALID,
102 },
103 HtmlElementInfo {
104 name: "param",
105 flags: HTML_HEAD | HTML_EMPTY | HTML_VALID,
106 },
107 HtmlElementInfo {
108 name: "source",
109 flags: HTML_INLINE | HTML_EMPTY | HTML_VALID,
110 },
111 HtmlElementInfo {
112 name: "track",
113 flags: HTML_INLINE | HTML_EMPTY | HTML_VALID,
114 },
115 HtmlElementInfo {
116 name: "wbr",
117 flags: HTML_INLINE | HTML_EMPTY | HTML_VALID,
118 },
119 HtmlElementInfo {
121 name: "html",
122 flags: HTML_BLOCK | HTML_VALID,
123 },
124 HtmlElementInfo {
125 name: "head",
126 flags: HTML_HEAD | HTML_BLOCK | HTML_VALID,
127 },
128 HtmlElementInfo {
129 name: "body",
130 flags: HTML_BODY | HTML_BLOCK | HTML_VALID,
131 },
132 HtmlElementInfo {
133 name: "div",
134 flags: HTML_BLOCK | HTML_VALID,
135 },
136 HtmlElementInfo {
137 name: "p",
138 flags: HTML_BLOCK | HTML_VALID | HTML_NO_END,
139 },
140 HtmlElementInfo {
141 name: "h1",
142 flags: HTML_BLOCK | HTML_VALID,
143 },
144 HtmlElementInfo {
145 name: "h2",
146 flags: HTML_BLOCK | HTML_VALID,
147 },
148 HtmlElementInfo {
149 name: "h3",
150 flags: HTML_BLOCK | HTML_VALID,
151 },
152 HtmlElementInfo {
153 name: "h4",
154 flags: HTML_BLOCK | HTML_VALID,
155 },
156 HtmlElementInfo {
157 name: "h5",
158 flags: HTML_BLOCK | HTML_VALID,
159 },
160 HtmlElementInfo {
161 name: "h6",
162 flags: HTML_BLOCK | HTML_VALID,
163 },
164 HtmlElementInfo {
165 name: "ul",
166 flags: HTML_BLOCK | HTML_VALID | HTML_OL,
167 },
168 HtmlElementInfo {
169 name: "ol",
170 flags: HTML_BLOCK | HTML_VALID | HTML_OL,
171 },
172 HtmlElementInfo {
173 name: "li",
174 flags: HTML_BLOCK | HTML_VALID | HTML_NO_END,
175 },
176 HtmlElementInfo {
177 name: "dl",
178 flags: HTML_BLOCK | HTML_VALID | HTML_DL,
179 },
180 HtmlElementInfo {
181 name: "dt",
182 flags: HTML_BLOCK | HTML_VALID | HTML_NO_END,
183 },
184 HtmlElementInfo {
185 name: "dd",
186 flags: HTML_BLOCK | HTML_VALID | HTML_NO_END,
187 },
188 HtmlElementInfo {
189 name: "table",
190 flags: HTML_BLOCK | HTML_VALID,
191 },
192 HtmlElementInfo {
193 name: "tr",
194 flags: HTML_BLOCK | HTML_VALID | HTML_NO_END,
195 },
196 HtmlElementInfo {
197 name: "td",
198 flags: HTML_BLOCK | HTML_VALID | HTML_NO_END,
199 },
200 HtmlElementInfo {
201 name: "th",
202 flags: HTML_BLOCK | HTML_VALID | HTML_NO_END,
203 },
204 HtmlElementInfo {
205 name: "thead",
206 flags: HTML_BLOCK | HTML_VALID,
207 },
208 HtmlElementInfo {
209 name: "tbody",
210 flags: HTML_BLOCK | HTML_VALID,
211 },
212 HtmlElementInfo {
213 name: "tfoot",
214 flags: HTML_BLOCK | HTML_VALID,
215 },
216 HtmlElementInfo {
217 name: "colgroup",
218 flags: HTML_BLOCK | HTML_VALID,
219 },
220 HtmlElementInfo {
221 name: "caption",
222 flags: HTML_BLOCK | HTML_VALID,
223 },
224 HtmlElementInfo {
225 name: "form",
226 flags: HTML_BLOCK | HTML_VALID,
227 },
228 HtmlElementInfo {
229 name: "fieldset",
230 flags: HTML_BLOCK | HTML_VALID,
231 },
232 HtmlElementInfo {
233 name: "legend",
234 flags: HTML_BLOCK | HTML_VALID,
235 },
236 HtmlElementInfo {
237 name: "pre",
238 flags: HTML_BLOCK | HTML_VALID,
239 },
240 HtmlElementInfo {
241 name: "blockquote",
242 flags: HTML_BLOCK | HTML_VALID,
243 },
244 HtmlElementInfo {
245 name: "address",
246 flags: HTML_BLOCK | HTML_VALID,
247 },
248 HtmlElementInfo {
249 name: "center",
250 flags: HTML_BLOCK | HTML_VALID,
251 },
252 HtmlElementInfo {
253 name: "dir",
254 flags: HTML_BLOCK | HTML_VALID,
255 },
256 HtmlElementInfo {
257 name: "menu",
258 flags: HTML_BLOCK | HTML_VALID,
259 },
260 HtmlElementInfo {
261 name: "noscript",
262 flags: HTML_BLOCK | HTML_VALID,
263 },
264 HtmlElementInfo {
265 name: "frameset",
266 flags: HTML_BLOCK | HTML_VALID,
267 },
268 HtmlElementInfo {
269 name: "frame",
270 flags: HTML_BLOCK | HTML_EMPTY | HTML_VALID,
271 },
272 HtmlElementInfo {
273 name: "iframe",
274 flags: HTML_BLOCK | HTML_VALID,
275 },
276 HtmlElementInfo {
277 name: "noframes",
278 flags: HTML_BLOCK | HTML_VALID,
279 },
280 HtmlElementInfo {
282 name: "a",
283 flags: HTML_INLINE | HTML_VALID,
284 },
285 HtmlElementInfo {
286 name: "abbr",
287 flags: HTML_INLINE | HTML_VALID,
288 },
289 HtmlElementInfo {
290 name: "acronym",
291 flags: HTML_INLINE | HTML_VALID,
292 },
293 HtmlElementInfo {
294 name: "b",
295 flags: HTML_INLINE | HTML_VALID,
296 },
297 HtmlElementInfo {
298 name: "basefont",
299 flags: HTML_INLINE | HTML_EMPTY | HTML_VALID,
300 },
301 HtmlElementInfo {
302 name: "bdo",
303 flags: HTML_INLINE | HTML_VALID,
304 },
305 HtmlElementInfo {
306 name: "big",
307 flags: HTML_INLINE | HTML_VALID,
308 },
309 HtmlElementInfo {
310 name: "cite",
311 flags: HTML_INLINE | HTML_VALID,
312 },
313 HtmlElementInfo {
314 name: "code",
315 flags: HTML_INLINE | HTML_VALID,
316 },
317 HtmlElementInfo {
318 name: "dfn",
319 flags: HTML_INLINE | HTML_VALID,
320 },
321 HtmlElementInfo {
322 name: "em",
323 flags: HTML_INLINE | HTML_VALID,
324 },
325 HtmlElementInfo {
326 name: "font",
327 flags: HTML_INLINE | HTML_VALID,
328 },
329 HtmlElementInfo {
330 name: "i",
331 flags: HTML_INLINE | HTML_VALID,
332 },
333 HtmlElementInfo {
334 name: "kbd",
335 flags: HTML_INLINE | HTML_VALID,
336 },
337 HtmlElementInfo {
338 name: "label",
339 flags: HTML_INLINE | HTML_VALID,
340 },
341 HtmlElementInfo {
342 name: "map",
343 flags: HTML_INLINE | HTML_VALID,
344 },
345 HtmlElementInfo {
346 name: "nobr",
347 flags: HTML_INLINE | HTML_VALID,
348 },
349 HtmlElementInfo {
350 name: "object",
351 flags: HTML_INLINE | HTML_VALID,
352 },
353 HtmlElementInfo {
354 name: "q",
355 flags: HTML_INLINE | HTML_VALID,
356 },
357 HtmlElementInfo {
358 name: "rb",
359 flags: HTML_INLINE | HTML_VALID,
360 },
361 HtmlElementInfo {
362 name: "rbc",
363 flags: HTML_INLINE | HTML_VALID,
364 },
365 HtmlElementInfo {
366 name: "rp",
367 flags: HTML_INLINE | HTML_VALID,
368 },
369 HtmlElementInfo {
370 name: "rt",
371 flags: HTML_INLINE | HTML_VALID,
372 },
373 HtmlElementInfo {
374 name: "rtc",
375 flags: HTML_INLINE | HTML_VALID,
376 },
377 HtmlElementInfo {
378 name: "ruby",
379 flags: HTML_INLINE | HTML_VALID,
380 },
381 HtmlElementInfo {
382 name: "s",
383 flags: HTML_INLINE | HTML_VALID,
384 },
385 HtmlElementInfo {
386 name: "samp",
387 flags: HTML_INLINE | HTML_VALID,
388 },
389 HtmlElementInfo {
390 name: "select",
391 flags: HTML_INLINE | HTML_VALID,
392 },
393 HtmlElementInfo {
394 name: "small",
395 flags: HTML_INLINE | HTML_VALID,
396 },
397 HtmlElementInfo {
398 name: "span",
399 flags: HTML_INLINE | HTML_VALID,
400 },
401 HtmlElementInfo {
402 name: "strike",
403 flags: HTML_INLINE | HTML_VALID,
404 },
405 HtmlElementInfo {
406 name: "strong",
407 flags: HTML_INLINE | HTML_VALID,
408 },
409 HtmlElementInfo {
410 name: "sub",
411 flags: HTML_INLINE | HTML_VALID,
412 },
413 HtmlElementInfo {
414 name: "sup",
415 flags: HTML_INLINE | HTML_VALID,
416 },
417 HtmlElementInfo {
418 name: "textarea",
419 flags: HTML_INLINE | HTML_VALID,
420 },
421 HtmlElementInfo {
422 name: "tt",
423 flags: HTML_INLINE | HTML_VALID,
424 },
425 HtmlElementInfo {
426 name: "u",
427 flags: HTML_INLINE | HTML_VALID,
428 },
429 HtmlElementInfo {
430 name: "var",
431 flags: HTML_INLINE | HTML_VALID,
432 },
433 HtmlElementInfo {
435 name: "header",
436 flags: HTML_BLOCK | HTML_VALID,
437 },
438 HtmlElementInfo {
439 name: "footer",
440 flags: HTML_BLOCK | HTML_VALID,
441 },
442 HtmlElementInfo {
443 name: "nav",
444 flags: HTML_BLOCK | HTML_VALID,
445 },
446 HtmlElementInfo {
447 name: "article",
448 flags: HTML_BLOCK | HTML_VALID,
449 },
450 HtmlElementInfo {
451 name: "section",
452 flags: HTML_BLOCK | HTML_VALID,
453 },
454 HtmlElementInfo {
455 name: "aside",
456 flags: HTML_BLOCK | HTML_VALID,
457 },
458 HtmlElementInfo {
459 name: "main",
460 flags: HTML_BLOCK | HTML_VALID,
461 },
462 HtmlElementInfo {
463 name: "figure",
464 flags: HTML_BLOCK | HTML_VALID,
465 },
466 HtmlElementInfo {
467 name: "figcaption",
468 flags: HTML_BLOCK | HTML_VALID,
469 },
470 HtmlElementInfo {
471 name: "details",
472 flags: HTML_BLOCK | HTML_VALID,
473 },
474 HtmlElementInfo {
475 name: "summary",
476 flags: HTML_BLOCK | HTML_VALID,
477 },
478 HtmlElementInfo {
480 name: "script",
481 flags: HTML_HEAD | HTML_BLOCK | HTML_VALID,
482 },
483 HtmlElementInfo {
484 name: "style",
485 flags: HTML_HEAD | HTML_BLOCK | HTML_VALID,
486 },
487 HtmlElementInfo {
488 name: "title",
489 flags: HTML_HEAD | HTML_BLOCK | HTML_VALID,
490 },
491];
492
493fn html_tag_lookup(name: &str) -> Option<&'static HtmlElementInfo> {
496 let lower: Vec<u8> = name.bytes().map(|b| b.to_ascii_lowercase()).collect();
498 let lower_str = match core::str::from_utf8(&lower) {
499 Ok(s) => s,
500 Err(_) => return None,
501 };
502 HTML_ELEMENTS.iter().find(|info| info.name == lower_str)
503}
504
505const HTML_ENTITIES: &[(&str, &str)] = &[
512 ("nbsp", "\u{00a0}"),
513 ("lt", "<"),
514 ("gt", ">"),
515 ("amp", "&"),
516 ("quot", "\""),
517 ("apos", "'"),
518 ("copy", "\u{00a9}"),
519 ("reg", "\u{00ae}"),
520 ("amp", "&"),
521 ("iexcl", "\u{00a1}"),
522 ("cent", "\u{00a2}"),
523 ("pound", "\u{00a3}"),
524 ("curren", "\u{00a4}"),
525 ("yen", "\u{00a5}"),
526 ("brvbar", "\u{00a6}"),
527 ("sect", "\u{00a7}"),
528 ("uml", "\u{00a8}"),
529 ("ordf", "\u{00aa}"),
530 ("laquo", "\u{00ab}"),
531 ("not", "\u{00ac}"),
532 ("shy", "\u{00ad}"),
533 ("macr", "\u{00ae}"),
534 ("deg", "\u{00b0}"),
535 ("plusmn", "\u{00b1}"),
536 ("sup2", "\u{00b2}"),
537 ("sup3", "\u{00b3}"),
538 ("acute", "\u{00b4}"),
539 ("micro", "\u{00b5}"),
540 ("para", "\u{00b6}"),
541 ("middot", "\u{00b7}"),
542 ("cedil", "\u{00b8}"),
543 ("sup1", "\u{00b9}"),
544 ("ordm", "\u{00ba}"),
545 ("raquo", "\u{00bb}"),
546 ("frac14", "\u{00bc}"),
547 ("frac12", "\u{00bd}"),
548 ("frac34", "\u{00be}"),
549 ("iquest", "\u{00bf}"),
550 ("times", "\u{00d7}"),
551 ("divide", "\u{00f7}"),
552 ("ETH", "\u{00d0}"),
553 ("eth", "\u{00f0}"),
554 ("THORN", "\u{00de}"),
555 ("thorn", "\u{00fe}"),
556 ("AElig", "\u{00c6}"),
557 ("aelig", "\u{00e6}"),
558 ("OElig", "\u{0152}"),
559 ("oelig", "\u{0153}"),
560 ("Scaron", "\u{0160}"),
561 ("scaron", "\u{0161}"),
562 ("Yuml", "\u{0178}"),
563 ("circ", "\u{02c6}"),
564 ("tilde", "\u{02dc}"),
565 ("ensp", "\u{2002}"),
566 ("emsp", "\u{2003}"),
567 ("thinsp", "\u{2009}"),
568 ("zwnj", "\u{200c}"),
569 ("zwj", "\u{200d}"),
570 ("lrm", "\u{200e}"),
571 ("rlm", "\u{200f}"),
572 ("ndash", "\u{2013}"),
573 ("mdash", "\u{2014}"),
574 ("lsquo", "\u{2018}"),
575 ("rsquo", "\u{2019}"),
576 ("sbquo", "\u{201a}"),
577 ("ldquo", "\u{201c}"),
578 ("rdquo", "\u{201d}"),
579 ("bdquo", "\u{201e}"),
580 ("dagger", "\u{2020}"),
581 ("Dagger", "\u{2021}"),
582 ("bull", "\u{2022}"),
583 ("hellip", "\u{2026}"),
584 ("permil", "\u{2030}"),
585 ("prime", "\u{2032}"),
586 ("Prime", "\u{2033}"),
587 ("lsaquo", "\u{2039}"),
588 ("rsaquo", "\u{203a}"),
589 ("oline", "\u{203e}"),
590 ("euro", "\u{20ac}"),
591 ("trade", "\u{2122}"),
592 ("larr", "\u{2190}"),
593 ("uarr", "\u{2191}"),
594 ("rarr", "\u{2192}"),
595 ("darr", "\u{2193}"),
596 ("harr", "\u{2194}"),
597 ("crarr", "\u{21b5}"),
598 ("lceil", "\u{2308}"),
599 ("rceil", "\u{2309}"),
600 ("lfloor", "\u{230a}"),
601 ("rfloor", "\u{230b}"),
602 ("loz", "\u{25ca}"),
603 ("spades", "\u{2660}"),
604 ("clubs", "\u{2663}"),
605 ("hearts", "\u{2665}"),
606 ("diams", "\u{2666}"),
607 ("Alpha", "\u{0391}"),
608 ("Beta", "\u{0392}"),
609 ("Gamma", "\u{0393}"),
610 ("Delta", "\u{0394}"),
611 ("Epsilon", "\u{0395}"),
612 ("Zeta", "\u{0396}"),
613 ("Eta", "\u{0397}"),
614 ("Theta", "\u{0398}"),
615 ("Iota", "\u{0399}"),
616 ("Kappa", "\u{039a}"),
617 ("Lambda", "\u{039b}"),
618 ("Mu", "\u{039c}"),
619 ("Nu", "\u{039d}"),
620 ("Xi", "\u{039e}"),
621 ("Omicron", "\u{039f}"),
622 ("Pi", "\u{03a0}"),
623 ("Rho", "\u{03a1}"),
624 ("Sigma", "\u{03a3}"),
625 ("Tau", "\u{03a4}"),
626 ("Upsilon", "\u{03a5}"),
627 ("Phi", "\u{03a6}"),
628 ("Chi", "\u{03a7}"),
629 ("Psi", "\u{03a8}"),
630 ("Omega", "\u{03a9}"),
631 ("alpha", "\u{03b1}"),
632 ("beta", "\u{03b2}"),
633 ("gamma", "\u{03b3}"),
634 ("delta", "\u{03b4}"),
635 ("epsilon", "\u{03b5}"),
636 ("zeta", "\u{03b6}"),
637 ("eta", "\u{03b7}"),
638 ("theta", "\u{03b8}"),
639 ("iota", "\u{03b9}"),
640 ("kappa", "\u{03ba}"),
641 ("lambda", "\u{03bb}"),
642 ("mu", "\u{03bc}"),
643 ("nu", "\u{03bd}"),
644 ("xi", "\u{03be}"),
645 ("omicron", "\u{03bf}"),
646 ("pi", "\u{03c0}"),
647 ("rho", "\u{03c1}"),
648 ("sigmaf", "\u{03c2}"),
649 ("sigma", "\u{03c3}"),
650 ("tau", "\u{03c4}"),
651 ("upsilon", "\u{03c5}"),
652 ("phi", "\u{03c6}"),
653 ("chi", "\u{03c7}"),
654 ("psi", "\u{03c8}"),
655 ("omega", "\u{03c9}"),
656 ("thetasym", "\u{03d1}"),
657 ("upsih", "\u{03d2}"),
658 ("piv", "\u{03d6}"),
659];
660
661fn html_entity_lookup(name: &str) -> Option<&'static str> {
664 HTML_ENTITIES
665 .iter()
666 .find(|(n, _)| *n == name)
667 .map(|(_, v)| *v)
668}
669
670struct HtmlParserCtxt {
676 doc: *mut _xmlDoc,
678 current: *mut _xmlNode,
680 html: *mut _xmlNode,
682 head: *mut _xmlNode,
684 body: *mut _xmlNode,
686 in_head: bool,
688 in_body: bool,
690 html_created: bool,
692 head_created: bool,
694 body_created: bool,
696 seen_body_content: bool,
698 input: *mut u8,
700 input_pos: usize,
702 input_len: usize,
704 line: c_int,
706 err: bool,
708 filename: *mut c_char,
710 encoding: *mut c_char,
712}
713
714impl HtmlParserCtxt {
715 fn new() -> Self {
716 HtmlParserCtxt {
717 doc: ptr::null_mut(),
718 current: ptr::null_mut(),
719 html: ptr::null_mut(),
720 head: ptr::null_mut(),
721 body: ptr::null_mut(),
722 in_head: false,
723 in_body: false,
724 html_created: false,
725 head_created: false,
726 body_created: false,
727 seen_body_content: false,
728 input: ptr::null_mut(),
729 input_pos: 0,
730 input_len: 0,
731 line: 1,
732 err: false,
733 filename: ptr::null_mut(),
734 encoding: ptr::null_mut(),
735 }
736 }
737
738 fn peek(&self) -> Option<u8> {
740 if self.input_pos < self.input_len {
741 unsafe { Some(*self.input.add(self.input_pos)) }
742 } else {
743 None
744 }
745 }
746
747 fn peek_at(&self, offset: usize) -> Option<u8> {
749 let pos = self.input_pos + offset;
750 if pos < self.input_len {
751 unsafe { Some(*self.input.add(pos)) }
752 } else {
753 None
754 }
755 }
756
757 fn next(&mut self) -> Option<u8> {
759 if self.input_pos < self.input_len {
760 let ch = unsafe { *self.input.add(self.input_pos) };
761 self.input_pos += 1;
762 if ch == b'\n' {
763 self.line += 1;
764 }
765 Some(ch)
766 } else {
767 None
768 }
769 }
770
771 fn skip_while<F: Fn(u8) -> bool>(&mut self, f: F) {
773 while let Some(ch) = self.peek() {
774 if f(ch) {
775 self.next();
776 } else {
777 break;
778 }
779 }
780 }
781
782 fn skip_whitespace(&mut self) {
784 self.skip_while(|ch| ch == b' ' || ch == b'\t' || ch == b'\n' || ch == b'\r');
785 }
786
787 fn is_eof(&self) -> bool {
789 self.input_pos >= self.input_len
790 }
791
792 fn read_while<F: Fn(u8) -> bool>(&mut self, f: F) -> Vec<u8> {
794 let start = self.input_pos;
795 while let Some(ch) = self.peek() {
796 if f(ch) {
797 self.next();
798 } else {
799 break;
800 }
801 }
802 unsafe { slice::from_raw_parts(self.input.add(start), self.input_pos - start).to_vec() }
803 }
804}
805
806fn is_heading(name: &str) -> bool {
812 matches!(name, "h1" | "h2" | "h3" | "h4" | "h5" | "h6")
813}
814
815unsafe fn get_parent_element(node: *mut _xmlNode) -> *mut _xmlNode {
818 if node.is_null() {
819 return ptr::null_mut();
820 }
821 let mut n = node;
822 loop {
823 let parent = unsafe { (*n).parent };
824 if parent.is_null() {
825 return ptr::null_mut();
826 }
827 let ptype = unsafe { (*parent).type_ };
828 if ptype == XML_ELEMENT_NODE as c_int
829 || ptype == XML_HTML_DOCUMENT_NODE as c_int
830 || ptype == XML_DOCUMENT_NODE as c_int
831 {
832 return parent;
833 }
834 n = parent;
835 }
836}
837
838unsafe fn auto_close_element(ctxt: &mut HtmlParserCtxt, tag_name: &str) {
841 let tag_lower: Vec<u8> = tag_name.bytes().map(|b| b.to_ascii_lowercase()).collect();
842 let tag_lower_str = match core::str::from_utf8(&tag_lower) {
843 Ok(s) => s,
844 Err(_) => return,
845 };
846
847 let info = html_tag_lookup(&tag_lower_str);
848
849 let mut current = ctxt.current;
850
851 let mut open_names: Vec<Vec<u8>> = Vec::new();
853 let mut cur = current;
854 while !cur.is_null() {
855 let ctype = unsafe { (*cur).type_ };
856 if ctype == XML_ELEMENT_NODE as c_int {
857 if !unsafe { (*cur).name.is_null() } {
858 let name_bytes = unsafe { xmlstr_to_bytes((*cur).name) };
859 open_names.push(name_bytes.to_vec());
860 }
861 }
862 cur = unsafe { (*cur).parent };
863 }
864
865 if tag_lower_str == "p" || info.map_or(false, |i| i.flags & HTML_BLOCK != 0) {
867 let mut cur2 = current;
869 while !cur2.is_null() {
870 let ctype = unsafe { (*cur2).type_ };
871 if ctype == XML_ELEMENT_NODE as c_int {
872 if !unsafe { (*cur2).name.is_null() } {
873 let name_bytes = unsafe { xmlstr_to_bytes((*cur2).name) };
874 let name_str = core::str::from_utf8(name_bytes).unwrap_or("");
875 if name_str.eq_ignore_ascii_case("p") {
876 current = unsafe { (*cur2).parent };
878 break;
879 }
880 }
881 }
882 cur2 = unsafe { (*cur2).parent };
883 }
884 }
885
886 if is_heading(&tag_lower_str) {
888 let mut cur2 = current;
889 while !cur2.is_null() {
890 let ctype = unsafe { (*cur2).type_ };
891 if ctype == XML_ELEMENT_NODE as c_int {
892 if !unsafe { (*cur2).name.is_null() } {
893 let name_bytes = unsafe { xmlstr_to_bytes((*cur2).name) };
894 let name_str = core::str::from_utf8(name_bytes).unwrap_or("");
895 if is_heading(name_str) {
896 current = unsafe { (*cur2).parent };
897 break;
898 }
899 }
900 }
901 cur2 = unsafe { (*cur2).parent };
902 }
903 }
904
905 if tag_lower_str == "li" {
907 let mut cur2 = current;
908 while !cur2.is_null() {
909 let ctype = unsafe { (*cur2).type_ };
910 if ctype == XML_ELEMENT_NODE as c_int {
911 if !unsafe { (*cur2).name.is_null() } {
912 let name_bytes = unsafe { xmlstr_to_bytes((*cur2).name) };
913 let name_str = core::str::from_utf8(name_bytes).unwrap_or("");
914 if name_str.eq_ignore_ascii_case("li") {
915 current = unsafe { (*cur2).parent };
916 break;
917 }
918 }
919 }
920 cur2 = unsafe { (*cur2).parent };
921 }
922 }
923
924 if tag_lower_str == "dt" || tag_lower_str == "dd" {
926 let mut cur2 = current;
927 while !cur2.is_null() {
928 let ctype = unsafe { (*cur2).type_ };
929 if ctype == XML_ELEMENT_NODE as c_int {
930 if !unsafe { (*cur2).name.is_null() } {
931 let name_bytes = unsafe { xmlstr_to_bytes((*cur2).name) };
932 let name_str = core::str::from_utf8(name_bytes).unwrap_or("");
933 if name_str == "dt" || name_str == "dd" {
934 current = unsafe { (*cur2).parent };
935 break;
936 }
937 }
938 }
939 cur2 = unsafe { (*cur2).parent };
940 }
941 }
942
943 if tag_lower_str == "tr" || tag_lower_str == "td" || tag_lower_str == "th" {
945 let mut cur2 = current;
946 while !cur2.is_null() {
947 let ctype = unsafe { (*cur2).type_ };
948 if ctype == XML_ELEMENT_NODE as c_int {
949 if !unsafe { (*cur2).name.is_null() } {
950 let name_bytes = unsafe { xmlstr_to_bytes((*cur2).name) };
951 let name_str = core::str::from_utf8(name_bytes).unwrap_or("");
952 if name_str == "tr" || name_str == "td" || name_str == "th" {
953 current = unsafe { (*cur2).parent };
954 break;
955 }
956 }
957 }
958 cur2 = unsafe { (*cur2).parent };
959 }
960 }
961
962 if matches!(tag_lower_str, "thead" | "tbody" | "tfoot") {
964 let mut cur2 = current;
965 while !cur2.is_null() {
966 let ctype = unsafe { (*cur2).type_ };
967 if ctype == XML_ELEMENT_NODE as c_int {
968 if !unsafe { (*cur2).name.is_null() } {
969 let name_bytes = unsafe { xmlstr_to_bytes((*cur2).name) };
970 let name_str = core::str::from_utf8(name_bytes).unwrap_or("");
971 if name_str == "thead" || name_str == "tbody" || name_str == "tfoot" {
972 current = unsafe { (*cur2).parent };
973 break;
974 }
975 }
976 }
977 cur2 = unsafe { (*cur2).parent };
978 }
979 }
980
981 if tag_lower_str == "colgroup" {
983 let mut cur2 = current;
984 while !cur2.is_null() {
985 let ctype = unsafe { (*cur2).type_ };
986 if ctype == XML_ELEMENT_NODE as c_int {
987 if !unsafe { (*cur2).name.is_null() } {
988 let name_bytes = unsafe { xmlstr_to_bytes((*cur2).name) };
989 let name_str = core::str::from_utf8(name_bytes).unwrap_or("");
990 if name_str == "colgroup" {
991 current = unsafe { (*cur2).parent };
992 break;
993 }
994 }
995 }
996 cur2 = unsafe { (*cur2).parent };
997 }
998 }
999
1000 if tag_lower_str == "caption" {
1002 let mut cur2 = current;
1003 while !cur2.is_null() {
1004 let ctype = unsafe { (*cur2).type_ };
1005 if ctype == XML_ELEMENT_NODE as c_int {
1006 if !unsafe { (*cur2).name.is_null() } {
1007 let name_bytes = unsafe { xmlstr_to_bytes((*cur2).name) };
1008 let name_str = core::str::from_utf8(name_bytes).unwrap_or("");
1009 if name_str == "caption" {
1010 current = unsafe { (*cur2).parent };
1011 break;
1012 }
1013 }
1014 }
1015 cur2 = unsafe { (*cur2).parent };
1016 }
1017 }
1018
1019 if tag_lower_str == "form" {
1021 let mut cur2 = current;
1022 while !cur2.is_null() {
1023 let ctype = unsafe { (*cur2).type_ };
1024 if ctype == XML_ELEMENT_NODE as c_int {
1025 if !unsafe { (*cur2).name.is_null() } {
1026 let name_bytes = unsafe { xmlstr_to_bytes((*cur2).name) };
1027 let name_str = core::str::from_utf8(name_bytes).unwrap_or("");
1028 if name_str.eq_ignore_ascii_case("form") {
1029 current = unsafe { (*cur2).parent };
1030 break;
1031 }
1032 }
1033 }
1034 cur2 = unsafe { (*cur2).parent };
1035 }
1036 }
1037
1038 ctxt.current = current;
1039}
1040
1041unsafe fn ensure_html(ctxt: &mut HtmlParserCtxt) -> *mut _xmlNode {
1047 if !ctxt.html.is_null() {
1048 return ctxt.html;
1049 }
1050
1051 let html_node = tree::new_node(ptr::null_mut(), b"html\0" as *const u8 as *const xmlChar);
1052 if html_node.is_null() {
1053 return ptr::null_mut();
1054 }
1055 unsafe {
1056 }
1059 ctxt.html = html_node;
1060 ctxt.html_created = true;
1061
1062 tree::add_child(ctxt.doc as *mut _xmlNode, html_node);
1064 ctxt.current = html_node;
1065
1066 html_node
1067}
1068
1069unsafe fn ensure_head(ctxt: &mut HtmlParserCtxt) -> *mut _xmlNode {
1071 if !ctxt.head.is_null() {
1072 return ctxt.head;
1073 }
1074
1075 ensure_html(ctxt);
1077
1078 let head_node = tree::new_node(ptr::null_mut(), b"head\0" as *const u8 as *const xmlChar);
1079 if head_node.is_null() {
1080 return ptr::null_mut();
1081 }
1082 ctxt.head = head_node;
1083 ctxt.head_created = true;
1084
1085 tree::add_child(ctxt.html, head_node);
1087 ctxt.current = head_node;
1088 ctxt.in_head = true;
1089
1090 head_node
1091}
1092
1093unsafe fn ensure_body(ctxt: &mut HtmlParserCtxt) -> *mut _xmlNode {
1095 if !ctxt.body.is_null() {
1096 return ctxt.body;
1097 }
1098
1099 ensure_html(ctxt);
1101
1102 let body_node = tree::new_node(ptr::null_mut(), b"body\0" as *const u8 as *const xmlChar);
1103 if body_node.is_null() {
1104 return ptr::null_mut();
1105 }
1106 ctxt.body = body_node;
1107 ctxt.body_created = true;
1108
1109 tree::add_child(ctxt.html, body_node);
1111 ctxt.current = body_node;
1112 ctxt.in_body = true;
1113
1114 body_node
1115}
1116
1117unsafe fn transition_to_body(ctxt: &mut HtmlParserCtxt) {
1119 if ctxt.in_head && !ctxt.seen_body_content {
1120 ctxt.seen_body_content = true;
1121 ctxt.in_head = false;
1122 ensure_body(ctxt);
1123 }
1124}
1125
1126struct HtmlAttr {
1132 name: Vec<u8>,
1133 value: Vec<u8>,
1134 quoted: bool,
1135}
1136
1137fn parse_attr_name(ctxt: &mut HtmlParserCtxt) -> Vec<u8> {
1139 let mut name = Vec::new();
1140 while let Some(ch) = ctxt.peek() {
1141 if ch == b'='
1142 || ch == b'>'
1143 || ch == b'/'
1144 || ch == b' '
1145 || ch == b'\t'
1146 || ch == b'\n'
1147 || ch == b'\r'
1148 {
1149 break;
1150 }
1151 name.push(ch);
1152 ctxt.next();
1153 }
1154 name
1155}
1156
1157fn parse_attr_value(ctxt: &mut HtmlParserCtxt) -> (Vec<u8>, bool) {
1159 ctxt.skip_whitespace();
1160
1161 let quote = match ctxt.peek() {
1162 Some(b'"') => {
1163 ctxt.next(); b'"'
1165 }
1166 Some(b'\'') => {
1167 ctxt.next(); b'\''
1169 }
1170 _ => {
1171 let value = ctxt.read_while(|ch| {
1173 ch != b'>' && ch != b' ' && ch != b'\t' && ch != b'\n' && ch != b'\r'
1174 });
1175 return (value, false);
1176 }
1177 };
1178
1179 let mut value = Vec::new();
1181 loop {
1182 match ctxt.next() {
1183 Some(ch) if ch == quote => break,
1184 Some(ch) => value.push(ch),
1185 None => break,
1186 }
1187 }
1188 (value, true)
1189}
1190
1191fn parse_attributes(ctxt: &mut HtmlParserCtxt) -> Vec<HtmlAttr> {
1193 let mut attrs = Vec::new();
1194
1195 loop {
1196 ctxt.skip_whitespace();
1197
1198 match ctxt.peek() {
1199 Some(b'>') | None => break,
1200 Some(b'/') => {
1201 if ctxt.peek_at(1) == Some(b'>') {
1203 break;
1204 }
1205 }
1207 _ => {}
1208 }
1209
1210 let name = parse_attr_name(ctxt);
1211 if name.is_empty() {
1212 break;
1213 }
1214
1215 ctxt.skip_whitespace();
1217 if ctxt.peek() == Some(b'=') {
1218 ctxt.next(); let (value, quoted) = parse_attr_value(ctxt);
1220 attrs.push(HtmlAttr {
1221 name,
1222 value,
1223 quoted,
1224 });
1225 } else {
1226 attrs.push(HtmlAttr {
1228 name,
1229 value: Vec::new(),
1230 quoted: false,
1231 });
1232 }
1233 }
1234
1235 attrs
1236}
1237
1238fn resolve_entity(name: &str) -> Vec<u8> {
1244 if let Some(replacement) = html_entity_lookup(name) {
1245 replacement.as_bytes().to_vec()
1246 } else {
1247 let mut result = Vec::new();
1249 result.push(b'&');
1250 result.extend_from_slice(name.as_bytes());
1251 result.push(b';');
1252 result
1253 }
1254}
1255
1256fn resolve_numeric_entity(value: &str, is_hex: bool) -> Vec<u8> {
1258 let codepoint = if is_hex {
1259 u32::from_str_radix(value, 16).unwrap_or(0xFFFD)
1260 } else {
1261 u32::from_str_radix(value, 10).unwrap_or(0xFFFD)
1262 };
1263
1264 if codepoint == 0 {
1265 return Vec::new();
1266 }
1267
1268 match char::from_u32(codepoint) {
1270 Some(c) => {
1271 let mut buf = [0u8; 4];
1272 let s = c.encode_utf8(&mut buf);
1273 s.as_bytes().to_vec()
1274 }
1275 None => vec![0xEF, 0xBF, 0xBD], }
1277}
1278
1279fn parse_entity(ctxt: &mut HtmlParserCtxt) -> Vec<u8> {
1282 match ctxt.peek() {
1284 Some(b'&') => {
1285 ctxt.next(); }
1287 _ => return vec![b'&'],
1288 }
1289
1290 if ctxt.peek() == Some(b'#') {
1292 ctxt.next(); let is_hex = ctxt.peek() == Some(b'x') || ctxt.peek() == Some(b'X');
1294 if is_hex {
1295 ctxt.next(); }
1297
1298 let digits = ctxt.read_while(|ch| {
1299 if is_hex {
1300 ch.is_ascii_hexdigit()
1301 } else {
1302 ch.is_ascii_digit()
1303 }
1304 });
1305
1306 let digits_str = core::str::from_utf8(&digits).unwrap_or("");
1307 if digits_str.is_empty() {
1308 let mut result = vec![b'&', b'#'];
1309 if is_hex {
1310 result.push(b'x');
1311 }
1312 return result;
1313 }
1314
1315 if ctxt.peek() == Some(b';') {
1317 ctxt.next();
1318 }
1319
1320 return resolve_numeric_entity(digits_str, is_hex);
1321 }
1322
1323 let name = ctxt.read_while(|ch| ch.is_ascii_alphanumeric() || ch == b'_' || ch == b'-');
1325 let name_str = core::str::from_utf8(&name).unwrap_or("");
1326
1327 if ctxt.peek() == Some(b';') {
1329 ctxt.next();
1330 }
1331
1332 resolve_entity(name_str)
1333}
1334
1335unsafe fn handle_text(ctxt: &mut HtmlParserCtxt, text: &[u8]) {
1337 if text.is_empty() {
1338 return;
1339 }
1340
1341 let parent = if ctxt.in_head {
1343 ctxt.head
1344 } else if ctxt.in_body || ctxt.body_created {
1345 ctxt.body
1346 } else if ctxt.html_created {
1347 ctxt.html
1348 } else {
1349 ctxt.doc as *mut _xmlNode
1350 };
1351
1352 let insertion_point = if ctxt.current.is_null() {
1353 parent
1354 } else {
1355 ctxt.current
1356 };
1357
1358 if insertion_point.is_null() {
1359 let text_node = tree::new_text(ptr::null_mut());
1361 if !text_node.is_null() {
1362 let content = bytes_to_xmlstr(text);
1364 if !content.is_null() {
1365 unsafe {
1366 (*text_node).content = content;
1367 }
1368 }
1369 tree::add_child(ctxt.doc as *mut _xmlNode, text_node);
1370 }
1371 return;
1372 }
1373
1374 let text_node = tree::new_text(ptr::null_mut());
1375 if text_node.is_null() {
1376 return;
1377 }
1378
1379 let content = bytes_to_xmlstr(text);
1381 if !content.is_null() {
1382 unsafe {
1383 (*text_node).content = content;
1384 }
1385 }
1386
1387 tree::add_child(insertion_point, text_node);
1388}
1389
1390unsafe fn handle_start_tag(ctxt: &mut HtmlParserCtxt, tag_name: &[u8], attrs: &[HtmlAttr]) {
1392 let tag_lower: Vec<u8> = tag_name.iter().map(|b| b.to_ascii_lowercase()).collect();
1393 let tag_str = core::str::from_utf8(&tag_lower).unwrap_or("");
1394
1395 let info = html_tag_lookup(tag_str);
1396
1397 let is_head_tag = info.map_or(false, |i| i.flags & HTML_HEAD != 0);
1399 let is_body_tag = info.map_or(false, |i| i.flags & HTML_BODY != 0);
1400 let is_empty = info.map_or(false, |i| i.flags & HTML_EMPTY != 0);
1401 let is_block = info.map_or(false, |i| i.flags & HTML_BLOCK != 0);
1402
1403 if tag_str == "html" {
1405 if !ctxt.html.is_null() && !ctxt.html_created {
1406 return;
1408 }
1409 if ctxt.html.is_null() {
1411 let html_node = tree::new_node(ptr::null_mut(), bytes_to_xmlstr(tag_name));
1412 if !html_node.is_null() {
1413 ctxt.html = html_node;
1414 ctxt.html_created = false; tree::add_child(ctxt.doc as *mut _xmlNode, html_node);
1416 ctxt.current = html_node;
1417 }
1418 } else {
1419 ctxt.current = ctxt.html;
1421 }
1422 return;
1423 }
1424
1425 if tag_str == "head" {
1426 if !ctxt.head.is_null() && !ctxt.head_created {
1427 return;
1429 }
1430 ensure_html(ctxt);
1432
1433 if ctxt.head.is_null() {
1434 let head_node = tree::new_node(ptr::null_mut(), bytes_to_xmlstr(tag_name));
1435 if !head_node.is_null() {
1436 ctxt.head = head_node;
1437 ctxt.head_created = false;
1438 tree::add_child(ctxt.html, head_node);
1439 ctxt.current = head_node;
1440 ctxt.in_head = true;
1441 }
1442 } else {
1443 ctxt.current = ctxt.head;
1444 ctxt.in_head = true;
1445 }
1446 return;
1447 }
1448
1449 if tag_str == "body" {
1450 if !ctxt.body.is_null() && !ctxt.body_created {
1451 return;
1453 }
1454 ensure_html(ctxt);
1456
1457 if ctxt.body.is_null() {
1458 let body_node = tree::new_node(ptr::null_mut(), bytes_to_xmlstr(tag_name));
1459 if !body_node.is_null() {
1460 ctxt.body = body_node;
1461 ctxt.body_created = false;
1462 tree::add_child(ctxt.html, body_node);
1463 ctxt.current = body_node;
1464 ctxt.in_body = true;
1465 ctxt.in_head = false;
1466 ctxt.seen_body_content = true;
1467 }
1468 } else {
1469 ctxt.current = ctxt.body;
1470 ctxt.in_body = true;
1471 ctxt.in_head = false;
1472 ctxt.seen_body_content = true;
1473 }
1474 return;
1475 }
1476
1477 if is_head_tag && !ctxt.seen_body_content {
1479 if ctxt.head.is_null() {
1480 ensure_head(ctxt);
1481 }
1482
1483 if is_empty {
1484 let node = tree::new_node(ptr::null_mut(), bytes_to_xmlstr(tag_name));
1486 if !node.is_null() {
1487 for attr in attrs {
1488 let name_c = bytes_to_xmlstr(&attr.name);
1489 let val_c = bytes_to_xmlstr(&attr.value);
1490 if !name_c.is_null() {
1491 tree::set_prop(node, name_c, val_c);
1492 xmlFree(name_c as *mut c_void);
1493 if !val_c.is_null() {
1494 xmlFree(val_c as *mut c_void);
1495 }
1496 }
1497 }
1498 tree::add_child(ctxt.current, node);
1499 }
1500 return;
1501 }
1502
1503 let node = tree::new_node(ptr::null_mut(), bytes_to_xmlstr(tag_name));
1504 if !node.is_null() {
1505 for attr in attrs {
1506 let name_c = bytes_to_xmlstr(&attr.name);
1507 let val_c = bytes_to_xmlstr(&attr.value);
1508 if !name_c.is_null() {
1509 tree::set_prop(node, name_c, val_c);
1510 xmlFree(name_c as *mut c_void);
1511 if !val_c.is_null() {
1512 xmlFree(val_c as *mut c_void);
1513 }
1514 }
1515 }
1516 tree::add_child(ctxt.current, node);
1517 ctxt.current = node;
1518 }
1519 return;
1520 }
1521
1522 if !is_head_tag || ctxt.seen_body_content {
1524 if !ctxt.seen_body_content {
1525 ctxt.seen_body_content = true;
1526 ctxt.in_head = false;
1527 if ctxt.body.is_null() {
1528 ensure_body(ctxt);
1529 } else {
1530 ctxt.current = ctxt.body;
1531 ctxt.in_body = true;
1532 }
1533 } else if ctxt.body.is_null() {
1534 ensure_body(ctxt);
1535 }
1536 }
1537
1538 if !ctxt.current.is_null() {
1540 auto_close_element(ctxt, tag_str);
1541 }
1542
1543 if is_empty {
1544 let node = tree::new_node(ptr::null_mut(), bytes_to_xmlstr(tag_name));
1546 if !node.is_null() {
1547 for attr in attrs {
1548 let name_c = bytes_to_xmlstr(&attr.name);
1549 let val_c = bytes_to_xmlstr(&attr.value);
1550 if !name_c.is_null() {
1551 tree::set_prop(node, name_c, val_c);
1552 xmlFree(name_c as *mut c_void);
1553 if !val_c.is_null() {
1554 xmlFree(val_c as *mut c_void);
1555 }
1556 }
1557 }
1558 let insertion_point = if ctxt.current.is_null() {
1559 ctxt.body
1560 } else {
1561 ctxt.current
1562 };
1563 if !insertion_point.is_null() {
1564 tree::add_child(insertion_point, node);
1565 }
1566 }
1567 return;
1568 }
1569
1570 let node = tree::new_node(ptr::null_mut(), bytes_to_xmlstr(tag_name));
1572 if !node.is_null() {
1573 for attr in attrs {
1574 let name_c = bytes_to_xmlstr(&attr.name);
1575 let val_c = bytes_to_xmlstr(&attr.value);
1576 if !name_c.is_null() {
1577 tree::set_prop(node, name_c, val_c);
1578 xmlFree(name_c as *mut c_void);
1579 if !val_c.is_null() {
1580 xmlFree(val_c as *mut c_void);
1581 }
1582 }
1583 }
1584
1585 let insertion_point = if ctxt.current.is_null() {
1586 if ctxt.in_body || ctxt.body_created {
1587 ctxt.body
1588 } else if ctxt.in_head || ctxt.head_created {
1589 ctxt.head
1590 } else if ctxt.html_created {
1591 ctxt.html
1592 } else {
1593 ctxt.doc as *mut _xmlNode
1594 }
1595 } else {
1596 ctxt.current
1597 };
1598
1599 if !insertion_point.is_null() {
1600 tree::add_child(insertion_point, node);
1601 ctxt.current = node;
1603 }
1604 }
1605}
1606
1607unsafe fn handle_end_tag(ctxt: &mut HtmlParserCtxt, tag_name: &[u8]) {
1609 let tag_lower: Vec<u8> = tag_name.iter().map(|b| b.to_ascii_lowercase()).collect();
1610 let tag_str = core::str::from_utf8(&tag_lower).unwrap_or("");
1611
1612 let info = html_tag_lookup(tag_str);
1613
1614 if info.map_or(false, |i| i.flags & HTML_EMPTY != 0) {
1617 return;
1618 }
1619
1620 if tag_str == "html" {
1621 ctxt.current = ctxt.doc as *mut _xmlNode;
1622 return;
1623 }
1624
1625 if tag_str == "head" {
1626 ctxt.in_head = false;
1627 ctxt.current = ctxt.html;
1628 return;
1629 }
1630
1631 if tag_str == "body" {
1632 ctxt.in_body = false;
1633 ctxt.current = ctxt.html;
1634 return;
1635 }
1636
1637 let mut cur = ctxt.current;
1639 while !cur.is_null() {
1640 let ctype = unsafe { (*cur).type_ };
1641 if ctype == XML_ELEMENT_NODE as c_int {
1642 if !unsafe { (*cur).name.is_null() } {
1643 let name_bytes = unsafe { xmlstr_to_bytes((*cur).name) };
1644 if name_bytes.eq_ignore_ascii_case(tag_name) {
1645 ctxt.current = unsafe { (*cur).parent };
1647 return;
1648 }
1649 }
1650 }
1651 cur = unsafe { (*cur).parent };
1652 }
1653
1654 }
1656
1657unsafe fn html_parse_buffer(
1667 ctxt: &mut HtmlParserCtxt,
1668 buffer: *const c_char,
1669 size: c_int,
1670) -> *mut _xmlDoc {
1671 if buffer.is_null() || size <= 0 {
1672 return ptr::null_mut();
1673 }
1674
1675 let doc = tree::new_doc(b"1.0\0" as *const u8 as *const xmlChar);
1677 if doc.is_null() {
1678 return ptr::null_mut();
1679 }
1680 unsafe {
1681 (*doc).type_ = XML_HTML_DOCUMENT_NODE as c_int;
1682 (*doc).properties = XML_DOC_WELLFORMED as c_int;
1683 }
1684 ctxt.doc = doc;
1685
1686 ctxt.input = buffer as *mut u8;
1688 ctxt.input_len = size as usize;
1689 ctxt.input_pos = 0;
1690 ctxt.line = 1;
1691
1692 loop {
1694 if ctxt.is_eof() {
1695 break;
1696 }
1697
1698 let ch = ctxt.peek().unwrap_or(0);
1699
1700 if ch == b'<' {
1701 ctxt.next(); if ctxt.peek() == Some(b'/') {
1705 ctxt.next(); let tag_name = ctxt.read_while(|ch| {
1707 ch != b'>' && ch != b' ' && ch != b'\t' && ch != b'\n' && ch != b'\r'
1708 });
1709
1710 while ctxt.peek() != Some(b'>') && !ctxt.is_eof() {
1712 ctxt.next();
1713 }
1714 if ctxt.peek() == Some(b'>') {
1715 ctxt.next(); }
1717
1718 if !tag_name.is_empty() {
1719 handle_end_tag(ctxt, &tag_name);
1720 }
1721 continue;
1722 }
1723
1724 if ctxt.peek() == Some(b'!')
1726 && ctxt.peek_at(1) == Some(b'-')
1727 && ctxt.peek_at(2) == Some(b'-')
1728 {
1729 ctxt.next(); ctxt.next(); ctxt.next(); let mut comment_content = Vec::new();
1735 loop {
1736 if ctxt.peek() == Some(b'-')
1737 && ctxt.peek_at(1) == Some(b'-')
1738 && ctxt.peek_at(2) == Some(b'>')
1739 {
1740 ctxt.next(); ctxt.next(); ctxt.next(); break;
1744 }
1745 match ctxt.next() {
1746 Some(ch) => comment_content.push(ch),
1747 None => break,
1748 }
1749 }
1750
1751 if !comment_content.is_empty() {
1753 let comment_node = tree::new_comment(bytes_to_xmlstr(&comment_content));
1754 if !comment_node.is_null() {
1755 let insertion_point = if !ctxt.current.is_null() {
1756 ctxt.current
1757 } else {
1758 ctxt.doc as *mut _xmlNode
1759 };
1760 tree::add_child(insertion_point, comment_node);
1761 }
1762 }
1763 continue;
1764 }
1765
1766 if ctxt.peek() == Some(b'!') {
1768 ctxt.next(); let rest = ctxt.read_while(|ch| ch != b'>');
1770 if ctxt.peek() == Some(b'>') {
1771 ctxt.next(); }
1773 continue;
1776 }
1777
1778 if ctxt.peek() == Some(b'?') {
1780 ctxt.next(); let mut pi_content = Vec::new();
1783 loop {
1784 if ctxt.peek() == Some(b'?') && ctxt.peek_at(1) == Some(b'>') {
1785 break;
1786 }
1787 match ctxt.next() {
1788 Some(ch) => pi_content.push(ch),
1789 None => break,
1790 }
1791 }
1792 if ctxt.peek() == Some(b'?') {
1794 ctxt.next();
1795 }
1796 if ctxt.peek() == Some(b'>') {
1797 ctxt.next();
1798 }
1799 if !pi_content.is_empty() {
1801 let mut parts = pi_content.splitn(2, |b| *b == b' ');
1803 let target = parts.next().unwrap_or(&pi_content);
1804 let value = parts.next().unwrap_or(b"");
1805
1806 let pi_node = tree::new_pi(bytes_to_xmlstr(target), bytes_to_xmlstr(value));
1807 if !pi_node.is_null() {
1808 let insertion_point = if !ctxt.current.is_null() {
1809 ctxt.current
1810 } else {
1811 ctxt.doc as *mut _xmlNode
1812 };
1813 tree::add_child(insertion_point, pi_node);
1814 }
1815 }
1816 continue;
1817 }
1818
1819 let tag_name = ctxt.read_while(|ch| {
1821 ch != b'>' && ch != b'/' && ch != b' ' && ch != b'\t' && ch != b'\n' && ch != b'\r'
1822 });
1823
1824 if tag_name.is_empty() {
1825 handle_text(ctxt, &[b'<']);
1827 continue;
1828 }
1829
1830 let attrs = parse_attributes(ctxt);
1832
1833 if ctxt.peek() == Some(b'/') {
1835 ctxt.next(); if ctxt.peek() == Some(b'>') {
1837 ctxt.next(); }
1839 } else if ctxt.peek() == Some(b'>') {
1840 ctxt.next(); }
1842
1843 let tag_lower: Vec<u8> = tag_name.iter().map(|b| b.to_ascii_lowercase()).collect();
1845 let tag_str = core::str::from_utf8(&tag_lower).unwrap_or("");
1846
1847 if tag_str == "script" || tag_str == "style" {
1848 let raw_node = tree::new_node(ptr::null_mut(), bytes_to_xmlstr(&tag_name));
1851 if !raw_node.is_null() {
1852 for attr in &attrs {
1853 let name_c = bytes_to_xmlstr(&attr.name);
1854 let val_c = bytes_to_xmlstr(&attr.value);
1855 if !name_c.is_null() {
1856 tree::set_prop(raw_node, name_c, val_c);
1857 xmlFree(name_c as *mut c_void);
1858 if !val_c.is_null() {
1859 xmlFree(val_c as *mut c_void);
1860 }
1861 }
1862 }
1863
1864 let insertion_point = if ctxt.current.is_null() {
1865 if ctxt.in_head {
1866 ensure_head(ctxt);
1867 ctxt.head
1868 } else {
1869 ensure_body(ctxt);
1870 ctxt.body
1871 }
1872 } else {
1873 ctxt.current
1874 };
1875
1876 if !insertion_point.is_null() {
1877 tree::add_child(insertion_point, raw_node);
1878
1879 let end_tag = format!("</{}", tag_str);
1881 let end_bytes = end_tag.as_bytes();
1882 let mut raw_text = Vec::new();
1883 let mut match_idx = 0;
1884
1885 loop {
1886 if ctxt.is_eof() {
1887 break;
1888 }
1889 let ch = ctxt.peek().unwrap();
1890 if ch.to_ascii_lowercase() == end_bytes[match_idx] {
1891 match_idx += 1;
1892 if match_idx == end_bytes.len() {
1893 if !raw_text.is_empty() {
1896 let text_node = tree::new_text(bytes_to_xmlstr(&raw_text));
1897 if !text_node.is_null() {
1898 tree::add_child(raw_node, text_node);
1899 }
1900 }
1901 ctxt.next(); let _suffix = ctxt.read_while(|ch| ch != b'>');
1905 if ctxt.peek() == Some(b'>') {
1906 ctxt.next();
1907 }
1908 ctxt.current = unsafe { (*raw_node).parent };
1910 break;
1911 }
1912 if match_idx == 1 {
1914 raw_text.push(ch);
1915 }
1916 ctxt.next();
1917 } else {
1918 if match_idx > 0 {
1920 match_idx = 0;
1922 }
1923 raw_text.push(ch);
1924 ctxt.next();
1925 }
1926 }
1927
1928 if match_idx < end_bytes.len() && !raw_text.is_empty() {
1930 let text_node = tree::new_text(bytes_to_xmlstr(&raw_text));
1931 if !text_node.is_null() {
1932 tree::add_child(raw_node, text_node);
1933 }
1934 ctxt.current = unsafe { (*raw_node).parent };
1935 }
1936 }
1937 }
1938 continue;
1939 }
1940
1941 handle_start_tag(ctxt, &tag_name, &attrs);
1943 } else {
1944 let mut text = Vec::new();
1946 loop {
1947 match ctxt.peek() {
1948 Some(b'<') => break,
1949 Some(b'&') => {
1950 let entity_text = parse_entity(ctxt);
1952 text.extend_from_slice(&entity_text);
1953 }
1954 Some(ch) => {
1955 text.push(ch);
1956 ctxt.next();
1957 }
1958 None => break,
1959 }
1960 }
1961
1962 if !text.is_empty() {
1963 handle_text(ctxt, &text);
1964 }
1965 }
1966 }
1967
1968 if ctxt.html.is_null() {
1970 ensure_html(ctxt);
1971 }
1972
1973 doc
1974}
1975
1976pub(crate) unsafe fn parse_file(filename: *const c_char, encoding: *const c_char) -> *mut _xmlDoc {
1991 if filename.is_null() {
1992 return ptr::null_mut();
1993 }
1994
1995 let filename_str = unsafe { std::ffi::CStr::from_ptr(filename) };
1997 let path = filename_str.to_str().unwrap_or("");
1998 let content = match std::fs::read(path) {
1999 Ok(data) => data,
2000 Err(_) => return ptr::null_mut(),
2001 };
2002
2003 let mut ctxt = HtmlParserCtxt::new();
2004 if !encoding.is_null() {
2005 let enc_cstr = unsafe { std::ffi::CStr::from_ptr(encoding) };
2006 ctxt.encoding = unsafe { c_strdup(encoding) };
2007 }
2008
2009 let doc = unsafe {
2010 html_parse_buffer(
2011 &mut ctxt,
2012 content.as_ptr() as *const c_char,
2013 content.len() as c_int,
2014 )
2015 };
2016
2017 if !doc.is_null() && !filename.is_null() {
2018 unsafe {
2019 (*doc).URL = c_strdup(filename) as *mut xmlChar;
2020 }
2021 }
2022
2023 doc
2024}
2025
2026pub(crate) unsafe fn parse_memory(buffer: *const c_char, size: c_int) -> *mut _xmlDoc {
2037 if buffer.is_null() || size <= 0 {
2038 return ptr::null_mut();
2039 }
2040
2041 let mut ctxt = HtmlParserCtxt::new();
2042 unsafe { html_parse_buffer(&mut ctxt, buffer, size) }
2043}
2044
2045pub(crate) unsafe fn parse_doc(cur: *const xmlChar, encoding: *const c_char) -> *mut _xmlDoc {
2056 if cur.is_null() {
2057 return ptr::null_mut();
2058 }
2059
2060 let len = unsafe { xml_strlen(cur) };
2061 let mut ctxt = HtmlParserCtxt::new();
2062 if !encoding.is_null() {
2063 ctxt.encoding = unsafe { c_strdup(encoding) };
2064 }
2065
2066 unsafe { html_parse_buffer(&mut ctxt, cur as *const c_char, len as c_int) }
2067}
2068
2069pub(crate) unsafe fn create_file_parser_ctxt(
2080 filename: *const c_char,
2081 encoding: *const c_char,
2082) -> *mut c_void {
2083 if filename.is_null() {
2084 return ptr::null_mut();
2085 }
2086
2087 let ctxt = unsafe { xmlMallocZero(size_of::<HtmlParserCtxt>() as usize) };
2088 if ctxt.is_null() {
2089 return ptr::null_mut();
2090 }
2091
2092 let ctxt = ctxt as *mut HtmlParserCtxt;
2093 unsafe {
2094 ptr::write(ctxt, HtmlParserCtxt::new());
2095 if !encoding.is_null() {
2096 (*ctxt).encoding = c_strdup(encoding);
2097 }
2098 }
2099
2100 ctxt as *mut c_void
2101}
2102
2103pub(crate) unsafe fn free_parser_ctxt(ctxt: *mut c_void) {
2113 if ctxt.is_null() {
2114 return;
2115 }
2116
2117 let ctxt = ctxt as *mut HtmlParserCtxt;
2118 unsafe {
2119 if !(*ctxt).filename.is_null() {
2120 xmlFree((*ctxt).filename as *mut c_void);
2121 }
2122 if !(*ctxt).encoding.is_null() {
2123 xmlFree((*ctxt).encoding as *mut c_void);
2124 }
2125 xmlFree(ctxt as *mut c_void);
2126 }
2127}
2128
2129pub(crate) fn init_parser() {
2135 }
2138
2139pub(crate) fn cleanup_parser() {
2145 }
2148
2149pub(crate) unsafe fn new_doc(version: *const xmlChar) -> *mut _xmlDoc {
2162 let doc = tree::new_doc(version);
2163 if doc.is_null() {
2164 return ptr::null_mut();
2165 }
2166
2167 unsafe {
2168 (*doc).type_ = XML_HTML_DOCUMENT_NODE as c_int;
2169 (*doc).properties = XML_DOC_WELLFORMED as c_int;
2170 }
2171
2172 let mut ctxt = HtmlParserCtxt::new();
2174 ctxt.doc = doc;
2175
2176 unsafe {
2177 ensure_html(&mut ctxt);
2178 ensure_head(&mut ctxt);
2179 ensure_body(&mut ctxt);
2180 }
2181
2182 doc
2183}
2184
2185pub(crate) unsafe fn new_doc_no_dtd(version: *const xmlChar) -> *mut _xmlDoc {
2198 let doc = tree::new_doc(version);
2199 if doc.is_null() {
2200 return ptr::null_mut();
2201 }
2202
2203 unsafe {
2204 (*doc).type_ = XML_HTML_DOCUMENT_NODE as c_int;
2205 (*doc).properties = XML_DOC_WELLFORMED as c_int;
2206 }
2207
2208 doc
2209}
2210
2211const HTML_VOID_ELEMENTS: &[&str] = &[
2217 "area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "param", "source",
2218 "track", "wbr", "frame",
2219];
2220
2221fn is_html_void(name: &str) -> bool {
2223 HTML_VOID_ELEMENTS
2224 .iter()
2225 .any(|v| v.eq_ignore_ascii_case(name))
2226}
2227
2228fn has_optional_end_tag(name: &str) -> bool {
2230 matches!(
2231 name.to_ascii_lowercase().as_str(),
2232 "p" | "li" | "tr" | "td" | "th" | "dt" | "dd"
2233 )
2234}
2235
2236unsafe fn html_serialize_text(buf: *mut _xmlBuffer, content: *const xmlChar, len: c_int) {
2241 if buf.is_null() || content.is_null() || len <= 0 {
2242 return;
2243 }
2244
2245 let mut i: c_int = 0;
2246 while i < len {
2247 let ch = unsafe { *content.add(i as usize) };
2248
2249 match ch {
2250 b'<' => {
2251 io::buf_add(buf, b"<" as *const u8, 4);
2252 }
2253 b'&' => {
2254 io::buf_add(buf, b"&" as *const u8, 5);
2255 }
2256 _ => {
2257 io::buf_add(buf, &ch as *const u8, 1);
2258 }
2259 }
2260 i += 1;
2261 }
2262}
2263
2264unsafe fn html_serialize_attr_value(buf: *mut _xmlBuffer, value: *const xmlChar) {
2268 if buf.is_null() || value.is_null() {
2269 return;
2270 }
2271
2272 let len = unsafe { xml_strlen(value) as c_int };
2273 let mut i: c_int = 0;
2274 while i < len {
2275 let ch = unsafe { *value.add(i as usize) };
2276
2277 match ch {
2278 b'&' => {
2279 io::buf_add(buf, b"&" as *const u8, 5);
2280 }
2281 b'"' => {
2282 io::buf_add(buf, b""" as *const u8, 6);
2283 }
2284 _ => {
2285 io::buf_add(buf, &ch as *const u8, 1);
2286 }
2287 }
2288 i += 1;
2289 }
2290}
2291
2292pub(crate) unsafe fn serialize_node(
2302 node: *mut _xmlNode,
2303 buf: *mut _xmlBuffer,
2304 format: c_int,
2305 level: c_int,
2306) {
2307 if node.is_null() || buf.is_null() {
2308 return;
2309 }
2310
2311 let n = unsafe { &*node };
2312
2313 match n.type_ {
2314 t if t == XML_ELEMENT_NODE as c_int => {
2315 let name = if n.name.is_null() {
2316 ""
2317 } else {
2318 unsafe { core::str::from_utf8(xmlstr_to_bytes(n.name)).unwrap_or("") }
2319 };
2320
2321 let is_void = is_html_void(name);
2322
2323 if format != 0 && level > 0 {
2325 io::buf_ccat(buf, b'\n');
2326 for _ in 0..level {
2327 io::buf_add(buf, b" " as *const u8, 2);
2328 }
2329 }
2330
2331 io::buf_ccat(buf, b'<');
2333 if !n.name.is_null() {
2334 io::buf_cat(buf, n.name);
2335 }
2336
2337 let mut attr = n.properties;
2339 while !attr.is_null() {
2340 let a = unsafe { &*attr };
2341 io::buf_ccat(buf, b' ');
2342 if !a.name.is_null() {
2343 io::buf_cat(buf, a.name);
2344 }
2345
2346 if !a.children.is_null() {
2348 let child = unsafe { &*a.children };
2349 if child.type_ == XML_TEXT_NODE as c_int && !child.content.is_null() {
2350 io::buf_ccat(buf, b'=');
2351 io::buf_ccat(buf, b'"');
2352 html_serialize_attr_value(buf, child.content);
2353 io::buf_ccat(buf, b'"');
2354 }
2355 }
2356
2357 attr = a.next;
2358 }
2359
2360 if is_void {
2361 io::buf_ccat(buf, b'>');
2363 } else if n.children.is_null() {
2364 io::buf_ccat(buf, b'>');
2366 if format != 0 {
2368 io::buf_ccat(buf, b'\n');
2369 for _ in 0..level {
2370 io::buf_add(buf, b" " as *const u8, 2);
2371 }
2372 }
2373 io::buf_add(buf, b"</" as *const u8, 2);
2374 if !n.name.is_null() {
2375 io::buf_cat(buf, n.name);
2376 }
2377 io::buf_ccat(buf, b'>');
2378 } else {
2379 io::buf_ccat(buf, b'>');
2381
2382 let is_text_only = n.children == n.last
2384 && !n.children.is_null()
2385 && unsafe { (*(n.children)).type_ } == XML_TEXT_NODE as c_int;
2386
2387 if is_text_only {
2388 let mut child = n.children;
2390 while !child.is_null() {
2391 serialize_node(child, buf, format, level + 1);
2392 child = unsafe { (*child).next };
2393 }
2394 } else {
2395 let mut child = n.children;
2397 while !child.is_null() {
2398 serialize_node(child, buf, format, level + 1);
2399 child = unsafe { (*child).next };
2400 }
2401
2402 if format != 0 {
2404 io::buf_ccat(buf, b'\n');
2405 for _ in 0..level {
2406 io::buf_add(buf, b" " as *const u8, 2);
2407 }
2408 }
2409 }
2410
2411 io::buf_add(buf, b"</" as *const u8, 2);
2413 if !n.name.is_null() {
2414 io::buf_cat(buf, n.name);
2415 }
2416 io::buf_ccat(buf, b'>');
2417 }
2418 }
2419 t if t == XML_TEXT_NODE as c_int => {
2420 html_serialize_text(buf, n.content, xml_strlen(n.content) as c_int);
2421 }
2422 t if t == XML_CDATA_SECTION_NODE as c_int => {
2423 io::buf_add(buf, b"<![CDATA[" as *const u8, 9);
2424 html_serialize_text(buf, n.content, xml_strlen(n.content) as c_int);
2425 io::buf_add(buf, b"]]>" as *const u8, 3);
2426 }
2427 t if t == XML_COMMENT_NODE as c_int => {
2428 if format != 0 && level > 0 {
2429 io::buf_ccat(buf, b'\n');
2430 for _ in 0..level {
2431 io::buf_add(buf, b" " as *const u8, 2);
2432 }
2433 }
2434 io::buf_add(buf, b"<!--" as *const u8, 4);
2435 if !n.content.is_null() {
2436 io::buf_cat(buf, n.content);
2437 }
2438 io::buf_add(buf, b"-->" as *const u8, 3);
2439 }
2440 t if t == XML_PI_NODE as c_int => {
2441 if format != 0 && level > 0 {
2442 io::buf_ccat(buf, b'\n');
2443 for _ in 0..level {
2444 io::buf_add(buf, b" " as *const u8, 2);
2445 }
2446 }
2447 io::buf_add(buf, b"<?" as *const u8, 2);
2448 if !n.name.is_null() {
2449 io::buf_cat(buf, n.name);
2450 }
2451 if !n.content.is_null() && unsafe { *n.content != 0 } {
2452 io::buf_ccat(buf, b' ');
2453 io::buf_cat(buf, n.content);
2454 }
2455 io::buf_add(buf, b"?>" as *const u8, 2);
2456 }
2457 t if t == XML_DOCUMENT_NODE as c_int || t == XML_HTML_DOCUMENT_NODE as c_int => {
2458 let mut child = n.children;
2461 while !child.is_null() {
2462 serialize_node(child, buf, format, 0);
2463 child = unsafe { (*child).next };
2464 }
2465 if format != 0 {
2466 io::buf_ccat(buf, b'\n');
2467 }
2468 }
2469 _ => {
2470 if !n.content.is_null() {
2471 html_serialize_text(buf, n.content, xml_strlen(n.content) as c_int);
2472 }
2473 }
2474 }
2475}
2476
2477pub(crate) unsafe fn doc_dump(buf: *mut _xmlBuffer, doc: *mut _xmlDoc) -> c_int {
2484 if buf.is_null() || doc.is_null() {
2485 return -1;
2486 }
2487
2488 let before = io::buf_length(buf);
2489 serialize_node(doc as *mut _xmlNode, buf, 0, 0);
2490 let after = io::buf_length(buf);
2491
2492 if after < 0 || before < 0 {
2493 return -1;
2494 }
2495 after - before
2496}
2497
2498#[cfg(test)]
2503mod tests {
2504 use super::*;
2505 use crate::abi::allocator::xmlFree;
2506 use crate::xml::io;
2507
2508 unsafe fn to_xmlstr(s: &[u8]) -> *mut xmlChar {
2510 bytes_to_xmlstr(s)
2511 }
2512
2513 unsafe fn html_doc_to_string(doc: *mut _xmlDoc) -> String {
2515 let buf = io::buf_create(-1);
2516 assert!(!buf.is_null());
2517 doc_dump(buf, doc);
2518 let content = io::buf_content(buf);
2519 let s = if !content.is_null() {
2520 let len = xml_strlen(content);
2521 let slice = slice::from_raw_parts(content, len as usize);
2522 String::from_utf8_lossy(slice).to_string()
2523 } else {
2524 String::new()
2525 };
2526 io::buf_free(buf);
2527 s
2528 }
2529
2530 #[test]
2535 fn test_html_tag_lookup() {
2536 assert!(html_tag_lookup("html").is_some());
2538 assert!(html_tag_lookup("HTML").is_some()); assert!(html_tag_lookup("p").is_some());
2540 assert!(html_tag_lookup("br").is_some());
2541 assert!(html_tag_lookup("div").is_some());
2542 assert!(html_tag_lookup("script").is_some());
2543
2544 assert!(html_tag_lookup("custom").is_none());
2546 assert!(html_tag_lookup("my-element").is_none());
2547 }
2548
2549 #[test]
2550 fn test_tag_flags() {
2551 let br = html_tag_lookup("br").unwrap();
2552 assert!(br.flags & HTML_INLINE != 0);
2553 assert!(br.flags & HTML_EMPTY != 0);
2554
2555 let div = html_tag_lookup("div").unwrap();
2556 assert!(div.flags & HTML_BLOCK != 0);
2557 assert!(div.flags & HTML_VALID != 0);
2558
2559 let p = html_tag_lookup("p").unwrap();
2560 assert!(p.flags & HTML_NO_END != 0);
2561
2562 let meta = html_tag_lookup("meta").unwrap();
2563 assert!(meta.flags & HTML_HEAD != 0);
2564 assert!(meta.flags & HTML_EMPTY != 0);
2565 }
2566
2567 #[test]
2572 fn test_html_entity_lookup() {
2573 assert_eq!(html_entity_lookup("amp"), Some("&"));
2574 assert_eq!(html_entity_lookup("lt"), Some("<"));
2575 assert_eq!(html_entity_lookup("gt"), Some(">"));
2576 assert_eq!(html_entity_lookup("quot"), Some("\""));
2577 assert_eq!(html_entity_lookup("nbsp"), Some("\u{00a0}"));
2578 assert_eq!(html_entity_lookup("copy"), Some("\u{00a9}"));
2579 assert!(html_entity_lookup("unknown_entity").is_none());
2580 }
2581
2582 #[test]
2587 fn test_parse_basic_html() {
2588 unsafe {
2589 let html = b"<html><head><title>Test</title></head><body><p>Hello</p></body></html>\0";
2590 let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
2591 assert!(!doc.is_null());
2592
2593 let s = html_doc_to_string(doc);
2594 assert!(s.contains("<html>"));
2595 assert!(s.contains("<head>"));
2596 assert!(s.contains("<title>Test</title>"));
2597 assert!(s.contains("<body>"));
2598 assert!(s.contains("<p>Hello</p>"));
2599
2600 tree::free_doc(doc);
2601 }
2602 }
2603
2604 #[test]
2605 fn test_parse_empty_document() {
2606 unsafe {
2607 let html = b"\0";
2608 let doc = parse_memory(html.as_ptr() as *const c_char, 0);
2609 assert!(doc.is_null());
2610 }
2611 }
2612
2613 #[test]
2618 fn test_implicit_html_head_body() {
2619 unsafe {
2620 let html = b"<p>Hello</p>\0";
2622 let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
2623 assert!(!doc.is_null());
2624
2625 let s = html_doc_to_string(doc);
2626 assert!(s.contains("<html>"));
2628 assert!(s.contains("<body>"));
2630 assert!(s.contains("<p>Hello</p>"));
2632
2633 tree::free_doc(doc);
2634 }
2635 }
2636
2637 #[test]
2638 fn test_implicit_head_with_title() {
2639 unsafe {
2640 let html = b"<title>My Page</title>\0";
2642 let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
2643 assert!(!doc.is_null());
2644
2645 let s = html_doc_to_string(doc);
2646 assert!(s.contains("<html>"));
2647 assert!(s.contains("<head>"));
2648 assert!(s.contains("<title>My Page</title>"));
2649
2650 tree::free_doc(doc);
2651 }
2652 }
2653
2654 #[test]
2659 fn test_auto_close_p() {
2660 unsafe {
2661 let html = b"<p>First<p>Second</p>\0";
2663 let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
2664 assert!(!doc.is_null());
2665
2666 let s = html_doc_to_string(doc);
2667 let first_pos = s.find("First");
2669 let second_pos = s.find("Second");
2670 assert!(first_pos.is_some());
2671 assert!(second_pos.is_some());
2672
2673 tree::free_doc(doc);
2674 }
2675 }
2676
2677 #[test]
2678 fn test_auto_close_heading() {
2679 unsafe {
2680 let html = b"<h1>Title</h1><h2>Subtitle</h2>\0";
2682 let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
2683 assert!(!doc.is_null());
2684
2685 let s = html_doc_to_string(doc);
2686 assert!(s.contains("<h1>Title</h1>"));
2687 assert!(s.contains("<h2>Subtitle</h2>"));
2688
2689 tree::free_doc(doc);
2690 }
2691 }
2692
2693 #[test]
2698 fn test_void_elements() {
2699 unsafe {
2700 let html = b"<br><hr><img src=\"test.jpg\"><input type=\"text\">\0";
2701 let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
2702 assert!(!doc.is_null());
2703
2704 let s = html_doc_to_string(doc);
2705 assert!(s.contains("<br>"));
2706 assert!(s.contains("<hr>"));
2707 assert!(s.contains("<img"));
2708 assert!(s.contains("<input"));
2709
2710 assert!(!s.contains("</br>"));
2712 assert!(!s.contains("</hr>"));
2713 assert!(!s.contains("</img>"));
2714
2715 tree::free_doc(doc);
2716 }
2717 }
2718
2719 #[test]
2724 fn test_unquoted_attributes() {
2725 unsafe {
2726 let html = b"<div class=main id=content>Text</div>\0";
2727 let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
2728 assert!(!doc.is_null());
2729
2730 let s = html_doc_to_string(doc);
2731 assert!(s.contains("class=\"main\""));
2732 assert!(s.contains("id=\"content\""));
2733
2734 tree::free_doc(doc);
2735 }
2736 }
2737
2738 #[test]
2739 fn test_minimized_attributes() {
2740 unsafe {
2741 let html = b"<option selected disabled>Value</option>\0";
2742 let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
2743 assert!(!doc.is_null());
2744
2745 let s = html_doc_to_string(doc);
2746 assert!(s.contains("selected"));
2748 assert!(s.contains("disabled"));
2749
2750 tree::free_doc(doc);
2751 }
2752 }
2753
2754 #[test]
2759 fn test_html_entities() {
2760 unsafe {
2761 let html = b"<p>& < > " ©</p>\0";
2762 let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
2763 assert!(!doc.is_null());
2764
2765 let s = html_doc_to_string(doc);
2766 assert!(s.contains("&")); assert!(s.contains("<")); assert!(s.contains(">")); assert!(s.contains("\u{00a0}")); tree::free_doc(doc);
2774 }
2775 }
2776
2777 #[test]
2778 fn test_numeric_entities() {
2779 unsafe {
2780 let html = b"<p>A A</p>\0";
2782 let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
2783 assert!(!doc.is_null());
2784
2785 let s = html_doc_to_string(doc);
2786 assert!(s.contains('A'));
2787
2788 tree::free_doc(doc);
2789 }
2790 }
2791
2792 #[test]
2797 fn test_nested_elements() {
2798 unsafe {
2799 let html = b"<div><ul><li>Item 1</li><li>Item 2</li></ul></div>\0";
2800 let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
2801 assert!(!doc.is_null());
2802
2803 let s = html_doc_to_string(doc);
2804 assert!(s.contains("<div>"));
2805 assert!(s.contains("<ul>"));
2806 assert!(s.contains("<li>Item 1</li>"));
2807 assert!(s.contains("<li>Item 2</li>"));
2808
2809 tree::free_doc(doc);
2810 }
2811 }
2812
2813 #[test]
2818 fn test_missing_end_tags() {
2819 unsafe {
2820 let html = b"<p>Paragraph without closing<div>Another div\0";
2822 let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
2823 assert!(!doc.is_null());
2824
2825 let s = html_doc_to_string(doc);
2826 assert!(s.contains("Paragraph without closing"));
2827 assert!(s.contains("Another div"));
2828
2829 tree::free_doc(doc);
2830 }
2831 }
2832
2833 #[test]
2834 fn test_mismatched_case() {
2835 unsafe {
2836 let html = b"<HTML><HEAD><TITLE>Test</TITLE></HEAD><BODY><P>Hello</P></BODY></HTML>\0";
2837 let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
2838 assert!(!doc.is_null());
2839
2840 let s = html_doc_to_string(doc);
2841 assert!(s.contains("<HTML>"));
2843 assert!(s.contains("<HEAD>"));
2844 assert!(s.contains("<BODY>"));
2845 assert!(s.contains("<P>Hello</P>"));
2846
2847 tree::free_doc(doc);
2848 }
2849 }
2850
2851 #[test]
2852 fn test_nested_malformed() {
2853 unsafe {
2854 let html = b"<div><p><span><b>Deep text</div></p>\0";
2856 let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
2857 assert!(!doc.is_null());
2858
2859 let s = html_doc_to_string(doc);
2860 assert!(s.contains("Deep text"));
2861
2862 tree::free_doc(doc);
2863 }
2864 }
2865
2866 #[test]
2871 fn test_serialization_round_trip_simple() {
2872 unsafe {
2873 let original = b"<p>Hello World</p>\0";
2874 let doc = parse_memory(
2875 original.as_ptr() as *const c_char,
2876 (original.len() - 1) as c_int,
2877 );
2878 assert!(!doc.is_null());
2879
2880 let s = html_doc_to_string(doc);
2881 assert!(s.contains("Hello World"));
2882
2883 tree::free_doc(doc);
2884 }
2885 }
2886
2887 #[test]
2888 fn test_serialize_void_elements_no_self_close() {
2889 unsafe {
2890 let html = b"<br><hr><img src=\"test.png\">\0";
2891 let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
2892 assert!(!doc.is_null());
2893
2894 let s = html_doc_to_string(doc);
2895 assert!(!s.contains("<br/>"));
2897 assert!(!s.contains("<hr/>"));
2898
2899 tree::free_doc(doc);
2900 }
2901 }
2902
2903 #[test]
2908 fn test_script_content() {
2909 unsafe {
2910 let html = b"<script>var x = 1;</script>\0";
2912 let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
2913 assert!(!doc.is_null());
2914
2915 let s = html_doc_to_string(doc);
2916 assert!(s.contains("<script>"));
2917 assert!(s.contains("var x = 1;"));
2919
2920 tree::free_doc(doc);
2921 }
2922 }
2923
2924 #[test]
2929 fn test_html_comment() {
2930 unsafe {
2931 let html = b"<html><!-- This is a comment --><body><p>Text</p></body></html>\0";
2932 let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
2933 assert!(!doc.is_null());
2934
2935 let s = html_doc_to_string(doc);
2936 assert!(s.contains("<!-- This is a comment -->"));
2937
2938 tree::free_doc(doc);
2939 }
2940 }
2941
2942 #[test]
2947 fn test_new_doc_creates_html_head_body() {
2948 unsafe {
2949 let doc = new_doc(ptr::null());
2950 assert!(!doc.is_null());
2951 assert_eq!((*doc).type_, XML_HTML_DOCUMENT_NODE as c_int);
2952
2953 let s = html_doc_to_string(doc);
2954 assert!(s.contains("<html>"));
2955 assert!(s.contains("<head>"));
2956 assert!(s.contains("<body>"));
2957
2958 tree::free_doc(doc);
2959 }
2960 }
2961
2962 #[test]
2963 fn test_new_doc_no_dtd() {
2964 unsafe {
2965 let doc = new_doc_no_dtd(ptr::null());
2966 assert!(!doc.is_null());
2967 assert_eq!((*doc).type_, XML_HTML_DOCUMENT_NODE as c_int);
2968
2969 let s = html_doc_to_string(doc);
2971 assert_eq!(s, "");
2973
2974 tree::free_doc(doc);
2975 }
2976 }
2977
2978 #[test]
2983 fn test_resolve_numeric_entity() {
2984 assert_eq!(resolve_numeric_entity("65", false), vec![b'A']);
2985 assert_eq!(resolve_numeric_entity("41", true), vec![b'A']);
2986 assert_eq!(resolve_numeric_entity("0", false), Vec::<u8>::new());
2987 }
2988
2989 #[test]
2990 fn test_resolve_entity_unknown() {
2991 let result = resolve_entity("unknown");
2992 assert_eq!(result, b"&unknown;");
2993 }
2994
2995 #[test]
3000 fn test_init_cleanup_parser() {
3001 init_parser();
3003 cleanup_parser();
3004 }
3005
3006 #[test]
3011 fn test_create_free_parser_ctxt() {
3012 unsafe {
3013 let ctxt =
3014 create_file_parser_ctxt(b"test.html\0" as *const u8 as *const c_char, ptr::null());
3015 assert!(!ctxt.is_null());
3016 free_parser_ctxt(ctxt);
3017 }
3018 }
3019
3020 #[test]
3025 fn test_complex_html_document() {
3026 unsafe {
3027 let html = b"<!DOCTYPE html>
3028<html>
3029<head>
3030 <meta charset=\"utf-8\">
3031 <title>Test Page</title>
3032 <link rel=\"stylesheet\" href=\"style.css\">
3033</head>
3034<body>
3035 <div id=\"main\">
3036 <h1>Title</h1>
3037 <p>First paragraph with <a href=\"link.html\">a link</a>.</p>
3038 <p>Second paragraph.</p>
3039 <ul>
3040 <li>Item 1</li>
3041 <li>Item 2</li>
3042 </ul>
3043 <br>
3044 <hr>
3045 <img src=\"image.jpg\" alt=\"An image\">
3046 </div>
3047 <script>alert('hello');</script>
3048</body>
3049</html>\0";
3050 let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
3051 assert!(!doc.is_null());
3052
3053 let s = html_doc_to_string(doc);
3054 assert!(s.contains("<html>"));
3055 assert!(s.contains("<head>"));
3056 assert!(s.contains("<title>Test Page</title>"));
3057 assert!(s.contains("<body>"));
3058 assert!(s.contains("<h1>Title</h1>"));
3059 assert!(s.contains("a link"));
3060 assert!(s.contains("Second paragraph"));
3061 assert!(s.contains("<br>"));
3062 assert!(s.contains("<hr>"));
3063 assert!(s.contains("<img"));
3064 assert!(s.contains("<script>"));
3065
3066 tree::free_doc(doc);
3067 }
3068 }
3069
3070 #[test]
3075 fn test_parse_doc() {
3076 unsafe {
3077 let html = b"<p>Hello from parse_doc</p>";
3078 let doc = parse_doc(html.as_ptr() as *const xmlChar, ptr::null());
3079 assert!(!doc.is_null());
3080
3081 let s = html_doc_to_string(doc);
3082 assert!(s.contains("Hello from parse_doc"));
3083
3084 tree::free_doc(doc);
3085 }
3086 }
3087
3088 #[test]
3093 fn test_table_element_auto_close() {
3094 unsafe {
3095 let html = b"<table><tr><td>Cell 1<td>Cell 2</td></tr></table>";
3096 let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
3097 assert!(!doc.is_null());
3098
3099 let s = html_doc_to_string(doc);
3100 assert!(s.contains("<td>Cell 1"));
3101 assert!(s.contains("<td>Cell 2"));
3102
3103 tree::free_doc(doc);
3104 }
3105 }
3106}