1use core::ffi::c_void;
73use core::ptr;
74use core::slice;
75use std::os::raw::{c_char, c_int};
76
77use crate::abi::allocator::{xmlFreeImpl, xmlMallocZero};
78use crate::abi::structs::*;
79use crate::abi::types::xmlDocProperties::XML_DOC_WELLFORMED;
80use crate::abi::types::xmlElementType::*;
81use crate::abi::types::*;
82use crate::xml::io;
83use crate::xml::string::*;
84use crate::xml::tree;
85
86const HTML_INLINE: u32 = 0x1;
92const HTML_BLOCK: u32 = 0x2;
93const HTML_EMPTY: u32 = 0x4;
94#[allow(dead_code)]
95const HTML_DEPRECATED: u32 = 0x8;
96const HTML_OL: u32 = 0x10;
97const HTML_DL: u32 = 0x20;
98#[allow(dead_code)]
99const HTML_COMPACT: u32 = 0x40;
100const HTML_HEAD: u32 = 0x80;
101const HTML_BODY: u32 = 0x100;
102#[allow(dead_code)]
103const HTML_HEADSTRUCK: u32 = 0x200;
104const HTML_VALID: u32 = 0x400;
105const HTML_NO_END: u32 = 0x800; #[allow(dead_code)]
107const HTML_IMPLIED: u32 = 0x1000; #[derive(Clone, Copy)]
111struct HtmlElementInfo {
112 name: &'static str,
113 flags: u32,
114}
115
116const HTML_ELEMENTS: &[HtmlElementInfo] = &[
119 HtmlElementInfo {
121 name: "br",
122 flags: HTML_INLINE | HTML_EMPTY | HTML_VALID,
123 },
124 HtmlElementInfo {
125 name: "hr",
126 flags: HTML_BLOCK | HTML_EMPTY | HTML_VALID,
127 },
128 HtmlElementInfo {
129 name: "img",
130 flags: HTML_INLINE | HTML_EMPTY | HTML_VALID,
131 },
132 HtmlElementInfo {
133 name: "input",
134 flags: HTML_INLINE | HTML_EMPTY | HTML_VALID,
135 },
136 HtmlElementInfo {
137 name: "meta",
138 flags: HTML_HEAD | HTML_EMPTY | HTML_VALID,
139 },
140 HtmlElementInfo {
141 name: "link",
142 flags: HTML_HEAD | HTML_EMPTY | HTML_VALID,
143 },
144 HtmlElementInfo {
145 name: "base",
146 flags: HTML_HEAD | HTML_EMPTY | HTML_VALID,
147 },
148 HtmlElementInfo {
149 name: "area",
150 flags: HTML_INLINE | HTML_EMPTY | HTML_VALID,
151 },
152 HtmlElementInfo {
153 name: "col",
154 flags: HTML_BLOCK | HTML_EMPTY | HTML_VALID,
155 },
156 HtmlElementInfo {
157 name: "embed",
158 flags: HTML_INLINE | HTML_EMPTY | HTML_VALID,
159 },
160 HtmlElementInfo {
161 name: "param",
162 flags: HTML_HEAD | HTML_EMPTY | HTML_VALID,
163 },
164 HtmlElementInfo {
165 name: "source",
166 flags: HTML_INLINE | HTML_EMPTY | HTML_VALID,
167 },
168 HtmlElementInfo {
169 name: "track",
170 flags: HTML_INLINE | HTML_EMPTY | HTML_VALID,
171 },
172 HtmlElementInfo {
173 name: "wbr",
174 flags: HTML_INLINE | HTML_EMPTY | HTML_VALID,
175 },
176 HtmlElementInfo {
178 name: "html",
179 flags: HTML_BLOCK | HTML_VALID,
180 },
181 HtmlElementInfo {
182 name: "head",
183 flags: HTML_HEAD | HTML_BLOCK | HTML_VALID,
184 },
185 HtmlElementInfo {
186 name: "body",
187 flags: HTML_BODY | HTML_BLOCK | HTML_VALID,
188 },
189 HtmlElementInfo {
190 name: "div",
191 flags: HTML_BLOCK | HTML_VALID,
192 },
193 HtmlElementInfo {
194 name: "p",
195 flags: HTML_BLOCK | HTML_VALID | HTML_NO_END,
196 },
197 HtmlElementInfo {
198 name: "h1",
199 flags: HTML_BLOCK | HTML_VALID,
200 },
201 HtmlElementInfo {
202 name: "h2",
203 flags: HTML_BLOCK | HTML_VALID,
204 },
205 HtmlElementInfo {
206 name: "h3",
207 flags: HTML_BLOCK | HTML_VALID,
208 },
209 HtmlElementInfo {
210 name: "h4",
211 flags: HTML_BLOCK | HTML_VALID,
212 },
213 HtmlElementInfo {
214 name: "h5",
215 flags: HTML_BLOCK | HTML_VALID,
216 },
217 HtmlElementInfo {
218 name: "h6",
219 flags: HTML_BLOCK | HTML_VALID,
220 },
221 HtmlElementInfo {
222 name: "ul",
223 flags: HTML_BLOCK | HTML_VALID | HTML_OL,
224 },
225 HtmlElementInfo {
226 name: "ol",
227 flags: HTML_BLOCK | HTML_VALID | HTML_OL,
228 },
229 HtmlElementInfo {
230 name: "li",
231 flags: HTML_BLOCK | HTML_VALID | HTML_NO_END,
232 },
233 HtmlElementInfo {
234 name: "dl",
235 flags: HTML_BLOCK | HTML_VALID | HTML_DL,
236 },
237 HtmlElementInfo {
238 name: "dt",
239 flags: HTML_BLOCK | HTML_VALID | HTML_NO_END,
240 },
241 HtmlElementInfo {
242 name: "dd",
243 flags: HTML_BLOCK | HTML_VALID | HTML_NO_END,
244 },
245 HtmlElementInfo {
246 name: "table",
247 flags: HTML_BLOCK | HTML_VALID,
248 },
249 HtmlElementInfo {
250 name: "tr",
251 flags: HTML_BLOCK | HTML_VALID | HTML_NO_END,
252 },
253 HtmlElementInfo {
254 name: "td",
255 flags: HTML_BLOCK | HTML_VALID | HTML_NO_END,
256 },
257 HtmlElementInfo {
258 name: "th",
259 flags: HTML_BLOCK | HTML_VALID | HTML_NO_END,
260 },
261 HtmlElementInfo {
262 name: "thead",
263 flags: HTML_BLOCK | HTML_VALID,
264 },
265 HtmlElementInfo {
266 name: "tbody",
267 flags: HTML_BLOCK | HTML_VALID,
268 },
269 HtmlElementInfo {
270 name: "tfoot",
271 flags: HTML_BLOCK | HTML_VALID,
272 },
273 HtmlElementInfo {
274 name: "colgroup",
275 flags: HTML_BLOCK | HTML_VALID,
276 },
277 HtmlElementInfo {
278 name: "caption",
279 flags: HTML_BLOCK | HTML_VALID,
280 },
281 HtmlElementInfo {
282 name: "form",
283 flags: HTML_BLOCK | HTML_VALID,
284 },
285 HtmlElementInfo {
286 name: "fieldset",
287 flags: HTML_BLOCK | HTML_VALID,
288 },
289 HtmlElementInfo {
290 name: "legend",
291 flags: HTML_BLOCK | HTML_VALID,
292 },
293 HtmlElementInfo {
294 name: "pre",
295 flags: HTML_BLOCK | HTML_VALID,
296 },
297 HtmlElementInfo {
298 name: "blockquote",
299 flags: HTML_BLOCK | HTML_VALID,
300 },
301 HtmlElementInfo {
302 name: "address",
303 flags: HTML_BLOCK | HTML_VALID,
304 },
305 HtmlElementInfo {
306 name: "center",
307 flags: HTML_BLOCK | HTML_VALID,
308 },
309 HtmlElementInfo {
310 name: "dir",
311 flags: HTML_BLOCK | HTML_VALID,
312 },
313 HtmlElementInfo {
314 name: "menu",
315 flags: HTML_BLOCK | HTML_VALID,
316 },
317 HtmlElementInfo {
318 name: "noscript",
319 flags: HTML_BLOCK | HTML_VALID,
320 },
321 HtmlElementInfo {
322 name: "frameset",
323 flags: HTML_BLOCK | HTML_VALID,
324 },
325 HtmlElementInfo {
326 name: "frame",
327 flags: HTML_BLOCK | HTML_EMPTY | HTML_VALID,
328 },
329 HtmlElementInfo {
330 name: "iframe",
331 flags: HTML_BLOCK | HTML_VALID,
332 },
333 HtmlElementInfo {
334 name: "noframes",
335 flags: HTML_BLOCK | HTML_VALID,
336 },
337 HtmlElementInfo {
339 name: "a",
340 flags: HTML_INLINE | HTML_VALID,
341 },
342 HtmlElementInfo {
343 name: "abbr",
344 flags: HTML_INLINE | HTML_VALID,
345 },
346 HtmlElementInfo {
347 name: "acronym",
348 flags: HTML_INLINE | HTML_VALID,
349 },
350 HtmlElementInfo {
351 name: "b",
352 flags: HTML_INLINE | HTML_VALID,
353 },
354 HtmlElementInfo {
355 name: "basefont",
356 flags: HTML_INLINE | HTML_EMPTY | HTML_VALID,
357 },
358 HtmlElementInfo {
359 name: "bdo",
360 flags: HTML_INLINE | HTML_VALID,
361 },
362 HtmlElementInfo {
363 name: "big",
364 flags: HTML_INLINE | HTML_VALID,
365 },
366 HtmlElementInfo {
367 name: "cite",
368 flags: HTML_INLINE | HTML_VALID,
369 },
370 HtmlElementInfo {
371 name: "code",
372 flags: HTML_INLINE | HTML_VALID,
373 },
374 HtmlElementInfo {
375 name: "dfn",
376 flags: HTML_INLINE | HTML_VALID,
377 },
378 HtmlElementInfo {
379 name: "em",
380 flags: HTML_INLINE | HTML_VALID,
381 },
382 HtmlElementInfo {
383 name: "font",
384 flags: HTML_INLINE | HTML_VALID,
385 },
386 HtmlElementInfo {
387 name: "i",
388 flags: HTML_INLINE | HTML_VALID,
389 },
390 HtmlElementInfo {
391 name: "kbd",
392 flags: HTML_INLINE | HTML_VALID,
393 },
394 HtmlElementInfo {
395 name: "label",
396 flags: HTML_INLINE | HTML_VALID,
397 },
398 HtmlElementInfo {
399 name: "map",
400 flags: HTML_INLINE | HTML_VALID,
401 },
402 HtmlElementInfo {
403 name: "nobr",
404 flags: HTML_INLINE | HTML_VALID,
405 },
406 HtmlElementInfo {
407 name: "object",
408 flags: HTML_INLINE | HTML_VALID,
409 },
410 HtmlElementInfo {
411 name: "q",
412 flags: HTML_INLINE | HTML_VALID,
413 },
414 HtmlElementInfo {
415 name: "rb",
416 flags: HTML_INLINE | HTML_VALID,
417 },
418 HtmlElementInfo {
419 name: "rbc",
420 flags: HTML_INLINE | HTML_VALID,
421 },
422 HtmlElementInfo {
423 name: "rp",
424 flags: HTML_INLINE | HTML_VALID,
425 },
426 HtmlElementInfo {
427 name: "rt",
428 flags: HTML_INLINE | HTML_VALID,
429 },
430 HtmlElementInfo {
431 name: "rtc",
432 flags: HTML_INLINE | HTML_VALID,
433 },
434 HtmlElementInfo {
435 name: "ruby",
436 flags: HTML_INLINE | HTML_VALID,
437 },
438 HtmlElementInfo {
439 name: "s",
440 flags: HTML_INLINE | HTML_VALID,
441 },
442 HtmlElementInfo {
443 name: "samp",
444 flags: HTML_INLINE | HTML_VALID,
445 },
446 HtmlElementInfo {
447 name: "select",
448 flags: HTML_INLINE | HTML_VALID,
449 },
450 HtmlElementInfo {
451 name: "small",
452 flags: HTML_INLINE | HTML_VALID,
453 },
454 HtmlElementInfo {
455 name: "span",
456 flags: HTML_INLINE | HTML_VALID,
457 },
458 HtmlElementInfo {
459 name: "strike",
460 flags: HTML_INLINE | HTML_VALID,
461 },
462 HtmlElementInfo {
463 name: "strong",
464 flags: HTML_INLINE | HTML_VALID,
465 },
466 HtmlElementInfo {
467 name: "sub",
468 flags: HTML_INLINE | HTML_VALID,
469 },
470 HtmlElementInfo {
471 name: "sup",
472 flags: HTML_INLINE | HTML_VALID,
473 },
474 HtmlElementInfo {
475 name: "textarea",
476 flags: HTML_INLINE | HTML_VALID,
477 },
478 HtmlElementInfo {
479 name: "tt",
480 flags: HTML_INLINE | HTML_VALID,
481 },
482 HtmlElementInfo {
483 name: "u",
484 flags: HTML_INLINE | HTML_VALID,
485 },
486 HtmlElementInfo {
487 name: "var",
488 flags: HTML_INLINE | HTML_VALID,
489 },
490 HtmlElementInfo {
492 name: "header",
493 flags: HTML_BLOCK | HTML_VALID,
494 },
495 HtmlElementInfo {
496 name: "footer",
497 flags: HTML_BLOCK | HTML_VALID,
498 },
499 HtmlElementInfo {
500 name: "nav",
501 flags: HTML_BLOCK | HTML_VALID,
502 },
503 HtmlElementInfo {
504 name: "article",
505 flags: HTML_BLOCK | HTML_VALID,
506 },
507 HtmlElementInfo {
508 name: "section",
509 flags: HTML_BLOCK | HTML_VALID,
510 },
511 HtmlElementInfo {
512 name: "aside",
513 flags: HTML_BLOCK | HTML_VALID,
514 },
515 HtmlElementInfo {
516 name: "main",
517 flags: HTML_BLOCK | HTML_VALID,
518 },
519 HtmlElementInfo {
520 name: "figure",
521 flags: HTML_BLOCK | HTML_VALID,
522 },
523 HtmlElementInfo {
524 name: "figcaption",
525 flags: HTML_BLOCK | HTML_VALID,
526 },
527 HtmlElementInfo {
528 name: "details",
529 flags: HTML_BLOCK | HTML_VALID,
530 },
531 HtmlElementInfo {
532 name: "summary",
533 flags: HTML_BLOCK | HTML_VALID,
534 },
535 HtmlElementInfo {
537 name: "script",
538 flags: HTML_HEAD | HTML_BLOCK | HTML_VALID,
539 },
540 HtmlElementInfo {
541 name: "style",
542 flags: HTML_HEAD | HTML_BLOCK | HTML_VALID,
543 },
544 HtmlElementInfo {
545 name: "title",
546 flags: HTML_HEAD | HTML_BLOCK | HTML_VALID,
547 },
548];
549
550fn html_tag_lookup(name: &str) -> Option<&'static HtmlElementInfo> {
553 let lower: Vec<u8> = name.bytes().map(|b| b.to_ascii_lowercase()).collect();
555 let lower_str = match core::str::from_utf8(&lower) {
556 Ok(s) => s,
557 Err(_) => return None,
558 };
559 HTML_ELEMENTS.iter().find(|info| info.name == lower_str)
560}
561
562const HTML_ENTITIES: &[(&str, &str)] = &[
569 ("nbsp", "\u{00a0}"),
570 ("lt", "<"),
571 ("gt", ">"),
572 ("amp", "&"),
573 ("quot", "\""),
574 ("apos", "'"),
575 ("copy", "\u{00a9}"),
576 ("reg", "\u{00ae}"),
577 ("amp", "&"),
578 ("iexcl", "\u{00a1}"),
579 ("cent", "\u{00a2}"),
580 ("pound", "\u{00a3}"),
581 ("curren", "\u{00a4}"),
582 ("yen", "\u{00a5}"),
583 ("brvbar", "\u{00a6}"),
584 ("sect", "\u{00a7}"),
585 ("uml", "\u{00a8}"),
586 ("ordf", "\u{00aa}"),
587 ("laquo", "\u{00ab}"),
588 ("not", "\u{00ac}"),
589 ("shy", "\u{00ad}"),
590 ("macr", "\u{00ae}"),
591 ("deg", "\u{00b0}"),
592 ("plusmn", "\u{00b1}"),
593 ("sup2", "\u{00b2}"),
594 ("sup3", "\u{00b3}"),
595 ("acute", "\u{00b4}"),
596 ("micro", "\u{00b5}"),
597 ("para", "\u{00b6}"),
598 ("middot", "\u{00b7}"),
599 ("cedil", "\u{00b8}"),
600 ("sup1", "\u{00b9}"),
601 ("ordm", "\u{00ba}"),
602 ("raquo", "\u{00bb}"),
603 ("frac14", "\u{00bc}"),
604 ("frac12", "\u{00bd}"),
605 ("frac34", "\u{00be}"),
606 ("iquest", "\u{00bf}"),
607 ("times", "\u{00d7}"),
608 ("divide", "\u{00f7}"),
609 ("ETH", "\u{00d0}"),
610 ("eth", "\u{00f0}"),
611 ("THORN", "\u{00de}"),
612 ("thorn", "\u{00fe}"),
613 ("AElig", "\u{00c6}"),
614 ("aelig", "\u{00e6}"),
615 ("OElig", "\u{0152}"),
616 ("oelig", "\u{0153}"),
617 ("Scaron", "\u{0160}"),
618 ("scaron", "\u{0161}"),
619 ("Yuml", "\u{0178}"),
620 ("circ", "\u{02c6}"),
621 ("tilde", "\u{02dc}"),
622 ("ensp", "\u{2002}"),
623 ("emsp", "\u{2003}"),
624 ("thinsp", "\u{2009}"),
625 ("zwnj", "\u{200c}"),
626 ("zwj", "\u{200d}"),
627 ("lrm", "\u{200e}"),
628 ("rlm", "\u{200f}"),
629 ("ndash", "\u{2013}"),
630 ("mdash", "\u{2014}"),
631 ("lsquo", "\u{2018}"),
632 ("rsquo", "\u{2019}"),
633 ("sbquo", "\u{201a}"),
634 ("ldquo", "\u{201c}"),
635 ("rdquo", "\u{201d}"),
636 ("bdquo", "\u{201e}"),
637 ("dagger", "\u{2020}"),
638 ("Dagger", "\u{2021}"),
639 ("bull", "\u{2022}"),
640 ("hellip", "\u{2026}"),
641 ("permil", "\u{2030}"),
642 ("prime", "\u{2032}"),
643 ("Prime", "\u{2033}"),
644 ("lsaquo", "\u{2039}"),
645 ("rsaquo", "\u{203a}"),
646 ("oline", "\u{203e}"),
647 ("euro", "\u{20ac}"),
648 ("trade", "\u{2122}"),
649 ("larr", "\u{2190}"),
650 ("uarr", "\u{2191}"),
651 ("rarr", "\u{2192}"),
652 ("darr", "\u{2193}"),
653 ("harr", "\u{2194}"),
654 ("crarr", "\u{21b5}"),
655 ("lceil", "\u{2308}"),
656 ("rceil", "\u{2309}"),
657 ("lfloor", "\u{230a}"),
658 ("rfloor", "\u{230b}"),
659 ("loz", "\u{25ca}"),
660 ("spades", "\u{2660}"),
661 ("clubs", "\u{2663}"),
662 ("hearts", "\u{2665}"),
663 ("diams", "\u{2666}"),
664 ("Alpha", "\u{0391}"),
665 ("Beta", "\u{0392}"),
666 ("Gamma", "\u{0393}"),
667 ("Delta", "\u{0394}"),
668 ("Epsilon", "\u{0395}"),
669 ("Zeta", "\u{0396}"),
670 ("Eta", "\u{0397}"),
671 ("Theta", "\u{0398}"),
672 ("Iota", "\u{0399}"),
673 ("Kappa", "\u{039a}"),
674 ("Lambda", "\u{039b}"),
675 ("Mu", "\u{039c}"),
676 ("Nu", "\u{039d}"),
677 ("Xi", "\u{039e}"),
678 ("Omicron", "\u{039f}"),
679 ("Pi", "\u{03a0}"),
680 ("Rho", "\u{03a1}"),
681 ("Sigma", "\u{03a3}"),
682 ("Tau", "\u{03a4}"),
683 ("Upsilon", "\u{03a5}"),
684 ("Phi", "\u{03a6}"),
685 ("Chi", "\u{03a7}"),
686 ("Psi", "\u{03a8}"),
687 ("Omega", "\u{03a9}"),
688 ("alpha", "\u{03b1}"),
689 ("beta", "\u{03b2}"),
690 ("gamma", "\u{03b3}"),
691 ("delta", "\u{03b4}"),
692 ("epsilon", "\u{03b5}"),
693 ("zeta", "\u{03b6}"),
694 ("eta", "\u{03b7}"),
695 ("theta", "\u{03b8}"),
696 ("iota", "\u{03b9}"),
697 ("kappa", "\u{03ba}"),
698 ("lambda", "\u{03bb}"),
699 ("mu", "\u{03bc}"),
700 ("nu", "\u{03bd}"),
701 ("xi", "\u{03be}"),
702 ("omicron", "\u{03bf}"),
703 ("pi", "\u{03c0}"),
704 ("rho", "\u{03c1}"),
705 ("sigmaf", "\u{03c2}"),
706 ("sigma", "\u{03c3}"),
707 ("tau", "\u{03c4}"),
708 ("upsilon", "\u{03c5}"),
709 ("phi", "\u{03c6}"),
710 ("chi", "\u{03c7}"),
711 ("psi", "\u{03c8}"),
712 ("omega", "\u{03c9}"),
713 ("thetasym", "\u{03d1}"),
714 ("upsih", "\u{03d2}"),
715 ("piv", "\u{03d6}"),
716];
717
718fn html_entity_lookup(name: &str) -> Option<&'static str> {
721 HTML_ENTITIES
722 .iter()
723 .find(|(n, _)| *n == name)
724 .map(|(_, v)| *v)
725}
726
727struct HtmlParserCtxt {
733 doc: *mut _xmlDoc,
735 current: *mut _xmlNode,
737 html: *mut _xmlNode,
739 head: *mut _xmlNode,
741 body: *mut _xmlNode,
743 in_head: bool,
745 in_body: bool,
747 html_created: bool,
749 head_created: bool,
751 body_created: bool,
753 seen_body_content: bool,
755 input: *mut u8,
757 input_pos: usize,
759 input_len: usize,
761 line: c_int,
763 #[allow(dead_code)]
765 err: bool,
766 filename: *mut c_char,
768 encoding: *mut c_char,
770}
771
772impl HtmlParserCtxt {
773 const fn new() -> Self {
774 HtmlParserCtxt {
775 doc: ptr::null_mut(),
776 current: ptr::null_mut(),
777 html: ptr::null_mut(),
778 head: ptr::null_mut(),
779 body: ptr::null_mut(),
780 in_head: false,
781 in_body: false,
782 html_created: false,
783 head_created: false,
784 body_created: false,
785 seen_body_content: false,
786 input: ptr::null_mut(),
787 input_pos: 0,
788 input_len: 0,
789 line: 1,
790 err: false,
791 filename: ptr::null_mut(),
792 encoding: ptr::null_mut(),
793 }
794 }
795
796 fn peek(&self) -> Option<u8> {
798 if self.input_pos < self.input_len {
799 unsafe { Some(*self.input.add(self.input_pos)) }
800 } else {
801 None
802 }
803 }
804
805 fn peek_at(&self, offset: usize) -> Option<u8> {
807 let pos = self.input_pos + offset;
808 if pos < self.input_len {
809 unsafe { Some(*self.input.add(pos)) }
810 } else {
811 None
812 }
813 }
814
815 fn next(&mut self) -> Option<u8> {
817 if self.input_pos < self.input_len {
818 let ch = unsafe { *self.input.add(self.input_pos) };
819 self.input_pos += 1;
820 if ch == b'\n' {
821 self.line += 1;
822 }
823 Some(ch)
824 } else {
825 None
826 }
827 }
828
829 fn skip_while<F: Fn(u8) -> bool>(&mut self, f: F) {
831 while let Some(ch) = self.peek() {
832 if f(ch) {
833 self.next();
834 } else {
835 break;
836 }
837 }
838 }
839
840 fn skip_whitespace(&mut self) {
842 self.skip_while(|ch| ch == b' ' || ch == b'\t' || ch == b'\n' || ch == b'\r');
843 }
844
845 const fn is_eof(&self) -> bool {
847 self.input_pos >= self.input_len
848 }
849
850 fn read_while<F: Fn(u8) -> bool>(&mut self, f: F) -> Vec<u8> {
852 let start = self.input_pos;
853 while let Some(ch) = self.peek() {
854 if f(ch) {
855 self.next();
856 } else {
857 break;
858 }
859 }
860 unsafe { slice::from_raw_parts(self.input.add(start), self.input_pos - start).to_vec() }
861 }
862}
863
864fn is_heading(name: &str) -> bool {
870 matches!(name, "h1" | "h2" | "h3" | "h4" | "h5" | "h6")
871}
872
873#[allow(dead_code)]
876unsafe fn get_parent_element(node: *mut _xmlNode) -> *mut _xmlNode {
877 if node.is_null() {
878 return ptr::null_mut();
879 }
880 let mut n = node;
881 loop {
882 let parent = unsafe { (*n).parent };
883 if parent.is_null() {
884 return ptr::null_mut();
885 }
886 let ptype = unsafe { (*parent).type_ };
887 if ptype == XML_ELEMENT_NODE as c_int
888 || ptype == XML_HTML_DOCUMENT_NODE as c_int
889 || ptype == XML_DOCUMENT_NODE as c_int
890 {
891 return parent;
892 }
893 n = parent;
894 }
895}
896
897unsafe fn auto_close_element(ctxt: &mut HtmlParserCtxt, tag_name: &str) {
900 let tag_lower: Vec<u8> = tag_name.bytes().map(|b| b.to_ascii_lowercase()).collect();
901 let tag_lower_str = match core::str::from_utf8(&tag_lower) {
902 Ok(s) => s,
903 Err(_) => return,
904 };
905
906 let info = html_tag_lookup(tag_lower_str);
907
908 let mut current = ctxt.current;
909
910 let mut open_names: Vec<Vec<u8>> = Vec::new();
912 let mut cur = current;
913 while !cur.is_null() {
914 let ctype = unsafe { (*cur).type_ };
915 if ctype == XML_ELEMENT_NODE as c_int && !unsafe { (*cur).name.is_null() } {
916 let name_bytes = unsafe { xmlstr_to_bytes((*cur).name) };
917 open_names.push(name_bytes.to_vec());
918 }
919 cur = unsafe { (*cur).parent };
920 }
921
922 if tag_lower_str == "p" || info.is_some_and(|i| i.flags & HTML_BLOCK != 0) {
924 let mut cur2 = current;
926 while !cur2.is_null() {
927 let ctype = unsafe { (*cur2).type_ };
928 if ctype == XML_ELEMENT_NODE as c_int && !unsafe { (*cur2).name.is_null() } {
929 let name_bytes = unsafe { xmlstr_to_bytes((*cur2).name) };
930 let name_str = core::str::from_utf8(name_bytes).unwrap_or("");
931 if name_str.eq_ignore_ascii_case("p") {
932 current = unsafe { (*cur2).parent };
934 break;
935 }
936 }
937 cur2 = unsafe { (*cur2).parent };
938 }
939 }
940
941 if is_heading(tag_lower_str) {
943 let mut cur2 = current;
944 while !cur2.is_null() {
945 let ctype = unsafe { (*cur2).type_ };
946 if ctype == XML_ELEMENT_NODE as c_int && !unsafe { (*cur2).name.is_null() } {
947 let name_bytes = unsafe { xmlstr_to_bytes((*cur2).name) };
948 let name_str = core::str::from_utf8(name_bytes).unwrap_or("");
949 if is_heading(name_str) {
950 current = unsafe { (*cur2).parent };
951 break;
952 }
953 }
954 cur2 = unsafe { (*cur2).parent };
955 }
956 }
957
958 if tag_lower_str == "li" {
960 let mut cur2 = current;
961 while !cur2.is_null() {
962 let ctype = unsafe { (*cur2).type_ };
963 if ctype == XML_ELEMENT_NODE as c_int && !unsafe { (*cur2).name.is_null() } {
964 let name_bytes = unsafe { xmlstr_to_bytes((*cur2).name) };
965 let name_str = core::str::from_utf8(name_bytes).unwrap_or("");
966 if name_str.eq_ignore_ascii_case("li") {
967 current = unsafe { (*cur2).parent };
968 break;
969 }
970 }
971 cur2 = unsafe { (*cur2).parent };
972 }
973 }
974
975 if tag_lower_str == "dt" || tag_lower_str == "dd" {
977 let mut cur2 = current;
978 while !cur2.is_null() {
979 let ctype = unsafe { (*cur2).type_ };
980 if ctype == XML_ELEMENT_NODE as c_int && !unsafe { (*cur2).name.is_null() } {
981 let name_bytes = unsafe { xmlstr_to_bytes((*cur2).name) };
982 let name_str = core::str::from_utf8(name_bytes).unwrap_or("");
983 if name_str == "dt" || name_str == "dd" {
984 current = unsafe { (*cur2).parent };
985 break;
986 }
987 }
988 cur2 = unsafe { (*cur2).parent };
989 }
990 }
991
992 if tag_lower_str == "tr" {
997 let mut cur2 = current;
998 while !cur2.is_null() {
999 let ctype = unsafe { (*cur2).type_ };
1000 if ctype == XML_ELEMENT_NODE as c_int && !unsafe { (*cur2).name.is_null() } {
1001 let name_bytes = unsafe { xmlstr_to_bytes((*cur2).name) };
1002 let name_str = core::str::from_utf8(name_bytes).unwrap_or("");
1003 if name_str == "tr" || name_str == "td" || name_str == "th" {
1004 current = unsafe { (*cur2).parent };
1005 break;
1006 }
1007 }
1008 cur2 = unsafe { (*cur2).parent };
1009 }
1010 } else if tag_lower_str == "td" || tag_lower_str == "th" {
1011 let mut cur2 = current;
1012 while !cur2.is_null() {
1013 let ctype = unsafe { (*cur2).type_ };
1014 if ctype == XML_ELEMENT_NODE as c_int && !unsafe { (*cur2).name.is_null() } {
1015 let name_bytes = unsafe { xmlstr_to_bytes((*cur2).name) };
1016 let name_str = core::str::from_utf8(name_bytes).unwrap_or("");
1017 if name_str == "td" || name_str == "th" {
1018 current = unsafe { (*cur2).parent };
1019 break;
1020 }
1021 }
1022 cur2 = unsafe { (*cur2).parent };
1023 }
1024 }
1025
1026 if matches!(tag_lower_str, "thead" | "tbody" | "tfoot") {
1028 let mut cur2 = current;
1029 while !cur2.is_null() {
1030 let ctype = unsafe { (*cur2).type_ };
1031 if ctype == XML_ELEMENT_NODE as c_int && !unsafe { (*cur2).name.is_null() } {
1032 let name_bytes = unsafe { xmlstr_to_bytes((*cur2).name) };
1033 let name_str = core::str::from_utf8(name_bytes).unwrap_or("");
1034 if name_str == "thead" || name_str == "tbody" || name_str == "tfoot" {
1035 current = unsafe { (*cur2).parent };
1036 break;
1037 }
1038 }
1039 cur2 = unsafe { (*cur2).parent };
1040 }
1041 }
1042
1043 if tag_lower_str == "colgroup" {
1045 let mut cur2 = current;
1046 while !cur2.is_null() {
1047 let ctype = unsafe { (*cur2).type_ };
1048 if ctype == XML_ELEMENT_NODE as c_int && !unsafe { (*cur2).name.is_null() } {
1049 let name_bytes = unsafe { xmlstr_to_bytes((*cur2).name) };
1050 let name_str = core::str::from_utf8(name_bytes).unwrap_or("");
1051 if name_str == "colgroup" {
1052 current = unsafe { (*cur2).parent };
1053 break;
1054 }
1055 }
1056 cur2 = unsafe { (*cur2).parent };
1057 }
1058 }
1059
1060 if tag_lower_str == "caption" {
1062 let mut cur2 = current;
1063 while !cur2.is_null() {
1064 let ctype = unsafe { (*cur2).type_ };
1065 if ctype == XML_ELEMENT_NODE as c_int && !unsafe { (*cur2).name.is_null() } {
1066 let name_bytes = unsafe { xmlstr_to_bytes((*cur2).name) };
1067 let name_str = core::str::from_utf8(name_bytes).unwrap_or("");
1068 if name_str == "caption" {
1069 current = unsafe { (*cur2).parent };
1070 break;
1071 }
1072 }
1073 cur2 = unsafe { (*cur2).parent };
1074 }
1075 }
1076
1077 if tag_lower_str == "form" {
1079 let mut cur2 = current;
1080 while !cur2.is_null() {
1081 let ctype = unsafe { (*cur2).type_ };
1082 if ctype == XML_ELEMENT_NODE as c_int && !unsafe { (*cur2).name.is_null() } {
1083 let name_bytes = unsafe { xmlstr_to_bytes((*cur2).name) };
1084 let name_str = core::str::from_utf8(name_bytes).unwrap_or("");
1085 if name_str.eq_ignore_ascii_case("form") {
1086 current = unsafe { (*cur2).parent };
1087 break;
1088 }
1089 }
1090 cur2 = unsafe { (*cur2).parent };
1091 }
1092 }
1093
1094 ctxt.current = current;
1095}
1096
1097unsafe fn ensure_html(ctxt: &mut HtmlParserCtxt) -> *mut _xmlNode {
1103 if !ctxt.html.is_null() {
1104 return ctxt.html;
1105 }
1106
1107 let html_node = tree::new_node(ptr::null_mut(), b"html\0" as *const u8 as *const xmlChar);
1108 if html_node.is_null() {
1109 return ptr::null_mut();
1110 }
1111 {
1112 }
1115 ctxt.html = html_node;
1116 ctxt.html_created = true;
1117
1118 tree::add_child(ctxt.doc as *mut _xmlNode, html_node);
1120 ctxt.current = html_node;
1121
1122 html_node
1123}
1124
1125unsafe fn ensure_head(ctxt: &mut HtmlParserCtxt) -> *mut _xmlNode {
1127 if !ctxt.head.is_null() {
1128 return ctxt.head;
1129 }
1130
1131 ensure_html(ctxt);
1133
1134 let head_node = tree::new_node(ptr::null_mut(), b"head\0" as *const u8 as *const xmlChar);
1135 if head_node.is_null() {
1136 return ptr::null_mut();
1137 }
1138 ctxt.head = head_node;
1139 ctxt.head_created = true;
1140
1141 tree::add_child(ctxt.html, head_node);
1143 ctxt.current = head_node;
1144 ctxt.in_head = true;
1145
1146 head_node
1147}
1148
1149unsafe fn ensure_body(ctxt: &mut HtmlParserCtxt) -> *mut _xmlNode {
1151 if !ctxt.body.is_null() {
1152 return ctxt.body;
1153 }
1154
1155 ensure_html(ctxt);
1157
1158 let body_node = tree::new_node(ptr::null_mut(), b"body\0" as *const u8 as *const xmlChar);
1159 if body_node.is_null() {
1160 return ptr::null_mut();
1161 }
1162 ctxt.body = body_node;
1163 ctxt.body_created = true;
1164
1165 tree::add_child(ctxt.html, body_node);
1167 ctxt.current = body_node;
1168 ctxt.in_body = true;
1169
1170 body_node
1171}
1172
1173#[allow(dead_code)]
1175unsafe fn transition_to_body(ctxt: &mut HtmlParserCtxt) {
1176 if ctxt.in_head && !ctxt.seen_body_content {
1177 ctxt.seen_body_content = true;
1178 ctxt.in_head = false;
1179 ensure_body(ctxt);
1180 }
1181}
1182
1183struct HtmlAttr {
1189 name: Vec<u8>,
1190 value: Vec<u8>,
1191 #[allow(dead_code)]
1192 quoted: bool,
1193}
1194
1195fn parse_attr_name(ctxt: &mut HtmlParserCtxt) -> Vec<u8> {
1197 let mut name = Vec::new();
1198 while let Some(ch) = ctxt.peek() {
1199 if ch == b'='
1200 || ch == b'>'
1201 || ch == b'/'
1202 || ch == b' '
1203 || ch == b'\t'
1204 || ch == b'\n'
1205 || ch == b'\r'
1206 {
1207 break;
1208 }
1209 name.push(ch);
1210 ctxt.next();
1211 }
1212 name
1213}
1214
1215fn parse_attr_value(ctxt: &mut HtmlParserCtxt) -> (Vec<u8>, bool) {
1217 ctxt.skip_whitespace();
1218
1219 let quote = match ctxt.peek() {
1220 Some(b'"') => {
1221 ctxt.next(); b'"'
1223 }
1224 Some(b'\'') => {
1225 ctxt.next(); b'\''
1227 }
1228 _ => {
1229 let value = ctxt.read_while(|ch| {
1231 ch != b'>' && ch != b' ' && ch != b'\t' && ch != b'\n' && ch != b'\r'
1232 });
1233 return (value, false);
1234 }
1235 };
1236
1237 let mut value = Vec::new();
1239 loop {
1240 match ctxt.next() {
1241 Some(ch) if ch == quote => break,
1242 Some(ch) => value.push(ch),
1243 None => break,
1244 }
1245 }
1246 (value, true)
1247}
1248
1249fn parse_attributes(ctxt: &mut HtmlParserCtxt) -> Vec<HtmlAttr> {
1251 let mut attrs = Vec::new();
1252
1253 loop {
1254 ctxt.skip_whitespace();
1255
1256 match ctxt.peek() {
1257 Some(b'>') | None => break,
1258 Some(b'/')
1259 if ctxt.peek_at(1) == Some(b'>') => {
1261 break;
1262 }
1263 _ => {}
1265 }
1266
1267 let name = parse_attr_name(ctxt);
1268 if name.is_empty() {
1269 break;
1270 }
1271
1272 ctxt.skip_whitespace();
1274 if ctxt.peek() == Some(b'=') {
1275 ctxt.next(); let (value, quoted) = parse_attr_value(ctxt);
1277 attrs.push(HtmlAttr {
1278 name,
1279 value,
1280 quoted,
1281 });
1282 } else {
1283 attrs.push(HtmlAttr {
1285 name,
1286 value: Vec::new(),
1287 quoted: false,
1288 });
1289 }
1290 }
1291
1292 attrs
1293}
1294
1295fn resolve_entity(name: &str) -> Vec<u8> {
1301 if let Some(replacement) = html_entity_lookup(name) {
1302 replacement.as_bytes().to_vec()
1303 } else {
1304 let mut result = Vec::new();
1306 result.push(b'&');
1307 result.extend_from_slice(name.as_bytes());
1308 result.push(b';');
1309 result
1310 }
1311}
1312
1313fn resolve_numeric_entity(value: &str, is_hex: bool) -> Vec<u8> {
1315 let codepoint = if is_hex {
1316 u32::from_str_radix(value, 16).unwrap_or(0xFFFD)
1317 } else {
1318 value.parse::<u32>().unwrap_or(0xFFFD)
1319 };
1320
1321 if codepoint == 0 {
1322 return Vec::new();
1323 }
1324
1325 match char::from_u32(codepoint) {
1327 Some(c) => {
1328 let mut buf = [0u8; 4];
1329 let s = c.encode_utf8(&mut buf);
1330 s.as_bytes().to_vec()
1331 }
1332 None => vec![0xEF, 0xBF, 0xBD], }
1334}
1335
1336fn parse_entity(ctxt: &mut HtmlParserCtxt) -> Vec<u8> {
1339 match ctxt.peek() {
1341 Some(b'&') => {
1342 ctxt.next(); }
1344 _ => return vec![b'&'],
1345 }
1346
1347 if ctxt.peek() == Some(b'#') {
1349 ctxt.next(); let is_hex = ctxt.peek() == Some(b'x') || ctxt.peek() == Some(b'X');
1351 if is_hex {
1352 ctxt.next(); }
1354
1355 let digits = ctxt.read_while(|ch| {
1356 if is_hex {
1357 ch.is_ascii_hexdigit()
1358 } else {
1359 ch.is_ascii_digit()
1360 }
1361 });
1362
1363 let digits_str = core::str::from_utf8(&digits).unwrap_or("");
1364 if digits_str.is_empty() {
1365 let mut result = vec![b'&', b'#'];
1366 if is_hex {
1367 result.push(b'x');
1368 }
1369 return result;
1370 }
1371
1372 if ctxt.peek() == Some(b';') {
1374 ctxt.next();
1375 }
1376
1377 return resolve_numeric_entity(digits_str, is_hex);
1378 }
1379
1380 let name = ctxt.read_while(|ch| ch.is_ascii_alphanumeric() || ch == b'_' || ch == b'-');
1382 let name_str = core::str::from_utf8(&name).unwrap_or("");
1383
1384 if ctxt.peek() == Some(b';') {
1386 ctxt.next();
1387 }
1388
1389 resolve_entity(name_str)
1390}
1391
1392unsafe fn handle_text(ctxt: &mut HtmlParserCtxt, text: &[u8]) {
1394 if text.is_empty() {
1395 return;
1396 }
1397
1398 let parent = if ctxt.in_head {
1400 ctxt.head
1401 } else if ctxt.in_body || ctxt.body_created {
1402 ctxt.body
1403 } else if ctxt.html_created {
1404 ctxt.html
1405 } else {
1406 ctxt.doc as *mut _xmlNode
1407 };
1408
1409 let insertion_point = if ctxt.current.is_null() {
1410 parent
1411 } else {
1412 ctxt.current
1413 };
1414
1415 if insertion_point.is_null() {
1416 let text_node = tree::new_text(ptr::null_mut());
1418 if !text_node.is_null() {
1419 let content = bytes_to_xmlstr(text);
1421 if !content.is_null() {
1422 unsafe {
1423 (*text_node).content = content;
1424 }
1425 }
1426 tree::add_child(ctxt.doc as *mut _xmlNode, text_node);
1427 }
1428 return;
1429 }
1430
1431 if insertion_point == ctxt.doc as *mut _xmlNode {
1436 if text.iter().all(|b| b.is_ascii_whitespace()) {
1437 return;
1438 }
1439 let content = trim_ascii_start(text);
1440 let html_node = tree::new_node(ptr::null_mut(), bytes_to_xmlstr(b"html"));
1441 if !html_node.is_null() {
1442 let content_c = bytes_to_xmlstr(content);
1443 let text_node = tree::new_text(ptr::null_mut());
1444 if !text_node.is_null() {
1445 unsafe {
1446 (*text_node).content = content_c;
1447 }
1448 tree::add_child(html_node, text_node);
1449 }
1450 tree::add_child(ctxt.doc as *mut _xmlNode, html_node);
1451 ctxt.html = html_node;
1452 ctxt.html_created = true;
1453 ctxt.current = html_node;
1454 }
1455 return;
1456 }
1457
1458 let text_node = tree::new_text(ptr::null_mut());
1459 if text_node.is_null() {
1460 return;
1461 }
1462
1463 let content = bytes_to_xmlstr(text);
1465 if !content.is_null() {
1466 unsafe {
1467 (*text_node).content = content;
1468 }
1469 }
1470
1471 tree::add_child(insertion_point, text_node);
1472}
1473
1474unsafe fn handle_start_tag(ctxt: &mut HtmlParserCtxt, tag_name: &[u8], attrs: &[HtmlAttr]) {
1476 let tag_lower: Vec<u8> = tag_name.iter().map(|b| b.to_ascii_lowercase()).collect();
1477 let tag_str = core::str::from_utf8(&tag_lower).unwrap_or("");
1478
1479 let info = html_tag_lookup(tag_str);
1480
1481 let is_head_tag = info.is_some_and(|i| i.flags & HTML_HEAD != 0);
1483 let _is_body_tag = info.is_some_and(|i| i.flags & HTML_BODY != 0);
1484 let is_empty = info.is_some_and(|i| i.flags & HTML_EMPTY != 0);
1485 let _is_block = info.is_some_and(|i| i.flags & HTML_BLOCK != 0);
1486
1487 if tag_str == "html" {
1489 if !ctxt.html.is_null() && !ctxt.html_created {
1490 return;
1492 }
1493 if ctxt.html.is_null() {
1495 let html_node = tree::new_node(ptr::null_mut(), bytes_to_xmlstr(tag_name));
1496 if !html_node.is_null() {
1497 ctxt.html = html_node;
1498 ctxt.html_created = false; tree::add_child(ctxt.doc as *mut _xmlNode, html_node);
1500 ctxt.current = html_node;
1501 }
1502 } else {
1503 ctxt.current = ctxt.html;
1505 }
1506 return;
1507 }
1508
1509 if tag_str == "head" {
1510 if !ctxt.head.is_null() && !ctxt.head_created {
1511 return;
1513 }
1514 ensure_html(ctxt);
1516
1517 if ctxt.head.is_null() {
1518 let head_node = tree::new_node(ptr::null_mut(), bytes_to_xmlstr(tag_name));
1519 if !head_node.is_null() {
1520 ctxt.head = head_node;
1521 ctxt.head_created = false;
1522 tree::add_child(ctxt.html, head_node);
1523 ctxt.current = head_node;
1524 ctxt.in_head = true;
1525 }
1526 } else {
1527 ctxt.current = ctxt.head;
1528 ctxt.in_head = true;
1529 }
1530 return;
1531 }
1532
1533 if tag_str == "body" {
1534 if !ctxt.body.is_null() && !ctxt.body_created {
1535 return;
1537 }
1538 ensure_html(ctxt);
1540
1541 if ctxt.body.is_null() {
1542 let body_node = tree::new_node(ptr::null_mut(), bytes_to_xmlstr(tag_name));
1543 if !body_node.is_null() {
1544 ctxt.body = body_node;
1545 ctxt.body_created = false;
1546 tree::add_child(ctxt.html, body_node);
1547 ctxt.current = body_node;
1548 ctxt.in_body = true;
1549 ctxt.in_head = false;
1550 ctxt.seen_body_content = true;
1551 }
1552 } else {
1553 ctxt.current = ctxt.body;
1554 ctxt.in_body = true;
1555 ctxt.in_head = false;
1556 ctxt.seen_body_content = true;
1557 }
1558 return;
1559 }
1560
1561 if is_head_tag && !ctxt.seen_body_content {
1563 if ctxt.head.is_null() {
1564 ensure_head(ctxt);
1565 }
1566
1567 if is_empty {
1568 let node = tree::new_node(ptr::null_mut(), bytes_to_xmlstr(tag_name));
1570 if !node.is_null() {
1571 for attr in attrs {
1572 let name_c = bytes_to_xmlstr(&attr.name);
1573 let val_c = bytes_to_xmlstr(&attr.value);
1574 if !name_c.is_null() {
1575 tree::set_prop(node, name_c, val_c);
1576 xmlFreeImpl(name_c as *mut c_void);
1577 if !val_c.is_null() {
1578 xmlFreeImpl(val_c as *mut c_void);
1579 }
1580 }
1581 }
1582 tree::add_child(ctxt.current, node);
1583 }
1584 return;
1585 }
1586
1587 let node = tree::new_node(ptr::null_mut(), bytes_to_xmlstr(tag_name));
1588 if !node.is_null() {
1589 for attr in attrs {
1590 let name_c = bytes_to_xmlstr(&attr.name);
1591 let val_c = bytes_to_xmlstr(&attr.value);
1592 if !name_c.is_null() {
1593 tree::set_prop(node, name_c, val_c);
1594 xmlFreeImpl(name_c as *mut c_void);
1595 if !val_c.is_null() {
1596 xmlFreeImpl(val_c as *mut c_void);
1597 }
1598 }
1599 }
1600 tree::add_child(ctxt.current, node);
1601 ctxt.current = node;
1602 }
1603 return;
1604 }
1605
1606 if !is_head_tag || ctxt.seen_body_content {
1608 if !ctxt.seen_body_content {
1609 ctxt.seen_body_content = true;
1610 ctxt.in_head = false;
1611 if ctxt.body.is_null() {
1612 ensure_body(ctxt);
1613 } else {
1614 ctxt.current = ctxt.body;
1615 ctxt.in_body = true;
1616 }
1617 } else if ctxt.body.is_null() {
1618 ensure_body(ctxt);
1619 }
1620 }
1621
1622 if !ctxt.current.is_null() {
1624 auto_close_element(ctxt, tag_str);
1625 }
1626
1627 if is_empty {
1628 let node = tree::new_node(ptr::null_mut(), bytes_to_xmlstr(tag_name));
1630 if !node.is_null() {
1631 for attr in attrs {
1632 let name_c = bytes_to_xmlstr(&attr.name);
1633 let val_c = bytes_to_xmlstr(&attr.value);
1634 if !name_c.is_null() {
1635 tree::set_prop(node, name_c, val_c);
1636 xmlFreeImpl(name_c as *mut c_void);
1637 if !val_c.is_null() {
1638 xmlFreeImpl(val_c as *mut c_void);
1639 }
1640 }
1641 }
1642 let insertion_point = if ctxt.current.is_null() {
1643 ctxt.body
1644 } else {
1645 ctxt.current
1646 };
1647 if !insertion_point.is_null() {
1648 tree::add_child(insertion_point, node);
1649 }
1650 }
1651 return;
1652 }
1653
1654 let node = tree::new_node(ptr::null_mut(), bytes_to_xmlstr(tag_name));
1656 if !node.is_null() {
1657 for attr in attrs {
1658 let name_c = bytes_to_xmlstr(&attr.name);
1659 let val_c = bytes_to_xmlstr(&attr.value);
1660 if !name_c.is_null() {
1661 tree::set_prop(node, name_c, val_c);
1662 xmlFreeImpl(name_c as *mut c_void);
1663 if !val_c.is_null() {
1664 xmlFreeImpl(val_c as *mut c_void);
1665 }
1666 }
1667 }
1668
1669 let insertion_point = if ctxt.current.is_null() {
1670 if ctxt.in_body || ctxt.body_created {
1671 ctxt.body
1672 } else if ctxt.in_head || ctxt.head_created {
1673 ctxt.head
1674 } else if ctxt.html_created {
1675 ctxt.html
1676 } else {
1677 ctxt.doc as *mut _xmlNode
1678 }
1679 } else {
1680 ctxt.current
1681 };
1682
1683 if !insertion_point.is_null() {
1684 tree::add_child(insertion_point, node);
1685 ctxt.current = node;
1687 }
1688 }
1689}
1690
1691unsafe fn handle_end_tag(ctxt: &mut HtmlParserCtxt, tag_name: &[u8]) {
1693 let tag_lower: Vec<u8> = tag_name.iter().map(|b| b.to_ascii_lowercase()).collect();
1694 let tag_str = core::str::from_utf8(&tag_lower).unwrap_or("");
1695
1696 let info = html_tag_lookup(tag_str);
1697
1698 if info.is_some_and(|i| i.flags & HTML_EMPTY != 0) {
1701 return;
1702 }
1703
1704 if tag_str == "html" {
1705 ctxt.current = ctxt.doc as *mut _xmlNode;
1706 return;
1707 }
1708
1709 if tag_str == "head" {
1710 ctxt.in_head = false;
1711 ctxt.current = ctxt.html;
1712 return;
1713 }
1714
1715 if tag_str == "body" {
1716 ctxt.in_body = false;
1717 ctxt.current = ctxt.html;
1718 return;
1719 }
1720
1721 let mut cur = ctxt.current;
1723 while !cur.is_null() {
1724 let ctype = unsafe { (*cur).type_ };
1725 if ctype == XML_ELEMENT_NODE as c_int && !unsafe { (*cur).name.is_null() } {
1726 let name_bytes = unsafe { xmlstr_to_bytes((*cur).name) };
1727 if name_bytes.eq_ignore_ascii_case(tag_name) {
1728 ctxt.current = unsafe { (*cur).parent };
1730 return;
1731 }
1732 }
1733 cur = unsafe { (*cur).parent };
1734 }
1735
1736 }
1738
1739unsafe fn html_parse_buffer(
1749 ctxt: &mut HtmlParserCtxt,
1750 buffer: *const c_char,
1751 size: c_int,
1752) -> *mut _xmlDoc {
1753 if buffer.is_null() || size <= 0 {
1754 return ptr::null_mut();
1755 }
1756
1757 let doc = tree::new_doc(ptr::null());
1759 if doc.is_null() {
1760 return ptr::null_mut();
1761 }
1762 unsafe {
1763 (*doc).type_ = XML_HTML_DOCUMENT_NODE as c_int;
1764 if !(*doc).version.is_null() {
1767 crate::abi::allocator::xmlFreeImpl((*doc).version as *mut c_void);
1768 }
1769 (*doc).version = ptr::null_mut();
1770 (*doc).properties = XML_DOC_WELLFORMED as c_int;
1771 (*doc).standalone = 1;
1774 }
1775 let default_dtd = crate::xml::dtd::create_int_subset(
1781 doc,
1782 b"html\0" as *const u8 as *const xmlChar,
1783 b"-//W3C//DTD HTML 4.0 Transitional//EN\0" as *const u8 as *const xmlChar,
1784 b"http://www.w3.org/TR/REC-html40/loose.dtd\0" as *const u8 as *const xmlChar,
1785 );
1786 let _ = default_dtd;
1787 ctxt.doc = doc;
1788
1789 ctxt.input = buffer as *mut u8;
1791 ctxt.input_len = size as usize;
1792 ctxt.input_pos = 0;
1793 ctxt.line = 1;
1794
1795 loop {
1797 if ctxt.is_eof() {
1798 break;
1799 }
1800
1801 let ch = ctxt.peek().unwrap_or(0);
1802
1803 if ch == b'<' {
1804 ctxt.next(); if ctxt.peek() == Some(b'/') {
1808 ctxt.next(); let tag_name = ctxt.read_while(|ch| {
1810 ch != b'>' && ch != b' ' && ch != b'\t' && ch != b'\n' && ch != b'\r'
1811 });
1812
1813 while ctxt.peek() != Some(b'>') && !ctxt.is_eof() {
1815 ctxt.next();
1816 }
1817 if ctxt.peek() == Some(b'>') {
1818 ctxt.next(); }
1820
1821 if !tag_name.is_empty() {
1822 handle_end_tag(ctxt, &tag_name);
1823 }
1824 continue;
1825 }
1826
1827 if ctxt.peek() == Some(b'!')
1829 && ctxt.peek_at(1) == Some(b'-')
1830 && ctxt.peek_at(2) == Some(b'-')
1831 {
1832 ctxt.next(); ctxt.next(); ctxt.next(); let mut comment_content = Vec::new();
1838 loop {
1839 if ctxt.peek() == Some(b'-')
1840 && ctxt.peek_at(1) == Some(b'-')
1841 && ctxt.peek_at(2) == Some(b'>')
1842 {
1843 ctxt.next(); ctxt.next(); ctxt.next(); break;
1847 }
1848 match ctxt.next() {
1849 Some(ch) => comment_content.push(ch),
1850 None => break,
1851 }
1852 }
1853
1854 if !comment_content.is_empty() {
1856 let comment_node = tree::new_comment(bytes_to_xmlstr(&comment_content));
1857 if !comment_node.is_null() {
1858 let insertion_point = if !ctxt.current.is_null() {
1859 ctxt.current
1860 } else {
1861 ctxt.doc as *mut _xmlNode
1862 };
1863 tree::add_child(insertion_point, comment_node);
1864 }
1865 }
1866 continue;
1867 }
1868
1869 if ctxt.peek() == Some(b'!') {
1871 ctxt.next(); let _rest = ctxt.read_while(|ch| ch != b'>');
1873 if ctxt.peek() == Some(b'>') {
1874 ctxt.next(); }
1876 continue;
1879 }
1880
1881 if ctxt.peek() == Some(b'?') {
1883 ctxt.next(); let mut pi_content = Vec::new();
1886 loop {
1887 if ctxt.peek() == Some(b'?') && ctxt.peek_at(1) == Some(b'>') {
1888 break;
1889 }
1890 match ctxt.next() {
1891 Some(ch) => pi_content.push(ch),
1892 None => break,
1893 }
1894 }
1895 if ctxt.peek() == Some(b'?') {
1897 ctxt.next();
1898 }
1899 if ctxt.peek() == Some(b'>') {
1900 ctxt.next();
1901 }
1902 if !pi_content.is_empty() {
1904 let mut parts = pi_content.splitn(2, |b| *b == b' ');
1906 let target = parts.next().unwrap_or(&pi_content);
1907 let value = parts.next().unwrap_or(b"");
1908
1909 let pi_node = tree::new_pi(bytes_to_xmlstr(target), bytes_to_xmlstr(value));
1910 if !pi_node.is_null() {
1911 let insertion_point = if !ctxt.current.is_null() {
1912 ctxt.current
1913 } else {
1914 ctxt.doc as *mut _xmlNode
1915 };
1916 tree::add_child(insertion_point, pi_node);
1917 }
1918 }
1919 continue;
1920 }
1921
1922 let tag_name = ctxt.read_while(|ch| {
1924 ch != b'>' && ch != b'/' && ch != b' ' && ch != b'\t' && ch != b'\n' && ch != b'\r'
1925 });
1926
1927 if tag_name.is_empty() {
1928 handle_text(ctxt, b"<");
1930 continue;
1931 }
1932
1933 let attrs = parse_attributes(ctxt);
1935
1936 if ctxt.peek() == Some(b'/') {
1938 ctxt.next(); if ctxt.peek() == Some(b'>') {
1940 ctxt.next(); }
1942 } else if ctxt.peek() == Some(b'>') {
1943 ctxt.next(); }
1945
1946 let tag_lower: Vec<u8> = tag_name.iter().map(|b| b.to_ascii_lowercase()).collect();
1948 let tag_str = core::str::from_utf8(&tag_lower).unwrap_or("");
1949
1950 if tag_str == "script" || tag_str == "style" {
1951 let raw_node = tree::new_node(ptr::null_mut(), bytes_to_xmlstr(&tag_name));
1954 if !raw_node.is_null() {
1955 for attr in &attrs {
1956 let name_c = bytes_to_xmlstr(&attr.name);
1957 let val_c = bytes_to_xmlstr(&attr.value);
1958 if !name_c.is_null() {
1959 tree::set_prop(raw_node, name_c, val_c);
1960 xmlFreeImpl(name_c as *mut c_void);
1961 if !val_c.is_null() {
1962 xmlFreeImpl(val_c as *mut c_void);
1963 }
1964 }
1965 }
1966
1967 let insertion_point = if ctxt.current.is_null() {
1968 if ctxt.in_head {
1969 ensure_head(ctxt);
1970 ctxt.head
1971 } else {
1972 ensure_body(ctxt);
1973 ctxt.body
1974 }
1975 } else {
1976 ctxt.current
1977 };
1978
1979 if !insertion_point.is_null() {
1980 tree::add_child(insertion_point, raw_node);
1981
1982 let end_tag = format!("</{}", tag_str);
1984 let end_bytes = end_tag.as_bytes();
1985 let mut raw_text = Vec::new();
1986 let mut match_idx = 0;
1987
1988 loop {
1989 if ctxt.is_eof() {
1990 break;
1991 }
1992 let ch = ctxt.peek().unwrap();
1993 if ch.to_ascii_lowercase() == end_bytes[match_idx] {
1994 match_idx += 1;
1995 if match_idx == end_bytes.len() {
1996 if !raw_text.is_empty() {
1999 let text_node = tree::new_text(bytes_to_xmlstr(&raw_text));
2000 if !text_node.is_null() {
2001 tree::add_child(raw_node, text_node);
2002 }
2003 }
2004 ctxt.next(); let _suffix = ctxt.read_while(|ch| ch != b'>');
2008 if ctxt.peek() == Some(b'>') {
2009 ctxt.next();
2010 }
2011 ctxt.current = unsafe { (*raw_node).parent };
2013 break;
2014 }
2015 if match_idx == 1 {
2017 raw_text.push(ch);
2018 }
2019 ctxt.next();
2020 } else {
2021 if match_idx > 0 {
2023 match_idx = 0;
2025 }
2026 raw_text.push(ch);
2027 ctxt.next();
2028 }
2029 }
2030
2031 if match_idx < end_bytes.len() && !raw_text.is_empty() {
2033 let text_node = tree::new_text(bytes_to_xmlstr(&raw_text));
2034 if !text_node.is_null() {
2035 tree::add_child(raw_node, text_node);
2036 }
2037 ctxt.current = unsafe { (*raw_node).parent };
2038 }
2039 }
2040 }
2041 continue;
2042 }
2043
2044 handle_start_tag(ctxt, &tag_name, &attrs);
2046 } else {
2047 let mut text = Vec::new();
2049 loop {
2050 match ctxt.peek() {
2051 Some(b'<') => break,
2052 Some(b'&') => {
2053 let entity_text = parse_entity(ctxt);
2055 text.extend_from_slice(&entity_text);
2056 }
2057 Some(ch) => {
2058 text.push(ch);
2059 ctxt.next();
2060 }
2061 None => break,
2062 }
2063 }
2064
2065 if !text.is_empty() {
2066 handle_text(ctxt, &text);
2067 }
2068 }
2069 }
2070
2071 if ctxt.html.is_null() {
2073 ensure_html(ctxt);
2074 }
2075
2076 doc
2077}
2078
2079pub unsafe fn parse_file(filename: *const c_char, encoding: *const c_char) -> *mut _xmlDoc {
2094 if filename.is_null() {
2095 return ptr::null_mut();
2096 }
2097
2098 let filename_str = unsafe { std::ffi::CStr::from_ptr(filename) };
2100 let path = filename_str.to_str().unwrap_or("");
2101 let content = match std::fs::read(path) {
2102 Ok(data) => data,
2103 Err(_) => return ptr::null_mut(),
2104 };
2105
2106 let mut ctxt = HtmlParserCtxt::new();
2107 if !encoding.is_null() {
2108 let _enc_cstr = unsafe { std::ffi::CStr::from_ptr(encoding) };
2109 ctxt.encoding = unsafe { c_strdup(encoding) };
2110 }
2111
2112 let doc = unsafe {
2113 html_parse_buffer(
2114 &mut ctxt,
2115 content.as_ptr() as *const c_char,
2116 content.len() as c_int,
2117 )
2118 };
2119
2120 if !doc.is_null() && !filename.is_null() {
2121 unsafe {
2122 (*doc).URL = c_strdup(filename) as *mut xmlChar;
2123 }
2124 }
2125
2126 doc
2127}
2128
2129pub unsafe fn parse_memory(buffer: *const c_char, size: c_int) -> *mut _xmlDoc {
2140 if buffer.is_null() || size <= 0 {
2141 return ptr::null_mut();
2142 }
2143
2144 let mut ctxt = HtmlParserCtxt::new();
2145 unsafe { html_parse_buffer(&mut ctxt, buffer, size) }
2146}
2147
2148pub(crate) unsafe fn parse_doc(cur: *const xmlChar, encoding: *const c_char) -> *mut _xmlDoc {
2159 if cur.is_null() {
2160 return ptr::null_mut();
2161 }
2162
2163 let len = unsafe { xml_strlen(cur) };
2164 let mut ctxt = HtmlParserCtxt::new();
2165 if !encoding.is_null() {
2166 ctxt.encoding = unsafe { c_strdup(encoding) };
2167 }
2168
2169 unsafe { html_parse_buffer(&mut ctxt, cur as *const c_char, len as c_int) }
2170}
2171
2172#[allow(dead_code)]
2183pub(crate) unsafe fn create_file_parser_ctxt(
2184 filename: *const c_char,
2185 encoding: *const c_char,
2186) -> *mut c_void {
2187 if filename.is_null() {
2188 return ptr::null_mut();
2189 }
2190
2191 let ctxt = unsafe { xmlMallocZero(size_of::<HtmlParserCtxt>() as usize) };
2192 if ctxt.is_null() {
2193 return ptr::null_mut();
2194 }
2195
2196 let ctxt = ctxt as *mut HtmlParserCtxt;
2197 unsafe {
2198 ptr::write(ctxt, HtmlParserCtxt::new());
2199 if !encoding.is_null() {
2200 (*ctxt).encoding = c_strdup(encoding);
2201 }
2202 }
2203
2204 ctxt as *mut c_void
2205}
2206
2207pub(crate) unsafe fn free_parser_ctxt(ctxt: *mut c_void) {
2217 if ctxt.is_null() {
2218 return;
2219 }
2220
2221 let ctxt = ctxt as *mut HtmlParserCtxt;
2222 unsafe {
2223 if !(*ctxt).filename.is_null() {
2224 xmlFreeImpl((*ctxt).filename as *mut c_void);
2225 }
2226 if !(*ctxt).encoding.is_null() {
2227 xmlFreeImpl((*ctxt).encoding as *mut c_void);
2228 }
2229 xmlFreeImpl(ctxt as *mut c_void);
2230 }
2231}
2232
2233#[allow(dead_code)]
2239pub(crate) const fn init_parser() {
2240 }
2243
2244#[allow(dead_code)]
2250pub(crate) const fn cleanup_parser() {
2251 }
2254
2255pub(crate) unsafe fn new_doc(version: *const xmlChar) -> *mut _xmlDoc {
2268 let doc = tree::new_doc(version);
2269 if doc.is_null() {
2270 return ptr::null_mut();
2271 }
2272
2273 unsafe {
2274 (*doc).type_ = XML_HTML_DOCUMENT_NODE as c_int;
2275 (*doc).properties = XML_DOC_WELLFORMED as c_int;
2276 }
2277
2278 let mut ctxt = HtmlParserCtxt::new();
2280 ctxt.doc = doc;
2281
2282 unsafe {
2283 ensure_html(&mut ctxt);
2284 ensure_head(&mut ctxt);
2285 ensure_body(&mut ctxt);
2286 }
2287
2288 doc
2289}
2290
2291pub(crate) unsafe fn new_doc_no_dtd(version: *const xmlChar) -> *mut _xmlDoc {
2304 let doc = tree::new_doc(version);
2305 if doc.is_null() {
2306 return ptr::null_mut();
2307 }
2308
2309 unsafe {
2310 (*doc).type_ = XML_HTML_DOCUMENT_NODE as c_int;
2311 (*doc).properties = XML_DOC_WELLFORMED as c_int;
2312 }
2313
2314 doc
2315}
2316
2317const HTML_VOID_ELEMENTS: &[&str] = &[
2323 "area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "param", "source",
2324 "track", "wbr", "frame",
2325];
2326
2327fn is_html_void(name: &str) -> bool {
2329 HTML_VOID_ELEMENTS
2330 .iter()
2331 .any(|v| v.eq_ignore_ascii_case(name))
2332}
2333
2334#[allow(dead_code)]
2336fn has_optional_end_tag(name: &str) -> bool {
2337 matches!(
2338 name.to_ascii_lowercase().as_str(),
2339 "p" | "li" | "tr" | "td" | "th" | "dt" | "dd"
2340 )
2341}
2342
2343unsafe fn html_write_quoted(buf: *mut _xmlBuffer, s: *const xmlChar) {
2349 if buf.is_null() || s.is_null() {
2350 return;
2351 }
2352 io::buf_ccat(buf, b'"');
2353 io::buf_cat(buf, s);
2354 io::buf_ccat(buf, b'"');
2355}
2356
2357fn trim_ascii_start(s: &[u8]) -> &[u8] {
2359 let start = s
2360 .iter()
2361 .position(|&b| !b.is_ascii_whitespace())
2362 .unwrap_or(s.len());
2363 &s[start..]
2364}
2365
2366unsafe fn html_serialize_text(buf: *mut _xmlBuffer, content: *const xmlChar, len: c_int) {
2367 if buf.is_null() || content.is_null() || len <= 0 {
2368 return;
2369 }
2370
2371 let mut i: c_int = 0;
2372 while i < len {
2373 let ch = unsafe { *content.add(i as usize) };
2374
2375 match ch {
2376 b'<' => {
2377 io::buf_add(buf, b"<" as *const u8, 4);
2378 }
2379 b'&' => {
2380 io::buf_add(buf, b"&" as *const u8, 5);
2381 }
2382 _ => {
2383 io::buf_add(buf, &ch as *const u8, 1);
2384 }
2385 }
2386 i += 1;
2387 }
2388}
2389
2390unsafe fn html_serialize_attr_value(buf: *mut _xmlBuffer, value: *const xmlChar) {
2394 if buf.is_null() || value.is_null() {
2395 return;
2396 }
2397
2398 let len = unsafe { xml_strlen(value) as c_int };
2399 let mut i: c_int = 0;
2400 while i < len {
2401 let ch = unsafe { *value.add(i as usize) };
2402
2403 match ch {
2404 b'&' => {
2405 io::buf_add(buf, b"&" as *const u8, 5);
2406 }
2407 b'"' => {
2408 io::buf_add(buf, b""" as *const u8, 6);
2409 }
2410 _ => {
2411 io::buf_add(buf, &ch as *const u8, 1);
2412 }
2413 }
2414 i += 1;
2415 }
2416}
2417
2418unsafe fn html_head_has_meta(child: *mut _xmlNode) -> bool {
2436 let mut c = child;
2437 while !c.is_null() {
2438 if (*c).type_ == XML_ELEMENT_NODE as c_int && !(*c).name.is_null() {
2439 let nm = xmlstr_to_bytes((*c).name);
2440 if nm.eq_ignore_ascii_case(b"meta") {
2441 return true;
2442 }
2443 }
2444 c = (*c).next;
2445 }
2446 false
2447}
2448
2449pub(crate) unsafe fn serialize_node(
2450 node: *mut _xmlNode,
2451 buf: *mut _xmlBuffer,
2452 format: c_int,
2453 level: c_int,
2454) {
2455 if node.is_null() || buf.is_null() {
2456 return;
2457 }
2458
2459 let n = unsafe { &*node };
2460
2461 match n.type_ {
2462 t if t == XML_ELEMENT_NODE as c_int => {
2463 let name = if n.name.is_null() {
2464 ""
2465 } else {
2466 unsafe { core::str::from_utf8(xmlstr_to_bytes(n.name)).unwrap_or("") }
2467 };
2468
2469 let is_void = is_html_void(name);
2470 let info = html_tag_lookup(name);
2475 let is_inline = info.is_none_or(|i| i.flags & HTML_INLINE != 0);
2476 let no_format = is_inline || name.starts_with('p');
2477
2478 io::buf_ccat(buf, b'<');
2480 if !n.name.is_null() {
2481 io::buf_cat(buf, n.name);
2482 }
2483
2484 let mut attr = n.properties;
2486 while !attr.is_null() {
2487 let a = unsafe { &*attr };
2488 io::buf_ccat(buf, b' ');
2489 if !a.name.is_null() {
2490 io::buf_cat(buf, a.name);
2491 }
2492
2493 if !a.children.is_null() {
2495 let child = unsafe { &*a.children };
2496 if child.type_ == XML_TEXT_NODE as c_int && !child.content.is_null() {
2497 io::buf_ccat(buf, b'=');
2498 io::buf_ccat(buf, b'"');
2499 html_serialize_attr_value(buf, child.content);
2500 io::buf_ccat(buf, b'"');
2501 }
2502 }
2503
2504 attr = a.next;
2505 }
2506
2507 let mut meta_bytes: Option<Vec<u8>> = None;
2514 if name.eq_ignore_ascii_case("head")
2515 && level == 1
2516 && !n.doc.is_null()
2517 && !(*n.doc).encoding.is_null()
2518 {
2519 let parent_is_html = !n.parent.is_null() && !(*n.parent).name.is_null() && {
2520 let pn = core::str::from_utf8(xmlstr_to_bytes((*n.parent).name)).unwrap_or("");
2521 pn.eq_ignore_ascii_case("html")
2522 };
2523 if parent_is_html && !html_head_has_meta(n.children) {
2524 meta_bytes = Some(xmlstr_to_bytes((*n.doc).encoding).to_vec());
2525 }
2526 }
2527 let meta_inserted = meta_bytes.is_some();
2528
2529 let has_children = !n.children.is_null();
2530 let first_child = if has_children {
2531 unsafe { (*n.children).type_ }
2532 } else {
2533 XML_TEXT_NODE as c_int
2534 };
2535 let first_is_text = first_child == XML_TEXT_NODE as c_int
2536 || first_child == XML_ENTITY_REF_NODE as c_int;
2537 let multi_child = (has_children && n.children != n.last) || meta_inserted;
2540
2541 if is_void {
2542 io::buf_ccat(buf, b'>');
2544 } else {
2548 io::buf_ccat(buf, b'>');
2550
2551 if format != 0 && !no_format && !first_is_text && multi_child {
2556 io::buf_ccat(buf, b'\n');
2557 }
2558
2559 if let Some(enc) = &meta_bytes {
2560 io::buf_add(buf, b"<meta charset=\"" as *const u8, 15);
2561 io::buf_add(buf, enc.as_ptr(), enc.len() as c_int);
2562 io::buf_add(buf, b"\">" as *const u8, 2);
2563 if format != 0 && has_children && !first_is_text && !name.starts_with('p') {
2566 io::buf_ccat(buf, b'\n');
2567 }
2568 }
2569
2570 let mut child = n.children;
2573 while !child.is_null() {
2574 serialize_node(child, buf, format, level + 1);
2575 let next = unsafe { (*child).next };
2579 if format != 0 && !next.is_null() && !name.starts_with('p') {
2580 let nt = unsafe { (*next).type_ };
2581 if nt != XML_TEXT_NODE as c_int && nt != XML_ENTITY_REF_NODE as c_int {
2582 let cname = if (*child).name.is_null() {
2583 ""
2584 } else {
2585 unsafe {
2586 core::str::from_utf8(xmlstr_to_bytes((*child).name))
2587 .unwrap_or("")
2588 }
2589 };
2590 let cinfo = html_tag_lookup(cname);
2591 let c_inline = cinfo.is_none_or(|i| i.flags & HTML_INLINE != 0);
2592 if !c_inline {
2593 io::buf_ccat(buf, b'\n');
2594 }
2595 }
2596 }
2597 child = next;
2598 }
2599
2600 let last_child = if has_children {
2604 unsafe { (*n.last).type_ }
2605 } else {
2606 XML_ELEMENT_NODE as c_int
2607 };
2608 let last_is_text = last_child == XML_TEXT_NODE as c_int
2609 || last_child == XML_ENTITY_REF_NODE as c_int;
2610 if format != 0 && !no_format && !last_is_text && multi_child {
2611 io::buf_ccat(buf, b'\n');
2612 }
2613
2614 io::buf_add(buf, b"</" as *const u8, 2);
2616 if !n.name.is_null() {
2617 io::buf_cat(buf, n.name);
2618 }
2619 io::buf_ccat(buf, b'>');
2620 }
2621 }
2622 t if t == XML_TEXT_NODE as c_int => {
2623 html_serialize_text(buf, n.content, xml_strlen(n.content) as c_int);
2624 }
2625 t if t == XML_CDATA_SECTION_NODE as c_int => {
2626 io::buf_add(buf, b"<![CDATA[" as *const u8, 9);
2627 html_serialize_text(buf, n.content, xml_strlen(n.content) as c_int);
2628 io::buf_add(buf, b"]]>" as *const u8, 3);
2629 }
2630 t if t == XML_COMMENT_NODE as c_int => {
2631 if format != 0 && level > 0 {
2632 io::buf_ccat(buf, b'\n');
2633 for _ in 0..level {
2634 io::buf_add(buf, b" " as *const u8, 2);
2635 }
2636 }
2637 io::buf_add(buf, b"<!--" as *const u8, 4);
2638 if !n.content.is_null() {
2639 io::buf_cat(buf, n.content);
2640 }
2641 io::buf_add(buf, b"-->" as *const u8, 3);
2642 }
2643 t if t == XML_PI_NODE as c_int => {
2644 if format != 0 && level > 0 {
2645 io::buf_ccat(buf, b'\n');
2646 for _ in 0..level {
2647 io::buf_add(buf, b" " as *const u8, 2);
2648 }
2649 }
2650 io::buf_add(buf, b"<?" as *const u8, 2);
2651 if !n.name.is_null() {
2652 io::buf_cat(buf, n.name);
2653 }
2654 if !n.content.is_null() && unsafe { *n.content != 0 } {
2655 io::buf_ccat(buf, b' ');
2656 io::buf_cat(buf, n.content);
2657 }
2658 io::buf_add(buf, b"?>" as *const u8, 2);
2659 }
2660 t if t == XML_DOCUMENT_NODE as c_int || t == XML_HTML_DOCUMENT_NODE as c_int => {
2661 let doc_ptr = n as *const _xmlNode as *mut _xmlDoc;
2664 let d = &*doc_ptr;
2665 if !d.intSubset.is_null() {
2666 let dtd = &*d.intSubset;
2667 io::buf_add(buf, b"<!DOCTYPE " as *const u8, 10);
2668 if !dtd.name.is_null() {
2669 io::buf_cat(buf, dtd.name);
2670 }
2671 if !dtd.ExternalID.is_null() {
2672 io::buf_add(buf, b" PUBLIC " as *const u8, 8);
2673 html_write_quoted(buf, dtd.ExternalID);
2674 io::buf_ccat(buf, b' ');
2675 html_write_quoted(buf, dtd.SystemID);
2676 } else if !dtd.SystemID.is_null() {
2677 io::buf_add(buf, b" SYSTEM " as *const u8, 8);
2678 html_write_quoted(buf, dtd.SystemID);
2679 }
2680 io::buf_ccat(buf, b'>');
2681 io::buf_ccat(buf, b'\n');
2682 }
2683 let mut child = n.children;
2686 while !child.is_null() {
2687 serialize_node(child, buf, format, 0);
2688 child = unsafe { (*child).next };
2689 }
2690 io::buf_ccat(buf, b'\n');
2693 }
2694 _ => {
2695 if !n.content.is_null() {
2696 html_serialize_text(buf, n.content, xml_strlen(n.content) as c_int);
2697 }
2698 }
2699 }
2700}
2701
2702pub(crate) unsafe fn doc_dump(buf: *mut _xmlBuffer, doc: *mut _xmlDoc) -> c_int {
2709 if buf.is_null() || doc.is_null() {
2710 return -1;
2711 }
2712
2713 let before = io::buf_length(buf);
2714 serialize_node(doc as *mut _xmlNode, buf, 0, 0);
2715 let after = io::buf_length(buf);
2716
2717 if after < 0 || before < 0 {
2718 return -1;
2719 }
2720 after - before
2721}
2722
2723#[cfg(test)]
2728mod tests {
2729 use super::*;
2730
2731 use crate::xml::io;
2732
2733 #[allow(dead_code)]
2735 unsafe fn to_xmlstr(s: &[u8]) -> *mut xmlChar {
2736 bytes_to_xmlstr(s)
2737 }
2738
2739 unsafe fn html_doc_to_string(doc: *mut _xmlDoc) -> String {
2741 let buf = io::buf_create(-1);
2742 assert!(!buf.is_null());
2743 doc_dump(buf, doc);
2744 let content = io::buf_content(buf);
2745 let s = if !content.is_null() {
2746 let len = xml_strlen(content);
2747 let slice = slice::from_raw_parts(content, len);
2748 String::from_utf8_lossy(slice).to_string()
2749 } else {
2750 String::new()
2751 };
2752 io::buf_free(buf);
2753 s
2754 }
2755
2756 #[test]
2761 fn test_html_tag_lookup() {
2762 assert!(html_tag_lookup("html").is_some());
2764 assert!(html_tag_lookup("HTML").is_some()); assert!(html_tag_lookup("p").is_some());
2766 assert!(html_tag_lookup("br").is_some());
2767 assert!(html_tag_lookup("div").is_some());
2768 assert!(html_tag_lookup("script").is_some());
2769
2770 assert!(html_tag_lookup("custom").is_none());
2772 assert!(html_tag_lookup("my-element").is_none());
2773 }
2774
2775 #[test]
2776 fn test_tag_flags() {
2777 let br = html_tag_lookup("br").unwrap();
2778 assert!(br.flags & HTML_INLINE != 0);
2779 assert!(br.flags & HTML_EMPTY != 0);
2780
2781 let div = html_tag_lookup("div").unwrap();
2782 assert!(div.flags & HTML_BLOCK != 0);
2783 assert!(div.flags & HTML_VALID != 0);
2784
2785 let p = html_tag_lookup("p").unwrap();
2786 assert!(p.flags & HTML_NO_END != 0);
2787
2788 let meta = html_tag_lookup("meta").unwrap();
2789 assert!(meta.flags & HTML_HEAD != 0);
2790 assert!(meta.flags & HTML_EMPTY != 0);
2791 }
2792
2793 #[test]
2798 fn test_html_entity_lookup() {
2799 assert_eq!(html_entity_lookup("amp"), Some("&"));
2800 assert_eq!(html_entity_lookup("lt"), Some("<"));
2801 assert_eq!(html_entity_lookup("gt"), Some(">"));
2802 assert_eq!(html_entity_lookup("quot"), Some("\""));
2803 assert_eq!(html_entity_lookup("nbsp"), Some("\u{00a0}"));
2804 assert_eq!(html_entity_lookup("copy"), Some("\u{00a9}"));
2805 assert!(html_entity_lookup("unknown_entity").is_none());
2806 }
2807
2808 #[test]
2813 fn test_parse_basic_html() {
2814 unsafe {
2815 let html = b"<html><head><title>Test</title></head><body><p>Hello</p></body></html>\0";
2816 let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
2817 assert!(!doc.is_null());
2818
2819 let s = html_doc_to_string(doc);
2820 assert!(s.contains("<html>"));
2821 assert!(s.contains("<head>"));
2822 assert!(s.contains("<title>Test</title>"));
2823 assert!(s.contains("<body>"));
2824 assert!(s.contains("<p>Hello</p>"));
2825
2826 tree::free_doc(doc);
2827 }
2828 }
2829
2830 #[test]
2831 fn test_parse_empty_document() {
2832 unsafe {
2833 let html = b"\0";
2834 let doc = parse_memory(html.as_ptr() as *const c_char, 0);
2835 assert!(doc.is_null());
2836 }
2837 }
2838
2839 #[test]
2844 fn test_implicit_html_head_body() {
2845 unsafe {
2846 let html = b"<p>Hello</p>\0";
2848 let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
2849 assert!(!doc.is_null());
2850
2851 let s = html_doc_to_string(doc);
2852 assert!(s.contains("<html>"));
2854 assert!(s.contains("<body>"));
2856 assert!(s.contains("<p>Hello</p>"));
2858
2859 tree::free_doc(doc);
2860 }
2861 }
2862
2863 #[test]
2864 fn test_implicit_head_with_title() {
2865 unsafe {
2866 let html = b"<title>My Page</title>\0";
2868 let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
2869 assert!(!doc.is_null());
2870
2871 let s = html_doc_to_string(doc);
2872 assert!(s.contains("<html>"));
2873 assert!(s.contains("<head>"));
2874 assert!(s.contains("<title>My Page</title>"));
2875
2876 tree::free_doc(doc);
2877 }
2878 }
2879
2880 #[test]
2885 fn test_auto_close_p() {
2886 unsafe {
2887 let html = b"<p>First<p>Second</p>\0";
2889 let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
2890 assert!(!doc.is_null());
2891
2892 let s = html_doc_to_string(doc);
2893 let first_pos = s.find("First");
2895 let second_pos = s.find("Second");
2896 assert!(first_pos.is_some());
2897 assert!(second_pos.is_some());
2898
2899 tree::free_doc(doc);
2900 }
2901 }
2902
2903 #[test]
2904 fn test_auto_close_heading() {
2905 unsafe {
2906 let html = b"<h1>Title</h1><h2>Subtitle</h2>\0";
2908 let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
2909 assert!(!doc.is_null());
2910
2911 let s = html_doc_to_string(doc);
2912 assert!(s.contains("<h1>Title</h1>"));
2913 assert!(s.contains("<h2>Subtitle</h2>"));
2914
2915 tree::free_doc(doc);
2916 }
2917 }
2918
2919 #[test]
2924 fn test_void_elements() {
2925 unsafe {
2926 let html = b"<br><hr><img src=\"test.jpg\"><input type=\"text\">\0";
2927 let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
2928 assert!(!doc.is_null());
2929
2930 let s = html_doc_to_string(doc);
2931 assert!(s.contains("<br>"));
2932 assert!(s.contains("<hr>"));
2933 assert!(s.contains("<img"));
2934 assert!(s.contains("<input"));
2935
2936 assert!(!s.contains("</br>"));
2938 assert!(!s.contains("</hr>"));
2939 assert!(!s.contains("</img>"));
2940
2941 tree::free_doc(doc);
2942 }
2943 }
2944
2945 #[test]
2950 fn test_unquoted_attributes() {
2951 unsafe {
2952 let html = b"<div class=main id=content>Text</div>\0";
2953 let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
2954 assert!(!doc.is_null());
2955
2956 let s = html_doc_to_string(doc);
2957 assert!(s.contains("class=\"main\""));
2958 assert!(s.contains("id=\"content\""));
2959
2960 tree::free_doc(doc);
2961 }
2962 }
2963
2964 #[test]
2965 fn test_minimized_attributes() {
2966 unsafe {
2967 let html = b"<option selected disabled>Value</option>\0";
2968 let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
2969 assert!(!doc.is_null());
2970
2971 let s = html_doc_to_string(doc);
2972 assert!(s.contains("selected"));
2974 assert!(s.contains("disabled"));
2975
2976 tree::free_doc(doc);
2977 }
2978 }
2979
2980 #[test]
2985 fn test_html_entities() {
2986 unsafe {
2987 let html = b"<p>& < > " ©</p>\0";
2988 let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
2989 assert!(!doc.is_null());
2990
2991 let s = html_doc_to_string(doc);
2992 assert!(s.contains("&")); assert!(s.contains("<")); assert!(s.contains(">")); assert!(s.contains("\u{00a0}")); tree::free_doc(doc);
3000 }
3001 }
3002
3003 #[test]
3004 fn test_numeric_entities() {
3005 unsafe {
3006 let html = b"<p>A A</p>\0";
3008 let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
3009 assert!(!doc.is_null());
3010
3011 let s = html_doc_to_string(doc);
3012 assert!(s.contains('A'));
3013
3014 tree::free_doc(doc);
3015 }
3016 }
3017
3018 #[test]
3023 fn test_nested_elements() {
3024 unsafe {
3025 let html = b"<div><ul><li>Item 1</li><li>Item 2</li></ul></div>\0";
3026 let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
3027 assert!(!doc.is_null());
3028
3029 let s = html_doc_to_string(doc);
3030 assert!(s.contains("<div>"));
3031 assert!(s.contains("<ul>"));
3032 assert!(s.contains("<li>Item 1</li>"));
3033 assert!(s.contains("<li>Item 2</li>"));
3034
3035 tree::free_doc(doc);
3036 }
3037 }
3038
3039 #[test]
3044 fn test_missing_end_tags() {
3045 unsafe {
3046 let html = b"<p>Paragraph without closing<div>Another div\0";
3048 let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
3049 assert!(!doc.is_null());
3050
3051 let s = html_doc_to_string(doc);
3052 assert!(s.contains("Paragraph without closing"));
3053 assert!(s.contains("Another div"));
3054
3055 tree::free_doc(doc);
3056 }
3057 }
3058
3059 #[test]
3060 fn test_mismatched_case() {
3061 unsafe {
3062 let html = b"<HTML><HEAD><TITLE>Test</TITLE></HEAD><BODY><P>Hello</P></BODY></HTML>\0";
3063 let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
3064 assert!(!doc.is_null());
3065
3066 let s = html_doc_to_string(doc);
3067 assert!(s.contains("<HTML>"));
3069 assert!(s.contains("<HEAD>"));
3070 assert!(s.contains("<BODY>"));
3071 assert!(s.contains("<P>Hello</P>"));
3072
3073 tree::free_doc(doc);
3074 }
3075 }
3076
3077 #[test]
3078 fn test_nested_malformed() {
3079 unsafe {
3080 let html = b"<div><p><span><b>Deep text</div></p>\0";
3082 let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
3083 assert!(!doc.is_null());
3084
3085 let s = html_doc_to_string(doc);
3086 assert!(s.contains("Deep text"));
3087
3088 tree::free_doc(doc);
3089 }
3090 }
3091
3092 #[test]
3097 fn test_serialization_round_trip_simple() {
3098 unsafe {
3099 let original = b"<p>Hello World</p>\0";
3100 let doc = parse_memory(
3101 original.as_ptr() as *const c_char,
3102 (original.len() - 1) as c_int,
3103 );
3104 assert!(!doc.is_null());
3105
3106 let s = html_doc_to_string(doc);
3107 assert!(s.contains("Hello World"));
3108
3109 tree::free_doc(doc);
3110 }
3111 }
3112
3113 #[test]
3114 fn test_serialize_void_elements_no_self_close() {
3115 unsafe {
3116 let html = b"<br><hr><img src=\"test.png\">\0";
3117 let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
3118 assert!(!doc.is_null());
3119
3120 let s = html_doc_to_string(doc);
3121 assert!(!s.contains("<br/>"));
3123 assert!(!s.contains("<hr/>"));
3124
3125 tree::free_doc(doc);
3126 }
3127 }
3128
3129 #[test]
3134 fn test_script_content() {
3135 unsafe {
3136 let html = b"<script>var x = 1;</script>\0";
3138 let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
3139 assert!(!doc.is_null());
3140
3141 let s = html_doc_to_string(doc);
3142 assert!(s.contains("<script>"));
3143 assert!(s.contains("var x = 1;"));
3145
3146 tree::free_doc(doc);
3147 }
3148 }
3149
3150 #[test]
3155 fn test_html_comment() {
3156 unsafe {
3157 let html = b"<html><!-- This is a comment --><body><p>Text</p></body></html>\0";
3158 let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
3159 assert!(!doc.is_null());
3160
3161 let s = html_doc_to_string(doc);
3162 assert!(s.contains("<!-- This is a comment -->"));
3163
3164 tree::free_doc(doc);
3165 }
3166 }
3167
3168 #[test]
3173 fn test_new_doc_creates_html_head_body() {
3174 unsafe {
3175 let doc = new_doc(ptr::null());
3176 assert!(!doc.is_null());
3177 assert_eq!((*doc).type_, XML_HTML_DOCUMENT_NODE as c_int);
3178
3179 let s = html_doc_to_string(doc);
3180 assert!(s.contains("<html>"));
3181 assert!(s.contains("<head>"));
3182 assert!(s.contains("<body>"));
3183
3184 tree::free_doc(doc);
3185 }
3186 }
3187
3188 #[test]
3189 fn test_new_doc_no_dtd() {
3190 unsafe {
3191 let doc = new_doc_no_dtd(ptr::null());
3192 assert!(!doc.is_null());
3193 assert_eq!((*doc).type_, XML_HTML_DOCUMENT_NODE as c_int);
3194
3195 let s = html_doc_to_string(doc);
3199 assert_eq!(s, "\n");
3200
3201 tree::free_doc(doc);
3202 }
3203 }
3204
3205 #[test]
3210 fn test_resolve_numeric_entity() {
3211 assert_eq!(resolve_numeric_entity("65", false), vec![b'A']);
3212 assert_eq!(resolve_numeric_entity("41", true), vec![b'A']);
3213 assert_eq!(resolve_numeric_entity("0", false), Vec::<u8>::new());
3214 }
3215
3216 #[test]
3217 fn test_resolve_entity_unknown() {
3218 let result = resolve_entity("unknown");
3219 assert_eq!(result, b"&unknown;");
3220 }
3221
3222 #[test]
3227 fn test_init_cleanup_parser() {
3228 init_parser();
3230 cleanup_parser();
3231 }
3232
3233 #[test]
3238 fn test_create_free_parser_ctxt() {
3239 unsafe {
3240 let ctxt =
3241 create_file_parser_ctxt(b"test.html\0" as *const u8 as *const c_char, ptr::null());
3242 assert!(!ctxt.is_null());
3243 free_parser_ctxt(ctxt);
3244 }
3245 }
3246
3247 #[test]
3252 fn test_complex_html_document() {
3253 unsafe {
3254 let html = b"<!DOCTYPE html>
3255<html>
3256<head>
3257 <meta charset=\"utf-8\">
3258 <title>Test Page</title>
3259 <link rel=\"stylesheet\" href=\"style.css\">
3260</head>
3261<body>
3262 <div id=\"main\">
3263 <h1>Title</h1>
3264 <p>First paragraph with <a href=\"link.html\">a link</a>.</p>
3265 <p>Second paragraph.</p>
3266 <ul>
3267 <li>Item 1</li>
3268 <li>Item 2</li>
3269 </ul>
3270 <br>
3271 <hr>
3272 <img src=\"image.jpg\" alt=\"An image\">
3273 </div>
3274 <script>alert('hello');</script>
3275</body>
3276</html>\0";
3277 let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
3278 assert!(!doc.is_null());
3279
3280 let s = html_doc_to_string(doc);
3281 assert!(s.contains("<html>"));
3282 assert!(s.contains("<head>"));
3283 assert!(s.contains("<title>Test Page</title>"));
3284 assert!(s.contains("<body>"));
3285 assert!(s.contains("<h1>Title</h1>"));
3286 assert!(s.contains("a link"));
3287 assert!(s.contains("Second paragraph"));
3288 assert!(s.contains("<br>"));
3289 assert!(s.contains("<hr>"));
3290 assert!(s.contains("<img"));
3291 assert!(s.contains("<script>"));
3292
3293 tree::free_doc(doc);
3294 }
3295 }
3296
3297 #[test]
3302 fn test_parse_doc() {
3303 unsafe {
3304 let html = b"<p>Hello from parse_doc</p>\0";
3305 let doc = parse_doc(html.as_ptr() as *const xmlChar, ptr::null());
3306 assert!(!doc.is_null());
3307
3308 let s = html_doc_to_string(doc);
3309 assert!(s.contains("Hello from parse_doc"));
3310
3311 tree::free_doc(doc);
3312 }
3313 }
3314
3315 #[test]
3320 fn test_table_element_auto_close() {
3321 unsafe {
3322 let html = b"<table><tr><td>Cell 1<td>Cell 2</td></tr></table>";
3323 let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
3324 assert!(!doc.is_null());
3325
3326 let s = html_doc_to_string(doc);
3327 assert!(s.contains("<td>Cell 1"));
3328 assert!(s.contains("<td>Cell 2"));
3329
3330 tree::free_doc(doc);
3331 }
3332 }
3333}