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 let lower: Vec<u8> = name.bytes().map(|b| b.to_ascii_lowercase()).collect();
576 let lower_str = match core::str::from_utf8(&lower) {
577 Ok(s) => s,
578 Err(_) => return None,
579 };
580 HTML_ELEMENTS.iter().find(|info| info.name == lower_str)
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 let mut open_names: Vec<Vec<u8>> = Vec::new();
979 let mut cur = current;
980 while !cur.is_null() {
981 let ctype = unsafe { (*cur).type_ };
982 if ctype == XML_ELEMENT_NODE as c_int && !unsafe { (*cur).name.is_null() } {
983 let name_bytes = unsafe { xmlstr_to_bytes((*cur).name) };
984 open_names.push(name_bytes.to_vec());
985 }
986 cur = unsafe { (*cur).parent };
987 }
988
989 if tag_lower_str == "p" || info.is_some_and(|i| i.flags & HTML_BLOCK != 0) {
991 let mut cur2 = current;
993 while !cur2.is_null() {
994 let ctype = unsafe { (*cur2).type_ };
995 if ctype == XML_ELEMENT_NODE as c_int && !unsafe { (*cur2).name.is_null() } {
996 let name_bytes = unsafe { xmlstr_to_bytes((*cur2).name) };
997 let name_str = core::str::from_utf8(name_bytes).unwrap_or("");
998 if name_str.eq_ignore_ascii_case("p") {
999 current = unsafe { (*cur2).parent };
1001 break;
1002 }
1003 }
1004 cur2 = unsafe { (*cur2).parent };
1005 }
1006 }
1007
1008 if is_heading(tag_lower_str) {
1010 let mut cur2 = current;
1011 while !cur2.is_null() {
1012 let ctype = unsafe { (*cur2).type_ };
1013 if ctype == XML_ELEMENT_NODE as c_int && !unsafe { (*cur2).name.is_null() } {
1014 let name_bytes = unsafe { xmlstr_to_bytes((*cur2).name) };
1015 let name_str = core::str::from_utf8(name_bytes).unwrap_or("");
1016 if is_heading(name_str) {
1017 current = unsafe { (*cur2).parent };
1018 break;
1019 }
1020 }
1021 cur2 = unsafe { (*cur2).parent };
1022 }
1023 }
1024
1025 if tag_lower_str == "li" {
1027 let mut cur2 = current;
1028 while !cur2.is_null() {
1029 let ctype = unsafe { (*cur2).type_ };
1030 if ctype == XML_ELEMENT_NODE as c_int && !unsafe { (*cur2).name.is_null() } {
1031 let name_bytes = unsafe { xmlstr_to_bytes((*cur2).name) };
1032 let name_str = core::str::from_utf8(name_bytes).unwrap_or("");
1033 if name_str.eq_ignore_ascii_case("li") {
1034 current = unsafe { (*cur2).parent };
1035 break;
1036 }
1037 }
1038 cur2 = unsafe { (*cur2).parent };
1039 }
1040 }
1041
1042 if tag_lower_str == "dt" || tag_lower_str == "dd" {
1044 let mut cur2 = current;
1045 while !cur2.is_null() {
1046 let ctype = unsafe { (*cur2).type_ };
1047 if ctype == XML_ELEMENT_NODE as c_int && !unsafe { (*cur2).name.is_null() } {
1048 let name_bytes = unsafe { xmlstr_to_bytes((*cur2).name) };
1049 let name_str = core::str::from_utf8(name_bytes).unwrap_or("");
1050 if name_str == "dt" || name_str == "dd" {
1051 current = unsafe { (*cur2).parent };
1052 break;
1053 }
1054 }
1055 cur2 = unsafe { (*cur2).parent };
1056 }
1057 }
1058
1059 if tag_lower_str == "tr" {
1067 let mut cur2 = current;
1068 while !cur2.is_null() {
1069 let ctype = unsafe { (*cur2).type_ };
1070 if ctype == XML_ELEMENT_NODE as c_int && !unsafe { (*cur2).name.is_null() } {
1071 let name_bytes = unsafe { xmlstr_to_bytes((*cur2).name) };
1072 let name_str = core::str::from_utf8(name_bytes).unwrap_or("");
1073 if name_str == "tr" {
1074 current = unsafe { (*cur2).parent };
1075 break;
1076 }
1077 }
1078 cur2 = unsafe { (*cur2).parent };
1079 }
1080 } else if tag_lower_str == "td" || tag_lower_str == "th" {
1081 let mut cur2 = current;
1082 while !cur2.is_null() {
1083 let ctype = unsafe { (*cur2).type_ };
1084 if ctype == XML_ELEMENT_NODE as c_int && !unsafe { (*cur2).name.is_null() } {
1085 let name_bytes = unsafe { xmlstr_to_bytes((*cur2).name) };
1086 let name_str = core::str::from_utf8(name_bytes).unwrap_or("");
1087 if name_str == "td" || name_str == "th" {
1088 current = unsafe { (*cur2).parent };
1089 break;
1090 }
1091 }
1092 cur2 = unsafe { (*cur2).parent };
1093 }
1094 }
1095
1096 if matches!(tag_lower_str, "thead" | "tbody" | "tfoot") {
1098 let mut cur2 = current;
1099 while !cur2.is_null() {
1100 let ctype = unsafe { (*cur2).type_ };
1101 if ctype == XML_ELEMENT_NODE as c_int && !unsafe { (*cur2).name.is_null() } {
1102 let name_bytes = unsafe { xmlstr_to_bytes((*cur2).name) };
1103 let name_str = core::str::from_utf8(name_bytes).unwrap_or("");
1104 if name_str == "thead" || name_str == "tbody" || name_str == "tfoot" {
1105 current = unsafe { (*cur2).parent };
1106 break;
1107 }
1108 }
1109 cur2 = unsafe { (*cur2).parent };
1110 }
1111 }
1112
1113 if tag_lower_str == "colgroup" {
1115 let mut cur2 = current;
1116 while !cur2.is_null() {
1117 let ctype = unsafe { (*cur2).type_ };
1118 if ctype == XML_ELEMENT_NODE as c_int && !unsafe { (*cur2).name.is_null() } {
1119 let name_bytes = unsafe { xmlstr_to_bytes((*cur2).name) };
1120 let name_str = core::str::from_utf8(name_bytes).unwrap_or("");
1121 if name_str == "colgroup" {
1122 current = unsafe { (*cur2).parent };
1123 break;
1124 }
1125 }
1126 cur2 = unsafe { (*cur2).parent };
1127 }
1128 }
1129
1130 if tag_lower_str == "caption" {
1132 let mut cur2 = current;
1133 while !cur2.is_null() {
1134 let ctype = unsafe { (*cur2).type_ };
1135 if ctype == XML_ELEMENT_NODE as c_int && !unsafe { (*cur2).name.is_null() } {
1136 let name_bytes = unsafe { xmlstr_to_bytes((*cur2).name) };
1137 let name_str = core::str::from_utf8(name_bytes).unwrap_or("");
1138 if name_str == "caption" {
1139 current = unsafe { (*cur2).parent };
1140 break;
1141 }
1142 }
1143 cur2 = unsafe { (*cur2).parent };
1144 }
1145 }
1146
1147 if tag_lower_str == "form" {
1149 let mut cur2 = current;
1150 while !cur2.is_null() {
1151 let ctype = unsafe { (*cur2).type_ };
1152 if ctype == XML_ELEMENT_NODE as c_int && !unsafe { (*cur2).name.is_null() } {
1153 let name_bytes = unsafe { xmlstr_to_bytes((*cur2).name) };
1154 let name_str = core::str::from_utf8(name_bytes).unwrap_or("");
1155 if name_str.eq_ignore_ascii_case("form") {
1156 current = unsafe { (*cur2).parent };
1157 break;
1158 }
1159 }
1160 cur2 = unsafe { (*cur2).parent };
1161 }
1162 }
1163
1164 ctxt.current = current;
1165}
1166
1167unsafe fn ensure_html(ctxt: &mut HtmlParserCtxt) -> *mut _xmlNode {
1173 if ctxt.options & HTML_PARSE_NOIMPLIED != 0 {
1176 return ptr::null_mut();
1177 }
1178 if !ctxt.html.is_null() {
1179 return ctxt.html;
1180 }
1181
1182 let html_node = tree::new_node(ptr::null_mut(), b"html\0" as *const u8 as *const xmlChar);
1183 if html_node.is_null() {
1184 return ptr::null_mut();
1185 }
1186 {
1187 }
1190 ctxt.html = html_node;
1191 ctxt.html_created = true;
1192
1193 tree::add_child(ctxt.doc as *mut _xmlNode, html_node);
1195 ctxt.current = html_node;
1196
1197 html_node
1198}
1199
1200unsafe fn ensure_head(ctxt: &mut HtmlParserCtxt) -> *mut _xmlNode {
1202 if ctxt.options & HTML_PARSE_NOIMPLIED != 0 {
1203 return ptr::null_mut();
1204 }
1205 if !ctxt.head.is_null() {
1206 return ctxt.head;
1207 }
1208
1209 ensure_html(ctxt);
1211
1212 let head_node = tree::new_node(ptr::null_mut(), b"head\0" as *const u8 as *const xmlChar);
1213 if head_node.is_null() {
1214 return ptr::null_mut();
1215 }
1216 ctxt.head = head_node;
1217 ctxt.head_created = true;
1218
1219 tree::add_child(ctxt.html, head_node);
1221 ctxt.current = head_node;
1222 ctxt.in_head = true;
1223
1224 head_node
1225}
1226
1227unsafe fn ensure_body(ctxt: &mut HtmlParserCtxt) -> *mut _xmlNode {
1229 if ctxt.options & HTML_PARSE_NOIMPLIED != 0 {
1230 return ptr::null_mut();
1231 }
1232 if !ctxt.body.is_null() {
1233 return ctxt.body;
1234 }
1235
1236 ensure_html(ctxt);
1238
1239 let body_node = tree::new_node(ptr::null_mut(), b"body\0" as *const u8 as *const xmlChar);
1240 if body_node.is_null() {
1241 return ptr::null_mut();
1242 }
1243 ctxt.body = body_node;
1244 ctxt.body_created = true;
1245
1246 tree::add_child(ctxt.html, body_node);
1248 ctxt.current = body_node;
1249 ctxt.in_body = true;
1250
1251 body_node
1252}
1253
1254#[allow(dead_code)]
1256unsafe fn transition_to_body(ctxt: &mut HtmlParserCtxt) {
1257 if ctxt.in_head && !ctxt.seen_body_content {
1258 ctxt.seen_body_content = true;
1259 ctxt.in_head = false;
1260 ensure_body(ctxt);
1261 }
1262}
1263
1264struct HtmlAttr {
1270 name: Vec<u8>,
1271 value: Vec<u8>,
1272 #[allow(dead_code)]
1273 quoted: bool,
1274}
1275
1276fn parse_attr_name(ctxt: &mut HtmlParserCtxt) -> Vec<u8> {
1278 let mut name = Vec::new();
1279 while let Some(ch) = ctxt.peek() {
1280 if ch == b'='
1281 || ch == b'>'
1282 || ch == b'/'
1283 || ch == b' '
1284 || ch == b'\t'
1285 || ch == b'\n'
1286 || ch == b'\r'
1287 {
1288 break;
1289 }
1290 name.push(ch);
1291 ctxt.next();
1292 }
1293 name
1294}
1295
1296fn parse_attr_value(ctxt: &mut HtmlParserCtxt) -> (Vec<u8>, bool) {
1298 ctxt.skip_whitespace();
1299
1300 let quote = match ctxt.peek() {
1301 Some(b'"') => {
1302 ctxt.next(); b'"'
1304 }
1305 Some(b'\'') => {
1306 ctxt.next(); b'\''
1308 }
1309 _ => {
1310 let value = ctxt.read_while(|ch| {
1312 ch != b'>' && ch != b' ' && ch != b'\t' && ch != b'\n' && ch != b'\r'
1313 });
1314 return (value, false);
1315 }
1316 };
1317
1318 let mut value = Vec::new();
1320 loop {
1321 match ctxt.next() {
1322 Some(ch) if ch == quote => break,
1323 Some(ch) => value.push(ch),
1324 None => break,
1325 }
1326 }
1327 (value, true)
1328}
1329
1330fn parse_attributes(ctxt: &mut HtmlParserCtxt) -> Vec<HtmlAttr> {
1332 let mut attrs = Vec::new();
1333
1334 loop {
1335 ctxt.skip_whitespace();
1336
1337 match ctxt.peek() {
1338 Some(b'>') | None => break,
1339 Some(b'/')
1340 if ctxt.peek_at(1) == Some(b'>') => {
1342 break;
1343 }
1344 _ => {}
1346 }
1347
1348 let name = parse_attr_name(ctxt);
1349 if name.is_empty() {
1350 break;
1351 }
1352
1353 ctxt.skip_whitespace();
1355 if ctxt.peek() == Some(b'=') {
1356 ctxt.next(); let (value, quoted) = parse_attr_value(ctxt);
1358 attrs.push(HtmlAttr {
1359 name,
1360 value,
1361 quoted,
1362 });
1363 } else {
1364 attrs.push(HtmlAttr {
1366 name,
1367 value: Vec::new(),
1368 quoted: false,
1369 });
1370 }
1371 }
1372
1373 attrs
1374}
1375
1376fn resolve_entity(name: &str) -> Vec<u8> {
1382 if let Some(replacement) = html_entity_lookup(name) {
1383 replacement.as_bytes().to_vec()
1384 } else {
1385 let mut result = Vec::new();
1387 result.push(b'&');
1388 result.extend_from_slice(name.as_bytes());
1389 result.push(b';');
1390 result
1391 }
1392}
1393
1394fn resolve_numeric_entity(value: &str, is_hex: bool) -> Vec<u8> {
1396 let codepoint = if is_hex {
1397 u32::from_str_radix(value, 16).unwrap_or(0xFFFD)
1398 } else {
1399 value.parse::<u32>().unwrap_or(0xFFFD)
1400 };
1401
1402 if codepoint == 0 {
1403 return Vec::new();
1404 }
1405
1406 match char::from_u32(codepoint) {
1408 Some(c) => {
1409 let mut buf = [0u8; 4];
1410 let s = c.encode_utf8(&mut buf);
1411 s.as_bytes().to_vec()
1412 }
1413 None => vec![0xEF, 0xBF, 0xBD], }
1415}
1416
1417fn parse_entity(ctxt: &mut HtmlParserCtxt) -> Vec<u8> {
1420 match ctxt.peek() {
1422 Some(b'&') => {
1423 ctxt.next(); }
1425 _ => return vec![b'&'],
1426 }
1427
1428 if ctxt.peek() == Some(b'#') {
1430 ctxt.next(); let is_hex = ctxt.peek() == Some(b'x') || ctxt.peek() == Some(b'X');
1432 if is_hex {
1433 ctxt.next(); }
1435
1436 let digits = ctxt.read_while(|ch| {
1437 if is_hex {
1438 ch.is_ascii_hexdigit()
1439 } else {
1440 ch.is_ascii_digit()
1441 }
1442 });
1443
1444 let digits_str = core::str::from_utf8(&digits).unwrap_or("");
1445 if digits_str.is_empty() {
1446 let mut result = vec![b'&', b'#'];
1447 if is_hex {
1448 result.push(b'x');
1449 }
1450 return result;
1451 }
1452
1453 if ctxt.peek() == Some(b';') {
1455 ctxt.next();
1456 }
1457
1458 return resolve_numeric_entity(digits_str, is_hex);
1459 }
1460
1461 let name = ctxt.read_while(|ch| ch.is_ascii_alphanumeric() || ch == b'_' || ch == b'-');
1463 let name_str = core::str::from_utf8(&name).unwrap_or("");
1464
1465 if ctxt.peek() == Some(b';') {
1467 ctxt.next();
1468 }
1469
1470 resolve_entity(name_str)
1471}
1472
1473unsafe fn handle_text(ctxt: &mut HtmlParserCtxt, text: &[u8]) {
1485 if text.is_empty() {
1486 return;
1487 }
1488
1489 let parent = if ctxt.in_head {
1491 ctxt.head
1492 } else if ctxt.in_body || ctxt.body_created {
1493 ctxt.body
1494 } else if ctxt.html_created {
1495 ctxt.html
1496 } else {
1497 ctxt.doc as *mut _xmlNode
1498 };
1499
1500 let insertion_point = if ctxt.current.is_null() {
1501 parent
1502 } else {
1503 ctxt.current
1504 };
1505
1506 if insertion_point.is_null() {
1507 let text_node = tree::new_text(ptr::null_mut());
1509 if !text_node.is_null() {
1510 let content = bytes_to_xmlstr(text);
1512 if !content.is_null() {
1513 unsafe {
1514 (*text_node).content = content;
1515 }
1516 }
1517 tree::add_child(ctxt.doc as *mut _xmlNode, text_node);
1518 }
1519 return;
1520 }
1521
1522 if insertion_point == ctxt.doc as *mut _xmlNode {
1527 if text.iter().all(|b| b.is_ascii_whitespace()) {
1528 return;
1529 }
1530 let content = trim_ascii_start(text);
1531 let html_node = tree::new_node(ptr::null_mut(), bytes_to_xmlstr(b"html"));
1532 if !html_node.is_null() {
1533 let content_c = bytes_to_xmlstr(content);
1534 let text_node = tree::new_text(ptr::null_mut());
1535 if !text_node.is_null() {
1536 unsafe {
1537 (*text_node).content = content_c;
1538 }
1539 tree::add_child(html_node, text_node);
1540 }
1541 tree::add_child(ctxt.doc as *mut _xmlNode, html_node);
1542 ctxt.html = html_node;
1543 ctxt.html_created = true;
1544 ctxt.current = html_node;
1545 }
1546 return;
1547 }
1548
1549 let text_node = tree::new_text(ptr::null_mut());
1550 if text_node.is_null() {
1551 return;
1552 }
1553
1554 let content = bytes_to_xmlstr(text);
1556 if !content.is_null() {
1557 unsafe {
1558 (*text_node).content = content;
1559 }
1560 }
1561
1562 tree::add_child(insertion_point, text_node);
1563}
1564
1565unsafe fn attach_attrs(node: *mut _xmlNode, attrs: &[HtmlAttr]) {
1572 for attr in attrs {
1573 let name_c = bytes_to_xmlstr(&attr.name);
1574 let val_c = bytes_to_xmlstr(&attr.value);
1575 if !name_c.is_null() {
1576 tree::set_prop(node, name_c, val_c);
1577 xmlFreeImpl(name_c as *mut c_void);
1578 if !val_c.is_null() {
1579 xmlFreeImpl(val_c as *mut c_void);
1580 }
1581 }
1582 }
1583}
1584
1585unsafe fn handle_start_tag(ctxt: &mut HtmlParserCtxt, tag_name: &[u8], attrs: &[HtmlAttr]) {
1587 let tag_lower: Vec<u8> = tag_name.iter().map(|b| b.to_ascii_lowercase()).collect();
1588 let tag_str = core::str::from_utf8(&tag_lower).unwrap_or("");
1589
1590 let info = html_tag_lookup(tag_str);
1591
1592 let is_head_tag = info.is_some_and(|i| i.flags & HTML_HEAD != 0);
1594 let _is_body_tag = info.is_some_and(|i| i.flags & HTML_BODY != 0);
1595 let is_empty = info.is_some_and(|i| i.flags & HTML_EMPTY != 0);
1596 let _is_block = info.is_some_and(|i| i.flags & HTML_BLOCK != 0);
1597
1598 if tag_str == "html" {
1600 if !ctxt.html.is_null() && !ctxt.html_created {
1601 return;
1603 }
1604 if ctxt.html.is_null() {
1606 let html_node = tree::new_node(ptr::null_mut(), bytes_to_xmlstr(tag_name));
1607 if !html_node.is_null() {
1608 attach_attrs(html_node, attrs);
1609 ctxt.html = html_node;
1610 ctxt.html_created = false; tree::add_child(ctxt.doc as *mut _xmlNode, html_node);
1612 ctxt.current = html_node;
1613 }
1614 } else {
1615 ctxt.current = ctxt.html;
1617 }
1618 return;
1619 }
1620
1621 if tag_str == "head" {
1622 if !ctxt.head.is_null() && !ctxt.head_created {
1623 return;
1625 }
1626 ensure_html(ctxt);
1628
1629 if ctxt.head.is_null() {
1630 let head_node = tree::new_node(ptr::null_mut(), bytes_to_xmlstr(tag_name));
1631 if !head_node.is_null() {
1632 attach_attrs(head_node, attrs);
1633 ctxt.head = head_node;
1634 ctxt.head_created = false;
1635 let parent = if ctxt.html.is_null() {
1638 ctxt.doc as *mut _xmlNode
1639 } else {
1640 ctxt.html
1641 };
1642 tree::add_child(parent, head_node);
1643 ctxt.current = head_node;
1644 ctxt.in_head = true;
1645 }
1646 } else {
1647 ctxt.current = ctxt.head;
1648 ctxt.in_head = true;
1649 }
1650 return;
1651 }
1652
1653 if tag_str == "body" {
1654 if !ctxt.body.is_null() && !ctxt.body_created {
1655 return;
1657 }
1658 ensure_html(ctxt);
1660
1661 if ctxt.body.is_null() {
1662 let body_node = tree::new_node(ptr::null_mut(), bytes_to_xmlstr(tag_name));
1663 if !body_node.is_null() {
1664 attach_attrs(body_node, attrs);
1665 ctxt.body = body_node;
1666 ctxt.body_created = false;
1667 let parent = if ctxt.html.is_null() {
1670 ctxt.doc as *mut _xmlNode
1671 } else {
1672 ctxt.html
1673 };
1674 tree::add_child(parent, body_node);
1675 ctxt.current = body_node;
1676 ctxt.in_body = true;
1677 ctxt.in_head = false;
1678 ctxt.seen_body_content = true;
1679 }
1680 } else {
1681 ctxt.current = ctxt.body;
1682 ctxt.in_body = true;
1683 ctxt.in_head = false;
1684 ctxt.seen_body_content = true;
1685 }
1686 return;
1687 }
1688
1689 if is_head_tag && !ctxt.seen_body_content {
1691 if ctxt.head.is_null() {
1692 ensure_head(ctxt);
1693 }
1694
1695 if is_empty {
1696 let node = tree::new_node(ptr::null_mut(), bytes_to_xmlstr(tag_name));
1698 if !node.is_null() {
1699 attach_attrs(node, attrs);
1700 let ip = if ctxt.current.is_null() {
1703 ctxt.doc as *mut _xmlNode
1704 } else {
1705 ctxt.current
1706 };
1707 tree::add_child(ip, node);
1708 }
1709 return;
1710 }
1711
1712 let node = tree::new_node(ptr::null_mut(), bytes_to_xmlstr(tag_name));
1713 if !node.is_null() {
1714 attach_attrs(node, attrs);
1715 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 ctxt.current = node;
1722 }
1723 return;
1724 }
1725
1726 if !is_head_tag || ctxt.seen_body_content {
1728 if !ctxt.seen_body_content {
1729 ctxt.seen_body_content = true;
1730 ctxt.in_head = false;
1731 if ctxt.options & HTML_PARSE_NOIMPLIED != 0 {
1735 } else if ctxt.body.is_null() {
1737 ensure_body(ctxt);
1738 } else {
1739 ctxt.current = ctxt.body;
1740 ctxt.in_body = true;
1741 }
1742 } else if ctxt.body.is_null() && ctxt.options & HTML_PARSE_NOIMPLIED == 0 {
1743 ensure_body(ctxt);
1744 }
1745 }
1746
1747 if !ctxt.current.is_null() {
1749 auto_close_element(ctxt, tag_str);
1750 }
1751
1752 if is_empty {
1753 let node = tree::new_node(ptr::null_mut(), bytes_to_xmlstr(tag_name));
1755 if !node.is_null() {
1756 for attr in attrs {
1757 let name_c = bytes_to_xmlstr(&attr.name);
1758 let val_c = bytes_to_xmlstr(&attr.value);
1759 if !name_c.is_null() {
1760 tree::set_prop(node, name_c, val_c);
1761 xmlFreeImpl(name_c as *mut c_void);
1762 if !val_c.is_null() {
1763 xmlFreeImpl(val_c as *mut c_void);
1764 }
1765 }
1766 }
1767 let insertion_point = if ctxt.current.is_null() {
1768 if ctxt.options & HTML_PARSE_NOIMPLIED != 0 {
1769 ctxt.doc as *mut _xmlNode
1770 } else {
1771 ctxt.body
1772 }
1773 } else {
1774 ctxt.current
1775 };
1776 if !insertion_point.is_null() {
1777 tree::add_child(insertion_point, node);
1778 }
1779 }
1780 return;
1781 }
1782
1783 let node = tree::new_node(ptr::null_mut(), bytes_to_xmlstr(tag_name));
1785 if !node.is_null() {
1786 for attr in attrs {
1787 let name_c = bytes_to_xmlstr(&attr.name);
1788 let val_c = bytes_to_xmlstr(&attr.value);
1789 if !name_c.is_null() {
1790 tree::set_prop(node, name_c, val_c);
1791 xmlFreeImpl(name_c as *mut c_void);
1792 if !val_c.is_null() {
1793 xmlFreeImpl(val_c as *mut c_void);
1794 }
1795 }
1796 }
1797
1798 let insertion_point = if ctxt.current.is_null() {
1799 if ctxt.in_body || ctxt.body_created {
1800 ctxt.body
1801 } else if ctxt.in_head || ctxt.head_created {
1802 ctxt.head
1803 } else if ctxt.html_created {
1804 ctxt.html
1805 } else {
1806 ctxt.doc as *mut _xmlNode
1807 }
1808 } else {
1809 ctxt.current
1810 };
1811
1812 if !insertion_point.is_null() {
1813 tree::add_child(insertion_point, node);
1814 ctxt.current = node;
1816 }
1817 }
1818}
1819
1820unsafe fn handle_end_tag(ctxt: &mut HtmlParserCtxt, tag_name: &[u8]) {
1831 let tag_lower: Vec<u8> = tag_name.iter().map(|b| b.to_ascii_lowercase()).collect();
1832 let tag_str = core::str::from_utf8(&tag_lower).unwrap_or("");
1833
1834 let info = html_tag_lookup(tag_str);
1835
1836 if info.is_some_and(|i| i.flags & HTML_EMPTY != 0) {
1839 return;
1840 }
1841
1842 if tag_str == "html" {
1843 ctxt.current = ctxt.doc as *mut _xmlNode;
1844 return;
1845 }
1846
1847 if tag_str == "head" {
1848 ctxt.in_head = false;
1849 ctxt.current = ctxt.html;
1850 return;
1851 }
1852
1853 if tag_str == "body" {
1854 ctxt.in_body = false;
1855 ctxt.current = ctxt.html;
1856 return;
1857 }
1858
1859 let mut cur = ctxt.current;
1861 while !cur.is_null() {
1862 let ctype = unsafe { (*cur).type_ };
1863 if ctype == XML_ELEMENT_NODE as c_int && !unsafe { (*cur).name.is_null() } {
1864 let name_bytes = unsafe { xmlstr_to_bytes((*cur).name) };
1865 if name_bytes.eq_ignore_ascii_case(tag_name) {
1866 ctxt.current = unsafe { (*cur).parent };
1868 return;
1869 }
1870 }
1871 cur = unsafe { (*cur).parent };
1872 }
1873
1874 }
1876
1877fn convert_input_to_utf8(encoding: *const c_char, data: &[u8]) -> Option<Vec<u8>> {
1890 if encoding.is_null() {
1891 return None;
1892 }
1893 let name = unsafe { core::ffi::CStr::from_ptr(encoding).to_bytes() };
1894 match crate::xml::encoding::encoding_from_name(name) {
1895 xmlCharEncoding::XML_CHAR_ENCODING_8859_1 => {
1896 Some(crate::xml::encoding::latin1_to_utf8(data))
1897 }
1898 xmlCharEncoding::XML_CHAR_ENCODING_UTF16LE => {
1899 crate::xml::encoding::utf16le_to_utf8(data).ok()
1900 }
1901 xmlCharEncoding::XML_CHAR_ENCODING_UTF16BE => {
1902 crate::xml::encoding::utf16be_to_utf8(data).ok()
1903 }
1904 xmlCharEncoding::XML_CHAR_ENCODING_UCS4LE => {
1909 crate::xml::encoding::ucs4le_to_utf8(data).ok()
1910 }
1911 xmlCharEncoding::XML_CHAR_ENCODING_UCS4BE => {
1912 crate::xml::encoding::ucs4be_to_utf8(data).ok()
1913 }
1914 _ => None,
1916 }
1917}
1918
1919fn parse_html_doctype_decl(input: &[u8]) -> Option<(Vec<u8>, Option<Vec<u8>>, Option<Vec<u8>>)> {
1926 let mut i = 0usize;
1927 while i < input.len() && input[i].is_ascii_whitespace() {
1928 i += 1;
1929 }
1930 if i + 2 >= input.len() || input[i] != b'<' || !(input[i + 1] == b'!') {
1931 return None;
1932 }
1933 let kw = b"DOCTYPE";
1934 if !input.get(i + 2..i + 2 + kw.len()).is_some_and(|s| {
1935 s.iter()
1936 .enumerate()
1937 .all(|(k, b)| b.to_ascii_uppercase() == kw[k])
1938 }) {
1939 return None;
1940 }
1941 i += 2 + kw.len();
1942 while i < input.len() && input[i].is_ascii_whitespace() {
1944 i += 1;
1945 }
1946 let name_start = i;
1947 while i < input.len() && !input[i].is_ascii_whitespace() && input[i] != b'>' {
1948 i += 1;
1949 }
1950 let name = if name_start == i {
1955 Vec::new()
1956 } else {
1957 input[name_start..i].to_vec()
1958 };
1959 if name.is_empty() {
1961 return Some((name, None, None));
1962 }
1963 let mut ext: Option<Vec<u8>> = None;
1965 let mut sys: Option<Vec<u8>> = None;
1966 while i < input.len() && input[i].is_ascii_whitespace() {
1967 i += 1;
1968 }
1969 if i < input.len() && input[i] != b'>' {
1970 let word_start = i;
1972 while i < input.len() && input[i].is_ascii_alphabetic() {
1973 i += 1;
1974 }
1975 let word = input[word_start..i].to_ascii_uppercase();
1976 if word == b"PUBLIC" {
1977 while i < input.len() && input[i].is_ascii_whitespace() {
1978 i += 1;
1979 }
1980 if i < input.len() && (input[i] == b'"' || input[i] == b'\'') {
1982 let q = input[i];
1983 i += 1;
1984 let v_start = i;
1985 while i < input.len() && input[i] != q {
1986 i += 1;
1987 }
1988 ext = Some(input[v_start..i].to_vec());
1989 if i < input.len() {
1990 i += 1;
1991 }
1992 }
1993 while i < input.len() && input[i].is_ascii_whitespace() {
1994 i += 1;
1995 }
1996 if i < input.len()
1998 && i + 1 < input.len()
1999 && (input[i] == b'"' || input[i] == b'\'')
2000 && input[i] != b'>'
2001 {
2002 let q = input[i];
2003 i += 1;
2004 let v_start = i;
2005 while i < input.len() && input[i] != q {
2006 i += 1;
2007 }
2008 sys = Some(input[v_start..i].to_vec());
2009 }
2010 } else if word == b"SYSTEM" {
2011 while i < input.len() && input[i].is_ascii_whitespace() {
2012 i += 1;
2013 }
2014 if i < input.len() && (input[i] == b'"' || input[i] == b'\'') {
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 }
2024 }
2025 Some((name, ext, sys))
2026}
2027
2028unsafe fn html_parse_buffer(
2034 ctxt: &mut HtmlParserCtxt,
2035 buffer: *const c_char,
2036 size: c_int,
2037) -> *mut _xmlDoc {
2038 if buffer.is_null() || size <= 0 {
2039 return ptr::null_mut();
2040 }
2041
2042 let doc = tree::new_doc(ptr::null());
2044 if doc.is_null() {
2045 return ptr::null_mut();
2046 }
2047 unsafe {
2048 (*doc).type_ = XML_HTML_DOCUMENT_NODE as c_int;
2049 if !(*doc).version.is_null() {
2052 crate::abi::allocator::xmlFreeImpl((*doc).version as *mut c_void);
2053 }
2054 (*doc).version = ptr::null_mut();
2055 (*doc).properties = crate::abi::types::xmlDocProperties::XML_DOC_HTML as c_int;
2062 (*doc).standalone = 1;
2065 }
2066 let raw_input = unsafe { slice::from_raw_parts(buffer as *const u8, size as usize) };
2073 if let Some((name, ext, sys)) = parse_html_doctype_decl(raw_input) {
2074 let name_cstr = if name.is_empty() {
2078 ptr::null()
2079 } else {
2080 crate::xml::string::bytes_to_xmlstr(&name)
2081 };
2082 let ext_cstr = ext
2083 .map(|s| crate::xml::string::bytes_to_xmlstr(&s))
2084 .unwrap_or(ptr::null_mut());
2085 let sys_cstr = sys
2086 .map(|s| crate::xml::string::bytes_to_xmlstr(&s))
2087 .unwrap_or(ptr::null_mut());
2088 unsafe {
2089 crate::xml::dtd::create_int_subset(
2090 doc,
2091 name_cstr as *const xmlChar,
2092 ext_cstr as *const xmlChar,
2093 sys_cstr as *const xmlChar,
2094 );
2095 }
2096 if !name_cstr.is_null() {
2097 unsafe { crate::abi::allocator::xmlFreeImpl(name_cstr as *mut c_void) };
2098 }
2099 if !ext_cstr.is_null() {
2100 unsafe { crate::abi::allocator::xmlFreeImpl(ext_cstr as *mut c_void) };
2101 }
2102 if !sys_cstr.is_null() {
2103 unsafe { crate::abi::allocator::xmlFreeImpl(sys_cstr as *mut c_void) };
2104 }
2105 } else {
2106 if ctxt.options & HTML_PARSE_NODEFDTD == 0 {
2109 unsafe {
2110 crate::xml::dtd::create_int_subset(
2111 doc,
2112 b"html\0" as *const u8 as *const xmlChar,
2113 b"-//W3C//DTD HTML 4.0 Transitional//EN\0" as *const u8 as *const xmlChar,
2114 b"http://www.w3.org/TR/REC-html40/loose.dtd\0" as *const u8 as *const xmlChar,
2115 );
2116 }
2117 }
2118 }
2119 ctxt.doc = doc;
2120
2121 let converted: Option<Vec<u8>> = if !ctxt.encoding.is_null() {
2126 let raw = unsafe { slice::from_raw_parts(buffer as *const u8, size as usize) };
2127 convert_input_to_utf8(ctxt.encoding, raw)
2128 } else {
2129 None
2130 };
2131 let (input_ptr, input_len): (*const u8, usize) = match &converted {
2132 Some(v) => (v.as_ptr(), v.len()),
2133 None => (buffer as *const u8, size as usize),
2134 };
2135
2136 ctxt.input = input_ptr as *mut u8;
2138 ctxt.input_len = input_len;
2139 ctxt.input_pos = 0;
2140 ctxt.line = 1;
2141
2142 loop {
2144 if ctxt.is_eof() {
2145 break;
2146 }
2147
2148 let ch = ctxt.peek().unwrap_or(0);
2149
2150 if ch == b'<' {
2151 ctxt.next(); if ctxt.peek() == Some(b'/') {
2155 ctxt.next(); let tag_name = ctxt.read_while(|ch| {
2157 ch != b'>' && ch != b' ' && ch != b'\t' && ch != b'\n' && ch != b'\r'
2158 });
2159
2160 while ctxt.peek() != Some(b'>') && !ctxt.is_eof() {
2162 ctxt.next();
2163 }
2164 if ctxt.peek() == Some(b'>') {
2165 ctxt.next(); }
2167
2168 if !tag_name.is_empty() {
2169 handle_end_tag(ctxt, &tag_name);
2170 }
2171 continue;
2172 }
2173
2174 if ctxt.peek() == Some(b'!')
2176 && ctxt.peek_at(1) == Some(b'-')
2177 && ctxt.peek_at(2) == Some(b'-')
2178 {
2179 ctxt.next(); ctxt.next(); ctxt.next(); let mut comment_content = Vec::new();
2185 loop {
2186 if ctxt.peek() == Some(b'-')
2187 && ctxt.peek_at(1) == Some(b'-')
2188 && ctxt.peek_at(2) == Some(b'>')
2189 {
2190 ctxt.next(); ctxt.next(); ctxt.next(); break;
2194 }
2195 match ctxt.next() {
2196 Some(ch) => comment_content.push(ch),
2197 None => break,
2198 }
2199 }
2200
2201 if !comment_content.is_empty() {
2203 let comment_node = tree::new_comment(bytes_to_xmlstr(&comment_content));
2204 if !comment_node.is_null() {
2205 let insertion_point = if !ctxt.current.is_null() {
2206 ctxt.current
2207 } else {
2208 ctxt.doc as *mut _xmlNode
2209 };
2210 tree::add_child(insertion_point, comment_node);
2211 }
2212 }
2213 continue;
2214 }
2215
2216 if ctxt.peek() == Some(b'!') {
2218 ctxt.next(); let _rest = ctxt.read_while(|ch| ch != b'>');
2220 if ctxt.peek() == Some(b'>') {
2221 ctxt.next(); }
2223 continue;
2226 }
2227
2228 if ctxt.peek() == Some(b'?') {
2230 ctxt.next(); let mut pi_content = Vec::new();
2233 loop {
2234 if ctxt.peek() == Some(b'?') && ctxt.peek_at(1) == Some(b'>') {
2235 break;
2236 }
2237 match ctxt.next() {
2238 Some(ch) => pi_content.push(ch),
2239 None => break,
2240 }
2241 }
2242 if ctxt.peek() == Some(b'?') {
2244 ctxt.next();
2245 }
2246 if ctxt.peek() == Some(b'>') {
2247 ctxt.next();
2248 }
2249 if !pi_content.is_empty() {
2251 let mut parts = pi_content.splitn(2, |b| *b == b' ');
2253 let target = parts.next().unwrap_or(&pi_content);
2254 let value = parts.next().unwrap_or(b"");
2255
2256 let pi_node = tree::new_pi(bytes_to_xmlstr(target), bytes_to_xmlstr(value));
2257 if !pi_node.is_null() {
2258 let insertion_point = if !ctxt.current.is_null() {
2259 ctxt.current
2260 } else {
2261 ctxt.doc as *mut _xmlNode
2262 };
2263 tree::add_child(insertion_point, pi_node);
2264 }
2265 }
2266 continue;
2267 }
2268
2269 let tag_name = ctxt.read_while(|ch| {
2271 ch != b'>' && ch != b'/' && ch != b' ' && ch != b'\t' && ch != b'\n' && ch != b'\r'
2272 });
2273
2274 if tag_name.is_empty() {
2275 handle_text(ctxt, b"<");
2277 continue;
2278 }
2279
2280 let attrs = parse_attributes(ctxt);
2282
2283 if ctxt.peek() == Some(b'/') {
2285 ctxt.next(); if ctxt.peek() == Some(b'>') {
2287 ctxt.next(); }
2289 } else if ctxt.peek() == Some(b'>') {
2290 ctxt.next(); }
2292
2293 let tag_lower: Vec<u8> = tag_name.iter().map(|b| b.to_ascii_lowercase()).collect();
2295 let tag_str = core::str::from_utf8(&tag_lower).unwrap_or("");
2296
2297 if tag_str == "script" || tag_str == "style" {
2298 let raw_node = tree::new_node(ptr::null_mut(), bytes_to_xmlstr(&tag_name));
2301 if !raw_node.is_null() {
2302 for attr in &attrs {
2303 let name_c = bytes_to_xmlstr(&attr.name);
2304 let val_c = bytes_to_xmlstr(&attr.value);
2305 if !name_c.is_null() {
2306 tree::set_prop(raw_node, name_c, val_c);
2307 xmlFreeImpl(name_c as *mut c_void);
2308 if !val_c.is_null() {
2309 xmlFreeImpl(val_c as *mut c_void);
2310 }
2311 }
2312 }
2313
2314 let insertion_point = if ctxt.current.is_null() {
2315 if ctxt.in_head {
2316 ensure_head(ctxt);
2317 ctxt.head
2318 } else {
2319 ensure_body(ctxt);
2320 ctxt.body
2321 }
2322 } else {
2323 ctxt.current
2324 };
2325
2326 if !insertion_point.is_null() {
2327 tree::add_child(insertion_point, raw_node);
2328
2329 let end_tag = format!("</{}", tag_str);
2331 let end_bytes = end_tag.as_bytes();
2332 let mut raw_text = Vec::new();
2333 let mut match_buf: Vec<u8> = Vec::new();
2340 let mut match_idx = 0;
2341
2342 loop {
2343 if ctxt.is_eof() {
2344 break;
2345 }
2346 let ch = ctxt.peek().unwrap();
2347 if ch.to_ascii_lowercase() == end_bytes[match_idx] {
2348 match_buf.push(ch);
2349 match_idx += 1;
2350 ctxt.next();
2351 if match_idx == end_bytes.len() {
2352 if !raw_text.is_empty() {
2355 let text_node = tree::new_text(bytes_to_xmlstr(&raw_text));
2356 if !text_node.is_null() {
2357 tree::add_child(raw_node, text_node);
2358 }
2359 }
2360 let _suffix = ctxt.read_while(|ch| ch != b'>');
2363 if ctxt.peek() == Some(b'>') {
2364 ctxt.next();
2365 }
2366 ctxt.current = unsafe { (*raw_node).parent };
2368 break;
2369 }
2370 } else {
2371 if match_idx > 0 {
2375 raw_text.extend_from_slice(&match_buf);
2376 match_buf.clear();
2377 match_idx = 0;
2378 }
2379 raw_text.push(ch);
2380 ctxt.next();
2381 }
2382 }
2383
2384 if match_idx < end_bytes.len() {
2387 raw_text.extend_from_slice(&match_buf);
2388 if !raw_text.is_empty() {
2389 let text_node = tree::new_text(bytes_to_xmlstr(&raw_text));
2390 if !text_node.is_null() {
2391 tree::add_child(raw_node, text_node);
2392 }
2393 }
2394 ctxt.current = unsafe { (*raw_node).parent };
2395 }
2396 }
2397 }
2398 continue;
2399 }
2400
2401 handle_start_tag(ctxt, &tag_name, &attrs);
2403 } else {
2404 let mut text = Vec::new();
2406 loop {
2407 match ctxt.peek() {
2408 Some(b'<') => break,
2409 Some(b'&') => {
2410 let entity_text = parse_entity(ctxt);
2412 text.extend_from_slice(&entity_text);
2413 }
2414 Some(0) => {
2415 ctxt.next();
2421 }
2422 Some(ch) => {
2423 text.push(ch);
2424 ctxt.next();
2425 }
2426 None => break,
2427 }
2428 }
2429
2430 if !text.is_empty() {
2431 handle_text(ctxt, &text);
2432 }
2433 }
2434 }
2435
2436 if ctxt.html.is_null() && ctxt.options & HTML_PARSE_NOIMPLIED == 0 {
2440 ensure_html(ctxt);
2441 }
2442
2443 if ctxt.options & (crate::abi::types::XML_PARSE_NOBLANKS as c_int) != 0 && !doc.is_null() {
2449 unsafe {
2450 drop_blank_text_nodes((*doc).children);
2451 }
2452 }
2453
2454 if !doc.is_null() {
2465 unsafe {
2466 register_html_ids(doc, (*doc).children);
2467 }
2468 }
2469
2470 doc
2471}
2472
2473unsafe fn register_html_ids(doc: *mut _xmlDoc, cur: *mut _xmlNode) {
2481 let mut n = cur;
2482 while !n.is_null() {
2483 let t = unsafe { (*n).type_ };
2484 if t == XML_ELEMENT_NODE as c_int {
2485 let el = n;
2486 let mut attr = unsafe { (*el).properties };
2487 while !attr.is_null() {
2488 if unsafe { (*attr).id }.is_null()
2489 && !unsafe { (*attr).children }.is_null()
2490 && unsafe { (*(*attr).children).type_ } == XML_TEXT_NODE as c_int
2491 && unsafe { (*(*attr).children).next }.is_null()
2492 {
2493 let v = unsafe { (*(*attr).children).content };
2494 if !v.is_null() {
2495 let id_res = crate::xml::validation::is_id(doc, el, attr);
2496 if id_res > 0 {
2497 crate::xml::validation::add_id(ptr::null_mut(), doc, v, attr);
2498 } else if crate::xml::validation::is_ref(doc, el, attr) > 0 {
2499 crate::xml::validation::add_ref(ptr::null_mut(), doc, v, attr);
2500 }
2501 }
2502 }
2503 attr = unsafe { (*attr).next };
2504 }
2505 if !unsafe { (*el).children }.is_null() {
2506 register_html_ids(doc, unsafe { (*el).children });
2507 }
2508 }
2509 n = unsafe { (*n).next };
2510 }
2511}
2512
2513unsafe fn drop_blank_text_nodes(cur: *mut _xmlNode) {
2521 let mut n = cur;
2522 while !n.is_null() {
2523 let next = unsafe { (*n).next };
2524 let t = unsafe { (*n).type_ };
2525 if t == XML_TEXT_NODE as c_int {
2526 let content = unsafe { (*n).content };
2527 let blank = if content.is_null() {
2528 true
2529 } else {
2530 let mut p = content;
2531 while unsafe { *p } != 0 {
2532 match unsafe { *p } {
2533 b' ' | b'\t' | b'\r' | b'\n' => {}
2534 _ => break,
2535 }
2536 p = unsafe { p.add(1) };
2537 }
2538 (unsafe { *p }) == 0
2539 };
2540 if blank {
2541 tree::unlink_node(n);
2542 tree::free_node(n);
2543 }
2544 } else if t == XML_ELEMENT_NODE as c_int && !unsafe { (*n).children }.is_null() {
2545 drop_blank_text_nodes(unsafe { (*n).children });
2546 }
2547 n = next;
2548 }
2549}
2550
2551pub unsafe fn parse_file(
2566 filename: *const c_char,
2567 encoding: *const c_char,
2568 options: c_int,
2569) -> *mut _xmlDoc {
2570 if filename.is_null() {
2571 return ptr::null_mut();
2572 }
2573
2574 let filename_str = unsafe { std::ffi::CStr::from_ptr(filename) };
2576 let path = filename_str.to_str().unwrap_or("");
2577 let content = match std::fs::read(path) {
2578 Ok(data) => data,
2579 Err(_) => return ptr::null_mut(),
2580 };
2581
2582 let mut ctxt = HtmlParserCtxt::new();
2583 ctxt.options = options;
2584 if !encoding.is_null() {
2585 let _enc_cstr = unsafe { std::ffi::CStr::from_ptr(encoding) };
2586 ctxt.encoding = unsafe { c_strdup(encoding) };
2587 }
2588
2589 let doc = unsafe {
2590 html_parse_buffer(
2591 &mut ctxt,
2592 content.as_ptr() as *const c_char,
2593 content.len() as c_int,
2594 )
2595 };
2596
2597 if !doc.is_null() && !filename.is_null() {
2598 unsafe {
2599 (*doc).URL = c_strdup(filename) as *mut xmlChar;
2600 }
2601 }
2602
2603 doc
2604}
2605
2606pub unsafe fn parse_memory(buffer: *const c_char, size: c_int) -> *mut _xmlDoc {
2617 unsafe { parse_memory_enc(buffer, size, ptr::null(), 0) }
2618}
2619
2620pub(crate) unsafe fn parse_memory_enc(
2633 buffer: *const c_char,
2634 size: c_int,
2635 encoding: *const c_char,
2636 options: c_int,
2637) -> *mut _xmlDoc {
2638 if buffer.is_null() || size <= 0 {
2639 return ptr::null_mut();
2640 }
2641
2642 let mut ctxt = HtmlParserCtxt::new();
2643 ctxt.options = options;
2644 if !encoding.is_null() {
2645 ctxt.encoding = unsafe { c_strdup(encoding) };
2646 }
2647 unsafe { html_parse_buffer(&mut ctxt, buffer, size) }
2648}
2649
2650pub(crate) unsafe fn parse_doc(
2661 cur: *const xmlChar,
2662 encoding: *const c_char,
2663 options: c_int,
2664) -> *mut _xmlDoc {
2665 if cur.is_null() {
2666 return ptr::null_mut();
2667 }
2668
2669 let len = unsafe { xml_strlen(cur) };
2670 let mut ctxt = HtmlParserCtxt::new();
2671 ctxt.options = options;
2672 if !encoding.is_null() {
2673 ctxt.encoding = unsafe { c_strdup(encoding) };
2674 }
2675
2676 unsafe { html_parse_buffer(&mut ctxt, cur as *const c_char, len as c_int) }
2677}
2678
2679#[allow(dead_code)]
2690pub(crate) unsafe fn create_file_parser_ctxt(
2691 filename: *const c_char,
2692 encoding: *const c_char,
2693) -> *mut c_void {
2694 if filename.is_null() {
2695 return ptr::null_mut();
2696 }
2697
2698 let total = size_of::<_xmlParserCtxt>() + size_of::<HtmlParserCtxt>();
2701 let mem = unsafe { xmlMallocZero(total) } as *mut u8;
2702 if mem.is_null() {
2703 return ptr::null_mut();
2704 }
2705
2706 let ctxt = mem.add(size_of::<_xmlParserCtxt>()) as *mut HtmlParserCtxt;
2707 unsafe {
2708 ptr::write(ctxt, HtmlParserCtxt::new());
2709 if !encoding.is_null() {
2710 (*ctxt).encoding = c_strdup(encoding);
2711 }
2712 (*(mem as *mut _xmlParserCtxt)).html = 1;
2713 }
2714
2715 mem as *mut c_void
2716}
2717
2718pub(crate) unsafe fn free_parser_ctxt(ctxt: *mut c_void) {
2728 if ctxt.is_null() {
2729 return;
2730 }
2731
2732 let state = (ctxt as *mut u8).add(size_of::<_xmlParserCtxt>()) as *mut HtmlParserCtxt;
2736 unsafe {
2737 if !(*state).input.is_null() {
2738 xmlFreeImpl((*state).input as *mut c_void);
2739 }
2740 if !(*state).filename.is_null() {
2741 xmlFreeImpl((*state).filename as *mut c_void);
2742 }
2743 if !(*state).encoding.is_null() {
2744 xmlFreeImpl((*state).encoding as *mut c_void);
2745 }
2746 xmlFreeImpl(ctxt);
2747 }
2748}
2749
2750#[allow(dead_code)]
2756pub(crate) const fn init_parser() {
2757 }
2760
2761#[allow(dead_code)]
2767pub(crate) const fn cleanup_parser() {
2768 }
2771
2772pub(crate) unsafe fn new_doc(version: *const xmlChar) -> *mut _xmlDoc {
2790 let doc = tree::new_doc(version);
2791 if doc.is_null() {
2792 return ptr::null_mut();
2793 }
2794
2795 unsafe {
2796 (*doc).type_ = XML_HTML_DOCUMENT_NODE as c_int;
2797 (*doc).properties = XML_DOC_WELLFORMED as c_int;
2798 }
2799
2800 doc
2801}
2802
2803pub(crate) unsafe fn new_doc_no_dtd(version: *const xmlChar) -> *mut _xmlDoc {
2816 let doc = tree::new_doc(version);
2817 if doc.is_null() {
2818 return ptr::null_mut();
2819 }
2820
2821 unsafe {
2822 (*doc).type_ = XML_HTML_DOCUMENT_NODE as c_int;
2823 (*doc).properties = XML_DOC_WELLFORMED as c_int;
2824 }
2825
2826 doc
2827}
2828
2829const HTML_VOID_ELEMENTS: &[&str] = &[
2835 "area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "param", "source",
2836 "track", "wbr", "frame",
2837];
2838
2839fn is_html_void(name: &str) -> bool {
2841 HTML_VOID_ELEMENTS
2842 .iter()
2843 .any(|v| v.eq_ignore_ascii_case(name))
2844}
2845
2846#[allow(dead_code)]
2848fn has_optional_end_tag(name: &str) -> bool {
2849 matches!(
2850 name.to_ascii_lowercase().as_str(),
2851 "p" | "li" | "tr" | "td" | "th" | "dt" | "dd"
2852 )
2853}
2854
2855unsafe fn html_write_quoted(buf: *mut _xmlBuffer, s: *const xmlChar) {
2861 if buf.is_null() || s.is_null() {
2862 return;
2863 }
2864 io::buf_ccat(buf, b'"');
2865 io::buf_cat(buf, s);
2866 io::buf_ccat(buf, b'"');
2867}
2868
2869fn trim_ascii_start(s: &[u8]) -> &[u8] {
2871 let start = s
2872 .iter()
2873 .position(|&b| !b.is_ascii_whitespace())
2874 .unwrap_or(s.len());
2875 &s[start..]
2876}
2877
2878unsafe fn html_serialize_text(buf: *mut _xmlBuffer, content: *const xmlChar, len: c_int) {
2890 if buf.is_null() || content.is_null() || len <= 0 {
2891 return;
2892 }
2893
2894 let mut i: c_int = 0;
2895 while i < len {
2896 let ch = unsafe { *content.add(i as usize) };
2897
2898 match ch {
2899 b'<' => {
2900 io::buf_add(buf, b"<" as *const u8, 4);
2901 }
2902 b'&' => {
2903 io::buf_add(buf, b"&" as *const u8, 5);
2904 }
2905 b'>' => {
2906 io::buf_add(buf, b">" as *const u8, 4);
2907 }
2908 _ => {
2909 io::buf_add(buf, &ch as *const u8, 1);
2910 }
2911 }
2912 i += 1;
2913 }
2914}
2915
2916unsafe fn html_serialize_attr_value(buf: *mut _xmlBuffer, value: *const xmlChar) {
2927 if buf.is_null() || value.is_null() {
2928 return;
2929 }
2930
2931 let len = unsafe { xml_strlen(value) as c_int };
2932 let mut i: c_int = 0;
2933 while i < len {
2934 let ch = unsafe { *value.add(i as usize) };
2935
2936 match ch {
2937 b'&' => {
2938 io::buf_add(buf, b"&" as *const u8, 5);
2939 }
2940 b'"' => {
2941 io::buf_add(buf, b""" as *const u8, 6);
2942 }
2943 _ => {
2944 io::buf_add(buf, &ch as *const u8, 1);
2945 }
2946 }
2947 i += 1;
2948 }
2949}
2950
2951unsafe fn html_head_has_meta(child: *mut _xmlNode) -> bool {
2969 let mut c = child;
2970 while !c.is_null() {
2971 if (*c).type_ == XML_ELEMENT_NODE as c_int && !(*c).name.is_null() {
2972 let nm = xmlstr_to_bytes((*c).name);
2973 if nm.eq_ignore_ascii_case(b"meta") {
2974 return true;
2975 }
2976 }
2977 c = (*c).next;
2978 }
2979 false
2980}
2981
2982pub(crate) unsafe fn serialize_node(
2993 node: *mut _xmlNode,
2994 buf: *mut _xmlBuffer,
2995 format: c_int,
2996 level: c_int,
2997) {
2998 unsafe { serialize_node_enc(node, buf, format, level, None) }
2999}
3000
3001pub(crate) unsafe fn serialize_node_enc(
3013 node: *mut _xmlNode,
3014 buf: *mut _xmlBuffer,
3015 format: c_int,
3016 level: c_int,
3017 encoding: Option<&[u8]>,
3018) {
3019 if node.is_null() || buf.is_null() {
3020 return;
3021 }
3022
3023 let n = unsafe { &*node };
3024
3025 match n.type_ {
3026 t if t == XML_ELEMENT_NODE as c_int => {
3027 let name = if n.name.is_null() {
3028 ""
3029 } else {
3030 unsafe { core::str::from_utf8(xmlstr_to_bytes(n.name)).unwrap_or("") }
3031 };
3032
3033 let is_void = is_html_void(name);
3034 let info = html_tag_lookup(name);
3039 let is_inline = info.is_none_or(|i| i.flags & HTML_INLINE != 0);
3040 let no_format = is_inline || name.starts_with('p');
3041
3042 io::buf_ccat(buf, b'<');
3044 if !n.ns.is_null() && !(*n.ns).prefix.is_null() {
3050 io::buf_cat(buf, (*n.ns).prefix);
3051 io::buf_ccat(buf, b':');
3052 }
3053 if !n.name.is_null() {
3054 io::buf_cat(buf, n.name);
3055 }
3056 if !n.nsDef.is_null() {
3057 let mut ns = n.nsDef;
3058 while !ns.is_null() {
3059 let nsp = unsafe { &*ns };
3060 let is_xml = !nsp.prefix.is_null() && xmlstr_to_bytes(nsp.prefix) == b"xml";
3064 if nsp.type_ == XML_LOCAL_NAMESPACE as c_int && !nsp.href.is_null() && !is_xml {
3065 io::buf_ccat(buf, b' ');
3066 if nsp.prefix.is_null() {
3067 io::buf_add(buf, b"xmlns=\"" as *const u8, 7);
3068 } else {
3069 io::buf_add(buf, b"xmlns:" as *const u8, 6);
3070 io::buf_cat(buf, nsp.prefix);
3071 io::buf_add(buf, b"=\"" as *const u8, 2);
3072 }
3073 html_serialize_attr_value(buf, nsp.href);
3074 io::buf_ccat(buf, b'\"');
3075 }
3076 ns = nsp.next;
3077 }
3078 }
3079
3080 let mut attr = n.properties;
3082 while !attr.is_null() {
3083 let a = unsafe { &*attr };
3084 io::buf_ccat(buf, b' ');
3085 if !a.name.is_null() {
3086 io::buf_cat(buf, a.name);
3087 }
3088
3089 if !a.children.is_null() {
3091 let child = unsafe { &*a.children };
3092 if child.type_ == XML_TEXT_NODE as c_int && !child.content.is_null() {
3093 io::buf_ccat(buf, b'=');
3094 io::buf_ccat(buf, b'"');
3095 html_serialize_attr_value(buf, child.content);
3096 io::buf_ccat(buf, b'"');
3097 }
3098 }
3099
3100 attr = a.next;
3101 }
3102
3103 let mut meta_bytes: Option<Vec<u8>> = None;
3111 if name.eq_ignore_ascii_case("head") && level == 1 {
3112 if let Some(enc) = encoding {
3113 let parent_is_html = !n.parent.is_null() && !(*n.parent).name.is_null() && {
3114 let pn =
3115 core::str::from_utf8(xmlstr_to_bytes((*n.parent).name)).unwrap_or("");
3116 pn.eq_ignore_ascii_case("html")
3117 };
3118 if parent_is_html && !html_head_has_meta(n.children) {
3119 meta_bytes = Some(enc.to_vec());
3120 }
3121 }
3122 }
3123 let meta_inserted = meta_bytes.is_some();
3124
3125 let has_children = !n.children.is_null();
3126 let first_child = if has_children {
3127 unsafe { (*n.children).type_ }
3128 } else {
3129 XML_TEXT_NODE as c_int
3130 };
3131 let first_is_text = first_child == XML_TEXT_NODE as c_int
3132 || first_child == XML_ENTITY_REF_NODE as c_int;
3133 let multi_child = (has_children && n.children != n.last) || meta_inserted;
3136
3137 if is_void {
3138 io::buf_ccat(buf, b'>');
3140 } else {
3144 io::buf_ccat(buf, b'>');
3146
3147 if format != 0 && !no_format && !first_is_text && multi_child {
3152 io::buf_ccat(buf, b'\n');
3153 }
3154
3155 if let Some(enc) = &meta_bytes {
3156 io::buf_add(buf, b"<meta charset=\"" as *const u8, 15);
3157 io::buf_add(buf, enc.as_ptr(), enc.len() as c_int);
3158 io::buf_add(buf, b"\">" as *const u8, 2);
3159 if format != 0 && has_children && !first_is_text && !name.starts_with('p') {
3162 io::buf_ccat(buf, b'\n');
3163 }
3164 }
3165
3166 let mut child = n.children;
3169 while !child.is_null() {
3170 serialize_node_enc(child, buf, format, level + 1, encoding);
3171 let next = unsafe { (*child).next };
3175 if format != 0 && !next.is_null() && !name.starts_with('p') {
3176 let nt = unsafe { (*next).type_ };
3177 if nt != XML_TEXT_NODE as c_int && nt != XML_ENTITY_REF_NODE as c_int {
3178 let cname = if (*child).name.is_null() {
3179 ""
3180 } else {
3181 unsafe {
3182 core::str::from_utf8(xmlstr_to_bytes((*child).name))
3183 .unwrap_or("")
3184 }
3185 };
3186 let cinfo = html_tag_lookup(cname);
3187 let c_inline = cinfo.is_none_or(|i| i.flags & HTML_INLINE != 0);
3188 if !c_inline {
3189 io::buf_ccat(buf, b'\n');
3190 }
3191 }
3192 }
3193 child = next;
3194 }
3195
3196 let last_child = if has_children {
3200 unsafe { (*n.last).type_ }
3201 } else {
3202 XML_ELEMENT_NODE as c_int
3203 };
3204 let last_is_text = last_child == XML_TEXT_NODE as c_int
3205 || last_child == XML_ENTITY_REF_NODE as c_int;
3206 if format != 0 && !no_format && !last_is_text && multi_child {
3207 io::buf_ccat(buf, b'\n');
3208 }
3209
3210 io::buf_add(buf, b"</" as *const u8, 2);
3212 if !n.ns.is_null() && !(*n.ns).prefix.is_null() {
3215 io::buf_cat(buf, (*n.ns).prefix);
3216 io::buf_ccat(buf, b':');
3217 }
3218 if !n.name.is_null() {
3219 io::buf_cat(buf, n.name);
3220 }
3221 io::buf_ccat(buf, b'>');
3222 }
3223 }
3224 t if t == XML_TEXT_NODE as c_int => {
3225 let parent_is_raw = !n.parent.is_null() && !(*n.parent).name.is_null() && {
3229 let pn = xmlstr_to_bytes((*n.parent).name);
3230 pn.eq_ignore_ascii_case(b"script") || pn.eq_ignore_ascii_case(b"style")
3231 };
3232 if parent_is_raw {
3233 io::buf_cat(buf, n.content);
3234 } else {
3235 html_serialize_text(buf, n.content, xml_strlen(n.content) as c_int);
3236 }
3237 }
3238 t if t == XML_CDATA_SECTION_NODE as c_int => {
3239 io::buf_add(buf, b"<![CDATA[" as *const u8, 9);
3240 html_serialize_text(buf, n.content, xml_strlen(n.content) as c_int);
3241 io::buf_add(buf, b"]]>" as *const u8, 3);
3242 }
3243 t if t == XML_COMMENT_NODE as c_int => {
3244 if format != 0 && level > 0 {
3245 io::buf_ccat(buf, b'\n');
3246 for _ in 0..level {
3247 io::buf_add(buf, b" " as *const u8, 2);
3248 }
3249 }
3250 io::buf_add(buf, b"<!--" as *const u8, 4);
3251 if !n.content.is_null() {
3252 io::buf_cat(buf, n.content);
3253 }
3254 io::buf_add(buf, b"-->" as *const u8, 3);
3255 }
3256 t if t == XML_PI_NODE as c_int => {
3257 if format != 0 && level > 0 {
3258 io::buf_ccat(buf, b'\n');
3259 for _ in 0..level {
3260 io::buf_add(buf, b" " as *const u8, 2);
3261 }
3262 }
3263 io::buf_add(buf, b"<?" as *const u8, 2);
3264 if !n.name.is_null() {
3265 io::buf_cat(buf, n.name);
3266 }
3267 if !n.content.is_null() && unsafe { *n.content != 0 } {
3268 io::buf_ccat(buf, b' ');
3269 io::buf_cat(buf, n.content);
3270 }
3271 io::buf_add(buf, b"?>" as *const u8, 2);
3272 }
3273 t if t == XML_DOCUMENT_NODE as c_int || t == XML_HTML_DOCUMENT_NODE as c_int => {
3274 let doc_ptr = n as *const _xmlNode as *mut _xmlDoc;
3277 let d = &*doc_ptr;
3278 if !d.intSubset.is_null() {
3279 let dtd = &*d.intSubset;
3280 io::buf_add(buf, b"<!DOCTYPE " as *const u8, 10);
3281 if !dtd.name.is_null() {
3282 io::buf_cat(buf, dtd.name);
3283 }
3284 if !dtd.ExternalID.is_null() {
3285 io::buf_add(buf, b" PUBLIC " as *const u8, 8);
3286 html_write_quoted(buf, dtd.ExternalID);
3287 io::buf_ccat(buf, b' ');
3288 html_write_quoted(buf, dtd.SystemID);
3289 } else if !dtd.SystemID.is_null() {
3290 io::buf_add(buf, b" SYSTEM " as *const u8, 8);
3291 html_write_quoted(buf, dtd.SystemID);
3292 }
3293 io::buf_ccat(buf, b'>');
3294 io::buf_ccat(buf, b'\n');
3295 }
3296 let mut child = n.children;
3299 while !child.is_null() {
3300 serialize_node_enc(child, buf, format, 0, encoding);
3301 child = unsafe { (*child).next };
3302 }
3303 io::buf_ccat(buf, b'\n');
3306 }
3307 _ => {
3308 if !n.content.is_null() {
3309 html_serialize_text(buf, n.content, xml_strlen(n.content) as c_int);
3310 }
3311 }
3312 }
3313}
3314
3315pub(crate) unsafe fn doc_dump(buf: *mut _xmlBuffer, doc: *mut _xmlDoc) -> c_int {
3322 if buf.is_null() || doc.is_null() {
3323 return -1;
3324 }
3325
3326 let before = io::buf_length(buf);
3327 serialize_node(doc as *mut _xmlNode, buf, 0, 0);
3328 let after = io::buf_length(buf);
3329
3330 if after < 0 || before < 0 {
3331 return -1;
3332 }
3333 after - before
3334}
3335
3336#[cfg(test)]
3341mod tests {
3342 use super::*;
3343
3344 use crate::xml::io;
3345
3346 #[allow(dead_code)]
3348 unsafe fn to_xmlstr(s: &[u8]) -> *mut xmlChar {
3349 bytes_to_xmlstr(s)
3350 }
3351
3352 unsafe fn html_doc_to_string(doc: *mut _xmlDoc) -> String {
3354 let buf = io::buf_create(-1);
3355 assert!(!buf.is_null());
3356 doc_dump(buf, doc);
3357 let content = io::buf_content(buf);
3358 let s = if !content.is_null() {
3359 let len = xml_strlen(content);
3360 let slice = slice::from_raw_parts(content, len);
3361 String::from_utf8_lossy(slice).to_string()
3362 } else {
3363 String::new()
3364 };
3365 io::buf_free(buf);
3366 s
3367 }
3368
3369 #[test]
3374 fn test_html_tag_lookup() {
3375 assert!(html_tag_lookup("html").is_some());
3377 assert!(html_tag_lookup("HTML").is_some()); assert!(html_tag_lookup("p").is_some());
3379 assert!(html_tag_lookup("br").is_some());
3380 assert!(html_tag_lookup("div").is_some());
3381 assert!(html_tag_lookup("script").is_some());
3382
3383 assert!(html_tag_lookup("custom").is_none());
3385 assert!(html_tag_lookup("my-element").is_none());
3386 }
3387
3388 #[test]
3389 fn test_tag_flags() {
3390 let br = html_tag_lookup("br").unwrap();
3391 assert!(br.flags & HTML_INLINE != 0);
3392 assert!(br.flags & HTML_EMPTY != 0);
3393
3394 let div = html_tag_lookup("div").unwrap();
3395 assert!(div.flags & HTML_BLOCK != 0);
3396 assert!(div.flags & HTML_VALID != 0);
3397
3398 let p = html_tag_lookup("p").unwrap();
3399 assert!(p.flags & HTML_NO_END != 0);
3400
3401 let meta = html_tag_lookup("meta").unwrap();
3402 assert!(meta.flags & HTML_HEAD != 0);
3403 assert!(meta.flags & HTML_EMPTY != 0);
3404 }
3405
3406 #[test]
3411 fn test_html_entity_lookup() {
3412 assert_eq!(html_entity_lookup("amp"), Some("&"));
3413 assert_eq!(html_entity_lookup("lt"), Some("<"));
3414 assert_eq!(html_entity_lookup("gt"), Some(">"));
3415 assert_eq!(html_entity_lookup("quot"), Some("\""));
3416 assert_eq!(html_entity_lookup("nbsp"), Some("\u{00a0}"));
3417 assert_eq!(html_entity_lookup("copy"), Some("\u{00a9}"));
3418 assert!(html_entity_lookup("unknown_entity").is_none());
3419 }
3420
3421 #[test]
3435 fn test_parse_basic_html() {
3436 unsafe {
3437 let html = b"<html><head><title>Test</title></head><body><p>Hello</p></body></html>\0";
3438 let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
3439 assert!(!doc.is_null());
3440
3441 let s = html_doc_to_string(doc);
3442 assert!(s.contains("<html>"));
3443 assert!(s.contains("<head>"));
3444 assert!(s.contains("<title>Test</title>"));
3445 assert!(s.contains("<body>"));
3446 assert!(s.contains("<p>Hello</p>"));
3447
3448 tree::free_doc(doc);
3449 }
3450 }
3451
3452 #[test]
3459 fn test_parse_empty_document() {
3460 unsafe {
3461 let html = b"\0";
3462 let doc = parse_memory(html.as_ptr() as *const c_char, 0);
3463 assert!(doc.is_null());
3464 }
3465 }
3466
3467 #[test]
3481 fn test_implicit_html_head_body() {
3482 unsafe {
3483 let html = b"<p>Hello</p>\0";
3485 let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
3486 assert!(!doc.is_null());
3487
3488 let s = html_doc_to_string(doc);
3489 assert!(s.contains("<html>"));
3491 assert!(s.contains("<body>"));
3493 assert!(s.contains("<p>Hello</p>"));
3495
3496 tree::free_doc(doc);
3497 }
3498 }
3499
3500 #[test]
3510 fn test_implicit_head_with_title() {
3511 unsafe {
3512 let html = b"<title>My Page</title>\0";
3514 let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
3515 assert!(!doc.is_null());
3516
3517 let s = html_doc_to_string(doc);
3518 assert!(s.contains("<html>"));
3519 assert!(s.contains("<head>"));
3520 assert!(s.contains("<title>My Page</title>"));
3521
3522 tree::free_doc(doc);
3523 }
3524 }
3525
3526 #[test]
3539 fn test_auto_close_p() {
3540 unsafe {
3541 let html = b"<p>First<p>Second</p>\0";
3543 let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
3544 assert!(!doc.is_null());
3545
3546 let s = html_doc_to_string(doc);
3547 let first_pos = s.find("First");
3549 let second_pos = s.find("Second");
3550 assert!(first_pos.is_some());
3551 assert!(second_pos.is_some());
3552
3553 tree::free_doc(doc);
3554 }
3555 }
3556
3557 #[test]
3566 fn test_auto_close_heading() {
3567 unsafe {
3568 let html = b"<h1>Title</h1><h2>Subtitle</h2>\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 assert!(s.contains("<h1>Title</h1>"));
3575 assert!(s.contains("<h2>Subtitle</h2>"));
3576
3577 tree::free_doc(doc);
3578 }
3579 }
3580
3581 #[test]
3594 fn test_void_elements() {
3595 unsafe {
3596 let html = b"<br><hr><img src=\"test.jpg\"><input type=\"text\">\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("<br>"));
3602 assert!(s.contains("<hr>"));
3603 assert!(s.contains("<img"));
3604 assert!(s.contains("<input"));
3605
3606 assert!(!s.contains("</br>"));
3608 assert!(!s.contains("</hr>"));
3609 assert!(!s.contains("</img>"));
3610
3611 tree::free_doc(doc);
3612 }
3613 }
3614
3615 #[test]
3629 fn test_unquoted_attributes() {
3630 unsafe {
3631 let html = b"<div class=main id=content>Text</div>\0";
3632 let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
3633 assert!(!doc.is_null());
3634
3635 let s = html_doc_to_string(doc);
3636 assert!(s.contains("class=\"main\""));
3637 assert!(s.contains("id=\"content\""));
3638
3639 tree::free_doc(doc);
3640 }
3641 }
3642
3643 #[test]
3652 fn test_minimized_attributes() {
3653 unsafe {
3654 let html = b"<option selected disabled>Value</option>\0";
3655 let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
3656 assert!(!doc.is_null());
3657
3658 let s = html_doc_to_string(doc);
3659 assert!(s.contains("selected"));
3661 assert!(s.contains("disabled"));
3662
3663 tree::free_doc(doc);
3664 }
3665 }
3666
3667 #[test]
3680 fn test_html_entities() {
3681 unsafe {
3682 let html = b"<p>& < > " ©</p>\0";
3683 let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
3684 assert!(!doc.is_null());
3685
3686 let s = html_doc_to_string(doc);
3687 assert!(s.contains("&")); assert!(s.contains("<")); assert!(s.contains(">")); assert!(s.contains("\u{00a0}")); tree::free_doc(doc);
3695 }
3696 }
3697
3698 #[test]
3707 fn test_numeric_entities() {
3708 unsafe {
3709 let html = b"<p>A A</p>\0";
3711 let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
3712 assert!(!doc.is_null());
3713
3714 let s = html_doc_to_string(doc);
3715 assert!(s.contains('A'));
3716
3717 tree::free_doc(doc);
3718 }
3719 }
3720
3721 #[test]
3734 fn test_nested_elements() {
3735 unsafe {
3736 let html = b"<div><ul><li>Item 1</li><li>Item 2</li></ul></div>\0";
3737 let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
3738 assert!(!doc.is_null());
3739
3740 let s = html_doc_to_string(doc);
3741 assert!(s.contains("<div>"));
3742 assert!(s.contains("<ul>"));
3743 assert!(s.contains("<li>Item 1</li>"));
3744 assert!(s.contains("<li>Item 2</li>"));
3745
3746 tree::free_doc(doc);
3747 }
3748 }
3749
3750 #[test]
3763 fn test_missing_end_tags() {
3764 unsafe {
3765 let html = b"<p>Paragraph without closing<div>Another div\0";
3767 let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
3768 assert!(!doc.is_null());
3769
3770 let s = html_doc_to_string(doc);
3771 assert!(s.contains("Paragraph without closing"));
3772 assert!(s.contains("Another div"));
3773
3774 tree::free_doc(doc);
3775 }
3776 }
3777
3778 #[test]
3787 fn test_mismatched_case() {
3788 unsafe {
3789 let html = b"<HTML><HEAD><TITLE>Test</TITLE></HEAD><BODY><P>Hello</P></BODY></HTML>\0";
3790 let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
3791 assert!(!doc.is_null());
3792
3793 let s = html_doc_to_string(doc);
3794 assert!(s.contains("<HTML>"));
3796 assert!(s.contains("<HEAD>"));
3797 assert!(s.contains("<BODY>"));
3798 assert!(s.contains("<P>Hello</P>"));
3799
3800 tree::free_doc(doc);
3801 }
3802 }
3803
3804 #[test]
3813 fn test_nested_malformed() {
3814 unsafe {
3815 let html = b"<div><p><span><b>Deep text</div></p>\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("Deep text"));
3822
3823 tree::free_doc(doc);
3824 }
3825 }
3826
3827 #[test]
3840 fn test_serialization_round_trip_simple() {
3841 unsafe {
3842 let original = b"<p>Hello World</p>\0";
3843 let doc = parse_memory(
3844 original.as_ptr() as *const c_char,
3845 (original.len() - 1) as c_int,
3846 );
3847 assert!(!doc.is_null());
3848
3849 let s = html_doc_to_string(doc);
3850 assert!(s.contains("Hello World"));
3851
3852 tree::free_doc(doc);
3853 }
3854 }
3855
3856 #[test]
3865 fn test_serialize_void_elements_no_self_close() {
3866 unsafe {
3867 let html = b"<br><hr><img src=\"test.png\">\0";
3868 let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
3869 assert!(!doc.is_null());
3870
3871 let s = html_doc_to_string(doc);
3872 assert!(!s.contains("<br/>"));
3874 assert!(!s.contains("<hr/>"));
3875
3876 tree::free_doc(doc);
3877 }
3878 }
3879
3880 #[test]
3893 fn test_script_content() {
3894 unsafe {
3895 let html = b"<script>var x = 1;</script>\0";
3897 let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
3898 assert!(!doc.is_null());
3899
3900 let s = html_doc_to_string(doc);
3901 assert!(s.contains("<script>"));
3902 assert!(s.contains("var x = 1;"));
3904
3905 let html2 = b"<script>if (a < b && c > d) { x(1); }</script>\0";
3908 let doc2 = parse_memory(html2.as_ptr() as *const c_char, (html2.len() - 1) as c_int);
3909 assert!(!doc2.is_null());
3910 let s2 = html_doc_to_string(doc2);
3911 assert!(
3912 s2.contains("if (a < b && c > d) { x(1); }"),
3913 "script content must be raw, got: {s2}"
3914 );
3915 assert!(
3916 !s2.contains("<"),
3917 "script content must not be escaped: {s2}"
3918 );
3919 tree::free_doc(doc2);
3920
3921 tree::free_doc(doc);
3922 }
3923 }
3924
3925 #[test]
3938 fn test_html_comment() {
3939 unsafe {
3940 let html = b"<html><!-- This is a comment --><body><p>Text</p></body></html>\0";
3941 let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
3942 assert!(!doc.is_null());
3943
3944 let s = html_doc_to_string(doc);
3945 assert!(s.contains("<!-- This is a comment -->"));
3946
3947 tree::free_doc(doc);
3948 }
3949 }
3950
3951 #[test]
3967 fn test_new_doc_creates_html_head_body() {
3968 unsafe {
3969 let doc = new_doc(ptr::null());
3970 assert!(!doc.is_null());
3971 assert_eq!((*doc).type_, XML_HTML_DOCUMENT_NODE as c_int);
3972
3973 let s = html_doc_to_string(doc);
3974 assert!(!s.contains("<html>"), "htmlNewDoc must not seed <html>");
3975 assert!(!s.contains("<head>"), "htmlNewDoc must not seed <head>");
3976 assert!(!s.contains("<body>"), "htmlNewDoc must not seed <body>");
3977
3978 tree::free_doc(doc);
3979 }
3980 }
3981
3982 #[test]
3992 fn test_new_doc_no_dtd() {
3993 unsafe {
3994 let doc = new_doc_no_dtd(ptr::null());
3995 assert!(!doc.is_null());
3996 assert_eq!((*doc).type_, XML_HTML_DOCUMENT_NODE as c_int);
3997
3998 let s = html_doc_to_string(doc);
4002 assert_eq!(s, "\n");
4003
4004 tree::free_doc(doc);
4005 }
4006 }
4007
4008 #[test]
4021 fn test_parsed_html_doc_flags() {
4022 unsafe {
4023 let doc = parse_memory(c"<html><body>x</body></html>".as_ptr(), 23);
4024 assert!(!doc.is_null());
4025 assert_eq!(
4026 (*doc).properties & (crate::abi::types::xmlDocProperties::XML_DOC_HTML as c_int),
4027 crate::abi::types::xmlDocProperties::XML_DOC_HTML as c_int,
4028 "html-parsed docs must carry XML_DOC_HTML"
4029 );
4030 assert_eq!(
4031 (*doc).standalone,
4032 1,
4033 "html-parsed docs default standalone=yes"
4034 );
4035 tree::free_doc(doc);
4036 }
4037 }
4038
4039 #[test]
4044 fn test_resolve_numeric_entity() {
4045 assert_eq!(resolve_numeric_entity("65", false), vec![b'A']);
4046 assert_eq!(resolve_numeric_entity("41", true), vec![b'A']);
4047 assert_eq!(resolve_numeric_entity("0", false), Vec::<u8>::new());
4048 }
4049
4050 #[test]
4051 fn test_resolve_entity_unknown() {
4052 let result = resolve_entity("unknown");
4053 assert_eq!(result, b"&unknown;");
4054 }
4055
4056 #[test]
4061 fn test_init_cleanup_parser() {
4062 init_parser();
4064 cleanup_parser();
4065 }
4066
4067 #[test]
4079 fn test_create_free_parser_ctxt() {
4080 unsafe {
4081 let ctxt =
4082 create_file_parser_ctxt(b"test.html\0" as *const u8 as *const c_char, ptr::null());
4083 assert!(!ctxt.is_null());
4084 free_parser_ctxt(ctxt);
4085 }
4086 }
4087
4088 #[test]
4101 fn test_complex_html_document() {
4102 unsafe {
4103 let html = b"<!DOCTYPE html>
4104<html>
4105<head>
4106 <meta charset=\"utf-8\">
4107 <title>Test Page</title>
4108 <link rel=\"stylesheet\" href=\"style.css\">
4109</head>
4110<body>
4111 <div id=\"main\">
4112 <h1>Title</h1>
4113 <p>First paragraph with <a href=\"link.html\">a link</a>.</p>
4114 <p>Second paragraph.</p>
4115 <ul>
4116 <li>Item 1</li>
4117 <li>Item 2</li>
4118 </ul>
4119 <br>
4120 <hr>
4121 <img src=\"image.jpg\" alt=\"An image\">
4122 </div>
4123 <script>alert('hello');</script>
4124</body>
4125</html>\0";
4126 let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
4127 assert!(!doc.is_null());
4128
4129 let s = html_doc_to_string(doc);
4130 assert!(s.contains("<html>"));
4131 assert!(s.contains("<head>"));
4132 assert!(s.contains("<title>Test Page</title>"));
4133 assert!(s.contains("<body>"));
4134 assert!(s.contains("<h1>Title</h1>"));
4135 assert!(s.contains("a link"));
4136 assert!(s.contains("Second paragraph"));
4137 assert!(s.contains("<br>"));
4138 assert!(s.contains("<hr>"));
4139 assert!(s.contains("<img"));
4140 assert!(s.contains("<script>"));
4141
4142 tree::free_doc(doc);
4143 }
4144 }
4145
4146 #[test]
4159 fn test_parse_doc() {
4160 unsafe {
4161 let html = b"<p>Hello from parse_doc</p>\0";
4162 let doc = parse_doc(html.as_ptr() as *const xmlChar, ptr::null(), 0);
4163 assert!(!doc.is_null());
4164
4165 let s = html_doc_to_string(doc);
4166 assert!(s.contains("Hello from parse_doc"));
4167
4168 tree::free_doc(doc);
4169 }
4170 }
4171
4172 #[test]
4185 fn test_table_element_auto_close() {
4186 unsafe {
4187 let html = b"<table><tr><td>Cell 1<td>Cell 2</td></tr></table>";
4188 let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
4189 assert!(!doc.is_null());
4190
4191 let s = html_doc_to_string(doc);
4192 assert!(s.contains("<td>Cell 1"));
4193 assert!(s.contains("<td>Cell 2"));
4194
4195 tree::free_doc(doc);
4196 }
4197 }
4198
4199 #[test]
4200 fn test_table_tr_auto_close() {
4201 unsafe {
4204 let html = b"<table><tr><td>1<td>2<tr><td>3</table>\0";
4205 let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
4206 assert!(!doc.is_null());
4207 let s = html_doc_to_string(doc);
4208 assert!(s.contains("</td></tr><tr>"));
4209 tree::free_doc(doc);
4210 }
4211 }
4212}