1use core::ffi::c_void;
88use core::mem::size_of;
89use core::ptr;
90use core::slice;
91use std::os::raw::{c_char, c_int};
92
93use crate::abi::allocator::{xmlFreeImpl, xmlMallocZero};
94use crate::abi::structs::*;
95use crate::abi::types::xmlDocProperties::XML_DOC_WELLFORMED;
96use crate::abi::types::xmlElementType::*;
97use crate::abi::types::*;
98use crate::xml::io;
99use crate::xml::string::*;
100use crate::xml::tree;
101
102const HTML_PARSE_NODEFDTD: c_int = 1 << 2;
110const HTML_PARSE_NOIMPLIED: c_int = 1 << 13;
111
112const HTML_INLINE: u32 = 0x1;
113const HTML_BLOCK: u32 = 0x2;
114const HTML_EMPTY: u32 = 0x4;
115#[allow(dead_code)]
116const HTML_DEPRECATED: u32 = 0x8;
117const HTML_OL: u32 = 0x10;
118const HTML_DL: u32 = 0x20;
119#[allow(dead_code)]
120const HTML_COMPACT: u32 = 0x40;
121const HTML_HEAD: u32 = 0x80;
122const HTML_BODY: u32 = 0x100;
123#[allow(dead_code)]
124const HTML_HEADSTRUCK: u32 = 0x200;
125const HTML_VALID: u32 = 0x400;
126const HTML_NO_END: u32 = 0x800; #[allow(dead_code)]
128const HTML_IMPLIED: u32 = 0x1000; #[derive(Clone, Copy)]
132struct HtmlElementInfo {
133 name: &'static str,
134 flags: u32,
135}
136
137const HTML_ELEMENTS: &[HtmlElementInfo] = &[
140 HtmlElementInfo {
142 name: "br",
143 flags: HTML_INLINE | HTML_EMPTY | HTML_VALID,
144 },
145 HtmlElementInfo {
146 name: "hr",
147 flags: HTML_BLOCK | HTML_EMPTY | HTML_VALID,
148 },
149 HtmlElementInfo {
150 name: "img",
151 flags: HTML_INLINE | HTML_EMPTY | HTML_VALID,
152 },
153 HtmlElementInfo {
154 name: "input",
155 flags: HTML_INLINE | HTML_EMPTY | HTML_VALID,
156 },
157 HtmlElementInfo {
158 name: "meta",
159 flags: HTML_HEAD | HTML_EMPTY | HTML_VALID,
160 },
161 HtmlElementInfo {
162 name: "link",
163 flags: HTML_HEAD | HTML_EMPTY | HTML_VALID,
164 },
165 HtmlElementInfo {
166 name: "base",
167 flags: HTML_HEAD | HTML_EMPTY | HTML_VALID,
168 },
169 HtmlElementInfo {
170 name: "area",
171 flags: HTML_INLINE | HTML_EMPTY | HTML_VALID,
172 },
173 HtmlElementInfo {
174 name: "col",
175 flags: HTML_BLOCK | HTML_EMPTY | HTML_VALID,
176 },
177 HtmlElementInfo {
178 name: "embed",
179 flags: HTML_INLINE | HTML_EMPTY | HTML_VALID,
180 },
181 HtmlElementInfo {
182 name: "param",
183 flags: HTML_HEAD | HTML_EMPTY | HTML_VALID,
184 },
185 HtmlElementInfo {
186 name: "source",
187 flags: HTML_INLINE | HTML_EMPTY | HTML_VALID,
188 },
189 HtmlElementInfo {
190 name: "track",
191 flags: HTML_INLINE | HTML_EMPTY | HTML_VALID,
192 },
193 HtmlElementInfo {
194 name: "wbr",
195 flags: HTML_INLINE | HTML_EMPTY | HTML_VALID,
196 },
197 HtmlElementInfo {
199 name: "html",
200 flags: HTML_BLOCK | HTML_VALID,
201 },
202 HtmlElementInfo {
203 name: "head",
204 flags: HTML_HEAD | HTML_BLOCK | HTML_VALID,
205 },
206 HtmlElementInfo {
207 name: "body",
208 flags: HTML_BODY | HTML_BLOCK | HTML_VALID,
209 },
210 HtmlElementInfo {
211 name: "div",
212 flags: HTML_BLOCK | HTML_VALID,
213 },
214 HtmlElementInfo {
215 name: "p",
216 flags: HTML_BLOCK | HTML_VALID | HTML_NO_END,
217 },
218 HtmlElementInfo {
219 name: "h1",
220 flags: HTML_BLOCK | HTML_VALID,
221 },
222 HtmlElementInfo {
223 name: "h2",
224 flags: HTML_BLOCK | HTML_VALID,
225 },
226 HtmlElementInfo {
227 name: "h3",
228 flags: HTML_BLOCK | HTML_VALID,
229 },
230 HtmlElementInfo {
231 name: "h4",
232 flags: HTML_BLOCK | HTML_VALID,
233 },
234 HtmlElementInfo {
235 name: "h5",
236 flags: HTML_BLOCK | HTML_VALID,
237 },
238 HtmlElementInfo {
239 name: "h6",
240 flags: HTML_BLOCK | HTML_VALID,
241 },
242 HtmlElementInfo {
243 name: "ul",
244 flags: HTML_BLOCK | HTML_VALID | HTML_OL,
245 },
246 HtmlElementInfo {
247 name: "ol",
248 flags: HTML_BLOCK | HTML_VALID | HTML_OL,
249 },
250 HtmlElementInfo {
251 name: "li",
252 flags: HTML_BLOCK | HTML_VALID | HTML_NO_END,
253 },
254 HtmlElementInfo {
255 name: "dl",
256 flags: HTML_BLOCK | HTML_VALID | HTML_DL,
257 },
258 HtmlElementInfo {
259 name: "dt",
260 flags: HTML_BLOCK | HTML_VALID | HTML_NO_END,
261 },
262 HtmlElementInfo {
263 name: "dd",
264 flags: HTML_BLOCK | HTML_VALID | HTML_NO_END,
265 },
266 HtmlElementInfo {
267 name: "table",
268 flags: HTML_BLOCK | HTML_VALID,
269 },
270 HtmlElementInfo {
271 name: "tr",
272 flags: HTML_BLOCK | HTML_VALID | HTML_NO_END,
273 },
274 HtmlElementInfo {
275 name: "td",
276 flags: HTML_BLOCK | HTML_VALID | HTML_NO_END,
277 },
278 HtmlElementInfo {
279 name: "th",
280 flags: HTML_BLOCK | HTML_VALID | HTML_NO_END,
281 },
282 HtmlElementInfo {
283 name: "thead",
284 flags: HTML_BLOCK | HTML_VALID,
285 },
286 HtmlElementInfo {
287 name: "tbody",
288 flags: HTML_BLOCK | HTML_VALID,
289 },
290 HtmlElementInfo {
291 name: "tfoot",
292 flags: HTML_BLOCK | HTML_VALID,
293 },
294 HtmlElementInfo {
295 name: "colgroup",
296 flags: HTML_BLOCK | HTML_VALID,
297 },
298 HtmlElementInfo {
299 name: "caption",
300 flags: HTML_BLOCK | HTML_VALID,
301 },
302 HtmlElementInfo {
303 name: "form",
304 flags: HTML_BLOCK | HTML_VALID,
305 },
306 HtmlElementInfo {
307 name: "fieldset",
308 flags: HTML_BLOCK | HTML_VALID,
309 },
310 HtmlElementInfo {
311 name: "legend",
312 flags: HTML_BLOCK | HTML_VALID,
313 },
314 HtmlElementInfo {
315 name: "pre",
316 flags: HTML_BLOCK | HTML_VALID,
317 },
318 HtmlElementInfo {
319 name: "blockquote",
320 flags: HTML_BLOCK | HTML_VALID,
321 },
322 HtmlElementInfo {
323 name: "address",
324 flags: HTML_BLOCK | HTML_VALID,
325 },
326 HtmlElementInfo {
327 name: "center",
328 flags: HTML_BLOCK | HTML_VALID,
329 },
330 HtmlElementInfo {
331 name: "dir",
332 flags: HTML_BLOCK | HTML_VALID,
333 },
334 HtmlElementInfo {
335 name: "menu",
336 flags: HTML_BLOCK | HTML_VALID,
337 },
338 HtmlElementInfo {
339 name: "noscript",
340 flags: HTML_BLOCK | HTML_VALID,
341 },
342 HtmlElementInfo {
343 name: "frameset",
344 flags: HTML_BLOCK | HTML_VALID,
345 },
346 HtmlElementInfo {
347 name: "frame",
348 flags: HTML_BLOCK | HTML_EMPTY | HTML_VALID,
349 },
350 HtmlElementInfo {
351 name: "iframe",
352 flags: HTML_BLOCK | HTML_VALID,
353 },
354 HtmlElementInfo {
355 name: "noframes",
356 flags: HTML_BLOCK | HTML_VALID,
357 },
358 HtmlElementInfo {
360 name: "a",
361 flags: HTML_INLINE | HTML_VALID,
362 },
363 HtmlElementInfo {
364 name: "abbr",
365 flags: HTML_INLINE | HTML_VALID,
366 },
367 HtmlElementInfo {
368 name: "acronym",
369 flags: HTML_INLINE | HTML_VALID,
370 },
371 HtmlElementInfo {
372 name: "b",
373 flags: HTML_INLINE | HTML_VALID,
374 },
375 HtmlElementInfo {
376 name: "basefont",
377 flags: HTML_INLINE | HTML_EMPTY | HTML_VALID,
378 },
379 HtmlElementInfo {
380 name: "bdo",
381 flags: HTML_INLINE | HTML_VALID,
382 },
383 HtmlElementInfo {
384 name: "big",
385 flags: HTML_INLINE | HTML_VALID,
386 },
387 HtmlElementInfo {
388 name: "cite",
389 flags: HTML_INLINE | HTML_VALID,
390 },
391 HtmlElementInfo {
392 name: "code",
393 flags: HTML_INLINE | HTML_VALID,
394 },
395 HtmlElementInfo {
396 name: "dfn",
397 flags: HTML_INLINE | HTML_VALID,
398 },
399 HtmlElementInfo {
400 name: "em",
401 flags: HTML_INLINE | HTML_VALID,
402 },
403 HtmlElementInfo {
404 name: "font",
405 flags: HTML_INLINE | HTML_VALID,
406 },
407 HtmlElementInfo {
408 name: "i",
409 flags: HTML_INLINE | HTML_VALID,
410 },
411 HtmlElementInfo {
412 name: "kbd",
413 flags: HTML_INLINE | HTML_VALID,
414 },
415 HtmlElementInfo {
416 name: "label",
417 flags: HTML_INLINE | HTML_VALID,
418 },
419 HtmlElementInfo {
420 name: "map",
421 flags: HTML_INLINE | HTML_VALID,
422 },
423 HtmlElementInfo {
424 name: "nobr",
425 flags: HTML_INLINE | HTML_VALID,
426 },
427 HtmlElementInfo {
428 name: "object",
429 flags: HTML_INLINE | HTML_VALID,
430 },
431 HtmlElementInfo {
432 name: "q",
433 flags: HTML_INLINE | HTML_VALID,
434 },
435 HtmlElementInfo {
436 name: "rb",
437 flags: HTML_INLINE | HTML_VALID,
438 },
439 HtmlElementInfo {
440 name: "rbc",
441 flags: HTML_INLINE | HTML_VALID,
442 },
443 HtmlElementInfo {
444 name: "rp",
445 flags: HTML_INLINE | HTML_VALID,
446 },
447 HtmlElementInfo {
448 name: "rt",
449 flags: HTML_INLINE | HTML_VALID,
450 },
451 HtmlElementInfo {
452 name: "rtc",
453 flags: HTML_INLINE | HTML_VALID,
454 },
455 HtmlElementInfo {
456 name: "ruby",
457 flags: HTML_INLINE | HTML_VALID,
458 },
459 HtmlElementInfo {
460 name: "s",
461 flags: HTML_INLINE | HTML_VALID,
462 },
463 HtmlElementInfo {
464 name: "samp",
465 flags: HTML_INLINE | HTML_VALID,
466 },
467 HtmlElementInfo {
468 name: "select",
469 flags: HTML_INLINE | HTML_VALID,
470 },
471 HtmlElementInfo {
472 name: "small",
473 flags: HTML_INLINE | HTML_VALID,
474 },
475 HtmlElementInfo {
476 name: "span",
477 flags: HTML_INLINE | HTML_VALID,
478 },
479 HtmlElementInfo {
480 name: "strike",
481 flags: HTML_INLINE | HTML_VALID,
482 },
483 HtmlElementInfo {
484 name: "strong",
485 flags: HTML_INLINE | HTML_VALID,
486 },
487 HtmlElementInfo {
488 name: "sub",
489 flags: HTML_INLINE | HTML_VALID,
490 },
491 HtmlElementInfo {
492 name: "sup",
493 flags: HTML_INLINE | HTML_VALID,
494 },
495 HtmlElementInfo {
496 name: "textarea",
497 flags: HTML_INLINE | HTML_VALID,
498 },
499 HtmlElementInfo {
500 name: "tt",
501 flags: HTML_INLINE | HTML_VALID,
502 },
503 HtmlElementInfo {
504 name: "u",
505 flags: HTML_INLINE | HTML_VALID,
506 },
507 HtmlElementInfo {
508 name: "var",
509 flags: HTML_INLINE | HTML_VALID,
510 },
511 HtmlElementInfo {
513 name: "header",
514 flags: HTML_BLOCK | HTML_VALID,
515 },
516 HtmlElementInfo {
517 name: "footer",
518 flags: HTML_BLOCK | HTML_VALID,
519 },
520 HtmlElementInfo {
521 name: "nav",
522 flags: HTML_BLOCK | HTML_VALID,
523 },
524 HtmlElementInfo {
525 name: "article",
526 flags: HTML_BLOCK | HTML_VALID,
527 },
528 HtmlElementInfo {
529 name: "section",
530 flags: HTML_BLOCK | HTML_VALID,
531 },
532 HtmlElementInfo {
533 name: "aside",
534 flags: HTML_BLOCK | HTML_VALID,
535 },
536 HtmlElementInfo {
537 name: "main",
538 flags: HTML_BLOCK | HTML_VALID,
539 },
540 HtmlElementInfo {
541 name: "figure",
542 flags: HTML_BLOCK | HTML_VALID,
543 },
544 HtmlElementInfo {
545 name: "figcaption",
546 flags: HTML_BLOCK | HTML_VALID,
547 },
548 HtmlElementInfo {
549 name: "details",
550 flags: HTML_BLOCK | HTML_VALID,
551 },
552 HtmlElementInfo {
553 name: "summary",
554 flags: HTML_BLOCK | HTML_VALID,
555 },
556 HtmlElementInfo {
558 name: "script",
559 flags: HTML_HEAD | HTML_BLOCK | HTML_VALID,
560 },
561 HtmlElementInfo {
562 name: "style",
563 flags: HTML_HEAD | HTML_BLOCK | HTML_VALID,
564 },
565 HtmlElementInfo {
566 name: "title",
567 flags: HTML_HEAD | HTML_BLOCK | HTML_VALID,
568 },
569];
570
571fn html_tag_lookup(name: &str) -> Option<&'static HtmlElementInfo> {
574 HTML_ELEMENTS
579 .iter()
580 .find(|info| info.name.eq_ignore_ascii_case(name))
581}
582
583const HTML_ENTITIES: &[(&str, &str)] = &[
590 ("nbsp", "\u{00a0}"),
591 ("lt", "<"),
592 ("gt", ">"),
593 ("amp", "&"),
594 ("quot", "\""),
595 ("apos", "'"),
596 ("copy", "\u{00a9}"),
597 ("reg", "\u{00ae}"),
598 ("amp", "&"),
599 ("iexcl", "\u{00a1}"),
600 ("cent", "\u{00a2}"),
601 ("pound", "\u{00a3}"),
602 ("curren", "\u{00a4}"),
603 ("yen", "\u{00a5}"),
604 ("brvbar", "\u{00a6}"),
605 ("sect", "\u{00a7}"),
606 ("uml", "\u{00a8}"),
607 ("ordf", "\u{00aa}"),
608 ("laquo", "\u{00ab}"),
609 ("not", "\u{00ac}"),
610 ("shy", "\u{00ad}"),
611 ("macr", "\u{00ae}"),
612 ("deg", "\u{00b0}"),
613 ("plusmn", "\u{00b1}"),
614 ("sup2", "\u{00b2}"),
615 ("sup3", "\u{00b3}"),
616 ("acute", "\u{00b4}"),
617 ("micro", "\u{00b5}"),
618 ("para", "\u{00b6}"),
619 ("middot", "\u{00b7}"),
620 ("cedil", "\u{00b8}"),
621 ("sup1", "\u{00b9}"),
622 ("ordm", "\u{00ba}"),
623 ("raquo", "\u{00bb}"),
624 ("frac14", "\u{00bc}"),
625 ("frac12", "\u{00bd}"),
626 ("frac34", "\u{00be}"),
627 ("iquest", "\u{00bf}"),
628 ("times", "\u{00d7}"),
629 ("divide", "\u{00f7}"),
630 ("ETH", "\u{00d0}"),
631 ("eth", "\u{00f0}"),
632 ("THORN", "\u{00de}"),
633 ("thorn", "\u{00fe}"),
634 ("AElig", "\u{00c6}"),
635 ("aelig", "\u{00e6}"),
636 ("OElig", "\u{0152}"),
637 ("oelig", "\u{0153}"),
638 ("Scaron", "\u{0160}"),
639 ("scaron", "\u{0161}"),
640 ("Yuml", "\u{0178}"),
641 ("circ", "\u{02c6}"),
642 ("tilde", "\u{02dc}"),
643 ("ensp", "\u{2002}"),
644 ("emsp", "\u{2003}"),
645 ("thinsp", "\u{2009}"),
646 ("zwnj", "\u{200c}"),
647 ("zwj", "\u{200d}"),
648 ("lrm", "\u{200e}"),
649 ("rlm", "\u{200f}"),
650 ("ndash", "\u{2013}"),
651 ("mdash", "\u{2014}"),
652 ("lsquo", "\u{2018}"),
653 ("rsquo", "\u{2019}"),
654 ("sbquo", "\u{201a}"),
655 ("ldquo", "\u{201c}"),
656 ("rdquo", "\u{201d}"),
657 ("bdquo", "\u{201e}"),
658 ("dagger", "\u{2020}"),
659 ("Dagger", "\u{2021}"),
660 ("bull", "\u{2022}"),
661 ("hellip", "\u{2026}"),
662 ("permil", "\u{2030}"),
663 ("prime", "\u{2032}"),
664 ("Prime", "\u{2033}"),
665 ("lsaquo", "\u{2039}"),
666 ("rsaquo", "\u{203a}"),
667 ("oline", "\u{203e}"),
668 ("euro", "\u{20ac}"),
669 ("trade", "\u{2122}"),
670 ("larr", "\u{2190}"),
671 ("uarr", "\u{2191}"),
672 ("rarr", "\u{2192}"),
673 ("darr", "\u{2193}"),
674 ("harr", "\u{2194}"),
675 ("crarr", "\u{21b5}"),
676 ("lceil", "\u{2308}"),
677 ("rceil", "\u{2309}"),
678 ("lfloor", "\u{230a}"),
679 ("rfloor", "\u{230b}"),
680 ("loz", "\u{25ca}"),
681 ("spades", "\u{2660}"),
682 ("clubs", "\u{2663}"),
683 ("hearts", "\u{2665}"),
684 ("diams", "\u{2666}"),
685 ("Alpha", "\u{0391}"),
686 ("Beta", "\u{0392}"),
687 ("Gamma", "\u{0393}"),
688 ("Delta", "\u{0394}"),
689 ("Epsilon", "\u{0395}"),
690 ("Zeta", "\u{0396}"),
691 ("Eta", "\u{0397}"),
692 ("Theta", "\u{0398}"),
693 ("Iota", "\u{0399}"),
694 ("Kappa", "\u{039a}"),
695 ("Lambda", "\u{039b}"),
696 ("Mu", "\u{039c}"),
697 ("Nu", "\u{039d}"),
698 ("Xi", "\u{039e}"),
699 ("Omicron", "\u{039f}"),
700 ("Pi", "\u{03a0}"),
701 ("Rho", "\u{03a1}"),
702 ("Sigma", "\u{03a3}"),
703 ("Tau", "\u{03a4}"),
704 ("Upsilon", "\u{03a5}"),
705 ("Phi", "\u{03a6}"),
706 ("Chi", "\u{03a7}"),
707 ("Psi", "\u{03a8}"),
708 ("Omega", "\u{03a9}"),
709 ("alpha", "\u{03b1}"),
710 ("beta", "\u{03b2}"),
711 ("gamma", "\u{03b3}"),
712 ("delta", "\u{03b4}"),
713 ("epsilon", "\u{03b5}"),
714 ("zeta", "\u{03b6}"),
715 ("eta", "\u{03b7}"),
716 ("theta", "\u{03b8}"),
717 ("iota", "\u{03b9}"),
718 ("kappa", "\u{03ba}"),
719 ("lambda", "\u{03bb}"),
720 ("mu", "\u{03bc}"),
721 ("nu", "\u{03bd}"),
722 ("xi", "\u{03be}"),
723 ("omicron", "\u{03bf}"),
724 ("pi", "\u{03c0}"),
725 ("rho", "\u{03c1}"),
726 ("sigmaf", "\u{03c2}"),
727 ("sigma", "\u{03c3}"),
728 ("tau", "\u{03c4}"),
729 ("upsilon", "\u{03c5}"),
730 ("phi", "\u{03c6}"),
731 ("chi", "\u{03c7}"),
732 ("psi", "\u{03c8}"),
733 ("omega", "\u{03c9}"),
734 ("thetasym", "\u{03d1}"),
735 ("upsih", "\u{03d2}"),
736 ("piv", "\u{03d6}"),
737];
738
739fn html_entity_lookup(name: &str) -> Option<&'static str> {
742 HTML_ENTITIES
743 .iter()
744 .find(|(n, _)| *n == name)
745 .map(|(_, v)| *v)
746}
747
748struct HtmlParserCtxt {
754 doc: *mut _xmlDoc,
756 current: *mut _xmlNode,
758 html: *mut _xmlNode,
760 head: *mut _xmlNode,
762 body: *mut _xmlNode,
764 in_head: bool,
766 in_body: bool,
768 html_created: bool,
770 head_created: bool,
772 body_created: bool,
774 seen_body_content: bool,
776 input: *mut u8,
778 input_pos: usize,
780 input_len: usize,
782 line: c_int,
784 #[allow(dead_code)]
786 err: bool,
787 filename: *mut c_char,
789 encoding: *mut c_char,
791 options: c_int,
794}
795
796impl HtmlParserCtxt {
797 const fn new() -> Self {
798 HtmlParserCtxt {
799 doc: ptr::null_mut(),
800 current: ptr::null_mut(),
801 html: ptr::null_mut(),
802 head: ptr::null_mut(),
803 body: ptr::null_mut(),
804 in_head: false,
805 in_body: false,
806 html_created: false,
807 head_created: false,
808 body_created: false,
809 seen_body_content: false,
810 input: ptr::null_mut(),
811 input_pos: 0,
812 input_len: 0,
813 line: 1,
814 err: false,
815 filename: ptr::null_mut(),
816 encoding: ptr::null_mut(),
817 options: 0,
818 }
819 }
820
821 fn peek(&self) -> Option<u8> {
829 if self.input_pos < self.input_len {
830 unsafe { Some(*self.input.add(self.input_pos)) }
831 } else {
832 None
833 }
834 }
835
836 fn peek_at(&self, offset: usize) -> Option<u8> {
846 let pos = self.input_pos + offset;
847 if pos < self.input_len {
848 unsafe { Some(*self.input.add(pos)) }
849 } else {
850 None
851 }
852 }
853
854 fn next(&mut self) -> Option<u8> {
862 if self.input_pos < self.input_len {
863 let ch = unsafe { *self.input.add(self.input_pos) };
864 self.input_pos += 1;
865 if ch == b'\n' {
866 self.line += 1;
867 }
868 Some(ch)
869 } else {
870 None
871 }
872 }
873
874 fn skip_while<F: Fn(u8) -> bool>(&mut self, f: F) {
876 while let Some(ch) = self.peek() {
877 if f(ch) {
878 self.next();
879 } else {
880 break;
881 }
882 }
883 }
884
885 fn skip_whitespace(&mut self) {
887 self.skip_while(|ch| ch == b' ' || ch == b'\t' || ch == b'\n' || ch == b'\r');
888 }
889
890 const fn is_eof(&self) -> bool {
892 self.input_pos >= self.input_len
893 }
894
895 fn read_while<F: Fn(u8) -> bool>(&mut self, f: F) -> Vec<u8> {
904 let start = self.input_pos;
905 while let Some(ch) = self.peek() {
906 if f(ch) {
907 self.next();
908 } else {
909 break;
910 }
911 }
912 unsafe { slice::from_raw_parts(self.input.add(start), self.input_pos - start).to_vec() }
913 }
914}
915
916fn is_heading(name: &str) -> bool {
922 matches!(name, "h1" | "h2" | "h3" | "h4" | "h5" | "h6")
923}
924
925#[allow(dead_code)]
934unsafe fn get_parent_element(node: *mut _xmlNode) -> *mut _xmlNode {
935 if node.is_null() {
936 return ptr::null_mut();
937 }
938 let mut n = node;
939 loop {
940 let parent = unsafe { (*n).parent };
941 if parent.is_null() {
942 return ptr::null_mut();
943 }
944 let ptype = unsafe { (*parent).type_ };
945 if ptype == XML_ELEMENT_NODE as c_int
946 || ptype == XML_HTML_DOCUMENT_NODE as c_int
947 || ptype == XML_DOCUMENT_NODE as c_int
948 {
949 return parent;
950 }
951 n = parent;
952 }
953}
954
955unsafe fn auto_close_element(ctxt: &mut HtmlParserCtxt, tag_name: &str) {
967 let tag_lower: Vec<u8> = tag_name.bytes().map(|b| b.to_ascii_lowercase()).collect();
968 let tag_lower_str = match core::str::from_utf8(&tag_lower) {
969 Ok(s) => s,
970 Err(_) => return,
971 };
972
973 let info = html_tag_lookup(tag_lower_str);
974
975 let mut current = ctxt.current;
976
977 if tag_lower_str == "p" || info.is_some_and(|i| i.flags & HTML_BLOCK != 0) {
979 let mut cur2 = current;
981 while !cur2.is_null() {
982 let ctype = unsafe { (*cur2).type_ };
983 if ctype == XML_ELEMENT_NODE as c_int && !unsafe { (*cur2).name.is_null() } {
984 let name_bytes = unsafe { xmlstr_to_bytes((*cur2).name) };
985 let name_str = core::str::from_utf8(name_bytes).unwrap_or("");
986 if name_str.eq_ignore_ascii_case("p") {
987 current = unsafe { (*cur2).parent };
989 break;
990 }
991 }
992 cur2 = unsafe { (*cur2).parent };
993 }
994 }
995
996 if is_heading(tag_lower_str) {
998 let mut cur2 = current;
999 while !cur2.is_null() {
1000 let ctype = unsafe { (*cur2).type_ };
1001 if ctype == XML_ELEMENT_NODE as c_int && !unsafe { (*cur2).name.is_null() } {
1002 let name_bytes = unsafe { xmlstr_to_bytes((*cur2).name) };
1003 let name_str = core::str::from_utf8(name_bytes).unwrap_or("");
1004 if is_heading(name_str) {
1005 current = unsafe { (*cur2).parent };
1006 break;
1007 }
1008 }
1009 cur2 = unsafe { (*cur2).parent };
1010 }
1011 }
1012
1013 if tag_lower_str == "li" {
1015 let mut cur2 = current;
1016 while !cur2.is_null() {
1017 let ctype = unsafe { (*cur2).type_ };
1018 if ctype == XML_ELEMENT_NODE as c_int && !unsafe { (*cur2).name.is_null() } {
1019 let name_bytes = unsafe { xmlstr_to_bytes((*cur2).name) };
1020 let name_str = core::str::from_utf8(name_bytes).unwrap_or("");
1021 if name_str.eq_ignore_ascii_case("li") {
1022 current = unsafe { (*cur2).parent };
1023 break;
1024 }
1025 }
1026 cur2 = unsafe { (*cur2).parent };
1027 }
1028 }
1029
1030 if tag_lower_str == "dt" || tag_lower_str == "dd" {
1032 let mut cur2 = current;
1033 while !cur2.is_null() {
1034 let ctype = unsafe { (*cur2).type_ };
1035 if ctype == XML_ELEMENT_NODE as c_int && !unsafe { (*cur2).name.is_null() } {
1036 let name_bytes = unsafe { xmlstr_to_bytes((*cur2).name) };
1037 let name_str = core::str::from_utf8(name_bytes).unwrap_or("");
1038 if name_str == "dt" || name_str == "dd" {
1039 current = unsafe { (*cur2).parent };
1040 break;
1041 }
1042 }
1043 cur2 = unsafe { (*cur2).parent };
1044 }
1045 }
1046
1047 if tag_lower_str == "tr" {
1055 let mut cur2 = current;
1056 while !cur2.is_null() {
1057 let ctype = unsafe { (*cur2).type_ };
1058 if ctype == XML_ELEMENT_NODE as c_int && !unsafe { (*cur2).name.is_null() } {
1059 let name_bytes = unsafe { xmlstr_to_bytes((*cur2).name) };
1060 let name_str = core::str::from_utf8(name_bytes).unwrap_or("");
1061 if name_str == "tr" {
1062 current = unsafe { (*cur2).parent };
1063 break;
1064 }
1065 }
1066 cur2 = unsafe { (*cur2).parent };
1067 }
1068 } else if tag_lower_str == "td" || tag_lower_str == "th" {
1069 let mut cur2 = current;
1070 while !cur2.is_null() {
1071 let ctype = unsafe { (*cur2).type_ };
1072 if ctype == XML_ELEMENT_NODE as c_int && !unsafe { (*cur2).name.is_null() } {
1073 let name_bytes = unsafe { xmlstr_to_bytes((*cur2).name) };
1074 let name_str = core::str::from_utf8(name_bytes).unwrap_or("");
1075 if name_str == "td" || name_str == "th" {
1076 current = unsafe { (*cur2).parent };
1077 break;
1078 }
1079 }
1080 cur2 = unsafe { (*cur2).parent };
1081 }
1082 }
1083
1084 if matches!(tag_lower_str, "thead" | "tbody" | "tfoot") {
1086 let mut cur2 = current;
1087 while !cur2.is_null() {
1088 let ctype = unsafe { (*cur2).type_ };
1089 if ctype == XML_ELEMENT_NODE as c_int && !unsafe { (*cur2).name.is_null() } {
1090 let name_bytes = unsafe { xmlstr_to_bytes((*cur2).name) };
1091 let name_str = core::str::from_utf8(name_bytes).unwrap_or("");
1092 if name_str == "thead" || name_str == "tbody" || name_str == "tfoot" {
1093 current = unsafe { (*cur2).parent };
1094 break;
1095 }
1096 }
1097 cur2 = unsafe { (*cur2).parent };
1098 }
1099 }
1100
1101 if tag_lower_str == "colgroup" {
1103 let mut cur2 = current;
1104 while !cur2.is_null() {
1105 let ctype = unsafe { (*cur2).type_ };
1106 if ctype == XML_ELEMENT_NODE as c_int && !unsafe { (*cur2).name.is_null() } {
1107 let name_bytes = unsafe { xmlstr_to_bytes((*cur2).name) };
1108 let name_str = core::str::from_utf8(name_bytes).unwrap_or("");
1109 if name_str == "colgroup" {
1110 current = unsafe { (*cur2).parent };
1111 break;
1112 }
1113 }
1114 cur2 = unsafe { (*cur2).parent };
1115 }
1116 }
1117
1118 if tag_lower_str == "caption" {
1120 let mut cur2 = current;
1121 while !cur2.is_null() {
1122 let ctype = unsafe { (*cur2).type_ };
1123 if ctype == XML_ELEMENT_NODE as c_int && !unsafe { (*cur2).name.is_null() } {
1124 let name_bytes = unsafe { xmlstr_to_bytes((*cur2).name) };
1125 let name_str = core::str::from_utf8(name_bytes).unwrap_or("");
1126 if name_str == "caption" {
1127 current = unsafe { (*cur2).parent };
1128 break;
1129 }
1130 }
1131 cur2 = unsafe { (*cur2).parent };
1132 }
1133 }
1134
1135 if tag_lower_str == "form" {
1137 let mut cur2 = current;
1138 while !cur2.is_null() {
1139 let ctype = unsafe { (*cur2).type_ };
1140 if ctype == XML_ELEMENT_NODE as c_int && !unsafe { (*cur2).name.is_null() } {
1141 let name_bytes = unsafe { xmlstr_to_bytes((*cur2).name) };
1142 let name_str = core::str::from_utf8(name_bytes).unwrap_or("");
1143 if name_str.eq_ignore_ascii_case("form") {
1144 current = unsafe { (*cur2).parent };
1145 break;
1146 }
1147 }
1148 cur2 = unsafe { (*cur2).parent };
1149 }
1150 }
1151
1152 ctxt.current = current;
1153}
1154
1155unsafe fn ensure_html(ctxt: &mut HtmlParserCtxt) -> *mut _xmlNode {
1161 if ctxt.options & HTML_PARSE_NOIMPLIED != 0 {
1164 return ptr::null_mut();
1165 }
1166 if !ctxt.html.is_null() {
1167 return ctxt.html;
1168 }
1169
1170 let html_node = tree::new_node(ptr::null_mut(), b"html\0" as *const u8 as *const xmlChar);
1171 if html_node.is_null() {
1172 return ptr::null_mut();
1173 }
1174 {
1175 }
1178 ctxt.html = html_node;
1179 ctxt.html_created = true;
1180
1181 tree::add_child(ctxt.doc as *mut _xmlNode, html_node);
1183 ctxt.current = html_node;
1184
1185 html_node
1186}
1187
1188unsafe fn ensure_head(ctxt: &mut HtmlParserCtxt) -> *mut _xmlNode {
1190 if ctxt.options & HTML_PARSE_NOIMPLIED != 0 {
1191 return ptr::null_mut();
1192 }
1193 if !ctxt.head.is_null() {
1194 return ctxt.head;
1195 }
1196
1197 ensure_html(ctxt);
1199
1200 let head_node = tree::new_node(ptr::null_mut(), b"head\0" as *const u8 as *const xmlChar);
1201 if head_node.is_null() {
1202 return ptr::null_mut();
1203 }
1204 ctxt.head = head_node;
1205 ctxt.head_created = true;
1206
1207 tree::add_child(ctxt.html, head_node);
1209 ctxt.current = head_node;
1210 ctxt.in_head = true;
1211
1212 head_node
1213}
1214
1215unsafe fn ensure_body(ctxt: &mut HtmlParserCtxt) -> *mut _xmlNode {
1217 if ctxt.options & HTML_PARSE_NOIMPLIED != 0 {
1218 return ptr::null_mut();
1219 }
1220 if !ctxt.body.is_null() {
1221 return ctxt.body;
1222 }
1223
1224 ensure_html(ctxt);
1226
1227 let body_node = tree::new_node(ptr::null_mut(), b"body\0" as *const u8 as *const xmlChar);
1228 if body_node.is_null() {
1229 return ptr::null_mut();
1230 }
1231 ctxt.body = body_node;
1232 ctxt.body_created = true;
1233
1234 tree::add_child(ctxt.html, body_node);
1236 ctxt.current = body_node;
1237 ctxt.in_body = true;
1238
1239 body_node
1240}
1241
1242#[allow(dead_code)]
1244unsafe fn transition_to_body(ctxt: &mut HtmlParserCtxt) {
1245 if ctxt.in_head && !ctxt.seen_body_content {
1246 ctxt.seen_body_content = true;
1247 ctxt.in_head = false;
1248 ensure_body(ctxt);
1249 }
1250}
1251
1252struct HtmlAttr {
1258 name: Vec<u8>,
1259 value: Vec<u8>,
1260 #[allow(dead_code)]
1261 quoted: bool,
1262}
1263
1264fn parse_attr_name(ctxt: &mut HtmlParserCtxt) -> Vec<u8> {
1266 let mut name = Vec::new();
1267 while let Some(ch) = ctxt.peek() {
1268 if ch == b'='
1269 || ch == b'>'
1270 || ch == b'/'
1271 || ch == b' '
1272 || ch == b'\t'
1273 || ch == b'\n'
1274 || ch == b'\r'
1275 {
1276 break;
1277 }
1278 name.push(ch);
1279 ctxt.next();
1280 }
1281 name
1282}
1283
1284fn parse_attr_value(ctxt: &mut HtmlParserCtxt) -> (Vec<u8>, bool) {
1286 ctxt.skip_whitespace();
1287
1288 let quote = match ctxt.peek() {
1289 Some(b'"') => {
1290 ctxt.next(); b'"'
1292 }
1293 Some(b'\'') => {
1294 ctxt.next(); b'\''
1296 }
1297 _ => {
1298 let value = ctxt.read_while(|ch| {
1300 ch != b'>' && ch != b' ' && ch != b'\t' && ch != b'\n' && ch != b'\r'
1301 });
1302 return (value, false);
1303 }
1304 };
1305
1306 let mut value = Vec::new();
1308 loop {
1309 match ctxt.next() {
1310 Some(ch) if ch == quote => break,
1311 Some(ch) => value.push(ch),
1312 None => break,
1313 }
1314 }
1315 (value, true)
1316}
1317
1318fn parse_attributes(ctxt: &mut HtmlParserCtxt) -> Vec<HtmlAttr> {
1320 let mut attrs = Vec::new();
1321
1322 loop {
1323 ctxt.skip_whitespace();
1324
1325 match ctxt.peek() {
1326 Some(b'>') | None => break,
1327 Some(b'/')
1328 if ctxt.peek_at(1) == Some(b'>') => {
1330 break;
1331 }
1332 _ => {}
1334 }
1335
1336 let name = parse_attr_name(ctxt);
1337 if name.is_empty() {
1338 break;
1339 }
1340
1341 ctxt.skip_whitespace();
1343 if ctxt.peek() == Some(b'=') {
1344 ctxt.next(); let (value, quoted) = parse_attr_value(ctxt);
1346 attrs.push(HtmlAttr {
1347 name,
1348 value,
1349 quoted,
1350 });
1351 } else {
1352 attrs.push(HtmlAttr {
1354 name,
1355 value: Vec::new(),
1356 quoted: false,
1357 });
1358 }
1359 }
1360
1361 attrs
1362}
1363
1364fn resolve_entity(name: &str) -> Vec<u8> {
1370 if let Some(replacement) = html_entity_lookup(name) {
1371 replacement.as_bytes().to_vec()
1372 } else {
1373 let mut result = Vec::new();
1375 result.push(b'&');
1376 result.extend_from_slice(name.as_bytes());
1377 result.push(b';');
1378 result
1379 }
1380}
1381
1382fn resolve_numeric_entity(value: &str, is_hex: bool) -> Vec<u8> {
1384 let codepoint = if is_hex {
1385 u32::from_str_radix(value, 16).unwrap_or(0xFFFD)
1386 } else {
1387 value.parse::<u32>().unwrap_or(0xFFFD)
1388 };
1389
1390 if codepoint == 0 {
1391 return Vec::new();
1392 }
1393
1394 match char::from_u32(codepoint) {
1396 Some(c) => {
1397 let mut buf = [0u8; 4];
1398 let s = c.encode_utf8(&mut buf);
1399 s.as_bytes().to_vec()
1400 }
1401 None => vec![0xEF, 0xBF, 0xBD], }
1403}
1404
1405fn parse_entity(ctxt: &mut HtmlParserCtxt) -> Vec<u8> {
1408 match ctxt.peek() {
1410 Some(b'&') => {
1411 ctxt.next(); }
1413 _ => return vec![b'&'],
1414 }
1415
1416 if ctxt.peek() == Some(b'#') {
1418 ctxt.next(); let is_hex = ctxt.peek() == Some(b'x') || ctxt.peek() == Some(b'X');
1420 if is_hex {
1421 ctxt.next(); }
1423
1424 let digits = ctxt.read_while(|ch| {
1425 if is_hex {
1426 ch.is_ascii_hexdigit()
1427 } else {
1428 ch.is_ascii_digit()
1429 }
1430 });
1431
1432 let digits_str = core::str::from_utf8(&digits).unwrap_or("");
1433 if digits_str.is_empty() {
1434 let mut result = vec![b'&', b'#'];
1435 if is_hex {
1436 result.push(b'x');
1437 }
1438 return result;
1439 }
1440
1441 if ctxt.peek() == Some(b';') {
1443 ctxt.next();
1444 }
1445
1446 return resolve_numeric_entity(digits_str, is_hex);
1447 }
1448
1449 let name = ctxt.read_while(|ch| ch.is_ascii_alphanumeric() || ch == b'_' || ch == b'-');
1451 let name_str = core::str::from_utf8(&name).unwrap_or("");
1452
1453 if ctxt.peek() == Some(b';') {
1455 ctxt.next();
1456 }
1457
1458 resolve_entity(name_str)
1459}
1460
1461unsafe fn new_element_node(ns: *mut _xmlNs, name: &[u8]) -> *mut _xmlNode {
1483 let name_c = bytes_to_xmlstr(name);
1484 let node = tree::new_node(ns, name_c);
1485 if !name_c.is_null() {
1486 xmlFreeImpl(name_c as *mut c_void);
1487 }
1488 node
1489}
1490
1491unsafe fn new_text_node(content: &[u8]) -> *mut _xmlNode {
1501 let node = tree::new_text(ptr::null_mut());
1502 if node.is_null() {
1503 return ptr::null_mut();
1504 }
1505 let content_c = bytes_to_xmlstr(content);
1506 unsafe {
1507 if !(*node).content.is_null() {
1508 xmlFreeImpl((*node).content as *mut c_void);
1509 }
1510 (*node).content = content_c;
1512 }
1513 node
1514}
1515
1516unsafe fn handle_text(ctxt: &mut HtmlParserCtxt, text: &[u8]) {
1517 if text.is_empty() {
1518 return;
1519 }
1520
1521 let parent = if ctxt.in_head {
1523 ctxt.head
1524 } else if ctxt.in_body || ctxt.body_created {
1525 ctxt.body
1526 } else if ctxt.html_created {
1527 ctxt.html
1528 } else {
1529 ctxt.doc as *mut _xmlNode
1530 };
1531
1532 let insertion_point = if ctxt.current.is_null() {
1533 parent
1534 } else {
1535 ctxt.current
1536 };
1537
1538 if insertion_point.is_null() {
1539 let text_node = new_text_node(text);
1541 if !text_node.is_null() {
1542 tree::add_child(ctxt.doc as *mut _xmlNode, text_node);
1543 }
1544 return;
1545 }
1546
1547 if insertion_point == ctxt.doc as *mut _xmlNode {
1552 if text.iter().all(|b| b.is_ascii_whitespace()) {
1553 return;
1554 }
1555 let content = trim_ascii_start(text);
1556 let html_node = new_element_node(ptr::null_mut(), b"html");
1557 if !html_node.is_null() {
1558 let text_node = new_text_node(content);
1559 if !text_node.is_null() {
1560 tree::add_child(html_node, text_node);
1561 }
1562 tree::add_child(ctxt.doc as *mut _xmlNode, html_node);
1563 ctxt.html = html_node;
1564 ctxt.html_created = true;
1565 ctxt.current = html_node;
1566 }
1567 return;
1568 }
1569
1570 let text_node = new_text_node(text);
1571 if text_node.is_null() {
1572 return;
1573 }
1574
1575 tree::add_child(insertion_point, text_node);
1576}
1577
1578unsafe fn attach_attrs(node: *mut _xmlNode, attrs: &[HtmlAttr]) {
1585 for attr in attrs {
1586 let name_c = bytes_to_xmlstr(&attr.name);
1587 let val_c = bytes_to_xmlstr(&attr.value);
1588 if !name_c.is_null() {
1589 tree::set_prop(node, name_c, val_c);
1590 xmlFreeImpl(name_c as *mut c_void);
1591 if !val_c.is_null() {
1592 xmlFreeImpl(val_c as *mut c_void);
1593 }
1594 }
1595 }
1596}
1597
1598unsafe fn handle_start_tag(ctxt: &mut HtmlParserCtxt, tag_name: &[u8], attrs: &[HtmlAttr]) {
1600 let tag_lower: Vec<u8> = tag_name.iter().map(|b| b.to_ascii_lowercase()).collect();
1601 let tag_str = core::str::from_utf8(&tag_lower).unwrap_or("");
1602
1603 let info = html_tag_lookup(tag_str);
1604
1605 let is_head_tag = info.is_some_and(|i| i.flags & HTML_HEAD != 0);
1607 let _is_body_tag = info.is_some_and(|i| i.flags & HTML_BODY != 0);
1608 let is_empty = info.is_some_and(|i| i.flags & HTML_EMPTY != 0);
1609 let _is_block = info.is_some_and(|i| i.flags & HTML_BLOCK != 0);
1610
1611 if tag_str == "html" {
1613 if !ctxt.html.is_null() && !ctxt.html_created {
1614 return;
1616 }
1617 if ctxt.html.is_null() {
1619 let html_node = new_element_node(ptr::null_mut(), tag_name);
1620 if !html_node.is_null() {
1621 attach_attrs(html_node, attrs);
1622 ctxt.html = html_node;
1623 ctxt.html_created = false; tree::add_child(ctxt.doc as *mut _xmlNode, html_node);
1625 ctxt.current = html_node;
1626 }
1627 } else {
1628 ctxt.current = ctxt.html;
1630 }
1631 return;
1632 }
1633
1634 if tag_str == "head" {
1635 if !ctxt.head.is_null() && !ctxt.head_created {
1636 return;
1638 }
1639 ensure_html(ctxt);
1641
1642 if ctxt.head.is_null() {
1643 let head_node = new_element_node(ptr::null_mut(), tag_name);
1644 if !head_node.is_null() {
1645 attach_attrs(head_node, attrs);
1646 ctxt.head = head_node;
1647 ctxt.head_created = false;
1648 let parent = if ctxt.html.is_null() {
1651 ctxt.doc as *mut _xmlNode
1652 } else {
1653 ctxt.html
1654 };
1655 tree::add_child(parent, head_node);
1656 ctxt.current = head_node;
1657 ctxt.in_head = true;
1658 }
1659 } else {
1660 ctxt.current = ctxt.head;
1661 ctxt.in_head = true;
1662 }
1663 return;
1664 }
1665
1666 if tag_str == "body" {
1667 if !ctxt.body.is_null() && !ctxt.body_created {
1668 return;
1670 }
1671 ensure_html(ctxt);
1673
1674 if ctxt.body.is_null() {
1675 let body_node = new_element_node(ptr::null_mut(), tag_name);
1676 if !body_node.is_null() {
1677 attach_attrs(body_node, attrs);
1678 ctxt.body = body_node;
1679 ctxt.body_created = false;
1680 let parent = if ctxt.html.is_null() {
1683 ctxt.doc as *mut _xmlNode
1684 } else {
1685 ctxt.html
1686 };
1687 tree::add_child(parent, body_node);
1688 ctxt.current = body_node;
1689 ctxt.in_body = true;
1690 ctxt.in_head = false;
1691 ctxt.seen_body_content = true;
1692 }
1693 } else {
1694 ctxt.current = ctxt.body;
1695 ctxt.in_body = true;
1696 ctxt.in_head = false;
1697 ctxt.seen_body_content = true;
1698 }
1699 return;
1700 }
1701
1702 if is_head_tag && !ctxt.seen_body_content {
1704 if ctxt.head.is_null() {
1705 ensure_head(ctxt);
1706 }
1707
1708 if is_empty {
1709 let node = new_element_node(ptr::null_mut(), tag_name);
1711 if !node.is_null() {
1712 attach_attrs(node, attrs);
1713 let ip = if ctxt.current.is_null() {
1716 ctxt.doc as *mut _xmlNode
1717 } else {
1718 ctxt.current
1719 };
1720 tree::add_child(ip, node);
1721 }
1722 return;
1723 }
1724
1725 let node = new_element_node(ptr::null_mut(), tag_name);
1726 if !node.is_null() {
1727 attach_attrs(node, attrs);
1728 let ip = if ctxt.current.is_null() {
1729 ctxt.doc as *mut _xmlNode
1730 } else {
1731 ctxt.current
1732 };
1733 tree::add_child(ip, node);
1734 ctxt.current = node;
1735 }
1736 return;
1737 }
1738
1739 if !is_head_tag || ctxt.seen_body_content {
1741 if !ctxt.seen_body_content {
1742 ctxt.seen_body_content = true;
1743 ctxt.in_head = false;
1744 if ctxt.options & HTML_PARSE_NOIMPLIED != 0 {
1748 } else if ctxt.body.is_null() {
1750 ensure_body(ctxt);
1751 } else {
1752 ctxt.current = ctxt.body;
1753 ctxt.in_body = true;
1754 }
1755 } else if ctxt.body.is_null() && ctxt.options & HTML_PARSE_NOIMPLIED == 0 {
1756 ensure_body(ctxt);
1757 }
1758 }
1759
1760 if !ctxt.current.is_null() {
1762 auto_close_element(ctxt, tag_str);
1763 }
1764
1765 if is_empty {
1766 let node = new_element_node(ptr::null_mut(), tag_name);
1768 if !node.is_null() {
1769 for attr in attrs {
1770 let name_c = bytes_to_xmlstr(&attr.name);
1771 let val_c = bytes_to_xmlstr(&attr.value);
1772 if !name_c.is_null() {
1773 tree::set_prop(node, name_c, val_c);
1774 xmlFreeImpl(name_c as *mut c_void);
1775 if !val_c.is_null() {
1776 xmlFreeImpl(val_c as *mut c_void);
1777 }
1778 }
1779 }
1780 let insertion_point = if ctxt.current.is_null() {
1781 if ctxt.options & HTML_PARSE_NOIMPLIED != 0 {
1782 ctxt.doc as *mut _xmlNode
1783 } else {
1784 ctxt.body
1785 }
1786 } else {
1787 ctxt.current
1788 };
1789 if !insertion_point.is_null() {
1790 tree::add_child(insertion_point, node);
1791 }
1792 }
1793 return;
1794 }
1795
1796 let node = new_element_node(ptr::null_mut(), tag_name);
1798 if !node.is_null() {
1799 for attr in attrs {
1800 let name_c = bytes_to_xmlstr(&attr.name);
1801 let val_c = bytes_to_xmlstr(&attr.value);
1802 if !name_c.is_null() {
1803 tree::set_prop(node, name_c, val_c);
1804 xmlFreeImpl(name_c as *mut c_void);
1805 if !val_c.is_null() {
1806 xmlFreeImpl(val_c as *mut c_void);
1807 }
1808 }
1809 }
1810
1811 let insertion_point = if ctxt.current.is_null() {
1812 if ctxt.in_body || ctxt.body_created {
1813 ctxt.body
1814 } else if ctxt.in_head || ctxt.head_created {
1815 ctxt.head
1816 } else if ctxt.html_created {
1817 ctxt.html
1818 } else {
1819 ctxt.doc as *mut _xmlNode
1820 }
1821 } else {
1822 ctxt.current
1823 };
1824
1825 if !insertion_point.is_null() {
1826 tree::add_child(insertion_point, node);
1827 ctxt.current = node;
1829 }
1830 }
1831}
1832
1833unsafe fn handle_end_tag(ctxt: &mut HtmlParserCtxt, tag_name: &[u8]) {
1844 let tag_lower: Vec<u8> = tag_name.iter().map(|b| b.to_ascii_lowercase()).collect();
1845 let tag_str = core::str::from_utf8(&tag_lower).unwrap_or("");
1846
1847 let info = html_tag_lookup(tag_str);
1848
1849 if info.is_some_and(|i| i.flags & HTML_EMPTY != 0) {
1852 return;
1853 }
1854
1855 if tag_str == "html" {
1856 ctxt.current = ctxt.doc as *mut _xmlNode;
1857 return;
1858 }
1859
1860 if tag_str == "head" {
1861 ctxt.in_head = false;
1862 ctxt.current = ctxt.html;
1863 return;
1864 }
1865
1866 if tag_str == "body" {
1867 ctxt.in_body = false;
1868 ctxt.current = ctxt.html;
1869 return;
1870 }
1871
1872 let mut cur = ctxt.current;
1874 while !cur.is_null() {
1875 let ctype = unsafe { (*cur).type_ };
1876 if ctype == XML_ELEMENT_NODE as c_int && !unsafe { (*cur).name.is_null() } {
1877 let name_bytes = unsafe { xmlstr_to_bytes((*cur).name) };
1878 if name_bytes.eq_ignore_ascii_case(tag_name) {
1879 ctxt.current = unsafe { (*cur).parent };
1881 return;
1882 }
1883 }
1884 cur = unsafe { (*cur).parent };
1885 }
1886
1887 }
1889
1890fn convert_input_to_utf8(encoding: *const c_char, data: &[u8]) -> Option<Vec<u8>> {
1903 if encoding.is_null() {
1904 return None;
1905 }
1906 let name = unsafe { core::ffi::CStr::from_ptr(encoding).to_bytes() };
1907 match crate::xml::encoding::encoding_from_name(name) {
1908 xmlCharEncoding::XML_CHAR_ENCODING_8859_1 => {
1909 Some(crate::xml::encoding::latin1_to_utf8(data))
1910 }
1911 xmlCharEncoding::XML_CHAR_ENCODING_UTF16LE => {
1912 crate::xml::encoding::utf16le_to_utf8(data).ok()
1913 }
1914 xmlCharEncoding::XML_CHAR_ENCODING_UTF16BE => {
1915 crate::xml::encoding::utf16be_to_utf8(data).ok()
1916 }
1917 xmlCharEncoding::XML_CHAR_ENCODING_UCS4LE => {
1922 crate::xml::encoding::ucs4le_to_utf8(data).ok()
1923 }
1924 xmlCharEncoding::XML_CHAR_ENCODING_UCS4BE => {
1925 crate::xml::encoding::ucs4be_to_utf8(data).ok()
1926 }
1927 _ => None,
1929 }
1930}
1931
1932fn parse_html_doctype_decl(input: &[u8]) -> Option<(Vec<u8>, Option<Vec<u8>>, Option<Vec<u8>>)> {
1939 let mut i = 0usize;
1940 while i < input.len() && input[i].is_ascii_whitespace() {
1941 i += 1;
1942 }
1943 if i + 2 >= input.len() || input[i] != b'<' || !(input[i + 1] == b'!') {
1944 return None;
1945 }
1946 let kw = b"DOCTYPE";
1947 if !input.get(i + 2..i + 2 + kw.len()).is_some_and(|s| {
1948 s.iter()
1949 .enumerate()
1950 .all(|(k, b)| b.to_ascii_uppercase() == kw[k])
1951 }) {
1952 return None;
1953 }
1954 i += 2 + kw.len();
1955 while i < input.len() && input[i].is_ascii_whitespace() {
1957 i += 1;
1958 }
1959 let name_start = i;
1960 while i < input.len() && !input[i].is_ascii_whitespace() && input[i] != b'>' {
1961 i += 1;
1962 }
1963 let name = if name_start == i {
1968 Vec::new()
1969 } else {
1970 input[name_start..i].to_vec()
1971 };
1972 if name.is_empty() {
1974 return Some((name, None, None));
1975 }
1976 let mut ext: Option<Vec<u8>> = None;
1978 let mut sys: Option<Vec<u8>> = None;
1979 while i < input.len() && input[i].is_ascii_whitespace() {
1980 i += 1;
1981 }
1982 if i < input.len() && input[i] != b'>' {
1983 let word_start = i;
1985 while i < input.len() && input[i].is_ascii_alphabetic() {
1986 i += 1;
1987 }
1988 let word = input[word_start..i].to_ascii_uppercase();
1989 if word == b"PUBLIC" {
1990 while i < input.len() && input[i].is_ascii_whitespace() {
1991 i += 1;
1992 }
1993 if i < input.len() && (input[i] == b'"' || input[i] == b'\'') {
1995 let q = input[i];
1996 i += 1;
1997 let v_start = i;
1998 while i < input.len() && input[i] != q {
1999 i += 1;
2000 }
2001 ext = Some(input[v_start..i].to_vec());
2002 if i < input.len() {
2003 i += 1;
2004 }
2005 }
2006 while i < input.len() && input[i].is_ascii_whitespace() {
2007 i += 1;
2008 }
2009 if i < input.len()
2011 && i + 1 < input.len()
2012 && (input[i] == b'"' || input[i] == b'\'')
2013 && input[i] != b'>'
2014 {
2015 let q = input[i];
2016 i += 1;
2017 let v_start = i;
2018 while i < input.len() && input[i] != q {
2019 i += 1;
2020 }
2021 sys = Some(input[v_start..i].to_vec());
2022 }
2023 } else if word == b"SYSTEM" {
2024 while i < input.len() && input[i].is_ascii_whitespace() {
2025 i += 1;
2026 }
2027 if i < input.len() && (input[i] == b'"' || input[i] == b'\'') {
2028 let q = input[i];
2029 i += 1;
2030 let v_start = i;
2031 while i < input.len() && input[i] != q {
2032 i += 1;
2033 }
2034 sys = Some(input[v_start..i].to_vec());
2035 }
2036 }
2037 }
2038 Some((name, ext, sys))
2039}
2040
2041unsafe fn html_parse_buffer(
2047 ctxt: &mut HtmlParserCtxt,
2048 buffer: *const c_char,
2049 size: c_int,
2050) -> *mut _xmlDoc {
2051 if buffer.is_null() || size <= 0 {
2052 return ptr::null_mut();
2053 }
2054
2055 let doc = tree::new_doc(ptr::null());
2057 if doc.is_null() {
2058 return ptr::null_mut();
2059 }
2060 unsafe {
2061 (*doc).type_ = XML_HTML_DOCUMENT_NODE as c_int;
2062 if !(*doc).version.is_null() {
2065 crate::abi::allocator::xmlFreeImpl((*doc).version as *mut c_void);
2066 }
2067 (*doc).version = ptr::null_mut();
2068 (*doc).properties = crate::abi::types::xmlDocProperties::XML_DOC_HTML as c_int;
2075 (*doc).standalone = 1;
2078 }
2079 let raw_input = unsafe { slice::from_raw_parts(buffer as *const u8, size as usize) };
2086 if let Some((name, ext, sys)) = parse_html_doctype_decl(raw_input) {
2087 let name_cstr = if name.is_empty() {
2091 ptr::null()
2092 } else {
2093 crate::xml::string::bytes_to_xmlstr(&name)
2094 };
2095 let ext_cstr = ext
2096 .map(|s| crate::xml::string::bytes_to_xmlstr(&s))
2097 .unwrap_or(ptr::null_mut());
2098 let sys_cstr = sys
2099 .map(|s| crate::xml::string::bytes_to_xmlstr(&s))
2100 .unwrap_or(ptr::null_mut());
2101 unsafe {
2102 crate::xml::dtd::create_int_subset(
2103 doc,
2104 name_cstr as *const xmlChar,
2105 ext_cstr as *const xmlChar,
2106 sys_cstr as *const xmlChar,
2107 );
2108 }
2109 if !name_cstr.is_null() {
2110 unsafe { crate::abi::allocator::xmlFreeImpl(name_cstr as *mut c_void) };
2111 }
2112 if !ext_cstr.is_null() {
2113 unsafe { crate::abi::allocator::xmlFreeImpl(ext_cstr as *mut c_void) };
2114 }
2115 if !sys_cstr.is_null() {
2116 unsafe { crate::abi::allocator::xmlFreeImpl(sys_cstr as *mut c_void) };
2117 }
2118 } else {
2119 if ctxt.options & HTML_PARSE_NODEFDTD == 0 {
2122 unsafe {
2123 crate::xml::dtd::create_int_subset(
2124 doc,
2125 b"html\0" as *const u8 as *const xmlChar,
2126 b"-//W3C//DTD HTML 4.0 Transitional//EN\0" as *const u8 as *const xmlChar,
2127 b"http://www.w3.org/TR/REC-html40/loose.dtd\0" as *const u8 as *const xmlChar,
2128 );
2129 }
2130 }
2131 }
2132 ctxt.doc = doc;
2133
2134 let converted: Option<Vec<u8>> = if !ctxt.encoding.is_null() {
2139 let raw = unsafe { slice::from_raw_parts(buffer as *const u8, size as usize) };
2140 convert_input_to_utf8(ctxt.encoding, raw)
2141 } else {
2142 None
2143 };
2144 let (input_ptr, input_len): (*const u8, usize) = match &converted {
2145 Some(v) => (v.as_ptr(), v.len()),
2146 None => (buffer as *const u8, size as usize),
2147 };
2148
2149 ctxt.input = input_ptr as *mut u8;
2151 ctxt.input_len = input_len;
2152 ctxt.input_pos = 0;
2153 ctxt.line = 1;
2154
2155 loop {
2157 if ctxt.is_eof() {
2158 break;
2159 }
2160
2161 let ch = ctxt.peek().unwrap_or(0);
2162
2163 if ch == b'<' {
2164 ctxt.next(); if ctxt.peek() == Some(b'/') {
2168 ctxt.next(); let tag_name = ctxt.read_while(|ch| {
2170 ch != b'>' && ch != b' ' && ch != b'\t' && ch != b'\n' && ch != b'\r'
2171 });
2172
2173 while ctxt.peek() != Some(b'>') && !ctxt.is_eof() {
2175 ctxt.next();
2176 }
2177 if ctxt.peek() == Some(b'>') {
2178 ctxt.next(); }
2180
2181 if !tag_name.is_empty() {
2182 handle_end_tag(ctxt, &tag_name);
2183 }
2184 continue;
2185 }
2186
2187 if ctxt.peek() == Some(b'!')
2189 && ctxt.peek_at(1) == Some(b'-')
2190 && ctxt.peek_at(2) == Some(b'-')
2191 {
2192 ctxt.next(); ctxt.next(); ctxt.next(); let mut comment_content = Vec::new();
2198 loop {
2199 if ctxt.peek() == Some(b'-')
2200 && ctxt.peek_at(1) == Some(b'-')
2201 && ctxt.peek_at(2) == Some(b'>')
2202 {
2203 ctxt.next(); ctxt.next(); ctxt.next(); break;
2207 }
2208 match ctxt.next() {
2209 Some(ch) => comment_content.push(ch),
2210 None => break,
2211 }
2212 }
2213
2214 if !comment_content.is_empty() {
2216 let cc = bytes_to_xmlstr(&comment_content);
2218 let comment_node = tree::new_comment(cc);
2219 if !cc.is_null() {
2220 xmlFreeImpl(cc as *mut c_void);
2221 }
2222 if !comment_node.is_null() {
2223 let insertion_point = if !ctxt.current.is_null() {
2224 ctxt.current
2225 } else {
2226 ctxt.doc as *mut _xmlNode
2227 };
2228 tree::add_child(insertion_point, comment_node);
2229 }
2230 }
2231 continue;
2232 }
2233
2234 if ctxt.peek() == Some(b'!') {
2236 ctxt.next(); let _rest = ctxt.read_while(|ch| ch != b'>');
2238 if ctxt.peek() == Some(b'>') {
2239 ctxt.next(); }
2241 continue;
2244 }
2245
2246 if ctxt.peek() == Some(b'?') {
2248 ctxt.next(); let mut pi_content = Vec::new();
2251 loop {
2252 if ctxt.peek() == Some(b'?') && ctxt.peek_at(1) == Some(b'>') {
2253 break;
2254 }
2255 match ctxt.next() {
2256 Some(ch) => pi_content.push(ch),
2257 None => break,
2258 }
2259 }
2260 if ctxt.peek() == Some(b'?') {
2262 ctxt.next();
2263 }
2264 if ctxt.peek() == Some(b'>') {
2265 ctxt.next();
2266 }
2267 if !pi_content.is_empty() {
2269 let mut parts = pi_content.splitn(2, |b| *b == b' ');
2271 let target = parts.next().unwrap_or(&pi_content);
2272 let value = parts.next().unwrap_or(b"");
2273
2274 let t_c = bytes_to_xmlstr(target);
2276 let v_c = bytes_to_xmlstr(value);
2277 let pi_node = tree::new_pi(t_c, v_c);
2278 if !t_c.is_null() {
2279 xmlFreeImpl(t_c as *mut c_void);
2280 }
2281 if !v_c.is_null() {
2282 xmlFreeImpl(v_c as *mut c_void);
2283 }
2284 if !pi_node.is_null() {
2285 let insertion_point = if !ctxt.current.is_null() {
2286 ctxt.current
2287 } else {
2288 ctxt.doc as *mut _xmlNode
2289 };
2290 tree::add_child(insertion_point, pi_node);
2291 }
2292 }
2293 continue;
2294 }
2295
2296 let tag_name = ctxt.read_while(|ch| {
2298 ch != b'>' && ch != b'/' && ch != b' ' && ch != b'\t' && ch != b'\n' && ch != b'\r'
2299 });
2300
2301 if tag_name.is_empty() {
2302 handle_text(ctxt, b"<");
2304 continue;
2305 }
2306
2307 let attrs = parse_attributes(ctxt);
2309
2310 if ctxt.peek() == Some(b'/') {
2312 ctxt.next(); if ctxt.peek() == Some(b'>') {
2314 ctxt.next(); }
2316 } else if ctxt.peek() == Some(b'>') {
2317 ctxt.next(); }
2319
2320 let tag_lower: Vec<u8> = tag_name.iter().map(|b| b.to_ascii_lowercase()).collect();
2322 let tag_str = core::str::from_utf8(&tag_lower).unwrap_or("");
2323
2324 if tag_str == "script" || tag_str == "style" {
2325 let raw_node = new_element_node(ptr::null_mut(), &tag_name);
2328 if !raw_node.is_null() {
2329 for attr in &attrs {
2330 let name_c = bytes_to_xmlstr(&attr.name);
2331 let val_c = bytes_to_xmlstr(&attr.value);
2332 if !name_c.is_null() {
2333 tree::set_prop(raw_node, name_c, val_c);
2334 xmlFreeImpl(name_c as *mut c_void);
2335 if !val_c.is_null() {
2336 xmlFreeImpl(val_c as *mut c_void);
2337 }
2338 }
2339 }
2340
2341 let insertion_point = if ctxt.current.is_null() {
2342 if ctxt.in_head {
2343 ensure_head(ctxt);
2344 ctxt.head
2345 } else {
2346 ensure_body(ctxt);
2347 ctxt.body
2348 }
2349 } else {
2350 ctxt.current
2351 };
2352
2353 if !insertion_point.is_null() {
2354 tree::add_child(insertion_point, raw_node);
2355
2356 let end_tag = format!("</{}", tag_str);
2358 let end_bytes = end_tag.as_bytes();
2359 let mut raw_text = Vec::new();
2360 let mut match_buf: Vec<u8> = Vec::new();
2367 let mut match_idx = 0;
2368
2369 loop {
2370 if ctxt.is_eof() {
2371 break;
2372 }
2373 let ch = ctxt.peek().unwrap();
2374 if ch.to_ascii_lowercase() == end_bytes[match_idx] {
2375 match_buf.push(ch);
2376 match_idx += 1;
2377 ctxt.next();
2378 if match_idx == end_bytes.len() {
2379 if !raw_text.is_empty() {
2382 let text_node = new_text_node(&raw_text);
2383 if !text_node.is_null() {
2384 tree::add_child(raw_node, text_node);
2385 }
2386 }
2387 let _suffix = ctxt.read_while(|ch| ch != b'>');
2390 if ctxt.peek() == Some(b'>') {
2391 ctxt.next();
2392 }
2393 ctxt.current = unsafe { (*raw_node).parent };
2395 break;
2396 }
2397 } else {
2398 if match_idx > 0 {
2402 raw_text.extend_from_slice(&match_buf);
2403 match_buf.clear();
2404 match_idx = 0;
2405 }
2406 raw_text.push(ch);
2407 ctxt.next();
2408 }
2409 }
2410
2411 if match_idx < end_bytes.len() {
2414 raw_text.extend_from_slice(&match_buf);
2415 if !raw_text.is_empty() {
2416 let text_node = new_text_node(&raw_text);
2417 if !text_node.is_null() {
2418 tree::add_child(raw_node, text_node);
2419 }
2420 }
2421 ctxt.current = unsafe { (*raw_node).parent };
2422 }
2423 }
2424 }
2425 continue;
2426 }
2427
2428 handle_start_tag(ctxt, &tag_name, &attrs);
2430 } else {
2431 let mut text = Vec::new();
2433 loop {
2434 match ctxt.peek() {
2435 Some(b'<') => break,
2436 Some(b'&') => {
2437 let entity_text = parse_entity(ctxt);
2439 text.extend_from_slice(&entity_text);
2440 }
2441 Some(0) => {
2442 ctxt.next();
2448 }
2449 Some(ch) => {
2450 text.push(ch);
2451 ctxt.next();
2452 }
2453 None => break,
2454 }
2455 }
2456
2457 if !text.is_empty() {
2458 handle_text(ctxt, &text);
2459 }
2460 }
2461 }
2462
2463 if ctxt.html.is_null() && ctxt.options & HTML_PARSE_NOIMPLIED == 0 {
2467 ensure_html(ctxt);
2468 }
2469
2470 if ctxt.options & (crate::abi::types::XML_PARSE_NOBLANKS as c_int) != 0 && !doc.is_null() {
2476 unsafe {
2477 drop_blank_text_nodes((*doc).children);
2478 }
2479 }
2480
2481 if !doc.is_null() {
2492 unsafe {
2493 register_html_ids(doc, (*doc).children);
2494 }
2495 }
2496
2497 doc
2498}
2499
2500unsafe fn register_html_ids(doc: *mut _xmlDoc, cur: *mut _xmlNode) {
2508 let mut n = cur;
2509 while !n.is_null() {
2510 let t = unsafe { (*n).type_ };
2511 if t == XML_ELEMENT_NODE as c_int {
2512 let el = n;
2513 let mut attr = unsafe { (*el).properties };
2514 while !attr.is_null() {
2515 if unsafe { (*attr).id }.is_null()
2516 && !unsafe { (*attr).children }.is_null()
2517 && unsafe { (*(*attr).children).type_ } == XML_TEXT_NODE as c_int
2518 && unsafe { (*(*attr).children).next }.is_null()
2519 {
2520 let v = unsafe { (*(*attr).children).content };
2521 if !v.is_null() {
2522 let id_res = crate::xml::validation::is_id(doc, el, attr);
2523 if id_res > 0 {
2524 crate::xml::validation::add_id(ptr::null_mut(), doc, v, attr);
2525 } else if crate::xml::validation::is_ref(doc, el, attr) > 0 {
2526 crate::xml::validation::add_ref(ptr::null_mut(), doc, v, attr);
2527 }
2528 }
2529 }
2530 attr = unsafe { (*attr).next };
2531 }
2532 if !unsafe { (*el).children }.is_null() {
2533 register_html_ids(doc, unsafe { (*el).children });
2534 }
2535 }
2536 n = unsafe { (*n).next };
2537 }
2538}
2539
2540unsafe fn drop_blank_text_nodes(cur: *mut _xmlNode) {
2548 let mut n = cur;
2549 while !n.is_null() {
2550 let next = unsafe { (*n).next };
2551 let t = unsafe { (*n).type_ };
2552 if t == XML_TEXT_NODE as c_int {
2553 let content = unsafe { (*n).content };
2554 let blank = if content.is_null() {
2555 true
2556 } else {
2557 let mut p = content;
2558 while unsafe { *p } != 0 {
2559 match unsafe { *p } {
2560 b' ' | b'\t' | b'\r' | b'\n' => {}
2561 _ => break,
2562 }
2563 p = unsafe { p.add(1) };
2564 }
2565 (unsafe { *p }) == 0
2566 };
2567 if blank {
2568 tree::unlink_node(n);
2569 tree::free_node(n);
2570 }
2571 } else if t == XML_ELEMENT_NODE as c_int && !unsafe { (*n).children }.is_null() {
2572 drop_blank_text_nodes(unsafe { (*n).children });
2573 }
2574 n = next;
2575 }
2576}
2577
2578pub unsafe fn parse_file(
2593 filename: *const c_char,
2594 encoding: *const c_char,
2595 options: c_int,
2596) -> *mut _xmlDoc {
2597 if filename.is_null() {
2598 return ptr::null_mut();
2599 }
2600
2601 let filename_str = unsafe { std::ffi::CStr::from_ptr(filename) };
2603 let path = filename_str.to_str().unwrap_or("");
2604 let content = match std::fs::read(path) {
2605 Ok(data) => data,
2606 Err(_) => return ptr::null_mut(),
2607 };
2608
2609 let mut ctxt = HtmlParserCtxt::new();
2610 ctxt.options = options;
2611 if !encoding.is_null() {
2612 let _enc_cstr = unsafe { std::ffi::CStr::from_ptr(encoding) };
2613 ctxt.encoding = unsafe { c_strdup(encoding) };
2614 }
2615
2616 let doc = unsafe {
2617 html_parse_buffer(
2618 &mut ctxt,
2619 content.as_ptr() as *const c_char,
2620 content.len() as c_int,
2621 )
2622 };
2623
2624 if !doc.is_null() && !filename.is_null() {
2625 unsafe {
2626 (*doc).URL = c_strdup(filename) as *mut xmlChar;
2627 }
2628 }
2629
2630 doc
2631}
2632
2633pub unsafe fn parse_memory(buffer: *const c_char, size: c_int) -> *mut _xmlDoc {
2644 unsafe { parse_memory_enc(buffer, size, ptr::null(), 0) }
2645}
2646
2647pub(crate) unsafe fn parse_memory_enc(
2660 buffer: *const c_char,
2661 size: c_int,
2662 encoding: *const c_char,
2663 options: c_int,
2664) -> *mut _xmlDoc {
2665 if buffer.is_null() || size <= 0 {
2666 return ptr::null_mut();
2667 }
2668
2669 let mut ctxt = HtmlParserCtxt::new();
2670 ctxt.options = options;
2671 if !encoding.is_null() {
2672 ctxt.encoding = unsafe { c_strdup(encoding) };
2673 }
2674 unsafe { html_parse_buffer(&mut ctxt, buffer, size) }
2675}
2676
2677pub(crate) unsafe fn parse_doc(
2688 cur: *const xmlChar,
2689 encoding: *const c_char,
2690 options: c_int,
2691) -> *mut _xmlDoc {
2692 if cur.is_null() {
2693 return ptr::null_mut();
2694 }
2695
2696 let len = unsafe { xml_strlen(cur) };
2697 let mut ctxt = HtmlParserCtxt::new();
2698 ctxt.options = options;
2699 if !encoding.is_null() {
2700 ctxt.encoding = unsafe { c_strdup(encoding) };
2701 }
2702
2703 unsafe { html_parse_buffer(&mut ctxt, cur as *const c_char, len as c_int) }
2704}
2705
2706#[allow(dead_code)]
2717pub(crate) unsafe fn create_file_parser_ctxt(
2718 filename: *const c_char,
2719 encoding: *const c_char,
2720) -> *mut c_void {
2721 if filename.is_null() {
2722 return ptr::null_mut();
2723 }
2724
2725 let total = size_of::<_xmlParserCtxt>() + size_of::<HtmlParserCtxt>();
2728 let mem = unsafe { xmlMallocZero(total) } as *mut u8;
2729 if mem.is_null() {
2730 return ptr::null_mut();
2731 }
2732
2733 let ctxt = mem.add(size_of::<_xmlParserCtxt>()) as *mut HtmlParserCtxt;
2734 unsafe {
2735 ptr::write(ctxt, HtmlParserCtxt::new());
2736 if !encoding.is_null() {
2737 (*ctxt).encoding = c_strdup(encoding);
2738 }
2739 (*(mem as *mut _xmlParserCtxt)).html = 1;
2740 }
2741
2742 mem as *mut c_void
2743}
2744
2745pub(crate) unsafe fn free_parser_ctxt(ctxt: *mut c_void) {
2755 if ctxt.is_null() {
2756 return;
2757 }
2758
2759 let state = (ctxt as *mut u8).add(size_of::<_xmlParserCtxt>()) as *mut HtmlParserCtxt;
2763 unsafe {
2764 if !(*state).input.is_null() {
2765 xmlFreeImpl((*state).input as *mut c_void);
2766 }
2767 if !(*state).filename.is_null() {
2768 xmlFreeImpl((*state).filename as *mut c_void);
2769 }
2770 if !(*state).encoding.is_null() {
2771 xmlFreeImpl((*state).encoding as *mut c_void);
2772 }
2773 xmlFreeImpl(ctxt);
2774 }
2775}
2776
2777#[allow(dead_code)]
2783pub(crate) const fn init_parser() {
2784 }
2787
2788#[allow(dead_code)]
2794pub(crate) const fn cleanup_parser() {
2795 }
2798
2799pub(crate) unsafe fn new_doc(version: *const xmlChar) -> *mut _xmlDoc {
2817 let doc = tree::new_doc(version);
2818 if doc.is_null() {
2819 return ptr::null_mut();
2820 }
2821
2822 unsafe {
2823 (*doc).type_ = XML_HTML_DOCUMENT_NODE as c_int;
2824 (*doc).properties = XML_DOC_WELLFORMED as c_int;
2825 }
2826
2827 doc
2828}
2829
2830pub(crate) unsafe fn new_doc_no_dtd(version: *const xmlChar) -> *mut _xmlDoc {
2843 let doc = tree::new_doc(version);
2844 if doc.is_null() {
2845 return ptr::null_mut();
2846 }
2847
2848 unsafe {
2849 (*doc).type_ = XML_HTML_DOCUMENT_NODE as c_int;
2850 (*doc).properties = XML_DOC_WELLFORMED as c_int;
2851 }
2852
2853 doc
2854}
2855
2856const HTML_VOID_ELEMENTS: &[&str] = &[
2862 "area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "param", "source",
2863 "track", "wbr", "frame",
2864];
2865
2866fn is_html_void(name: &str) -> bool {
2868 HTML_VOID_ELEMENTS
2869 .iter()
2870 .any(|v| v.eq_ignore_ascii_case(name))
2871}
2872
2873#[allow(dead_code)]
2875fn has_optional_end_tag(name: &str) -> bool {
2876 matches!(
2877 name.to_ascii_lowercase().as_str(),
2878 "p" | "li" | "tr" | "td" | "th" | "dt" | "dd"
2879 )
2880}
2881
2882unsafe fn html_write_quoted(buf: *mut _xmlBuffer, s: *const xmlChar) {
2888 if buf.is_null() || s.is_null() {
2889 return;
2890 }
2891 io::buf_ccat(buf, b'"');
2892 io::buf_cat(buf, s);
2893 io::buf_ccat(buf, b'"');
2894}
2895
2896fn trim_ascii_start(s: &[u8]) -> &[u8] {
2898 let start = s
2899 .iter()
2900 .position(|&b| !b.is_ascii_whitespace())
2901 .unwrap_or(s.len());
2902 &s[start..]
2903}
2904
2905unsafe fn html_serialize_text(buf: *mut _xmlBuffer, content: *const xmlChar, len: c_int) {
2917 if buf.is_null() || content.is_null() || len <= 0 {
2918 return;
2919 }
2920
2921 let mut i: c_int = 0;
2922 while i < len {
2923 let ch = unsafe { *content.add(i as usize) };
2924
2925 match ch {
2926 b'<' => {
2927 io::buf_add(buf, b"<" as *const u8, 4);
2928 }
2929 b'&' => {
2930 io::buf_add(buf, b"&" as *const u8, 5);
2931 }
2932 b'>' => {
2933 io::buf_add(buf, b">" as *const u8, 4);
2934 }
2935 _ => {
2936 io::buf_add(buf, &ch as *const u8, 1);
2937 }
2938 }
2939 i += 1;
2940 }
2941}
2942
2943unsafe fn html_serialize_attr_value(buf: *mut _xmlBuffer, value: *const xmlChar) {
2954 if buf.is_null() || value.is_null() {
2955 return;
2956 }
2957
2958 let len = unsafe { xml_strlen(value) as c_int };
2959 let mut i: c_int = 0;
2960 while i < len {
2961 let ch = unsafe { *value.add(i as usize) };
2962
2963 match ch {
2964 b'&' => {
2965 io::buf_add(buf, b"&" as *const u8, 5);
2966 }
2967 b'"' => {
2968 io::buf_add(buf, b""" as *const u8, 6);
2969 }
2970 _ => {
2971 io::buf_add(buf, &ch as *const u8, 1);
2972 }
2973 }
2974 i += 1;
2975 }
2976}
2977
2978unsafe fn html_head_has_meta(child: *mut _xmlNode) -> bool {
2996 let mut c = child;
2997 while !c.is_null() {
2998 if (*c).type_ == XML_ELEMENT_NODE as c_int && !(*c).name.is_null() {
2999 let nm = xmlstr_to_bytes((*c).name);
3000 if nm.eq_ignore_ascii_case(b"meta") {
3001 return true;
3002 }
3003 }
3004 c = (*c).next;
3005 }
3006 false
3007}
3008
3009pub(crate) unsafe fn serialize_node(
3020 node: *mut _xmlNode,
3021 buf: *mut _xmlBuffer,
3022 format: c_int,
3023 level: c_int,
3024) {
3025 unsafe { serialize_node_enc(node, buf, format, level, None) }
3026}
3027
3028pub(crate) unsafe fn serialize_node_enc(
3040 node: *mut _xmlNode,
3041 buf: *mut _xmlBuffer,
3042 format: c_int,
3043 level: c_int,
3044 encoding: Option<&[u8]>,
3045) {
3046 if node.is_null() || buf.is_null() {
3047 return;
3048 }
3049
3050 let n = unsafe { &*node };
3051
3052 match n.type_ {
3053 t if t == XML_ELEMENT_NODE as c_int => {
3054 let name = if n.name.is_null() {
3055 ""
3056 } else {
3057 unsafe { core::str::from_utf8(xmlstr_to_bytes(n.name)).unwrap_or("") }
3058 };
3059
3060 let is_void = is_html_void(name);
3061 let info = html_tag_lookup(name);
3066 let is_inline = info.is_none_or(|i| i.flags & HTML_INLINE != 0);
3067 let no_format = is_inline || name.starts_with('p');
3068
3069 io::buf_ccat(buf, b'<');
3071 if !n.ns.is_null() && !(*n.ns).prefix.is_null() {
3077 io::buf_cat(buf, (*n.ns).prefix);
3078 io::buf_ccat(buf, b':');
3079 }
3080 if !n.name.is_null() {
3081 io::buf_cat(buf, n.name);
3082 }
3083 if !n.nsDef.is_null() {
3084 let mut ns = n.nsDef;
3085 while !ns.is_null() {
3086 let nsp = unsafe { &*ns };
3087 let is_xml = !nsp.prefix.is_null() && xmlstr_to_bytes(nsp.prefix) == b"xml";
3091 if nsp.type_ == XML_LOCAL_NAMESPACE as c_int && !nsp.href.is_null() && !is_xml {
3092 io::buf_ccat(buf, b' ');
3093 if nsp.prefix.is_null() {
3094 io::buf_add(buf, b"xmlns=\"" as *const u8, 7);
3095 } else {
3096 io::buf_add(buf, b"xmlns:" as *const u8, 6);
3097 io::buf_cat(buf, nsp.prefix);
3098 io::buf_add(buf, b"=\"" as *const u8, 2);
3099 }
3100 html_serialize_attr_value(buf, nsp.href);
3101 io::buf_ccat(buf, b'\"');
3102 }
3103 ns = nsp.next;
3104 }
3105 }
3106
3107 let mut attr = n.properties;
3109 while !attr.is_null() {
3110 let a = unsafe { &*attr };
3111 io::buf_ccat(buf, b' ');
3112 if !a.name.is_null() {
3113 io::buf_cat(buf, a.name);
3114 }
3115
3116 if !a.children.is_null() {
3118 let child = unsafe { &*a.children };
3119 if child.type_ == XML_TEXT_NODE as c_int && !child.content.is_null() {
3120 io::buf_ccat(buf, b'=');
3121 io::buf_ccat(buf, b'"');
3122 html_serialize_attr_value(buf, child.content);
3123 io::buf_ccat(buf, b'"');
3124 }
3125 }
3126
3127 attr = a.next;
3128 }
3129
3130 let mut meta_bytes: Option<Vec<u8>> = None;
3138 if name.eq_ignore_ascii_case("head") && level == 1 {
3139 if let Some(enc) = encoding {
3140 let parent_is_html = !n.parent.is_null() && !(*n.parent).name.is_null() && {
3141 let pn =
3142 core::str::from_utf8(xmlstr_to_bytes((*n.parent).name)).unwrap_or("");
3143 pn.eq_ignore_ascii_case("html")
3144 };
3145 if parent_is_html && !html_head_has_meta(n.children) {
3146 meta_bytes = Some(enc.to_vec());
3147 }
3148 }
3149 }
3150 let meta_inserted = meta_bytes.is_some();
3151
3152 let has_children = !n.children.is_null();
3153 let first_child = if has_children {
3154 unsafe { (*n.children).type_ }
3155 } else {
3156 XML_TEXT_NODE as c_int
3157 };
3158 let first_is_text = first_child == XML_TEXT_NODE as c_int
3159 || first_child == XML_ENTITY_REF_NODE as c_int;
3160 let multi_child = (has_children && n.children != n.last) || meta_inserted;
3163
3164 if is_void {
3165 io::buf_ccat(buf, b'>');
3167 } else {
3171 io::buf_ccat(buf, b'>');
3173
3174 if format != 0 && !no_format && !first_is_text && multi_child {
3179 io::buf_ccat(buf, b'\n');
3180 }
3181
3182 if let Some(enc) = &meta_bytes {
3183 io::buf_add(buf, b"<meta charset=\"" as *const u8, 15);
3184 io::buf_add(buf, enc.as_ptr(), enc.len() as c_int);
3185 io::buf_add(buf, b"\">" as *const u8, 2);
3186 if format != 0 && has_children && !first_is_text && !name.starts_with('p') {
3189 io::buf_ccat(buf, b'\n');
3190 }
3191 }
3192
3193 let mut child = n.children;
3196 while !child.is_null() {
3197 serialize_node_enc(child, buf, format, level + 1, encoding);
3198 let next = unsafe { (*child).next };
3202 if format != 0 && !next.is_null() && !name.starts_with('p') {
3203 let nt = unsafe { (*next).type_ };
3204 if nt != XML_TEXT_NODE as c_int && nt != XML_ENTITY_REF_NODE as c_int {
3205 let cname = if (*child).name.is_null() {
3206 ""
3207 } else {
3208 unsafe {
3209 core::str::from_utf8(xmlstr_to_bytes((*child).name))
3210 .unwrap_or("")
3211 }
3212 };
3213 let cinfo = html_tag_lookup(cname);
3214 let c_inline = cinfo.is_none_or(|i| i.flags & HTML_INLINE != 0);
3215 if !c_inline {
3216 io::buf_ccat(buf, b'\n');
3217 }
3218 }
3219 }
3220 child = next;
3221 }
3222
3223 let last_child = if has_children {
3227 unsafe { (*n.last).type_ }
3228 } else {
3229 XML_ELEMENT_NODE as c_int
3230 };
3231 let last_is_text = last_child == XML_TEXT_NODE as c_int
3232 || last_child == XML_ENTITY_REF_NODE as c_int;
3233 if format != 0 && !no_format && !last_is_text && multi_child {
3234 io::buf_ccat(buf, b'\n');
3235 }
3236
3237 io::buf_add(buf, b"</" as *const u8, 2);
3239 if !n.ns.is_null() && !(*n.ns).prefix.is_null() {
3242 io::buf_cat(buf, (*n.ns).prefix);
3243 io::buf_ccat(buf, b':');
3244 }
3245 if !n.name.is_null() {
3246 io::buf_cat(buf, n.name);
3247 }
3248 io::buf_ccat(buf, b'>');
3249 }
3250 }
3251 t if t == XML_TEXT_NODE as c_int => {
3252 let parent_is_raw = !n.parent.is_null() && !(*n.parent).name.is_null() && {
3256 let pn = xmlstr_to_bytes((*n.parent).name);
3257 pn.eq_ignore_ascii_case(b"script") || pn.eq_ignore_ascii_case(b"style")
3258 };
3259 if parent_is_raw {
3260 io::buf_cat(buf, n.content);
3261 } else {
3262 html_serialize_text(buf, n.content, xml_strlen(n.content) as c_int);
3263 }
3264 }
3265 t if t == XML_CDATA_SECTION_NODE as c_int => {
3266 io::buf_add(buf, b"<![CDATA[" as *const u8, 9);
3267 html_serialize_text(buf, n.content, xml_strlen(n.content) as c_int);
3268 io::buf_add(buf, b"]]>" as *const u8, 3);
3269 }
3270 t if t == XML_COMMENT_NODE as c_int => {
3271 if format != 0 && level > 0 {
3272 io::buf_ccat(buf, b'\n');
3273 for _ in 0..level {
3274 io::buf_add(buf, b" " as *const u8, 2);
3275 }
3276 }
3277 io::buf_add(buf, b"<!--" as *const u8, 4);
3278 if !n.content.is_null() {
3279 io::buf_cat(buf, n.content);
3280 }
3281 io::buf_add(buf, b"-->" as *const u8, 3);
3282 }
3283 t if t == XML_PI_NODE as c_int => {
3284 if format != 0 && level > 0 {
3285 io::buf_ccat(buf, b'\n');
3286 for _ in 0..level {
3287 io::buf_add(buf, b" " as *const u8, 2);
3288 }
3289 }
3290 io::buf_add(buf, b"<?" as *const u8, 2);
3291 if !n.name.is_null() {
3292 io::buf_cat(buf, n.name);
3293 }
3294 if !n.content.is_null() && unsafe { *n.content != 0 } {
3295 io::buf_ccat(buf, b' ');
3296 io::buf_cat(buf, n.content);
3297 }
3298 io::buf_add(buf, b"?>" as *const u8, 2);
3299 }
3300 t if t == XML_DOCUMENT_NODE as c_int || t == XML_HTML_DOCUMENT_NODE as c_int => {
3301 let doc_ptr = n as *const _xmlNode as *mut _xmlDoc;
3304 let d = &*doc_ptr;
3305 if !d.intSubset.is_null() {
3306 let dtd = &*d.intSubset;
3307 io::buf_add(buf, b"<!DOCTYPE " as *const u8, 10);
3308 if !dtd.name.is_null() {
3309 io::buf_cat(buf, dtd.name);
3310 }
3311 if !dtd.ExternalID.is_null() {
3312 io::buf_add(buf, b" PUBLIC " as *const u8, 8);
3313 html_write_quoted(buf, dtd.ExternalID);
3314 io::buf_ccat(buf, b' ');
3315 html_write_quoted(buf, dtd.SystemID);
3316 } else if !dtd.SystemID.is_null() {
3317 io::buf_add(buf, b" SYSTEM " as *const u8, 8);
3318 html_write_quoted(buf, dtd.SystemID);
3319 }
3320 io::buf_ccat(buf, b'>');
3321 io::buf_ccat(buf, b'\n');
3322 }
3323 let mut child = n.children;
3326 while !child.is_null() {
3327 serialize_node_enc(child, buf, format, 0, encoding);
3328 child = unsafe { (*child).next };
3329 }
3330 io::buf_ccat(buf, b'\n');
3333 }
3334 _ => {
3335 if !n.content.is_null() {
3336 html_serialize_text(buf, n.content, xml_strlen(n.content) as c_int);
3337 }
3338 }
3339 }
3340}
3341
3342pub(crate) unsafe fn doc_dump(buf: *mut _xmlBuffer, doc: *mut _xmlDoc) -> c_int {
3349 if buf.is_null() || doc.is_null() {
3350 return -1;
3351 }
3352
3353 let before = io::buf_length(buf);
3354 serialize_node(doc as *mut _xmlNode, buf, 0, 0);
3355 let after = io::buf_length(buf);
3356
3357 if after < 0 || before < 0 {
3358 return -1;
3359 }
3360 after - before
3361}
3362
3363#[cfg(test)]
3368mod tests {
3369 use super::*;
3370
3371 use crate::xml::io;
3372
3373 #[allow(dead_code)]
3375 unsafe fn to_xmlstr(s: &[u8]) -> *mut xmlChar {
3376 bytes_to_xmlstr(s)
3377 }
3378
3379 unsafe fn html_doc_to_string(doc: *mut _xmlDoc) -> String {
3381 let buf = io::buf_create(-1);
3382 assert!(!buf.is_null());
3383 doc_dump(buf, doc);
3384 let content = io::buf_content(buf);
3385 let s = if !content.is_null() {
3386 let len = xml_strlen(content);
3387 let slice = slice::from_raw_parts(content, len);
3388 String::from_utf8_lossy(slice).to_string()
3389 } else {
3390 String::new()
3391 };
3392 io::buf_free(buf);
3393 s
3394 }
3395
3396 #[test]
3401 fn test_html_tag_lookup() {
3402 assert!(html_tag_lookup("html").is_some());
3404 assert!(html_tag_lookup("HTML").is_some()); assert!(html_tag_lookup("p").is_some());
3406 assert!(html_tag_lookup("br").is_some());
3407 assert!(html_tag_lookup("div").is_some());
3408 assert!(html_tag_lookup("script").is_some());
3409
3410 assert!(html_tag_lookup("custom").is_none());
3412 assert!(html_tag_lookup("my-element").is_none());
3413 }
3414
3415 #[test]
3416 fn test_tag_flags() {
3417 let br = html_tag_lookup("br").unwrap();
3418 assert!(br.flags & HTML_INLINE != 0);
3419 assert!(br.flags & HTML_EMPTY != 0);
3420
3421 let div = html_tag_lookup("div").unwrap();
3422 assert!(div.flags & HTML_BLOCK != 0);
3423 assert!(div.flags & HTML_VALID != 0);
3424
3425 let p = html_tag_lookup("p").unwrap();
3426 assert!(p.flags & HTML_NO_END != 0);
3427
3428 let meta = html_tag_lookup("meta").unwrap();
3429 assert!(meta.flags & HTML_HEAD != 0);
3430 assert!(meta.flags & HTML_EMPTY != 0);
3431 }
3432
3433 #[test]
3438 fn test_html_entity_lookup() {
3439 assert_eq!(html_entity_lookup("amp"), Some("&"));
3440 assert_eq!(html_entity_lookup("lt"), Some("<"));
3441 assert_eq!(html_entity_lookup("gt"), Some(">"));
3442 assert_eq!(html_entity_lookup("quot"), Some("\""));
3443 assert_eq!(html_entity_lookup("nbsp"), Some("\u{00a0}"));
3444 assert_eq!(html_entity_lookup("copy"), Some("\u{00a9}"));
3445 assert!(html_entity_lookup("unknown_entity").is_none());
3446 }
3447
3448 #[test]
3462 fn test_parse_basic_html() {
3463 unsafe {
3464 let html = b"<html><head><title>Test</title></head><body><p>Hello</p></body></html>\0";
3465 let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
3466 assert!(!doc.is_null());
3467
3468 let s = html_doc_to_string(doc);
3469 assert!(s.contains("<html>"));
3470 assert!(s.contains("<head>"));
3471 assert!(s.contains("<title>Test</title>"));
3472 assert!(s.contains("<body>"));
3473 assert!(s.contains("<p>Hello</p>"));
3474
3475 tree::free_doc(doc);
3476 }
3477 }
3478
3479 #[test]
3486 fn test_parse_empty_document() {
3487 unsafe {
3488 let html = b"\0";
3489 let doc = parse_memory(html.as_ptr() as *const c_char, 0);
3490 assert!(doc.is_null());
3491 }
3492 }
3493
3494 #[test]
3508 fn test_implicit_html_head_body() {
3509 unsafe {
3510 let html = b"<p>Hello</p>\0";
3512 let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
3513 assert!(!doc.is_null());
3514
3515 let s = html_doc_to_string(doc);
3516 assert!(s.contains("<html>"));
3518 assert!(s.contains("<body>"));
3520 assert!(s.contains("<p>Hello</p>"));
3522
3523 tree::free_doc(doc);
3524 }
3525 }
3526
3527 #[test]
3537 fn test_implicit_head_with_title() {
3538 unsafe {
3539 let html = b"<title>My Page</title>\0";
3541 let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
3542 assert!(!doc.is_null());
3543
3544 let s = html_doc_to_string(doc);
3545 assert!(s.contains("<html>"));
3546 assert!(s.contains("<head>"));
3547 assert!(s.contains("<title>My Page</title>"));
3548
3549 tree::free_doc(doc);
3550 }
3551 }
3552
3553 #[test]
3566 fn test_auto_close_p() {
3567 unsafe {
3568 let html = b"<p>First<p>Second</p>\0";
3570 let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
3571 assert!(!doc.is_null());
3572
3573 let s = html_doc_to_string(doc);
3574 let first_pos = s.find("First");
3576 let second_pos = s.find("Second");
3577 assert!(first_pos.is_some());
3578 assert!(second_pos.is_some());
3579
3580 tree::free_doc(doc);
3581 }
3582 }
3583
3584 #[test]
3593 fn test_auto_close_heading() {
3594 unsafe {
3595 let html = b"<h1>Title</h1><h2>Subtitle</h2>\0";
3597 let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
3598 assert!(!doc.is_null());
3599
3600 let s = html_doc_to_string(doc);
3601 assert!(s.contains("<h1>Title</h1>"));
3602 assert!(s.contains("<h2>Subtitle</h2>"));
3603
3604 tree::free_doc(doc);
3605 }
3606 }
3607
3608 #[test]
3621 fn test_void_elements() {
3622 unsafe {
3623 let html = b"<br><hr><img src=\"test.jpg\"><input type=\"text\">\0";
3624 let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
3625 assert!(!doc.is_null());
3626
3627 let s = html_doc_to_string(doc);
3628 assert!(s.contains("<br>"));
3629 assert!(s.contains("<hr>"));
3630 assert!(s.contains("<img"));
3631 assert!(s.contains("<input"));
3632
3633 assert!(!s.contains("</br>"));
3635 assert!(!s.contains("</hr>"));
3636 assert!(!s.contains("</img>"));
3637
3638 tree::free_doc(doc);
3639 }
3640 }
3641
3642 #[test]
3656 fn test_unquoted_attributes() {
3657 unsafe {
3658 let html = b"<div class=main id=content>Text</div>\0";
3659 let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
3660 assert!(!doc.is_null());
3661
3662 let s = html_doc_to_string(doc);
3663 assert!(s.contains("class=\"main\""));
3664 assert!(s.contains("id=\"content\""));
3665
3666 tree::free_doc(doc);
3667 }
3668 }
3669
3670 #[test]
3679 fn test_minimized_attributes() {
3680 unsafe {
3681 let html = b"<option selected disabled>Value</option>\0";
3682 let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
3683 assert!(!doc.is_null());
3684
3685 let s = html_doc_to_string(doc);
3686 assert!(s.contains("selected"));
3688 assert!(s.contains("disabled"));
3689
3690 tree::free_doc(doc);
3691 }
3692 }
3693
3694 #[test]
3707 fn test_html_entities() {
3708 unsafe {
3709 let html = b"<p>& < > " ©</p>\0";
3710 let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
3711 assert!(!doc.is_null());
3712
3713 let s = html_doc_to_string(doc);
3714 assert!(s.contains("&")); assert!(s.contains("<")); assert!(s.contains(">")); assert!(s.contains("\u{00a0}")); tree::free_doc(doc);
3722 }
3723 }
3724
3725 #[test]
3734 fn test_numeric_entities() {
3735 unsafe {
3736 let html = b"<p>A A</p>\0";
3738 let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
3739 assert!(!doc.is_null());
3740
3741 let s = html_doc_to_string(doc);
3742 assert!(s.contains('A'));
3743
3744 tree::free_doc(doc);
3745 }
3746 }
3747
3748 #[test]
3761 fn test_nested_elements() {
3762 unsafe {
3763 let html = b"<div><ul><li>Item 1</li><li>Item 2</li></ul></div>\0";
3764 let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
3765 assert!(!doc.is_null());
3766
3767 let s = html_doc_to_string(doc);
3768 assert!(s.contains("<div>"));
3769 assert!(s.contains("<ul>"));
3770 assert!(s.contains("<li>Item 1</li>"));
3771 assert!(s.contains("<li>Item 2</li>"));
3772
3773 tree::free_doc(doc);
3774 }
3775 }
3776
3777 #[test]
3790 fn test_missing_end_tags() {
3791 unsafe {
3792 let html = b"<p>Paragraph without closing<div>Another div\0";
3794 let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
3795 assert!(!doc.is_null());
3796
3797 let s = html_doc_to_string(doc);
3798 assert!(s.contains("Paragraph without closing"));
3799 assert!(s.contains("Another div"));
3800
3801 tree::free_doc(doc);
3802 }
3803 }
3804
3805 #[test]
3814 fn test_mismatched_case() {
3815 unsafe {
3816 let html = b"<HTML><HEAD><TITLE>Test</TITLE></HEAD><BODY><P>Hello</P></BODY></HTML>\0";
3817 let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
3818 assert!(!doc.is_null());
3819
3820 let s = html_doc_to_string(doc);
3821 assert!(s.contains("<HTML>"));
3823 assert!(s.contains("<HEAD>"));
3824 assert!(s.contains("<BODY>"));
3825 assert!(s.contains("<P>Hello</P>"));
3826
3827 tree::free_doc(doc);
3828 }
3829 }
3830
3831 #[test]
3840 fn test_nested_malformed() {
3841 unsafe {
3842 let html = b"<div><p><span><b>Deep text</div></p>\0";
3844 let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
3845 assert!(!doc.is_null());
3846
3847 let s = html_doc_to_string(doc);
3848 assert!(s.contains("Deep text"));
3849
3850 tree::free_doc(doc);
3851 }
3852 }
3853
3854 #[test]
3867 fn test_serialization_round_trip_simple() {
3868 unsafe {
3869 let original = b"<p>Hello World</p>\0";
3870 let doc = parse_memory(
3871 original.as_ptr() as *const c_char,
3872 (original.len() - 1) as c_int,
3873 );
3874 assert!(!doc.is_null());
3875
3876 let s = html_doc_to_string(doc);
3877 assert!(s.contains("Hello World"));
3878
3879 tree::free_doc(doc);
3880 }
3881 }
3882
3883 #[test]
3892 fn test_serialize_void_elements_no_self_close() {
3893 unsafe {
3894 let html = b"<br><hr><img src=\"test.png\">\0";
3895 let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
3896 assert!(!doc.is_null());
3897
3898 let s = html_doc_to_string(doc);
3899 assert!(!s.contains("<br/>"));
3901 assert!(!s.contains("<hr/>"));
3902
3903 tree::free_doc(doc);
3904 }
3905 }
3906
3907 #[test]
3920 fn test_script_content() {
3921 unsafe {
3922 let html = b"<script>var x = 1;</script>\0";
3924 let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
3925 assert!(!doc.is_null());
3926
3927 let s = html_doc_to_string(doc);
3928 assert!(s.contains("<script>"));
3929 assert!(s.contains("var x = 1;"));
3931
3932 let html2 = b"<script>if (a < b && c > d) { x(1); }</script>\0";
3935 let doc2 = parse_memory(html2.as_ptr() as *const c_char, (html2.len() - 1) as c_int);
3936 assert!(!doc2.is_null());
3937 let s2 = html_doc_to_string(doc2);
3938 assert!(
3939 s2.contains("if (a < b && c > d) { x(1); }"),
3940 "script content must be raw, got: {s2}"
3941 );
3942 assert!(
3943 !s2.contains("<"),
3944 "script content must not be escaped: {s2}"
3945 );
3946 tree::free_doc(doc2);
3947
3948 tree::free_doc(doc);
3949 }
3950 }
3951
3952 #[test]
3965 fn test_html_comment() {
3966 unsafe {
3967 let html = b"<html><!-- This is a comment --><body><p>Text</p></body></html>\0";
3968 let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
3969 assert!(!doc.is_null());
3970
3971 let s = html_doc_to_string(doc);
3972 assert!(s.contains("<!-- This is a comment -->"));
3973
3974 tree::free_doc(doc);
3975 }
3976 }
3977
3978 #[test]
3994 fn test_new_doc_creates_html_head_body() {
3995 unsafe {
3996 let doc = new_doc(ptr::null());
3997 assert!(!doc.is_null());
3998 assert_eq!((*doc).type_, XML_HTML_DOCUMENT_NODE as c_int);
3999
4000 let s = html_doc_to_string(doc);
4001 assert!(!s.contains("<html>"), "htmlNewDoc must not seed <html>");
4002 assert!(!s.contains("<head>"), "htmlNewDoc must not seed <head>");
4003 assert!(!s.contains("<body>"), "htmlNewDoc must not seed <body>");
4004
4005 tree::free_doc(doc);
4006 }
4007 }
4008
4009 #[test]
4019 fn test_new_doc_no_dtd() {
4020 unsafe {
4021 let doc = new_doc_no_dtd(ptr::null());
4022 assert!(!doc.is_null());
4023 assert_eq!((*doc).type_, XML_HTML_DOCUMENT_NODE as c_int);
4024
4025 let s = html_doc_to_string(doc);
4029 assert_eq!(s, "\n");
4030
4031 tree::free_doc(doc);
4032 }
4033 }
4034
4035 #[test]
4048 fn test_parsed_html_doc_flags() {
4049 unsafe {
4050 let doc = parse_memory(c"<html><body>x</body></html>".as_ptr(), 23);
4051 assert!(!doc.is_null());
4052 assert_eq!(
4053 (*doc).properties & (crate::abi::types::xmlDocProperties::XML_DOC_HTML as c_int),
4054 crate::abi::types::xmlDocProperties::XML_DOC_HTML as c_int,
4055 "html-parsed docs must carry XML_DOC_HTML"
4056 );
4057 assert_eq!(
4058 (*doc).standalone,
4059 1,
4060 "html-parsed docs default standalone=yes"
4061 );
4062 tree::free_doc(doc);
4063 }
4064 }
4065
4066 #[test]
4071 fn test_resolve_numeric_entity() {
4072 assert_eq!(resolve_numeric_entity("65", false), vec![b'A']);
4073 assert_eq!(resolve_numeric_entity("41", true), vec![b'A']);
4074 assert_eq!(resolve_numeric_entity("0", false), Vec::<u8>::new());
4075 }
4076
4077 #[test]
4078 fn test_resolve_entity_unknown() {
4079 let result = resolve_entity("unknown");
4080 assert_eq!(result, b"&unknown;");
4081 }
4082
4083 #[test]
4088 fn test_init_cleanup_parser() {
4089 init_parser();
4091 cleanup_parser();
4092 }
4093
4094 #[test]
4106 fn test_create_free_parser_ctxt() {
4107 unsafe {
4108 let ctxt =
4109 create_file_parser_ctxt(b"test.html\0" as *const u8 as *const c_char, ptr::null());
4110 assert!(!ctxt.is_null());
4111 free_parser_ctxt(ctxt);
4112 }
4113 }
4114
4115 #[test]
4128 fn test_complex_html_document() {
4129 unsafe {
4130 let html = b"<!DOCTYPE html>
4131<html>
4132<head>
4133 <meta charset=\"utf-8\">
4134 <title>Test Page</title>
4135 <link rel=\"stylesheet\" href=\"style.css\">
4136</head>
4137<body>
4138 <div id=\"main\">
4139 <h1>Title</h1>
4140 <p>First paragraph with <a href=\"link.html\">a link</a>.</p>
4141 <p>Second paragraph.</p>
4142 <ul>
4143 <li>Item 1</li>
4144 <li>Item 2</li>
4145 </ul>
4146 <br>
4147 <hr>
4148 <img src=\"image.jpg\" alt=\"An image\">
4149 </div>
4150 <script>alert('hello');</script>
4151</body>
4152</html>\0";
4153 let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
4154 assert!(!doc.is_null());
4155
4156 let s = html_doc_to_string(doc);
4157 assert!(s.contains("<html>"));
4158 assert!(s.contains("<head>"));
4159 assert!(s.contains("<title>Test Page</title>"));
4160 assert!(s.contains("<body>"));
4161 assert!(s.contains("<h1>Title</h1>"));
4162 assert!(s.contains("a link"));
4163 assert!(s.contains("Second paragraph"));
4164 assert!(s.contains("<br>"));
4165 assert!(s.contains("<hr>"));
4166 assert!(s.contains("<img"));
4167 assert!(s.contains("<script>"));
4168
4169 tree::free_doc(doc);
4170 }
4171 }
4172
4173 #[test]
4186 fn test_parse_doc() {
4187 unsafe {
4188 let html = b"<p>Hello from parse_doc</p>\0";
4189 let doc = parse_doc(html.as_ptr() as *const xmlChar, ptr::null(), 0);
4190 assert!(!doc.is_null());
4191
4192 let s = html_doc_to_string(doc);
4193 assert!(s.contains("Hello from parse_doc"));
4194
4195 tree::free_doc(doc);
4196 }
4197 }
4198
4199 #[test]
4212 fn test_table_element_auto_close() {
4213 unsafe {
4214 let html = b"<table><tr><td>Cell 1<td>Cell 2</td></tr></table>";
4215 let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
4216 assert!(!doc.is_null());
4217
4218 let s = html_doc_to_string(doc);
4219 assert!(s.contains("<td>Cell 1"));
4220 assert!(s.contains("<td>Cell 2"));
4221
4222 tree::free_doc(doc);
4223 }
4224 }
4225
4226 #[test]
4227 fn test_table_tr_auto_close() {
4228 unsafe {
4231 let html = b"<table><tr><td>1<td>2<tr><td>3</table>\0";
4232 let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
4233 assert!(!doc.is_null());
4234 let s = html_doc_to_string(doc);
4235 assert!(s.contains("</td></tr><tr>"));
4236 tree::free_doc(doc);
4237 }
4238 }
4239}