1use core::ffi::c_void;
88use core::ptr;
89use core::slice;
90use std::os::raw::{c_char, c_int};
91
92use crate::abi::allocator::{xmlFreeImpl, xmlMallocZero};
93use crate::abi::structs::*;
94use crate::abi::types::xmlDocProperties::XML_DOC_WELLFORMED;
95use crate::abi::types::xmlElementType::*;
96use crate::abi::types::*;
97use crate::xml::io;
98use crate::xml::string::*;
99use crate::xml::tree;
100
101const HTML_INLINE: u32 = 0x1;
107const HTML_BLOCK: u32 = 0x2;
108const HTML_EMPTY: u32 = 0x4;
109#[allow(dead_code)]
110const HTML_DEPRECATED: u32 = 0x8;
111const HTML_OL: u32 = 0x10;
112const HTML_DL: u32 = 0x20;
113#[allow(dead_code)]
114const HTML_COMPACT: u32 = 0x40;
115const HTML_HEAD: u32 = 0x80;
116const HTML_BODY: u32 = 0x100;
117#[allow(dead_code)]
118const HTML_HEADSTRUCK: u32 = 0x200;
119const HTML_VALID: u32 = 0x400;
120const HTML_NO_END: u32 = 0x800; #[allow(dead_code)]
122const HTML_IMPLIED: u32 = 0x1000; #[derive(Clone, Copy)]
126struct HtmlElementInfo {
127 name: &'static str,
128 flags: u32,
129}
130
131const HTML_ELEMENTS: &[HtmlElementInfo] = &[
134 HtmlElementInfo {
136 name: "br",
137 flags: HTML_INLINE | HTML_EMPTY | HTML_VALID,
138 },
139 HtmlElementInfo {
140 name: "hr",
141 flags: HTML_BLOCK | HTML_EMPTY | HTML_VALID,
142 },
143 HtmlElementInfo {
144 name: "img",
145 flags: HTML_INLINE | HTML_EMPTY | HTML_VALID,
146 },
147 HtmlElementInfo {
148 name: "input",
149 flags: HTML_INLINE | HTML_EMPTY | HTML_VALID,
150 },
151 HtmlElementInfo {
152 name: "meta",
153 flags: HTML_HEAD | HTML_EMPTY | HTML_VALID,
154 },
155 HtmlElementInfo {
156 name: "link",
157 flags: HTML_HEAD | HTML_EMPTY | HTML_VALID,
158 },
159 HtmlElementInfo {
160 name: "base",
161 flags: HTML_HEAD | HTML_EMPTY | HTML_VALID,
162 },
163 HtmlElementInfo {
164 name: "area",
165 flags: HTML_INLINE | HTML_EMPTY | HTML_VALID,
166 },
167 HtmlElementInfo {
168 name: "col",
169 flags: HTML_BLOCK | HTML_EMPTY | HTML_VALID,
170 },
171 HtmlElementInfo {
172 name: "embed",
173 flags: HTML_INLINE | HTML_EMPTY | HTML_VALID,
174 },
175 HtmlElementInfo {
176 name: "param",
177 flags: HTML_HEAD | HTML_EMPTY | HTML_VALID,
178 },
179 HtmlElementInfo {
180 name: "source",
181 flags: HTML_INLINE | HTML_EMPTY | HTML_VALID,
182 },
183 HtmlElementInfo {
184 name: "track",
185 flags: HTML_INLINE | HTML_EMPTY | HTML_VALID,
186 },
187 HtmlElementInfo {
188 name: "wbr",
189 flags: HTML_INLINE | HTML_EMPTY | HTML_VALID,
190 },
191 HtmlElementInfo {
193 name: "html",
194 flags: HTML_BLOCK | HTML_VALID,
195 },
196 HtmlElementInfo {
197 name: "head",
198 flags: HTML_HEAD | HTML_BLOCK | HTML_VALID,
199 },
200 HtmlElementInfo {
201 name: "body",
202 flags: HTML_BODY | HTML_BLOCK | HTML_VALID,
203 },
204 HtmlElementInfo {
205 name: "div",
206 flags: HTML_BLOCK | HTML_VALID,
207 },
208 HtmlElementInfo {
209 name: "p",
210 flags: HTML_BLOCK | HTML_VALID | HTML_NO_END,
211 },
212 HtmlElementInfo {
213 name: "h1",
214 flags: HTML_BLOCK | HTML_VALID,
215 },
216 HtmlElementInfo {
217 name: "h2",
218 flags: HTML_BLOCK | HTML_VALID,
219 },
220 HtmlElementInfo {
221 name: "h3",
222 flags: HTML_BLOCK | HTML_VALID,
223 },
224 HtmlElementInfo {
225 name: "h4",
226 flags: HTML_BLOCK | HTML_VALID,
227 },
228 HtmlElementInfo {
229 name: "h5",
230 flags: HTML_BLOCK | HTML_VALID,
231 },
232 HtmlElementInfo {
233 name: "h6",
234 flags: HTML_BLOCK | HTML_VALID,
235 },
236 HtmlElementInfo {
237 name: "ul",
238 flags: HTML_BLOCK | HTML_VALID | HTML_OL,
239 },
240 HtmlElementInfo {
241 name: "ol",
242 flags: HTML_BLOCK | HTML_VALID | HTML_OL,
243 },
244 HtmlElementInfo {
245 name: "li",
246 flags: HTML_BLOCK | HTML_VALID | HTML_NO_END,
247 },
248 HtmlElementInfo {
249 name: "dl",
250 flags: HTML_BLOCK | HTML_VALID | HTML_DL,
251 },
252 HtmlElementInfo {
253 name: "dt",
254 flags: HTML_BLOCK | HTML_VALID | HTML_NO_END,
255 },
256 HtmlElementInfo {
257 name: "dd",
258 flags: HTML_BLOCK | HTML_VALID | HTML_NO_END,
259 },
260 HtmlElementInfo {
261 name: "table",
262 flags: HTML_BLOCK | HTML_VALID,
263 },
264 HtmlElementInfo {
265 name: "tr",
266 flags: HTML_BLOCK | HTML_VALID | HTML_NO_END,
267 },
268 HtmlElementInfo {
269 name: "td",
270 flags: HTML_BLOCK | HTML_VALID | HTML_NO_END,
271 },
272 HtmlElementInfo {
273 name: "th",
274 flags: HTML_BLOCK | HTML_VALID | HTML_NO_END,
275 },
276 HtmlElementInfo {
277 name: "thead",
278 flags: HTML_BLOCK | HTML_VALID,
279 },
280 HtmlElementInfo {
281 name: "tbody",
282 flags: HTML_BLOCK | HTML_VALID,
283 },
284 HtmlElementInfo {
285 name: "tfoot",
286 flags: HTML_BLOCK | HTML_VALID,
287 },
288 HtmlElementInfo {
289 name: "colgroup",
290 flags: HTML_BLOCK | HTML_VALID,
291 },
292 HtmlElementInfo {
293 name: "caption",
294 flags: HTML_BLOCK | HTML_VALID,
295 },
296 HtmlElementInfo {
297 name: "form",
298 flags: HTML_BLOCK | HTML_VALID,
299 },
300 HtmlElementInfo {
301 name: "fieldset",
302 flags: HTML_BLOCK | HTML_VALID,
303 },
304 HtmlElementInfo {
305 name: "legend",
306 flags: HTML_BLOCK | HTML_VALID,
307 },
308 HtmlElementInfo {
309 name: "pre",
310 flags: HTML_BLOCK | HTML_VALID,
311 },
312 HtmlElementInfo {
313 name: "blockquote",
314 flags: HTML_BLOCK | HTML_VALID,
315 },
316 HtmlElementInfo {
317 name: "address",
318 flags: HTML_BLOCK | HTML_VALID,
319 },
320 HtmlElementInfo {
321 name: "center",
322 flags: HTML_BLOCK | HTML_VALID,
323 },
324 HtmlElementInfo {
325 name: "dir",
326 flags: HTML_BLOCK | HTML_VALID,
327 },
328 HtmlElementInfo {
329 name: "menu",
330 flags: HTML_BLOCK | HTML_VALID,
331 },
332 HtmlElementInfo {
333 name: "noscript",
334 flags: HTML_BLOCK | HTML_VALID,
335 },
336 HtmlElementInfo {
337 name: "frameset",
338 flags: HTML_BLOCK | HTML_VALID,
339 },
340 HtmlElementInfo {
341 name: "frame",
342 flags: HTML_BLOCK | HTML_EMPTY | HTML_VALID,
343 },
344 HtmlElementInfo {
345 name: "iframe",
346 flags: HTML_BLOCK | HTML_VALID,
347 },
348 HtmlElementInfo {
349 name: "noframes",
350 flags: HTML_BLOCK | HTML_VALID,
351 },
352 HtmlElementInfo {
354 name: "a",
355 flags: HTML_INLINE | HTML_VALID,
356 },
357 HtmlElementInfo {
358 name: "abbr",
359 flags: HTML_INLINE | HTML_VALID,
360 },
361 HtmlElementInfo {
362 name: "acronym",
363 flags: HTML_INLINE | HTML_VALID,
364 },
365 HtmlElementInfo {
366 name: "b",
367 flags: HTML_INLINE | HTML_VALID,
368 },
369 HtmlElementInfo {
370 name: "basefont",
371 flags: HTML_INLINE | HTML_EMPTY | HTML_VALID,
372 },
373 HtmlElementInfo {
374 name: "bdo",
375 flags: HTML_INLINE | HTML_VALID,
376 },
377 HtmlElementInfo {
378 name: "big",
379 flags: HTML_INLINE | HTML_VALID,
380 },
381 HtmlElementInfo {
382 name: "cite",
383 flags: HTML_INLINE | HTML_VALID,
384 },
385 HtmlElementInfo {
386 name: "code",
387 flags: HTML_INLINE | HTML_VALID,
388 },
389 HtmlElementInfo {
390 name: "dfn",
391 flags: HTML_INLINE | HTML_VALID,
392 },
393 HtmlElementInfo {
394 name: "em",
395 flags: HTML_INLINE | HTML_VALID,
396 },
397 HtmlElementInfo {
398 name: "font",
399 flags: HTML_INLINE | HTML_VALID,
400 },
401 HtmlElementInfo {
402 name: "i",
403 flags: HTML_INLINE | HTML_VALID,
404 },
405 HtmlElementInfo {
406 name: "kbd",
407 flags: HTML_INLINE | HTML_VALID,
408 },
409 HtmlElementInfo {
410 name: "label",
411 flags: HTML_INLINE | HTML_VALID,
412 },
413 HtmlElementInfo {
414 name: "map",
415 flags: HTML_INLINE | HTML_VALID,
416 },
417 HtmlElementInfo {
418 name: "nobr",
419 flags: HTML_INLINE | HTML_VALID,
420 },
421 HtmlElementInfo {
422 name: "object",
423 flags: HTML_INLINE | HTML_VALID,
424 },
425 HtmlElementInfo {
426 name: "q",
427 flags: HTML_INLINE | HTML_VALID,
428 },
429 HtmlElementInfo {
430 name: "rb",
431 flags: HTML_INLINE | HTML_VALID,
432 },
433 HtmlElementInfo {
434 name: "rbc",
435 flags: HTML_INLINE | HTML_VALID,
436 },
437 HtmlElementInfo {
438 name: "rp",
439 flags: HTML_INLINE | HTML_VALID,
440 },
441 HtmlElementInfo {
442 name: "rt",
443 flags: HTML_INLINE | HTML_VALID,
444 },
445 HtmlElementInfo {
446 name: "rtc",
447 flags: HTML_INLINE | HTML_VALID,
448 },
449 HtmlElementInfo {
450 name: "ruby",
451 flags: HTML_INLINE | HTML_VALID,
452 },
453 HtmlElementInfo {
454 name: "s",
455 flags: HTML_INLINE | HTML_VALID,
456 },
457 HtmlElementInfo {
458 name: "samp",
459 flags: HTML_INLINE | HTML_VALID,
460 },
461 HtmlElementInfo {
462 name: "select",
463 flags: HTML_INLINE | HTML_VALID,
464 },
465 HtmlElementInfo {
466 name: "small",
467 flags: HTML_INLINE | HTML_VALID,
468 },
469 HtmlElementInfo {
470 name: "span",
471 flags: HTML_INLINE | HTML_VALID,
472 },
473 HtmlElementInfo {
474 name: "strike",
475 flags: HTML_INLINE | HTML_VALID,
476 },
477 HtmlElementInfo {
478 name: "strong",
479 flags: HTML_INLINE | HTML_VALID,
480 },
481 HtmlElementInfo {
482 name: "sub",
483 flags: HTML_INLINE | HTML_VALID,
484 },
485 HtmlElementInfo {
486 name: "sup",
487 flags: HTML_INLINE | HTML_VALID,
488 },
489 HtmlElementInfo {
490 name: "textarea",
491 flags: HTML_INLINE | HTML_VALID,
492 },
493 HtmlElementInfo {
494 name: "tt",
495 flags: HTML_INLINE | HTML_VALID,
496 },
497 HtmlElementInfo {
498 name: "u",
499 flags: HTML_INLINE | HTML_VALID,
500 },
501 HtmlElementInfo {
502 name: "var",
503 flags: HTML_INLINE | HTML_VALID,
504 },
505 HtmlElementInfo {
507 name: "header",
508 flags: HTML_BLOCK | HTML_VALID,
509 },
510 HtmlElementInfo {
511 name: "footer",
512 flags: HTML_BLOCK | HTML_VALID,
513 },
514 HtmlElementInfo {
515 name: "nav",
516 flags: HTML_BLOCK | HTML_VALID,
517 },
518 HtmlElementInfo {
519 name: "article",
520 flags: HTML_BLOCK | HTML_VALID,
521 },
522 HtmlElementInfo {
523 name: "section",
524 flags: HTML_BLOCK | HTML_VALID,
525 },
526 HtmlElementInfo {
527 name: "aside",
528 flags: HTML_BLOCK | HTML_VALID,
529 },
530 HtmlElementInfo {
531 name: "main",
532 flags: HTML_BLOCK | HTML_VALID,
533 },
534 HtmlElementInfo {
535 name: "figure",
536 flags: HTML_BLOCK | HTML_VALID,
537 },
538 HtmlElementInfo {
539 name: "figcaption",
540 flags: HTML_BLOCK | HTML_VALID,
541 },
542 HtmlElementInfo {
543 name: "details",
544 flags: HTML_BLOCK | HTML_VALID,
545 },
546 HtmlElementInfo {
547 name: "summary",
548 flags: HTML_BLOCK | HTML_VALID,
549 },
550 HtmlElementInfo {
552 name: "script",
553 flags: HTML_HEAD | HTML_BLOCK | HTML_VALID,
554 },
555 HtmlElementInfo {
556 name: "style",
557 flags: HTML_HEAD | HTML_BLOCK | HTML_VALID,
558 },
559 HtmlElementInfo {
560 name: "title",
561 flags: HTML_HEAD | HTML_BLOCK | HTML_VALID,
562 },
563];
564
565fn html_tag_lookup(name: &str) -> Option<&'static HtmlElementInfo> {
568 let lower: Vec<u8> = name.bytes().map(|b| b.to_ascii_lowercase()).collect();
570 let lower_str = match core::str::from_utf8(&lower) {
571 Ok(s) => s,
572 Err(_) => return None,
573 };
574 HTML_ELEMENTS.iter().find(|info| info.name == lower_str)
575}
576
577const HTML_ENTITIES: &[(&str, &str)] = &[
584 ("nbsp", "\u{00a0}"),
585 ("lt", "<"),
586 ("gt", ">"),
587 ("amp", "&"),
588 ("quot", "\""),
589 ("apos", "'"),
590 ("copy", "\u{00a9}"),
591 ("reg", "\u{00ae}"),
592 ("amp", "&"),
593 ("iexcl", "\u{00a1}"),
594 ("cent", "\u{00a2}"),
595 ("pound", "\u{00a3}"),
596 ("curren", "\u{00a4}"),
597 ("yen", "\u{00a5}"),
598 ("brvbar", "\u{00a6}"),
599 ("sect", "\u{00a7}"),
600 ("uml", "\u{00a8}"),
601 ("ordf", "\u{00aa}"),
602 ("laquo", "\u{00ab}"),
603 ("not", "\u{00ac}"),
604 ("shy", "\u{00ad}"),
605 ("macr", "\u{00ae}"),
606 ("deg", "\u{00b0}"),
607 ("plusmn", "\u{00b1}"),
608 ("sup2", "\u{00b2}"),
609 ("sup3", "\u{00b3}"),
610 ("acute", "\u{00b4}"),
611 ("micro", "\u{00b5}"),
612 ("para", "\u{00b6}"),
613 ("middot", "\u{00b7}"),
614 ("cedil", "\u{00b8}"),
615 ("sup1", "\u{00b9}"),
616 ("ordm", "\u{00ba}"),
617 ("raquo", "\u{00bb}"),
618 ("frac14", "\u{00bc}"),
619 ("frac12", "\u{00bd}"),
620 ("frac34", "\u{00be}"),
621 ("iquest", "\u{00bf}"),
622 ("times", "\u{00d7}"),
623 ("divide", "\u{00f7}"),
624 ("ETH", "\u{00d0}"),
625 ("eth", "\u{00f0}"),
626 ("THORN", "\u{00de}"),
627 ("thorn", "\u{00fe}"),
628 ("AElig", "\u{00c6}"),
629 ("aelig", "\u{00e6}"),
630 ("OElig", "\u{0152}"),
631 ("oelig", "\u{0153}"),
632 ("Scaron", "\u{0160}"),
633 ("scaron", "\u{0161}"),
634 ("Yuml", "\u{0178}"),
635 ("circ", "\u{02c6}"),
636 ("tilde", "\u{02dc}"),
637 ("ensp", "\u{2002}"),
638 ("emsp", "\u{2003}"),
639 ("thinsp", "\u{2009}"),
640 ("zwnj", "\u{200c}"),
641 ("zwj", "\u{200d}"),
642 ("lrm", "\u{200e}"),
643 ("rlm", "\u{200f}"),
644 ("ndash", "\u{2013}"),
645 ("mdash", "\u{2014}"),
646 ("lsquo", "\u{2018}"),
647 ("rsquo", "\u{2019}"),
648 ("sbquo", "\u{201a}"),
649 ("ldquo", "\u{201c}"),
650 ("rdquo", "\u{201d}"),
651 ("bdquo", "\u{201e}"),
652 ("dagger", "\u{2020}"),
653 ("Dagger", "\u{2021}"),
654 ("bull", "\u{2022}"),
655 ("hellip", "\u{2026}"),
656 ("permil", "\u{2030}"),
657 ("prime", "\u{2032}"),
658 ("Prime", "\u{2033}"),
659 ("lsaquo", "\u{2039}"),
660 ("rsaquo", "\u{203a}"),
661 ("oline", "\u{203e}"),
662 ("euro", "\u{20ac}"),
663 ("trade", "\u{2122}"),
664 ("larr", "\u{2190}"),
665 ("uarr", "\u{2191}"),
666 ("rarr", "\u{2192}"),
667 ("darr", "\u{2193}"),
668 ("harr", "\u{2194}"),
669 ("crarr", "\u{21b5}"),
670 ("lceil", "\u{2308}"),
671 ("rceil", "\u{2309}"),
672 ("lfloor", "\u{230a}"),
673 ("rfloor", "\u{230b}"),
674 ("loz", "\u{25ca}"),
675 ("spades", "\u{2660}"),
676 ("clubs", "\u{2663}"),
677 ("hearts", "\u{2665}"),
678 ("diams", "\u{2666}"),
679 ("Alpha", "\u{0391}"),
680 ("Beta", "\u{0392}"),
681 ("Gamma", "\u{0393}"),
682 ("Delta", "\u{0394}"),
683 ("Epsilon", "\u{0395}"),
684 ("Zeta", "\u{0396}"),
685 ("Eta", "\u{0397}"),
686 ("Theta", "\u{0398}"),
687 ("Iota", "\u{0399}"),
688 ("Kappa", "\u{039a}"),
689 ("Lambda", "\u{039b}"),
690 ("Mu", "\u{039c}"),
691 ("Nu", "\u{039d}"),
692 ("Xi", "\u{039e}"),
693 ("Omicron", "\u{039f}"),
694 ("Pi", "\u{03a0}"),
695 ("Rho", "\u{03a1}"),
696 ("Sigma", "\u{03a3}"),
697 ("Tau", "\u{03a4}"),
698 ("Upsilon", "\u{03a5}"),
699 ("Phi", "\u{03a6}"),
700 ("Chi", "\u{03a7}"),
701 ("Psi", "\u{03a8}"),
702 ("Omega", "\u{03a9}"),
703 ("alpha", "\u{03b1}"),
704 ("beta", "\u{03b2}"),
705 ("gamma", "\u{03b3}"),
706 ("delta", "\u{03b4}"),
707 ("epsilon", "\u{03b5}"),
708 ("zeta", "\u{03b6}"),
709 ("eta", "\u{03b7}"),
710 ("theta", "\u{03b8}"),
711 ("iota", "\u{03b9}"),
712 ("kappa", "\u{03ba}"),
713 ("lambda", "\u{03bb}"),
714 ("mu", "\u{03bc}"),
715 ("nu", "\u{03bd}"),
716 ("xi", "\u{03be}"),
717 ("omicron", "\u{03bf}"),
718 ("pi", "\u{03c0}"),
719 ("rho", "\u{03c1}"),
720 ("sigmaf", "\u{03c2}"),
721 ("sigma", "\u{03c3}"),
722 ("tau", "\u{03c4}"),
723 ("upsilon", "\u{03c5}"),
724 ("phi", "\u{03c6}"),
725 ("chi", "\u{03c7}"),
726 ("psi", "\u{03c8}"),
727 ("omega", "\u{03c9}"),
728 ("thetasym", "\u{03d1}"),
729 ("upsih", "\u{03d2}"),
730 ("piv", "\u{03d6}"),
731];
732
733fn html_entity_lookup(name: &str) -> Option<&'static str> {
736 HTML_ENTITIES
737 .iter()
738 .find(|(n, _)| *n == name)
739 .map(|(_, v)| *v)
740}
741
742struct HtmlParserCtxt {
748 doc: *mut _xmlDoc,
750 current: *mut _xmlNode,
752 html: *mut _xmlNode,
754 head: *mut _xmlNode,
756 body: *mut _xmlNode,
758 in_head: bool,
760 in_body: bool,
762 html_created: bool,
764 head_created: bool,
766 body_created: bool,
768 seen_body_content: bool,
770 input: *mut u8,
772 input_pos: usize,
774 input_len: usize,
776 line: c_int,
778 #[allow(dead_code)]
780 err: bool,
781 filename: *mut c_char,
783 encoding: *mut c_char,
785}
786
787impl HtmlParserCtxt {
788 const fn new() -> Self {
789 HtmlParserCtxt {
790 doc: ptr::null_mut(),
791 current: ptr::null_mut(),
792 html: ptr::null_mut(),
793 head: ptr::null_mut(),
794 body: ptr::null_mut(),
795 in_head: false,
796 in_body: false,
797 html_created: false,
798 head_created: false,
799 body_created: false,
800 seen_body_content: false,
801 input: ptr::null_mut(),
802 input_pos: 0,
803 input_len: 0,
804 line: 1,
805 err: false,
806 filename: ptr::null_mut(),
807 encoding: ptr::null_mut(),
808 }
809 }
810
811 fn peek(&self) -> Option<u8> {
819 if self.input_pos < self.input_len {
820 unsafe { Some(*self.input.add(self.input_pos)) }
821 } else {
822 None
823 }
824 }
825
826 fn peek_at(&self, offset: usize) -> Option<u8> {
836 let pos = self.input_pos + offset;
837 if pos < self.input_len {
838 unsafe { Some(*self.input.add(pos)) }
839 } else {
840 None
841 }
842 }
843
844 fn next(&mut self) -> Option<u8> {
852 if self.input_pos < self.input_len {
853 let ch = unsafe { *self.input.add(self.input_pos) };
854 self.input_pos += 1;
855 if ch == b'\n' {
856 self.line += 1;
857 }
858 Some(ch)
859 } else {
860 None
861 }
862 }
863
864 fn skip_while<F: Fn(u8) -> bool>(&mut self, f: F) {
866 while let Some(ch) = self.peek() {
867 if f(ch) {
868 self.next();
869 } else {
870 break;
871 }
872 }
873 }
874
875 fn skip_whitespace(&mut self) {
877 self.skip_while(|ch| ch == b' ' || ch == b'\t' || ch == b'\n' || ch == b'\r');
878 }
879
880 const fn is_eof(&self) -> bool {
882 self.input_pos >= self.input_len
883 }
884
885 fn read_while<F: Fn(u8) -> bool>(&mut self, f: F) -> Vec<u8> {
894 let start = self.input_pos;
895 while let Some(ch) = self.peek() {
896 if f(ch) {
897 self.next();
898 } else {
899 break;
900 }
901 }
902 unsafe { slice::from_raw_parts(self.input.add(start), self.input_pos - start).to_vec() }
903 }
904}
905
906fn is_heading(name: &str) -> bool {
912 matches!(name, "h1" | "h2" | "h3" | "h4" | "h5" | "h6")
913}
914
915#[allow(dead_code)]
924unsafe fn get_parent_element(node: *mut _xmlNode) -> *mut _xmlNode {
925 if node.is_null() {
926 return ptr::null_mut();
927 }
928 let mut n = node;
929 loop {
930 let parent = unsafe { (*n).parent };
931 if parent.is_null() {
932 return ptr::null_mut();
933 }
934 let ptype = unsafe { (*parent).type_ };
935 if ptype == XML_ELEMENT_NODE as c_int
936 || ptype == XML_HTML_DOCUMENT_NODE as c_int
937 || ptype == XML_DOCUMENT_NODE as c_int
938 {
939 return parent;
940 }
941 n = parent;
942 }
943}
944
945unsafe fn auto_close_element(ctxt: &mut HtmlParserCtxt, tag_name: &str) {
957 let tag_lower: Vec<u8> = tag_name.bytes().map(|b| b.to_ascii_lowercase()).collect();
958 let tag_lower_str = match core::str::from_utf8(&tag_lower) {
959 Ok(s) => s,
960 Err(_) => return,
961 };
962
963 let info = html_tag_lookup(tag_lower_str);
964
965 let mut current = ctxt.current;
966
967 let mut open_names: Vec<Vec<u8>> = Vec::new();
969 let mut cur = current;
970 while !cur.is_null() {
971 let ctype = unsafe { (*cur).type_ };
972 if ctype == XML_ELEMENT_NODE as c_int && !unsafe { (*cur).name.is_null() } {
973 let name_bytes = unsafe { xmlstr_to_bytes((*cur).name) };
974 open_names.push(name_bytes.to_vec());
975 }
976 cur = unsafe { (*cur).parent };
977 }
978
979 if tag_lower_str == "p" || info.is_some_and(|i| i.flags & HTML_BLOCK != 0) {
981 let mut cur2 = current;
983 while !cur2.is_null() {
984 let ctype = unsafe { (*cur2).type_ };
985 if ctype == XML_ELEMENT_NODE as c_int && !unsafe { (*cur2).name.is_null() } {
986 let name_bytes = unsafe { xmlstr_to_bytes((*cur2).name) };
987 let name_str = core::str::from_utf8(name_bytes).unwrap_or("");
988 if name_str.eq_ignore_ascii_case("p") {
989 current = unsafe { (*cur2).parent };
991 break;
992 }
993 }
994 cur2 = unsafe { (*cur2).parent };
995 }
996 }
997
998 if is_heading(tag_lower_str) {
1000 let mut cur2 = current;
1001 while !cur2.is_null() {
1002 let ctype = unsafe { (*cur2).type_ };
1003 if ctype == XML_ELEMENT_NODE as c_int && !unsafe { (*cur2).name.is_null() } {
1004 let name_bytes = unsafe { xmlstr_to_bytes((*cur2).name) };
1005 let name_str = core::str::from_utf8(name_bytes).unwrap_or("");
1006 if is_heading(name_str) {
1007 current = unsafe { (*cur2).parent };
1008 break;
1009 }
1010 }
1011 cur2 = unsafe { (*cur2).parent };
1012 }
1013 }
1014
1015 if tag_lower_str == "li" {
1017 let mut cur2 = current;
1018 while !cur2.is_null() {
1019 let ctype = unsafe { (*cur2).type_ };
1020 if ctype == XML_ELEMENT_NODE as c_int && !unsafe { (*cur2).name.is_null() } {
1021 let name_bytes = unsafe { xmlstr_to_bytes((*cur2).name) };
1022 let name_str = core::str::from_utf8(name_bytes).unwrap_or("");
1023 if name_str.eq_ignore_ascii_case("li") {
1024 current = unsafe { (*cur2).parent };
1025 break;
1026 }
1027 }
1028 cur2 = unsafe { (*cur2).parent };
1029 }
1030 }
1031
1032 if tag_lower_str == "dt" || tag_lower_str == "dd" {
1034 let mut cur2 = current;
1035 while !cur2.is_null() {
1036 let ctype = unsafe { (*cur2).type_ };
1037 if ctype == XML_ELEMENT_NODE as c_int && !unsafe { (*cur2).name.is_null() } {
1038 let name_bytes = unsafe { xmlstr_to_bytes((*cur2).name) };
1039 let name_str = core::str::from_utf8(name_bytes).unwrap_or("");
1040 if name_str == "dt" || name_str == "dd" {
1041 current = unsafe { (*cur2).parent };
1042 break;
1043 }
1044 }
1045 cur2 = unsafe { (*cur2).parent };
1046 }
1047 }
1048
1049 if tag_lower_str == "tr" {
1054 let mut cur2 = current;
1055 while !cur2.is_null() {
1056 let ctype = unsafe { (*cur2).type_ };
1057 if ctype == XML_ELEMENT_NODE as c_int && !unsafe { (*cur2).name.is_null() } {
1058 let name_bytes = unsafe { xmlstr_to_bytes((*cur2).name) };
1059 let name_str = core::str::from_utf8(name_bytes).unwrap_or("");
1060 if name_str == "tr" || name_str == "td" || name_str == "th" {
1061 current = unsafe { (*cur2).parent };
1062 break;
1063 }
1064 }
1065 cur2 = unsafe { (*cur2).parent };
1066 }
1067 } else if tag_lower_str == "td" || tag_lower_str == "th" {
1068 let mut cur2 = current;
1069 while !cur2.is_null() {
1070 let ctype = unsafe { (*cur2).type_ };
1071 if ctype == XML_ELEMENT_NODE as c_int && !unsafe { (*cur2).name.is_null() } {
1072 let name_bytes = unsafe { xmlstr_to_bytes((*cur2).name) };
1073 let name_str = core::str::from_utf8(name_bytes).unwrap_or("");
1074 if name_str == "td" || name_str == "th" {
1075 current = unsafe { (*cur2).parent };
1076 break;
1077 }
1078 }
1079 cur2 = unsafe { (*cur2).parent };
1080 }
1081 }
1082
1083 if matches!(tag_lower_str, "thead" | "tbody" | "tfoot") {
1085 let mut cur2 = current;
1086 while !cur2.is_null() {
1087 let ctype = unsafe { (*cur2).type_ };
1088 if ctype == XML_ELEMENT_NODE as c_int && !unsafe { (*cur2).name.is_null() } {
1089 let name_bytes = unsafe { xmlstr_to_bytes((*cur2).name) };
1090 let name_str = core::str::from_utf8(name_bytes).unwrap_or("");
1091 if name_str == "thead" || name_str == "tbody" || name_str == "tfoot" {
1092 current = unsafe { (*cur2).parent };
1093 break;
1094 }
1095 }
1096 cur2 = unsafe { (*cur2).parent };
1097 }
1098 }
1099
1100 if tag_lower_str == "colgroup" {
1102 let mut cur2 = current;
1103 while !cur2.is_null() {
1104 let ctype = unsafe { (*cur2).type_ };
1105 if ctype == XML_ELEMENT_NODE as c_int && !unsafe { (*cur2).name.is_null() } {
1106 let name_bytes = unsafe { xmlstr_to_bytes((*cur2).name) };
1107 let name_str = core::str::from_utf8(name_bytes).unwrap_or("");
1108 if name_str == "colgroup" {
1109 current = unsafe { (*cur2).parent };
1110 break;
1111 }
1112 }
1113 cur2 = unsafe { (*cur2).parent };
1114 }
1115 }
1116
1117 if tag_lower_str == "caption" {
1119 let mut cur2 = current;
1120 while !cur2.is_null() {
1121 let ctype = unsafe { (*cur2).type_ };
1122 if ctype == XML_ELEMENT_NODE as c_int && !unsafe { (*cur2).name.is_null() } {
1123 let name_bytes = unsafe { xmlstr_to_bytes((*cur2).name) };
1124 let name_str = core::str::from_utf8(name_bytes).unwrap_or("");
1125 if name_str == "caption" {
1126 current = unsafe { (*cur2).parent };
1127 break;
1128 }
1129 }
1130 cur2 = unsafe { (*cur2).parent };
1131 }
1132 }
1133
1134 if tag_lower_str == "form" {
1136 let mut cur2 = current;
1137 while !cur2.is_null() {
1138 let ctype = unsafe { (*cur2).type_ };
1139 if ctype == XML_ELEMENT_NODE as c_int && !unsafe { (*cur2).name.is_null() } {
1140 let name_bytes = unsafe { xmlstr_to_bytes((*cur2).name) };
1141 let name_str = core::str::from_utf8(name_bytes).unwrap_or("");
1142 if name_str.eq_ignore_ascii_case("form") {
1143 current = unsafe { (*cur2).parent };
1144 break;
1145 }
1146 }
1147 cur2 = unsafe { (*cur2).parent };
1148 }
1149 }
1150
1151 ctxt.current = current;
1152}
1153
1154unsafe fn ensure_html(ctxt: &mut HtmlParserCtxt) -> *mut _xmlNode {
1160 if !ctxt.html.is_null() {
1161 return ctxt.html;
1162 }
1163
1164 let html_node = tree::new_node(ptr::null_mut(), b"html\0" as *const u8 as *const xmlChar);
1165 if html_node.is_null() {
1166 return ptr::null_mut();
1167 }
1168 {
1169 }
1172 ctxt.html = html_node;
1173 ctxt.html_created = true;
1174
1175 tree::add_child(ctxt.doc as *mut _xmlNode, html_node);
1177 ctxt.current = html_node;
1178
1179 html_node
1180}
1181
1182unsafe fn ensure_head(ctxt: &mut HtmlParserCtxt) -> *mut _xmlNode {
1184 if !ctxt.head.is_null() {
1185 return ctxt.head;
1186 }
1187
1188 ensure_html(ctxt);
1190
1191 let head_node = tree::new_node(ptr::null_mut(), b"head\0" as *const u8 as *const xmlChar);
1192 if head_node.is_null() {
1193 return ptr::null_mut();
1194 }
1195 ctxt.head = head_node;
1196 ctxt.head_created = true;
1197
1198 tree::add_child(ctxt.html, head_node);
1200 ctxt.current = head_node;
1201 ctxt.in_head = true;
1202
1203 head_node
1204}
1205
1206unsafe fn ensure_body(ctxt: &mut HtmlParserCtxt) -> *mut _xmlNode {
1208 if !ctxt.body.is_null() {
1209 return ctxt.body;
1210 }
1211
1212 ensure_html(ctxt);
1214
1215 let body_node = tree::new_node(ptr::null_mut(), b"body\0" as *const u8 as *const xmlChar);
1216 if body_node.is_null() {
1217 return ptr::null_mut();
1218 }
1219 ctxt.body = body_node;
1220 ctxt.body_created = true;
1221
1222 tree::add_child(ctxt.html, body_node);
1224 ctxt.current = body_node;
1225 ctxt.in_body = true;
1226
1227 body_node
1228}
1229
1230#[allow(dead_code)]
1232unsafe fn transition_to_body(ctxt: &mut HtmlParserCtxt) {
1233 if ctxt.in_head && !ctxt.seen_body_content {
1234 ctxt.seen_body_content = true;
1235 ctxt.in_head = false;
1236 ensure_body(ctxt);
1237 }
1238}
1239
1240struct HtmlAttr {
1246 name: Vec<u8>,
1247 value: Vec<u8>,
1248 #[allow(dead_code)]
1249 quoted: bool,
1250}
1251
1252fn parse_attr_name(ctxt: &mut HtmlParserCtxt) -> Vec<u8> {
1254 let mut name = Vec::new();
1255 while let Some(ch) = ctxt.peek() {
1256 if ch == b'='
1257 || ch == b'>'
1258 || ch == b'/'
1259 || ch == b' '
1260 || ch == b'\t'
1261 || ch == b'\n'
1262 || ch == b'\r'
1263 {
1264 break;
1265 }
1266 name.push(ch);
1267 ctxt.next();
1268 }
1269 name
1270}
1271
1272fn parse_attr_value(ctxt: &mut HtmlParserCtxt) -> (Vec<u8>, bool) {
1274 ctxt.skip_whitespace();
1275
1276 let quote = match ctxt.peek() {
1277 Some(b'"') => {
1278 ctxt.next(); b'"'
1280 }
1281 Some(b'\'') => {
1282 ctxt.next(); b'\''
1284 }
1285 _ => {
1286 let value = ctxt.read_while(|ch| {
1288 ch != b'>' && ch != b' ' && ch != b'\t' && ch != b'\n' && ch != b'\r'
1289 });
1290 return (value, false);
1291 }
1292 };
1293
1294 let mut value = Vec::new();
1296 loop {
1297 match ctxt.next() {
1298 Some(ch) if ch == quote => break,
1299 Some(ch) => value.push(ch),
1300 None => break,
1301 }
1302 }
1303 (value, true)
1304}
1305
1306fn parse_attributes(ctxt: &mut HtmlParserCtxt) -> Vec<HtmlAttr> {
1308 let mut attrs = Vec::new();
1309
1310 loop {
1311 ctxt.skip_whitespace();
1312
1313 match ctxt.peek() {
1314 Some(b'>') | None => break,
1315 Some(b'/')
1316 if ctxt.peek_at(1) == Some(b'>') => {
1318 break;
1319 }
1320 _ => {}
1322 }
1323
1324 let name = parse_attr_name(ctxt);
1325 if name.is_empty() {
1326 break;
1327 }
1328
1329 ctxt.skip_whitespace();
1331 if ctxt.peek() == Some(b'=') {
1332 ctxt.next(); let (value, quoted) = parse_attr_value(ctxt);
1334 attrs.push(HtmlAttr {
1335 name,
1336 value,
1337 quoted,
1338 });
1339 } else {
1340 attrs.push(HtmlAttr {
1342 name,
1343 value: Vec::new(),
1344 quoted: false,
1345 });
1346 }
1347 }
1348
1349 attrs
1350}
1351
1352fn resolve_entity(name: &str) -> Vec<u8> {
1358 if let Some(replacement) = html_entity_lookup(name) {
1359 replacement.as_bytes().to_vec()
1360 } else {
1361 let mut result = Vec::new();
1363 result.push(b'&');
1364 result.extend_from_slice(name.as_bytes());
1365 result.push(b';');
1366 result
1367 }
1368}
1369
1370fn resolve_numeric_entity(value: &str, is_hex: bool) -> Vec<u8> {
1372 let codepoint = if is_hex {
1373 u32::from_str_radix(value, 16).unwrap_or(0xFFFD)
1374 } else {
1375 value.parse::<u32>().unwrap_or(0xFFFD)
1376 };
1377
1378 if codepoint == 0 {
1379 return Vec::new();
1380 }
1381
1382 match char::from_u32(codepoint) {
1384 Some(c) => {
1385 let mut buf = [0u8; 4];
1386 let s = c.encode_utf8(&mut buf);
1387 s.as_bytes().to_vec()
1388 }
1389 None => vec![0xEF, 0xBF, 0xBD], }
1391}
1392
1393fn parse_entity(ctxt: &mut HtmlParserCtxt) -> Vec<u8> {
1396 match ctxt.peek() {
1398 Some(b'&') => {
1399 ctxt.next(); }
1401 _ => return vec![b'&'],
1402 }
1403
1404 if ctxt.peek() == Some(b'#') {
1406 ctxt.next(); let is_hex = ctxt.peek() == Some(b'x') || ctxt.peek() == Some(b'X');
1408 if is_hex {
1409 ctxt.next(); }
1411
1412 let digits = ctxt.read_while(|ch| {
1413 if is_hex {
1414 ch.is_ascii_hexdigit()
1415 } else {
1416 ch.is_ascii_digit()
1417 }
1418 });
1419
1420 let digits_str = core::str::from_utf8(&digits).unwrap_or("");
1421 if digits_str.is_empty() {
1422 let mut result = vec![b'&', b'#'];
1423 if is_hex {
1424 result.push(b'x');
1425 }
1426 return result;
1427 }
1428
1429 if ctxt.peek() == Some(b';') {
1431 ctxt.next();
1432 }
1433
1434 return resolve_numeric_entity(digits_str, is_hex);
1435 }
1436
1437 let name = ctxt.read_while(|ch| ch.is_ascii_alphanumeric() || ch == b'_' || ch == b'-');
1439 let name_str = core::str::from_utf8(&name).unwrap_or("");
1440
1441 if ctxt.peek() == Some(b';') {
1443 ctxt.next();
1444 }
1445
1446 resolve_entity(name_str)
1447}
1448
1449unsafe fn handle_text(ctxt: &mut HtmlParserCtxt, text: &[u8]) {
1461 if text.is_empty() {
1462 return;
1463 }
1464
1465 let parent = if ctxt.in_head {
1467 ctxt.head
1468 } else if ctxt.in_body || ctxt.body_created {
1469 ctxt.body
1470 } else if ctxt.html_created {
1471 ctxt.html
1472 } else {
1473 ctxt.doc as *mut _xmlNode
1474 };
1475
1476 let insertion_point = if ctxt.current.is_null() {
1477 parent
1478 } else {
1479 ctxt.current
1480 };
1481
1482 if insertion_point.is_null() {
1483 let text_node = tree::new_text(ptr::null_mut());
1485 if !text_node.is_null() {
1486 let content = bytes_to_xmlstr(text);
1488 if !content.is_null() {
1489 unsafe {
1490 (*text_node).content = content;
1491 }
1492 }
1493 tree::add_child(ctxt.doc as *mut _xmlNode, text_node);
1494 }
1495 return;
1496 }
1497
1498 if insertion_point == ctxt.doc as *mut _xmlNode {
1503 if text.iter().all(|b| b.is_ascii_whitespace()) {
1504 return;
1505 }
1506 let content = trim_ascii_start(text);
1507 let html_node = tree::new_node(ptr::null_mut(), bytes_to_xmlstr(b"html"));
1508 if !html_node.is_null() {
1509 let content_c = bytes_to_xmlstr(content);
1510 let text_node = tree::new_text(ptr::null_mut());
1511 if !text_node.is_null() {
1512 unsafe {
1513 (*text_node).content = content_c;
1514 }
1515 tree::add_child(html_node, text_node);
1516 }
1517 tree::add_child(ctxt.doc as *mut _xmlNode, html_node);
1518 ctxt.html = html_node;
1519 ctxt.html_created = true;
1520 ctxt.current = html_node;
1521 }
1522 return;
1523 }
1524
1525 let text_node = tree::new_text(ptr::null_mut());
1526 if text_node.is_null() {
1527 return;
1528 }
1529
1530 let content = bytes_to_xmlstr(text);
1532 if !content.is_null() {
1533 unsafe {
1534 (*text_node).content = content;
1535 }
1536 }
1537
1538 tree::add_child(insertion_point, text_node);
1539}
1540
1541unsafe fn handle_start_tag(ctxt: &mut HtmlParserCtxt, tag_name: &[u8], attrs: &[HtmlAttr]) {
1543 let tag_lower: Vec<u8> = tag_name.iter().map(|b| b.to_ascii_lowercase()).collect();
1544 let tag_str = core::str::from_utf8(&tag_lower).unwrap_or("");
1545
1546 let info = html_tag_lookup(tag_str);
1547
1548 let is_head_tag = info.is_some_and(|i| i.flags & HTML_HEAD != 0);
1550 let _is_body_tag = info.is_some_and(|i| i.flags & HTML_BODY != 0);
1551 let is_empty = info.is_some_and(|i| i.flags & HTML_EMPTY != 0);
1552 let _is_block = info.is_some_and(|i| i.flags & HTML_BLOCK != 0);
1553
1554 if tag_str == "html" {
1556 if !ctxt.html.is_null() && !ctxt.html_created {
1557 return;
1559 }
1560 if ctxt.html.is_null() {
1562 let html_node = tree::new_node(ptr::null_mut(), bytes_to_xmlstr(tag_name));
1563 if !html_node.is_null() {
1564 ctxt.html = html_node;
1565 ctxt.html_created = false; tree::add_child(ctxt.doc as *mut _xmlNode, html_node);
1567 ctxt.current = html_node;
1568 }
1569 } else {
1570 ctxt.current = ctxt.html;
1572 }
1573 return;
1574 }
1575
1576 if tag_str == "head" {
1577 if !ctxt.head.is_null() && !ctxt.head_created {
1578 return;
1580 }
1581 ensure_html(ctxt);
1583
1584 if ctxt.head.is_null() {
1585 let head_node = tree::new_node(ptr::null_mut(), bytes_to_xmlstr(tag_name));
1586 if !head_node.is_null() {
1587 ctxt.head = head_node;
1588 ctxt.head_created = false;
1589 tree::add_child(ctxt.html, head_node);
1590 ctxt.current = head_node;
1591 ctxt.in_head = true;
1592 }
1593 } else {
1594 ctxt.current = ctxt.head;
1595 ctxt.in_head = true;
1596 }
1597 return;
1598 }
1599
1600 if tag_str == "body" {
1601 if !ctxt.body.is_null() && !ctxt.body_created {
1602 return;
1604 }
1605 ensure_html(ctxt);
1607
1608 if ctxt.body.is_null() {
1609 let body_node = tree::new_node(ptr::null_mut(), bytes_to_xmlstr(tag_name));
1610 if !body_node.is_null() {
1611 ctxt.body = body_node;
1612 ctxt.body_created = false;
1613 tree::add_child(ctxt.html, body_node);
1614 ctxt.current = body_node;
1615 ctxt.in_body = true;
1616 ctxt.in_head = false;
1617 ctxt.seen_body_content = true;
1618 }
1619 } else {
1620 ctxt.current = ctxt.body;
1621 ctxt.in_body = true;
1622 ctxt.in_head = false;
1623 ctxt.seen_body_content = true;
1624 }
1625 return;
1626 }
1627
1628 if is_head_tag && !ctxt.seen_body_content {
1630 if ctxt.head.is_null() {
1631 ensure_head(ctxt);
1632 }
1633
1634 if is_empty {
1635 let node = tree::new_node(ptr::null_mut(), bytes_to_xmlstr(tag_name));
1637 if !node.is_null() {
1638 for attr in attrs {
1639 let name_c = bytes_to_xmlstr(&attr.name);
1640 let val_c = bytes_to_xmlstr(&attr.value);
1641 if !name_c.is_null() {
1642 tree::set_prop(node, name_c, val_c);
1643 xmlFreeImpl(name_c as *mut c_void);
1644 if !val_c.is_null() {
1645 xmlFreeImpl(val_c as *mut c_void);
1646 }
1647 }
1648 }
1649 tree::add_child(ctxt.current, node);
1650 }
1651 return;
1652 }
1653
1654 let node = tree::new_node(ptr::null_mut(), bytes_to_xmlstr(tag_name));
1655 if !node.is_null() {
1656 for attr in attrs {
1657 let name_c = bytes_to_xmlstr(&attr.name);
1658 let val_c = bytes_to_xmlstr(&attr.value);
1659 if !name_c.is_null() {
1660 tree::set_prop(node, name_c, val_c);
1661 xmlFreeImpl(name_c as *mut c_void);
1662 if !val_c.is_null() {
1663 xmlFreeImpl(val_c as *mut c_void);
1664 }
1665 }
1666 }
1667 tree::add_child(ctxt.current, node);
1668 ctxt.current = node;
1669 }
1670 return;
1671 }
1672
1673 if !is_head_tag || ctxt.seen_body_content {
1675 if !ctxt.seen_body_content {
1676 ctxt.seen_body_content = true;
1677 ctxt.in_head = false;
1678 if ctxt.body.is_null() {
1679 ensure_body(ctxt);
1680 } else {
1681 ctxt.current = ctxt.body;
1682 ctxt.in_body = true;
1683 }
1684 } else if ctxt.body.is_null() {
1685 ensure_body(ctxt);
1686 }
1687 }
1688
1689 if !ctxt.current.is_null() {
1691 auto_close_element(ctxt, tag_str);
1692 }
1693
1694 if is_empty {
1695 let node = tree::new_node(ptr::null_mut(), bytes_to_xmlstr(tag_name));
1697 if !node.is_null() {
1698 for attr in attrs {
1699 let name_c = bytes_to_xmlstr(&attr.name);
1700 let val_c = bytes_to_xmlstr(&attr.value);
1701 if !name_c.is_null() {
1702 tree::set_prop(node, name_c, val_c);
1703 xmlFreeImpl(name_c as *mut c_void);
1704 if !val_c.is_null() {
1705 xmlFreeImpl(val_c as *mut c_void);
1706 }
1707 }
1708 }
1709 let insertion_point = if ctxt.current.is_null() {
1710 ctxt.body
1711 } else {
1712 ctxt.current
1713 };
1714 if !insertion_point.is_null() {
1715 tree::add_child(insertion_point, node);
1716 }
1717 }
1718 return;
1719 }
1720
1721 let node = tree::new_node(ptr::null_mut(), bytes_to_xmlstr(tag_name));
1723 if !node.is_null() {
1724 for attr in attrs {
1725 let name_c = bytes_to_xmlstr(&attr.name);
1726 let val_c = bytes_to_xmlstr(&attr.value);
1727 if !name_c.is_null() {
1728 tree::set_prop(node, name_c, val_c);
1729 xmlFreeImpl(name_c as *mut c_void);
1730 if !val_c.is_null() {
1731 xmlFreeImpl(val_c as *mut c_void);
1732 }
1733 }
1734 }
1735
1736 let insertion_point = if ctxt.current.is_null() {
1737 if ctxt.in_body || ctxt.body_created {
1738 ctxt.body
1739 } else if ctxt.in_head || ctxt.head_created {
1740 ctxt.head
1741 } else if ctxt.html_created {
1742 ctxt.html
1743 } else {
1744 ctxt.doc as *mut _xmlNode
1745 }
1746 } else {
1747 ctxt.current
1748 };
1749
1750 if !insertion_point.is_null() {
1751 tree::add_child(insertion_point, node);
1752 ctxt.current = node;
1754 }
1755 }
1756}
1757
1758unsafe fn handle_end_tag(ctxt: &mut HtmlParserCtxt, tag_name: &[u8]) {
1769 let tag_lower: Vec<u8> = tag_name.iter().map(|b| b.to_ascii_lowercase()).collect();
1770 let tag_str = core::str::from_utf8(&tag_lower).unwrap_or("");
1771
1772 let info = html_tag_lookup(tag_str);
1773
1774 if info.is_some_and(|i| i.flags & HTML_EMPTY != 0) {
1777 return;
1778 }
1779
1780 if tag_str == "html" {
1781 ctxt.current = ctxt.doc as *mut _xmlNode;
1782 return;
1783 }
1784
1785 if tag_str == "head" {
1786 ctxt.in_head = false;
1787 ctxt.current = ctxt.html;
1788 return;
1789 }
1790
1791 if tag_str == "body" {
1792 ctxt.in_body = false;
1793 ctxt.current = ctxt.html;
1794 return;
1795 }
1796
1797 let mut cur = ctxt.current;
1799 while !cur.is_null() {
1800 let ctype = unsafe { (*cur).type_ };
1801 if ctype == XML_ELEMENT_NODE as c_int && !unsafe { (*cur).name.is_null() } {
1802 let name_bytes = unsafe { xmlstr_to_bytes((*cur).name) };
1803 if name_bytes.eq_ignore_ascii_case(tag_name) {
1804 ctxt.current = unsafe { (*cur).parent };
1806 return;
1807 }
1808 }
1809 cur = unsafe { (*cur).parent };
1810 }
1811
1812 }
1814
1815unsafe fn html_parse_buffer(
1825 ctxt: &mut HtmlParserCtxt,
1826 buffer: *const c_char,
1827 size: c_int,
1828) -> *mut _xmlDoc {
1829 if buffer.is_null() || size <= 0 {
1830 return ptr::null_mut();
1831 }
1832
1833 let doc = tree::new_doc(ptr::null());
1835 if doc.is_null() {
1836 return ptr::null_mut();
1837 }
1838 unsafe {
1839 (*doc).type_ = XML_HTML_DOCUMENT_NODE as c_int;
1840 if !(*doc).version.is_null() {
1843 crate::abi::allocator::xmlFreeImpl((*doc).version as *mut c_void);
1844 }
1845 (*doc).version = ptr::null_mut();
1846 (*doc).properties = XML_DOC_WELLFORMED as c_int;
1847 (*doc).standalone = 1;
1850 }
1851 let default_dtd = crate::xml::dtd::create_int_subset(
1857 doc,
1858 b"html\0" as *const u8 as *const xmlChar,
1859 b"-//W3C//DTD HTML 4.0 Transitional//EN\0" as *const u8 as *const xmlChar,
1860 b"http://www.w3.org/TR/REC-html40/loose.dtd\0" as *const u8 as *const xmlChar,
1861 );
1862 let _ = default_dtd;
1863 ctxt.doc = doc;
1864
1865 ctxt.input = buffer as *mut u8;
1867 ctxt.input_len = size as usize;
1868 ctxt.input_pos = 0;
1869 ctxt.line = 1;
1870
1871 loop {
1873 if ctxt.is_eof() {
1874 break;
1875 }
1876
1877 let ch = ctxt.peek().unwrap_or(0);
1878
1879 if ch == b'<' {
1880 ctxt.next(); if ctxt.peek() == Some(b'/') {
1884 ctxt.next(); let tag_name = ctxt.read_while(|ch| {
1886 ch != b'>' && ch != b' ' && ch != b'\t' && ch != b'\n' && ch != b'\r'
1887 });
1888
1889 while ctxt.peek() != Some(b'>') && !ctxt.is_eof() {
1891 ctxt.next();
1892 }
1893 if ctxt.peek() == Some(b'>') {
1894 ctxt.next(); }
1896
1897 if !tag_name.is_empty() {
1898 handle_end_tag(ctxt, &tag_name);
1899 }
1900 continue;
1901 }
1902
1903 if ctxt.peek() == Some(b'!')
1905 && ctxt.peek_at(1) == Some(b'-')
1906 && ctxt.peek_at(2) == Some(b'-')
1907 {
1908 ctxt.next(); ctxt.next(); ctxt.next(); let mut comment_content = Vec::new();
1914 loop {
1915 if ctxt.peek() == Some(b'-')
1916 && ctxt.peek_at(1) == Some(b'-')
1917 && ctxt.peek_at(2) == Some(b'>')
1918 {
1919 ctxt.next(); ctxt.next(); ctxt.next(); break;
1923 }
1924 match ctxt.next() {
1925 Some(ch) => comment_content.push(ch),
1926 None => break,
1927 }
1928 }
1929
1930 if !comment_content.is_empty() {
1932 let comment_node = tree::new_comment(bytes_to_xmlstr(&comment_content));
1933 if !comment_node.is_null() {
1934 let insertion_point = if !ctxt.current.is_null() {
1935 ctxt.current
1936 } else {
1937 ctxt.doc as *mut _xmlNode
1938 };
1939 tree::add_child(insertion_point, comment_node);
1940 }
1941 }
1942 continue;
1943 }
1944
1945 if ctxt.peek() == Some(b'!') {
1947 ctxt.next(); let _rest = ctxt.read_while(|ch| ch != b'>');
1949 if ctxt.peek() == Some(b'>') {
1950 ctxt.next(); }
1952 continue;
1955 }
1956
1957 if ctxt.peek() == Some(b'?') {
1959 ctxt.next(); let mut pi_content = Vec::new();
1962 loop {
1963 if ctxt.peek() == Some(b'?') && ctxt.peek_at(1) == Some(b'>') {
1964 break;
1965 }
1966 match ctxt.next() {
1967 Some(ch) => pi_content.push(ch),
1968 None => break,
1969 }
1970 }
1971 if ctxt.peek() == Some(b'?') {
1973 ctxt.next();
1974 }
1975 if ctxt.peek() == Some(b'>') {
1976 ctxt.next();
1977 }
1978 if !pi_content.is_empty() {
1980 let mut parts = pi_content.splitn(2, |b| *b == b' ');
1982 let target = parts.next().unwrap_or(&pi_content);
1983 let value = parts.next().unwrap_or(b"");
1984
1985 let pi_node = tree::new_pi(bytes_to_xmlstr(target), bytes_to_xmlstr(value));
1986 if !pi_node.is_null() {
1987 let insertion_point = if !ctxt.current.is_null() {
1988 ctxt.current
1989 } else {
1990 ctxt.doc as *mut _xmlNode
1991 };
1992 tree::add_child(insertion_point, pi_node);
1993 }
1994 }
1995 continue;
1996 }
1997
1998 let tag_name = ctxt.read_while(|ch| {
2000 ch != b'>' && ch != b'/' && ch != b' ' && ch != b'\t' && ch != b'\n' && ch != b'\r'
2001 });
2002
2003 if tag_name.is_empty() {
2004 handle_text(ctxt, b"<");
2006 continue;
2007 }
2008
2009 let attrs = parse_attributes(ctxt);
2011
2012 if ctxt.peek() == Some(b'/') {
2014 ctxt.next(); if ctxt.peek() == Some(b'>') {
2016 ctxt.next(); }
2018 } else if ctxt.peek() == Some(b'>') {
2019 ctxt.next(); }
2021
2022 let tag_lower: Vec<u8> = tag_name.iter().map(|b| b.to_ascii_lowercase()).collect();
2024 let tag_str = core::str::from_utf8(&tag_lower).unwrap_or("");
2025
2026 if tag_str == "script" || tag_str == "style" {
2027 let raw_node = tree::new_node(ptr::null_mut(), bytes_to_xmlstr(&tag_name));
2030 if !raw_node.is_null() {
2031 for attr in &attrs {
2032 let name_c = bytes_to_xmlstr(&attr.name);
2033 let val_c = bytes_to_xmlstr(&attr.value);
2034 if !name_c.is_null() {
2035 tree::set_prop(raw_node, name_c, val_c);
2036 xmlFreeImpl(name_c as *mut c_void);
2037 if !val_c.is_null() {
2038 xmlFreeImpl(val_c as *mut c_void);
2039 }
2040 }
2041 }
2042
2043 let insertion_point = if ctxt.current.is_null() {
2044 if ctxt.in_head {
2045 ensure_head(ctxt);
2046 ctxt.head
2047 } else {
2048 ensure_body(ctxt);
2049 ctxt.body
2050 }
2051 } else {
2052 ctxt.current
2053 };
2054
2055 if !insertion_point.is_null() {
2056 tree::add_child(insertion_point, raw_node);
2057
2058 let end_tag = format!("</{}", tag_str);
2060 let end_bytes = end_tag.as_bytes();
2061 let mut raw_text = Vec::new();
2062 let mut match_idx = 0;
2063
2064 loop {
2065 if ctxt.is_eof() {
2066 break;
2067 }
2068 let ch = ctxt.peek().unwrap();
2069 if ch.to_ascii_lowercase() == end_bytes[match_idx] {
2070 match_idx += 1;
2071 if match_idx == end_bytes.len() {
2072 if !raw_text.is_empty() {
2075 let text_node = tree::new_text(bytes_to_xmlstr(&raw_text));
2076 if !text_node.is_null() {
2077 tree::add_child(raw_node, text_node);
2078 }
2079 }
2080 ctxt.next(); let _suffix = ctxt.read_while(|ch| ch != b'>');
2084 if ctxt.peek() == Some(b'>') {
2085 ctxt.next();
2086 }
2087 ctxt.current = unsafe { (*raw_node).parent };
2089 break;
2090 }
2091 if match_idx == 1 {
2093 raw_text.push(ch);
2094 }
2095 ctxt.next();
2096 } else {
2097 if match_idx > 0 {
2099 match_idx = 0;
2101 }
2102 raw_text.push(ch);
2103 ctxt.next();
2104 }
2105 }
2106
2107 if match_idx < end_bytes.len() && !raw_text.is_empty() {
2109 let text_node = tree::new_text(bytes_to_xmlstr(&raw_text));
2110 if !text_node.is_null() {
2111 tree::add_child(raw_node, text_node);
2112 }
2113 ctxt.current = unsafe { (*raw_node).parent };
2114 }
2115 }
2116 }
2117 continue;
2118 }
2119
2120 handle_start_tag(ctxt, &tag_name, &attrs);
2122 } else {
2123 let mut text = Vec::new();
2125 loop {
2126 match ctxt.peek() {
2127 Some(b'<') => break,
2128 Some(b'&') => {
2129 let entity_text = parse_entity(ctxt);
2131 text.extend_from_slice(&entity_text);
2132 }
2133 Some(ch) => {
2134 text.push(ch);
2135 ctxt.next();
2136 }
2137 None => break,
2138 }
2139 }
2140
2141 if !text.is_empty() {
2142 handle_text(ctxt, &text);
2143 }
2144 }
2145 }
2146
2147 if ctxt.html.is_null() {
2149 ensure_html(ctxt);
2150 }
2151
2152 doc
2153}
2154
2155pub unsafe fn parse_file(filename: *const c_char, encoding: *const c_char) -> *mut _xmlDoc {
2170 if filename.is_null() {
2171 return ptr::null_mut();
2172 }
2173
2174 let filename_str = unsafe { std::ffi::CStr::from_ptr(filename) };
2176 let path = filename_str.to_str().unwrap_or("");
2177 let content = match std::fs::read(path) {
2178 Ok(data) => data,
2179 Err(_) => return ptr::null_mut(),
2180 };
2181
2182 let mut ctxt = HtmlParserCtxt::new();
2183 if !encoding.is_null() {
2184 let _enc_cstr = unsafe { std::ffi::CStr::from_ptr(encoding) };
2185 ctxt.encoding = unsafe { c_strdup(encoding) };
2186 }
2187
2188 let doc = unsafe {
2189 html_parse_buffer(
2190 &mut ctxt,
2191 content.as_ptr() as *const c_char,
2192 content.len() as c_int,
2193 )
2194 };
2195
2196 if !doc.is_null() && !filename.is_null() {
2197 unsafe {
2198 (*doc).URL = c_strdup(filename) as *mut xmlChar;
2199 }
2200 }
2201
2202 doc
2203}
2204
2205pub unsafe fn parse_memory(buffer: *const c_char, size: c_int) -> *mut _xmlDoc {
2216 if buffer.is_null() || size <= 0 {
2217 return ptr::null_mut();
2218 }
2219
2220 let mut ctxt = HtmlParserCtxt::new();
2221 unsafe { html_parse_buffer(&mut ctxt, buffer, size) }
2222}
2223
2224pub(crate) unsafe fn parse_doc(cur: *const xmlChar, encoding: *const c_char) -> *mut _xmlDoc {
2235 if cur.is_null() {
2236 return ptr::null_mut();
2237 }
2238
2239 let len = unsafe { xml_strlen(cur) };
2240 let mut ctxt = HtmlParserCtxt::new();
2241 if !encoding.is_null() {
2242 ctxt.encoding = unsafe { c_strdup(encoding) };
2243 }
2244
2245 unsafe { html_parse_buffer(&mut ctxt, cur as *const c_char, len as c_int) }
2246}
2247
2248#[allow(dead_code)]
2259pub(crate) unsafe fn create_file_parser_ctxt(
2260 filename: *const c_char,
2261 encoding: *const c_char,
2262) -> *mut c_void {
2263 if filename.is_null() {
2264 return ptr::null_mut();
2265 }
2266
2267 let ctxt = unsafe { xmlMallocZero(size_of::<HtmlParserCtxt>() as usize) };
2268 if ctxt.is_null() {
2269 return ptr::null_mut();
2270 }
2271
2272 let ctxt = ctxt as *mut HtmlParserCtxt;
2273 unsafe {
2274 ptr::write(ctxt, HtmlParserCtxt::new());
2275 if !encoding.is_null() {
2276 (*ctxt).encoding = c_strdup(encoding);
2277 }
2278 }
2279
2280 ctxt as *mut c_void
2281}
2282
2283pub(crate) unsafe fn free_parser_ctxt(ctxt: *mut c_void) {
2293 if ctxt.is_null() {
2294 return;
2295 }
2296
2297 let ctxt = ctxt as *mut HtmlParserCtxt;
2298 unsafe {
2299 if !(*ctxt).filename.is_null() {
2300 xmlFreeImpl((*ctxt).filename as *mut c_void);
2301 }
2302 if !(*ctxt).encoding.is_null() {
2303 xmlFreeImpl((*ctxt).encoding as *mut c_void);
2304 }
2305 xmlFreeImpl(ctxt as *mut c_void);
2306 }
2307}
2308
2309#[allow(dead_code)]
2315pub(crate) const fn init_parser() {
2316 }
2319
2320#[allow(dead_code)]
2326pub(crate) const fn cleanup_parser() {
2327 }
2330
2331pub(crate) unsafe fn new_doc(version: *const xmlChar) -> *mut _xmlDoc {
2344 let doc = tree::new_doc(version);
2345 if doc.is_null() {
2346 return ptr::null_mut();
2347 }
2348
2349 unsafe {
2350 (*doc).type_ = XML_HTML_DOCUMENT_NODE as c_int;
2351 (*doc).properties = XML_DOC_WELLFORMED as c_int;
2352 }
2353
2354 let mut ctxt = HtmlParserCtxt::new();
2356 ctxt.doc = doc;
2357
2358 unsafe {
2359 ensure_html(&mut ctxt);
2360 ensure_head(&mut ctxt);
2361 ensure_body(&mut ctxt);
2362 }
2363
2364 doc
2365}
2366
2367pub(crate) unsafe fn new_doc_no_dtd(version: *const xmlChar) -> *mut _xmlDoc {
2380 let doc = tree::new_doc(version);
2381 if doc.is_null() {
2382 return ptr::null_mut();
2383 }
2384
2385 unsafe {
2386 (*doc).type_ = XML_HTML_DOCUMENT_NODE as c_int;
2387 (*doc).properties = XML_DOC_WELLFORMED as c_int;
2388 }
2389
2390 doc
2391}
2392
2393const HTML_VOID_ELEMENTS: &[&str] = &[
2399 "area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "param", "source",
2400 "track", "wbr", "frame",
2401];
2402
2403fn is_html_void(name: &str) -> bool {
2405 HTML_VOID_ELEMENTS
2406 .iter()
2407 .any(|v| v.eq_ignore_ascii_case(name))
2408}
2409
2410#[allow(dead_code)]
2412fn has_optional_end_tag(name: &str) -> bool {
2413 matches!(
2414 name.to_ascii_lowercase().as_str(),
2415 "p" | "li" | "tr" | "td" | "th" | "dt" | "dd"
2416 )
2417}
2418
2419unsafe fn html_write_quoted(buf: *mut _xmlBuffer, s: *const xmlChar) {
2425 if buf.is_null() || s.is_null() {
2426 return;
2427 }
2428 io::buf_ccat(buf, b'"');
2429 io::buf_cat(buf, s);
2430 io::buf_ccat(buf, b'"');
2431}
2432
2433fn trim_ascii_start(s: &[u8]) -> &[u8] {
2435 let start = s
2436 .iter()
2437 .position(|&b| !b.is_ascii_whitespace())
2438 .unwrap_or(s.len());
2439 &s[start..]
2440}
2441
2442unsafe fn html_serialize_text(buf: *mut _xmlBuffer, content: *const xmlChar, len: c_int) {
2452 if buf.is_null() || content.is_null() || len <= 0 {
2453 return;
2454 }
2455
2456 let mut i: c_int = 0;
2457 while i < len {
2458 let ch = unsafe { *content.add(i as usize) };
2459
2460 match ch {
2461 b'<' => {
2462 io::buf_add(buf, b"<" as *const u8, 4);
2463 }
2464 b'&' => {
2465 io::buf_add(buf, b"&" as *const u8, 5);
2466 }
2467 _ => {
2468 io::buf_add(buf, &ch as *const u8, 1);
2469 }
2470 }
2471 i += 1;
2472 }
2473}
2474
2475unsafe fn html_serialize_attr_value(buf: *mut _xmlBuffer, value: *const xmlChar) {
2486 if buf.is_null() || value.is_null() {
2487 return;
2488 }
2489
2490 let len = unsafe { xml_strlen(value) as c_int };
2491 let mut i: c_int = 0;
2492 while i < len {
2493 let ch = unsafe { *value.add(i as usize) };
2494
2495 match ch {
2496 b'&' => {
2497 io::buf_add(buf, b"&" as *const u8, 5);
2498 }
2499 b'"' => {
2500 io::buf_add(buf, b""" as *const u8, 6);
2501 }
2502 _ => {
2503 io::buf_add(buf, &ch as *const u8, 1);
2504 }
2505 }
2506 i += 1;
2507 }
2508}
2509
2510unsafe fn html_head_has_meta(child: *mut _xmlNode) -> bool {
2528 let mut c = child;
2529 while !c.is_null() {
2530 if (*c).type_ == XML_ELEMENT_NODE as c_int && !(*c).name.is_null() {
2531 let nm = xmlstr_to_bytes((*c).name);
2532 if nm.eq_ignore_ascii_case(b"meta") {
2533 return true;
2534 }
2535 }
2536 c = (*c).next;
2537 }
2538 false
2539}
2540
2541pub(crate) unsafe fn serialize_node(
2552 node: *mut _xmlNode,
2553 buf: *mut _xmlBuffer,
2554 format: c_int,
2555 level: c_int,
2556) {
2557 if node.is_null() || buf.is_null() {
2558 return;
2559 }
2560
2561 let n = unsafe { &*node };
2562
2563 match n.type_ {
2564 t if t == XML_ELEMENT_NODE as c_int => {
2565 let name = if n.name.is_null() {
2566 ""
2567 } else {
2568 unsafe { core::str::from_utf8(xmlstr_to_bytes(n.name)).unwrap_or("") }
2569 };
2570
2571 let is_void = is_html_void(name);
2572 let info = html_tag_lookup(name);
2577 let is_inline = info.is_none_or(|i| i.flags & HTML_INLINE != 0);
2578 let no_format = is_inline || name.starts_with('p');
2579
2580 io::buf_ccat(buf, b'<');
2582 if !n.name.is_null() {
2583 io::buf_cat(buf, n.name);
2584 }
2585
2586 let mut attr = n.properties;
2588 while !attr.is_null() {
2589 let a = unsafe { &*attr };
2590 io::buf_ccat(buf, b' ');
2591 if !a.name.is_null() {
2592 io::buf_cat(buf, a.name);
2593 }
2594
2595 if !a.children.is_null() {
2597 let child = unsafe { &*a.children };
2598 if child.type_ == XML_TEXT_NODE as c_int && !child.content.is_null() {
2599 io::buf_ccat(buf, b'=');
2600 io::buf_ccat(buf, b'"');
2601 html_serialize_attr_value(buf, child.content);
2602 io::buf_ccat(buf, b'"');
2603 }
2604 }
2605
2606 attr = a.next;
2607 }
2608
2609 let mut meta_bytes: Option<Vec<u8>> = None;
2616 if name.eq_ignore_ascii_case("head")
2617 && level == 1
2618 && !n.doc.is_null()
2619 && !(*n.doc).encoding.is_null()
2620 {
2621 let parent_is_html = !n.parent.is_null() && !(*n.parent).name.is_null() && {
2622 let pn = core::str::from_utf8(xmlstr_to_bytes((*n.parent).name)).unwrap_or("");
2623 pn.eq_ignore_ascii_case("html")
2624 };
2625 if parent_is_html && !html_head_has_meta(n.children) {
2626 meta_bytes = Some(xmlstr_to_bytes((*n.doc).encoding).to_vec());
2627 }
2628 }
2629 let meta_inserted = meta_bytes.is_some();
2630
2631 let has_children = !n.children.is_null();
2632 let first_child = if has_children {
2633 unsafe { (*n.children).type_ }
2634 } else {
2635 XML_TEXT_NODE as c_int
2636 };
2637 let first_is_text = first_child == XML_TEXT_NODE as c_int
2638 || first_child == XML_ENTITY_REF_NODE as c_int;
2639 let multi_child = (has_children && n.children != n.last) || meta_inserted;
2642
2643 if is_void {
2644 io::buf_ccat(buf, b'>');
2646 } else {
2650 io::buf_ccat(buf, b'>');
2652
2653 if format != 0 && !no_format && !first_is_text && multi_child {
2658 io::buf_ccat(buf, b'\n');
2659 }
2660
2661 if let Some(enc) = &meta_bytes {
2662 io::buf_add(buf, b"<meta charset=\"" as *const u8, 15);
2663 io::buf_add(buf, enc.as_ptr(), enc.len() as c_int);
2664 io::buf_add(buf, b"\">" as *const u8, 2);
2665 if format != 0 && has_children && !first_is_text && !name.starts_with('p') {
2668 io::buf_ccat(buf, b'\n');
2669 }
2670 }
2671
2672 let mut child = n.children;
2675 while !child.is_null() {
2676 serialize_node(child, buf, format, level + 1);
2677 let next = unsafe { (*child).next };
2681 if format != 0 && !next.is_null() && !name.starts_with('p') {
2682 let nt = unsafe { (*next).type_ };
2683 if nt != XML_TEXT_NODE as c_int && nt != XML_ENTITY_REF_NODE as c_int {
2684 let cname = if (*child).name.is_null() {
2685 ""
2686 } else {
2687 unsafe {
2688 core::str::from_utf8(xmlstr_to_bytes((*child).name))
2689 .unwrap_or("")
2690 }
2691 };
2692 let cinfo = html_tag_lookup(cname);
2693 let c_inline = cinfo.is_none_or(|i| i.flags & HTML_INLINE != 0);
2694 if !c_inline {
2695 io::buf_ccat(buf, b'\n');
2696 }
2697 }
2698 }
2699 child = next;
2700 }
2701
2702 let last_child = if has_children {
2706 unsafe { (*n.last).type_ }
2707 } else {
2708 XML_ELEMENT_NODE as c_int
2709 };
2710 let last_is_text = last_child == XML_TEXT_NODE as c_int
2711 || last_child == XML_ENTITY_REF_NODE as c_int;
2712 if format != 0 && !no_format && !last_is_text && multi_child {
2713 io::buf_ccat(buf, b'\n');
2714 }
2715
2716 io::buf_add(buf, b"</" as *const u8, 2);
2718 if !n.name.is_null() {
2719 io::buf_cat(buf, n.name);
2720 }
2721 io::buf_ccat(buf, b'>');
2722 }
2723 }
2724 t if t == XML_TEXT_NODE as c_int => {
2725 html_serialize_text(buf, n.content, xml_strlen(n.content) as c_int);
2726 }
2727 t if t == XML_CDATA_SECTION_NODE as c_int => {
2728 io::buf_add(buf, b"<![CDATA[" as *const u8, 9);
2729 html_serialize_text(buf, n.content, xml_strlen(n.content) as c_int);
2730 io::buf_add(buf, b"]]>" as *const u8, 3);
2731 }
2732 t if t == XML_COMMENT_NODE as c_int => {
2733 if format != 0 && level > 0 {
2734 io::buf_ccat(buf, b'\n');
2735 for _ in 0..level {
2736 io::buf_add(buf, b" " as *const u8, 2);
2737 }
2738 }
2739 io::buf_add(buf, b"<!--" as *const u8, 4);
2740 if !n.content.is_null() {
2741 io::buf_cat(buf, n.content);
2742 }
2743 io::buf_add(buf, b"-->" as *const u8, 3);
2744 }
2745 t if t == XML_PI_NODE as c_int => {
2746 if format != 0 && level > 0 {
2747 io::buf_ccat(buf, b'\n');
2748 for _ in 0..level {
2749 io::buf_add(buf, b" " as *const u8, 2);
2750 }
2751 }
2752 io::buf_add(buf, b"<?" as *const u8, 2);
2753 if !n.name.is_null() {
2754 io::buf_cat(buf, n.name);
2755 }
2756 if !n.content.is_null() && unsafe { *n.content != 0 } {
2757 io::buf_ccat(buf, b' ');
2758 io::buf_cat(buf, n.content);
2759 }
2760 io::buf_add(buf, b"?>" as *const u8, 2);
2761 }
2762 t if t == XML_DOCUMENT_NODE as c_int || t == XML_HTML_DOCUMENT_NODE as c_int => {
2763 let doc_ptr = n as *const _xmlNode as *mut _xmlDoc;
2766 let d = &*doc_ptr;
2767 if !d.intSubset.is_null() {
2768 let dtd = &*d.intSubset;
2769 io::buf_add(buf, b"<!DOCTYPE " as *const u8, 10);
2770 if !dtd.name.is_null() {
2771 io::buf_cat(buf, dtd.name);
2772 }
2773 if !dtd.ExternalID.is_null() {
2774 io::buf_add(buf, b" PUBLIC " as *const u8, 8);
2775 html_write_quoted(buf, dtd.ExternalID);
2776 io::buf_ccat(buf, b' ');
2777 html_write_quoted(buf, dtd.SystemID);
2778 } else if !dtd.SystemID.is_null() {
2779 io::buf_add(buf, b" SYSTEM " as *const u8, 8);
2780 html_write_quoted(buf, dtd.SystemID);
2781 }
2782 io::buf_ccat(buf, b'>');
2783 io::buf_ccat(buf, b'\n');
2784 }
2785 let mut child = n.children;
2788 while !child.is_null() {
2789 serialize_node(child, buf, format, 0);
2790 child = unsafe { (*child).next };
2791 }
2792 io::buf_ccat(buf, b'\n');
2795 }
2796 _ => {
2797 if !n.content.is_null() {
2798 html_serialize_text(buf, n.content, xml_strlen(n.content) as c_int);
2799 }
2800 }
2801 }
2802}
2803
2804pub(crate) unsafe fn doc_dump(buf: *mut _xmlBuffer, doc: *mut _xmlDoc) -> c_int {
2811 if buf.is_null() || doc.is_null() {
2812 return -1;
2813 }
2814
2815 let before = io::buf_length(buf);
2816 serialize_node(doc as *mut _xmlNode, buf, 0, 0);
2817 let after = io::buf_length(buf);
2818
2819 if after < 0 || before < 0 {
2820 return -1;
2821 }
2822 after - before
2823}
2824
2825#[cfg(test)]
2830mod tests {
2831 use super::*;
2832
2833 use crate::xml::io;
2834
2835 #[allow(dead_code)]
2837 unsafe fn to_xmlstr(s: &[u8]) -> *mut xmlChar {
2838 bytes_to_xmlstr(s)
2839 }
2840
2841 unsafe fn html_doc_to_string(doc: *mut _xmlDoc) -> String {
2843 let buf = io::buf_create(-1);
2844 assert!(!buf.is_null());
2845 doc_dump(buf, doc);
2846 let content = io::buf_content(buf);
2847 let s = if !content.is_null() {
2848 let len = xml_strlen(content);
2849 let slice = slice::from_raw_parts(content, len);
2850 String::from_utf8_lossy(slice).to_string()
2851 } else {
2852 String::new()
2853 };
2854 io::buf_free(buf);
2855 s
2856 }
2857
2858 #[test]
2863 fn test_html_tag_lookup() {
2864 assert!(html_tag_lookup("html").is_some());
2866 assert!(html_tag_lookup("HTML").is_some()); assert!(html_tag_lookup("p").is_some());
2868 assert!(html_tag_lookup("br").is_some());
2869 assert!(html_tag_lookup("div").is_some());
2870 assert!(html_tag_lookup("script").is_some());
2871
2872 assert!(html_tag_lookup("custom").is_none());
2874 assert!(html_tag_lookup("my-element").is_none());
2875 }
2876
2877 #[test]
2878 fn test_tag_flags() {
2879 let br = html_tag_lookup("br").unwrap();
2880 assert!(br.flags & HTML_INLINE != 0);
2881 assert!(br.flags & HTML_EMPTY != 0);
2882
2883 let div = html_tag_lookup("div").unwrap();
2884 assert!(div.flags & HTML_BLOCK != 0);
2885 assert!(div.flags & HTML_VALID != 0);
2886
2887 let p = html_tag_lookup("p").unwrap();
2888 assert!(p.flags & HTML_NO_END != 0);
2889
2890 let meta = html_tag_lookup("meta").unwrap();
2891 assert!(meta.flags & HTML_HEAD != 0);
2892 assert!(meta.flags & HTML_EMPTY != 0);
2893 }
2894
2895 #[test]
2900 fn test_html_entity_lookup() {
2901 assert_eq!(html_entity_lookup("amp"), Some("&"));
2902 assert_eq!(html_entity_lookup("lt"), Some("<"));
2903 assert_eq!(html_entity_lookup("gt"), Some(">"));
2904 assert_eq!(html_entity_lookup("quot"), Some("\""));
2905 assert_eq!(html_entity_lookup("nbsp"), Some("\u{00a0}"));
2906 assert_eq!(html_entity_lookup("copy"), Some("\u{00a9}"));
2907 assert!(html_entity_lookup("unknown_entity").is_none());
2908 }
2909
2910 #[test]
2924 fn test_parse_basic_html() {
2925 unsafe {
2926 let html = b"<html><head><title>Test</title></head><body><p>Hello</p></body></html>\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("<html>"));
2932 assert!(s.contains("<head>"));
2933 assert!(s.contains("<title>Test</title>"));
2934 assert!(s.contains("<body>"));
2935 assert!(s.contains("<p>Hello</p>"));
2936
2937 tree::free_doc(doc);
2938 }
2939 }
2940
2941 #[test]
2948 fn test_parse_empty_document() {
2949 unsafe {
2950 let html = b"\0";
2951 let doc = parse_memory(html.as_ptr() as *const c_char, 0);
2952 assert!(doc.is_null());
2953 }
2954 }
2955
2956 #[test]
2970 fn test_implicit_html_head_body() {
2971 unsafe {
2972 let html = b"<p>Hello</p>\0";
2974 let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
2975 assert!(!doc.is_null());
2976
2977 let s = html_doc_to_string(doc);
2978 assert!(s.contains("<html>"));
2980 assert!(s.contains("<body>"));
2982 assert!(s.contains("<p>Hello</p>"));
2984
2985 tree::free_doc(doc);
2986 }
2987 }
2988
2989 #[test]
2999 fn test_implicit_head_with_title() {
3000 unsafe {
3001 let html = b"<title>My Page</title>\0";
3003 let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
3004 assert!(!doc.is_null());
3005
3006 let s = html_doc_to_string(doc);
3007 assert!(s.contains("<html>"));
3008 assert!(s.contains("<head>"));
3009 assert!(s.contains("<title>My Page</title>"));
3010
3011 tree::free_doc(doc);
3012 }
3013 }
3014
3015 #[test]
3028 fn test_auto_close_p() {
3029 unsafe {
3030 let html = b"<p>First<p>Second</p>\0";
3032 let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
3033 assert!(!doc.is_null());
3034
3035 let s = html_doc_to_string(doc);
3036 let first_pos = s.find("First");
3038 let second_pos = s.find("Second");
3039 assert!(first_pos.is_some());
3040 assert!(second_pos.is_some());
3041
3042 tree::free_doc(doc);
3043 }
3044 }
3045
3046 #[test]
3055 fn test_auto_close_heading() {
3056 unsafe {
3057 let html = b"<h1>Title</h1><h2>Subtitle</h2>\0";
3059 let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
3060 assert!(!doc.is_null());
3061
3062 let s = html_doc_to_string(doc);
3063 assert!(s.contains("<h1>Title</h1>"));
3064 assert!(s.contains("<h2>Subtitle</h2>"));
3065
3066 tree::free_doc(doc);
3067 }
3068 }
3069
3070 #[test]
3083 fn test_void_elements() {
3084 unsafe {
3085 let html = b"<br><hr><img src=\"test.jpg\"><input type=\"text\">\0";
3086 let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
3087 assert!(!doc.is_null());
3088
3089 let s = html_doc_to_string(doc);
3090 assert!(s.contains("<br>"));
3091 assert!(s.contains("<hr>"));
3092 assert!(s.contains("<img"));
3093 assert!(s.contains("<input"));
3094
3095 assert!(!s.contains("</br>"));
3097 assert!(!s.contains("</hr>"));
3098 assert!(!s.contains("</img>"));
3099
3100 tree::free_doc(doc);
3101 }
3102 }
3103
3104 #[test]
3118 fn test_unquoted_attributes() {
3119 unsafe {
3120 let html = b"<div class=main id=content>Text</div>\0";
3121 let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
3122 assert!(!doc.is_null());
3123
3124 let s = html_doc_to_string(doc);
3125 assert!(s.contains("class=\"main\""));
3126 assert!(s.contains("id=\"content\""));
3127
3128 tree::free_doc(doc);
3129 }
3130 }
3131
3132 #[test]
3141 fn test_minimized_attributes() {
3142 unsafe {
3143 let html = b"<option selected disabled>Value</option>\0";
3144 let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
3145 assert!(!doc.is_null());
3146
3147 let s = html_doc_to_string(doc);
3148 assert!(s.contains("selected"));
3150 assert!(s.contains("disabled"));
3151
3152 tree::free_doc(doc);
3153 }
3154 }
3155
3156 #[test]
3169 fn test_html_entities() {
3170 unsafe {
3171 let html = b"<p>& < > " ©</p>\0";
3172 let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
3173 assert!(!doc.is_null());
3174
3175 let s = html_doc_to_string(doc);
3176 assert!(s.contains("&")); assert!(s.contains("<")); assert!(s.contains(">")); assert!(s.contains("\u{00a0}")); tree::free_doc(doc);
3184 }
3185 }
3186
3187 #[test]
3196 fn test_numeric_entities() {
3197 unsafe {
3198 let html = b"<p>A A</p>\0";
3200 let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
3201 assert!(!doc.is_null());
3202
3203 let s = html_doc_to_string(doc);
3204 assert!(s.contains('A'));
3205
3206 tree::free_doc(doc);
3207 }
3208 }
3209
3210 #[test]
3223 fn test_nested_elements() {
3224 unsafe {
3225 let html = b"<div><ul><li>Item 1</li><li>Item 2</li></ul></div>\0";
3226 let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
3227 assert!(!doc.is_null());
3228
3229 let s = html_doc_to_string(doc);
3230 assert!(s.contains("<div>"));
3231 assert!(s.contains("<ul>"));
3232 assert!(s.contains("<li>Item 1</li>"));
3233 assert!(s.contains("<li>Item 2</li>"));
3234
3235 tree::free_doc(doc);
3236 }
3237 }
3238
3239 #[test]
3252 fn test_missing_end_tags() {
3253 unsafe {
3254 let html = b"<p>Paragraph without closing<div>Another div\0";
3256 let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
3257 assert!(!doc.is_null());
3258
3259 let s = html_doc_to_string(doc);
3260 assert!(s.contains("Paragraph without closing"));
3261 assert!(s.contains("Another div"));
3262
3263 tree::free_doc(doc);
3264 }
3265 }
3266
3267 #[test]
3276 fn test_mismatched_case() {
3277 unsafe {
3278 let html = b"<HTML><HEAD><TITLE>Test</TITLE></HEAD><BODY><P>Hello</P></BODY></HTML>\0";
3279 let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
3280 assert!(!doc.is_null());
3281
3282 let s = html_doc_to_string(doc);
3283 assert!(s.contains("<HTML>"));
3285 assert!(s.contains("<HEAD>"));
3286 assert!(s.contains("<BODY>"));
3287 assert!(s.contains("<P>Hello</P>"));
3288
3289 tree::free_doc(doc);
3290 }
3291 }
3292
3293 #[test]
3302 fn test_nested_malformed() {
3303 unsafe {
3304 let html = b"<div><p><span><b>Deep text</div></p>\0";
3306 let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
3307 assert!(!doc.is_null());
3308
3309 let s = html_doc_to_string(doc);
3310 assert!(s.contains("Deep text"));
3311
3312 tree::free_doc(doc);
3313 }
3314 }
3315
3316 #[test]
3329 fn test_serialization_round_trip_simple() {
3330 unsafe {
3331 let original = b"<p>Hello World</p>\0";
3332 let doc = parse_memory(
3333 original.as_ptr() as *const c_char,
3334 (original.len() - 1) as c_int,
3335 );
3336 assert!(!doc.is_null());
3337
3338 let s = html_doc_to_string(doc);
3339 assert!(s.contains("Hello World"));
3340
3341 tree::free_doc(doc);
3342 }
3343 }
3344
3345 #[test]
3354 fn test_serialize_void_elements_no_self_close() {
3355 unsafe {
3356 let html = b"<br><hr><img src=\"test.png\">\0";
3357 let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
3358 assert!(!doc.is_null());
3359
3360 let s = html_doc_to_string(doc);
3361 assert!(!s.contains("<br/>"));
3363 assert!(!s.contains("<hr/>"));
3364
3365 tree::free_doc(doc);
3366 }
3367 }
3368
3369 #[test]
3382 fn test_script_content() {
3383 unsafe {
3384 let html = b"<script>var x = 1;</script>\0";
3386 let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
3387 assert!(!doc.is_null());
3388
3389 let s = html_doc_to_string(doc);
3390 assert!(s.contains("<script>"));
3391 assert!(s.contains("var x = 1;"));
3393
3394 tree::free_doc(doc);
3395 }
3396 }
3397
3398 #[test]
3411 fn test_html_comment() {
3412 unsafe {
3413 let html = b"<html><!-- This is a comment --><body><p>Text</p></body></html>\0";
3414 let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
3415 assert!(!doc.is_null());
3416
3417 let s = html_doc_to_string(doc);
3418 assert!(s.contains("<!-- This is a comment -->"));
3419
3420 tree::free_doc(doc);
3421 }
3422 }
3423
3424 #[test]
3438 fn test_new_doc_creates_html_head_body() {
3439 unsafe {
3440 let doc = new_doc(ptr::null());
3441 assert!(!doc.is_null());
3442 assert_eq!((*doc).type_, XML_HTML_DOCUMENT_NODE as c_int);
3443
3444 let s = html_doc_to_string(doc);
3445 assert!(s.contains("<html>"));
3446 assert!(s.contains("<head>"));
3447 assert!(s.contains("<body>"));
3448
3449 tree::free_doc(doc);
3450 }
3451 }
3452
3453 #[test]
3463 fn test_new_doc_no_dtd() {
3464 unsafe {
3465 let doc = new_doc_no_dtd(ptr::null());
3466 assert!(!doc.is_null());
3467 assert_eq!((*doc).type_, XML_HTML_DOCUMENT_NODE as c_int);
3468
3469 let s = html_doc_to_string(doc);
3473 assert_eq!(s, "\n");
3474
3475 tree::free_doc(doc);
3476 }
3477 }
3478
3479 #[test]
3484 fn test_resolve_numeric_entity() {
3485 assert_eq!(resolve_numeric_entity("65", false), vec![b'A']);
3486 assert_eq!(resolve_numeric_entity("41", true), vec![b'A']);
3487 assert_eq!(resolve_numeric_entity("0", false), Vec::<u8>::new());
3488 }
3489
3490 #[test]
3491 fn test_resolve_entity_unknown() {
3492 let result = resolve_entity("unknown");
3493 assert_eq!(result, b"&unknown;");
3494 }
3495
3496 #[test]
3501 fn test_init_cleanup_parser() {
3502 init_parser();
3504 cleanup_parser();
3505 }
3506
3507 #[test]
3519 fn test_create_free_parser_ctxt() {
3520 unsafe {
3521 let ctxt =
3522 create_file_parser_ctxt(b"test.html\0" as *const u8 as *const c_char, ptr::null());
3523 assert!(!ctxt.is_null());
3524 free_parser_ctxt(ctxt);
3525 }
3526 }
3527
3528 #[test]
3541 fn test_complex_html_document() {
3542 unsafe {
3543 let html = b"<!DOCTYPE html>
3544<html>
3545<head>
3546 <meta charset=\"utf-8\">
3547 <title>Test Page</title>
3548 <link rel=\"stylesheet\" href=\"style.css\">
3549</head>
3550<body>
3551 <div id=\"main\">
3552 <h1>Title</h1>
3553 <p>First paragraph with <a href=\"link.html\">a link</a>.</p>
3554 <p>Second paragraph.</p>
3555 <ul>
3556 <li>Item 1</li>
3557 <li>Item 2</li>
3558 </ul>
3559 <br>
3560 <hr>
3561 <img src=\"image.jpg\" alt=\"An image\">
3562 </div>
3563 <script>alert('hello');</script>
3564</body>
3565</html>\0";
3566 let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
3567 assert!(!doc.is_null());
3568
3569 let s = html_doc_to_string(doc);
3570 assert!(s.contains("<html>"));
3571 assert!(s.contains("<head>"));
3572 assert!(s.contains("<title>Test Page</title>"));
3573 assert!(s.contains("<body>"));
3574 assert!(s.contains("<h1>Title</h1>"));
3575 assert!(s.contains("a link"));
3576 assert!(s.contains("Second paragraph"));
3577 assert!(s.contains("<br>"));
3578 assert!(s.contains("<hr>"));
3579 assert!(s.contains("<img"));
3580 assert!(s.contains("<script>"));
3581
3582 tree::free_doc(doc);
3583 }
3584 }
3585
3586 #[test]
3599 fn test_parse_doc() {
3600 unsafe {
3601 let html = b"<p>Hello from parse_doc</p>\0";
3602 let doc = parse_doc(html.as_ptr() as *const xmlChar, ptr::null());
3603 assert!(!doc.is_null());
3604
3605 let s = html_doc_to_string(doc);
3606 assert!(s.contains("Hello from parse_doc"));
3607
3608 tree::free_doc(doc);
3609 }
3610 }
3611
3612 #[test]
3625 fn test_table_element_auto_close() {
3626 unsafe {
3627 let html = b"<table><tr><td>Cell 1<td>Cell 2</td></tr></table>";
3628 let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
3629 assert!(!doc.is_null());
3630
3631 let s = html_doc_to_string(doc);
3632 assert!(s.contains("<td>Cell 1"));
3633 assert!(s.contains("<td>Cell 2"));
3634
3635 tree::free_doc(doc);
3636 }
3637 }
3638}