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 _ => None,
1906 }
1907}
1908
1909fn parse_html_doctype_decl(input: &[u8]) -> Option<(Vec<u8>, Option<Vec<u8>>, Option<Vec<u8>>)> {
1916 let mut i = 0usize;
1917 while i < input.len() && input[i].is_ascii_whitespace() {
1918 i += 1;
1919 }
1920 if i + 2 >= input.len() || input[i] != b'<' || !(input[i + 1] == b'!') {
1921 return None;
1922 }
1923 let kw = b"DOCTYPE";
1924 if !input.get(i + 2..i + 2 + kw.len()).is_some_and(|s| {
1925 s.iter()
1926 .enumerate()
1927 .all(|(k, b)| b.to_ascii_uppercase() == kw[k])
1928 }) {
1929 return None;
1930 }
1931 i += 2 + kw.len();
1932 while i < input.len() && input[i].is_ascii_whitespace() {
1934 i += 1;
1935 }
1936 let name_start = i;
1937 while i < input.len() && !input[i].is_ascii_whitespace() && input[i] != b'>' {
1938 i += 1;
1939 }
1940 let name = if name_start == i {
1945 Vec::new()
1946 } else {
1947 input[name_start..i].to_vec()
1948 };
1949 if name.is_empty() {
1951 return Some((name, None, None));
1952 }
1953 let mut ext: Option<Vec<u8>> = None;
1955 let mut sys: Option<Vec<u8>> = None;
1956 while i < input.len() && input[i].is_ascii_whitespace() {
1957 i += 1;
1958 }
1959 if i < input.len() && input[i] != b'>' {
1960 let word_start = i;
1962 while i < input.len() && input[i].is_ascii_alphabetic() {
1963 i += 1;
1964 }
1965 let word = input[word_start..i].to_ascii_uppercase();
1966 if word == b"PUBLIC" {
1967 while i < input.len() && input[i].is_ascii_whitespace() {
1968 i += 1;
1969 }
1970 if i < input.len() && (input[i] == b'"' || input[i] == b'\'') {
1972 let q = input[i];
1973 i += 1;
1974 let v_start = i;
1975 while i < input.len() && input[i] != q {
1976 i += 1;
1977 }
1978 ext = Some(input[v_start..i].to_vec());
1979 if i < input.len() {
1980 i += 1;
1981 }
1982 }
1983 while i < input.len() && input[i].is_ascii_whitespace() {
1984 i += 1;
1985 }
1986 if i < input.len()
1988 && i + 1 < input.len()
1989 && (input[i] == b'"' || input[i] == b'\'')
1990 && input[i] != b'>'
1991 {
1992 let q = input[i];
1993 i += 1;
1994 let v_start = i;
1995 while i < input.len() && input[i] != q {
1996 i += 1;
1997 }
1998 sys = Some(input[v_start..i].to_vec());
1999 }
2000 } else if word == b"SYSTEM" {
2001 while i < input.len() && input[i].is_ascii_whitespace() {
2002 i += 1;
2003 }
2004 if i < input.len() && (input[i] == b'"' || input[i] == b'\'') {
2005 let q = input[i];
2006 i += 1;
2007 let v_start = i;
2008 while i < input.len() && input[i] != q {
2009 i += 1;
2010 }
2011 sys = Some(input[v_start..i].to_vec());
2012 }
2013 }
2014 }
2015 Some((name, ext, sys))
2016}
2017
2018unsafe fn html_parse_buffer(
2024 ctxt: &mut HtmlParserCtxt,
2025 buffer: *const c_char,
2026 size: c_int,
2027) -> *mut _xmlDoc {
2028 if buffer.is_null() || size <= 0 {
2029 return ptr::null_mut();
2030 }
2031
2032 let doc = tree::new_doc(ptr::null());
2034 if doc.is_null() {
2035 return ptr::null_mut();
2036 }
2037 unsafe {
2038 (*doc).type_ = XML_HTML_DOCUMENT_NODE as c_int;
2039 if !(*doc).version.is_null() {
2042 crate::abi::allocator::xmlFreeImpl((*doc).version as *mut c_void);
2043 }
2044 (*doc).version = ptr::null_mut();
2045 (*doc).properties = crate::abi::types::xmlDocProperties::XML_DOC_HTML as c_int;
2052 (*doc).standalone = 1;
2055 }
2056 let raw_input = unsafe { slice::from_raw_parts(buffer as *const u8, size as usize) };
2063 if let Some((name, ext, sys)) = parse_html_doctype_decl(raw_input) {
2064 let name_cstr = if name.is_empty() {
2068 ptr::null()
2069 } else {
2070 crate::xml::string::bytes_to_xmlstr(&name)
2071 };
2072 let ext_cstr = ext
2073 .map(|s| crate::xml::string::bytes_to_xmlstr(&s))
2074 .unwrap_or(ptr::null_mut());
2075 let sys_cstr = sys
2076 .map(|s| crate::xml::string::bytes_to_xmlstr(&s))
2077 .unwrap_or(ptr::null_mut());
2078 unsafe {
2079 crate::xml::dtd::create_int_subset(
2080 doc,
2081 name_cstr as *const xmlChar,
2082 ext_cstr as *const xmlChar,
2083 sys_cstr as *const xmlChar,
2084 );
2085 }
2086 if !name_cstr.is_null() {
2087 unsafe { crate::abi::allocator::xmlFreeImpl(name_cstr as *mut c_void) };
2088 }
2089 if !ext_cstr.is_null() {
2090 unsafe { crate::abi::allocator::xmlFreeImpl(ext_cstr as *mut c_void) };
2091 }
2092 if !sys_cstr.is_null() {
2093 unsafe { crate::abi::allocator::xmlFreeImpl(sys_cstr as *mut c_void) };
2094 }
2095 } else {
2096 if ctxt.options & HTML_PARSE_NODEFDTD == 0 {
2099 unsafe {
2100 crate::xml::dtd::create_int_subset(
2101 doc,
2102 b"html\0" as *const u8 as *const xmlChar,
2103 b"-//W3C//DTD HTML 4.0 Transitional//EN\0" as *const u8 as *const xmlChar,
2104 b"http://www.w3.org/TR/REC-html40/loose.dtd\0" as *const u8 as *const xmlChar,
2105 );
2106 }
2107 }
2108 }
2109 ctxt.doc = doc;
2110
2111 let converted: Option<Vec<u8>> = if !ctxt.encoding.is_null() {
2116 let raw = unsafe { slice::from_raw_parts(buffer as *const u8, size as usize) };
2117 convert_input_to_utf8(ctxt.encoding, raw)
2118 } else {
2119 None
2120 };
2121 let (input_ptr, input_len): (*const u8, usize) = match &converted {
2122 Some(v) => (v.as_ptr(), v.len()),
2123 None => (buffer as *const u8, size as usize),
2124 };
2125
2126 ctxt.input = input_ptr as *mut u8;
2128 ctxt.input_len = input_len;
2129 ctxt.input_pos = 0;
2130 ctxt.line = 1;
2131
2132 loop {
2134 if ctxt.is_eof() {
2135 break;
2136 }
2137
2138 let ch = ctxt.peek().unwrap_or(0);
2139
2140 if ch == b'<' {
2141 ctxt.next(); if ctxt.peek() == Some(b'/') {
2145 ctxt.next(); let tag_name = ctxt.read_while(|ch| {
2147 ch != b'>' && ch != b' ' && ch != b'\t' && ch != b'\n' && ch != b'\r'
2148 });
2149
2150 while ctxt.peek() != Some(b'>') && !ctxt.is_eof() {
2152 ctxt.next();
2153 }
2154 if ctxt.peek() == Some(b'>') {
2155 ctxt.next(); }
2157
2158 if !tag_name.is_empty() {
2159 handle_end_tag(ctxt, &tag_name);
2160 }
2161 continue;
2162 }
2163
2164 if ctxt.peek() == Some(b'!')
2166 && ctxt.peek_at(1) == Some(b'-')
2167 && ctxt.peek_at(2) == Some(b'-')
2168 {
2169 ctxt.next(); ctxt.next(); ctxt.next(); let mut comment_content = Vec::new();
2175 loop {
2176 if ctxt.peek() == Some(b'-')
2177 && ctxt.peek_at(1) == Some(b'-')
2178 && ctxt.peek_at(2) == Some(b'>')
2179 {
2180 ctxt.next(); ctxt.next(); ctxt.next(); break;
2184 }
2185 match ctxt.next() {
2186 Some(ch) => comment_content.push(ch),
2187 None => break,
2188 }
2189 }
2190
2191 if !comment_content.is_empty() {
2193 let comment_node = tree::new_comment(bytes_to_xmlstr(&comment_content));
2194 if !comment_node.is_null() {
2195 let insertion_point = if !ctxt.current.is_null() {
2196 ctxt.current
2197 } else {
2198 ctxt.doc as *mut _xmlNode
2199 };
2200 tree::add_child(insertion_point, comment_node);
2201 }
2202 }
2203 continue;
2204 }
2205
2206 if ctxt.peek() == Some(b'!') {
2208 ctxt.next(); let _rest = ctxt.read_while(|ch| ch != b'>');
2210 if ctxt.peek() == Some(b'>') {
2211 ctxt.next(); }
2213 continue;
2216 }
2217
2218 if ctxt.peek() == Some(b'?') {
2220 ctxt.next(); let mut pi_content = Vec::new();
2223 loop {
2224 if ctxt.peek() == Some(b'?') && ctxt.peek_at(1) == Some(b'>') {
2225 break;
2226 }
2227 match ctxt.next() {
2228 Some(ch) => pi_content.push(ch),
2229 None => break,
2230 }
2231 }
2232 if ctxt.peek() == Some(b'?') {
2234 ctxt.next();
2235 }
2236 if ctxt.peek() == Some(b'>') {
2237 ctxt.next();
2238 }
2239 if !pi_content.is_empty() {
2241 let mut parts = pi_content.splitn(2, |b| *b == b' ');
2243 let target = parts.next().unwrap_or(&pi_content);
2244 let value = parts.next().unwrap_or(b"");
2245
2246 let pi_node = tree::new_pi(bytes_to_xmlstr(target), bytes_to_xmlstr(value));
2247 if !pi_node.is_null() {
2248 let insertion_point = if !ctxt.current.is_null() {
2249 ctxt.current
2250 } else {
2251 ctxt.doc as *mut _xmlNode
2252 };
2253 tree::add_child(insertion_point, pi_node);
2254 }
2255 }
2256 continue;
2257 }
2258
2259 let tag_name = ctxt.read_while(|ch| {
2261 ch != b'>' && ch != b'/' && ch != b' ' && ch != b'\t' && ch != b'\n' && ch != b'\r'
2262 });
2263
2264 if tag_name.is_empty() {
2265 handle_text(ctxt, b"<");
2267 continue;
2268 }
2269
2270 let attrs = parse_attributes(ctxt);
2272
2273 if ctxt.peek() == Some(b'/') {
2275 ctxt.next(); if ctxt.peek() == Some(b'>') {
2277 ctxt.next(); }
2279 } else if ctxt.peek() == Some(b'>') {
2280 ctxt.next(); }
2282
2283 let tag_lower: Vec<u8> = tag_name.iter().map(|b| b.to_ascii_lowercase()).collect();
2285 let tag_str = core::str::from_utf8(&tag_lower).unwrap_or("");
2286
2287 if tag_str == "script" || tag_str == "style" {
2288 let raw_node = tree::new_node(ptr::null_mut(), bytes_to_xmlstr(&tag_name));
2291 if !raw_node.is_null() {
2292 for attr in &attrs {
2293 let name_c = bytes_to_xmlstr(&attr.name);
2294 let val_c = bytes_to_xmlstr(&attr.value);
2295 if !name_c.is_null() {
2296 tree::set_prop(raw_node, name_c, val_c);
2297 xmlFreeImpl(name_c as *mut c_void);
2298 if !val_c.is_null() {
2299 xmlFreeImpl(val_c as *mut c_void);
2300 }
2301 }
2302 }
2303
2304 let insertion_point = if ctxt.current.is_null() {
2305 if ctxt.in_head {
2306 ensure_head(ctxt);
2307 ctxt.head
2308 } else {
2309 ensure_body(ctxt);
2310 ctxt.body
2311 }
2312 } else {
2313 ctxt.current
2314 };
2315
2316 if !insertion_point.is_null() {
2317 tree::add_child(insertion_point, raw_node);
2318
2319 let end_tag = format!("</{}", tag_str);
2321 let end_bytes = end_tag.as_bytes();
2322 let mut raw_text = Vec::new();
2323 let mut match_buf: Vec<u8> = Vec::new();
2330 let mut match_idx = 0;
2331
2332 loop {
2333 if ctxt.is_eof() {
2334 break;
2335 }
2336 let ch = ctxt.peek().unwrap();
2337 if ch.to_ascii_lowercase() == end_bytes[match_idx] {
2338 match_buf.push(ch);
2339 match_idx += 1;
2340 ctxt.next();
2341 if match_idx == end_bytes.len() {
2342 if !raw_text.is_empty() {
2345 let text_node = tree::new_text(bytes_to_xmlstr(&raw_text));
2346 if !text_node.is_null() {
2347 tree::add_child(raw_node, text_node);
2348 }
2349 }
2350 let _suffix = ctxt.read_while(|ch| ch != b'>');
2353 if ctxt.peek() == Some(b'>') {
2354 ctxt.next();
2355 }
2356 ctxt.current = unsafe { (*raw_node).parent };
2358 break;
2359 }
2360 } else {
2361 if match_idx > 0 {
2365 raw_text.extend_from_slice(&match_buf);
2366 match_buf.clear();
2367 match_idx = 0;
2368 }
2369 raw_text.push(ch);
2370 ctxt.next();
2371 }
2372 }
2373
2374 if match_idx < end_bytes.len() {
2377 raw_text.extend_from_slice(&match_buf);
2378 if !raw_text.is_empty() {
2379 let text_node = tree::new_text(bytes_to_xmlstr(&raw_text));
2380 if !text_node.is_null() {
2381 tree::add_child(raw_node, text_node);
2382 }
2383 }
2384 ctxt.current = unsafe { (*raw_node).parent };
2385 }
2386 }
2387 }
2388 continue;
2389 }
2390
2391 handle_start_tag(ctxt, &tag_name, &attrs);
2393 } else {
2394 let mut text = Vec::new();
2396 loop {
2397 match ctxt.peek() {
2398 Some(b'<') => break,
2399 Some(b'&') => {
2400 let entity_text = parse_entity(ctxt);
2402 text.extend_from_slice(&entity_text);
2403 }
2404 Some(0) => {
2405 ctxt.next();
2411 }
2412 Some(ch) => {
2413 text.push(ch);
2414 ctxt.next();
2415 }
2416 None => break,
2417 }
2418 }
2419
2420 if !text.is_empty() {
2421 handle_text(ctxt, &text);
2422 }
2423 }
2424 }
2425
2426 if ctxt.html.is_null() && ctxt.options & HTML_PARSE_NOIMPLIED == 0 {
2430 ensure_html(ctxt);
2431 }
2432
2433 if ctxt.options & (crate::abi::types::XML_PARSE_NOBLANKS as c_int) != 0 && !doc.is_null() {
2439 unsafe {
2440 drop_blank_text_nodes((*doc).children);
2441 }
2442 }
2443
2444 if !doc.is_null() {
2455 unsafe {
2456 register_html_ids(doc, (*doc).children);
2457 }
2458 }
2459
2460 doc
2461}
2462
2463unsafe fn register_html_ids(doc: *mut _xmlDoc, cur: *mut _xmlNode) {
2471 let mut n = cur;
2472 while !n.is_null() {
2473 let t = unsafe { (*n).type_ };
2474 if t == XML_ELEMENT_NODE as c_int {
2475 let el = n;
2476 let mut attr = unsafe { (*el).properties };
2477 while !attr.is_null() {
2478 if unsafe { (*attr).id }.is_null()
2479 && !unsafe { (*attr).children }.is_null()
2480 && unsafe { (*(*attr).children).type_ } == XML_TEXT_NODE as c_int
2481 && unsafe { (*(*attr).children).next }.is_null()
2482 {
2483 let v = unsafe { (*(*attr).children).content };
2484 if !v.is_null() {
2485 let id_res = crate::xml::validation::is_id(doc, el, attr);
2486 if id_res > 0 {
2487 crate::xml::validation::add_id(ptr::null_mut(), doc, v, attr);
2488 } else if crate::xml::validation::is_ref(doc, el, attr) > 0 {
2489 crate::xml::validation::add_ref(ptr::null_mut(), doc, v, attr);
2490 }
2491 }
2492 }
2493 attr = unsafe { (*attr).next };
2494 }
2495 if !unsafe { (*el).children }.is_null() {
2496 register_html_ids(doc, unsafe { (*el).children });
2497 }
2498 }
2499 n = unsafe { (*n).next };
2500 }
2501}
2502
2503unsafe fn drop_blank_text_nodes(cur: *mut _xmlNode) {
2511 let mut n = cur;
2512 while !n.is_null() {
2513 let next = unsafe { (*n).next };
2514 let t = unsafe { (*n).type_ };
2515 if t == XML_TEXT_NODE as c_int {
2516 let content = unsafe { (*n).content };
2517 let blank = if content.is_null() {
2518 true
2519 } else {
2520 let mut p = content;
2521 while unsafe { *p } != 0 {
2522 match unsafe { *p } {
2523 b' ' | b'\t' | b'\r' | b'\n' => {}
2524 _ => break,
2525 }
2526 p = unsafe { p.add(1) };
2527 }
2528 (unsafe { *p }) == 0
2529 };
2530 if blank {
2531 tree::unlink_node(n);
2532 tree::free_node(n);
2533 }
2534 } else if t == XML_ELEMENT_NODE as c_int && !unsafe { (*n).children }.is_null() {
2535 drop_blank_text_nodes(unsafe { (*n).children });
2536 }
2537 n = next;
2538 }
2539}
2540
2541pub unsafe fn parse_file(
2556 filename: *const c_char,
2557 encoding: *const c_char,
2558 options: c_int,
2559) -> *mut _xmlDoc {
2560 if filename.is_null() {
2561 return ptr::null_mut();
2562 }
2563
2564 let filename_str = unsafe { std::ffi::CStr::from_ptr(filename) };
2566 let path = filename_str.to_str().unwrap_or("");
2567 let content = match std::fs::read(path) {
2568 Ok(data) => data,
2569 Err(_) => return ptr::null_mut(),
2570 };
2571
2572 let mut ctxt = HtmlParserCtxt::new();
2573 ctxt.options = options;
2574 if !encoding.is_null() {
2575 let _enc_cstr = unsafe { std::ffi::CStr::from_ptr(encoding) };
2576 ctxt.encoding = unsafe { c_strdup(encoding) };
2577 }
2578
2579 let doc = unsafe {
2580 html_parse_buffer(
2581 &mut ctxt,
2582 content.as_ptr() as *const c_char,
2583 content.len() as c_int,
2584 )
2585 };
2586
2587 if !doc.is_null() && !filename.is_null() {
2588 unsafe {
2589 (*doc).URL = c_strdup(filename) as *mut xmlChar;
2590 }
2591 }
2592
2593 doc
2594}
2595
2596pub unsafe fn parse_memory(buffer: *const c_char, size: c_int) -> *mut _xmlDoc {
2607 unsafe { parse_memory_enc(buffer, size, ptr::null(), 0) }
2608}
2609
2610pub(crate) unsafe fn parse_memory_enc(
2623 buffer: *const c_char,
2624 size: c_int,
2625 encoding: *const c_char,
2626 options: c_int,
2627) -> *mut _xmlDoc {
2628 if buffer.is_null() || size <= 0 {
2629 return ptr::null_mut();
2630 }
2631
2632 let mut ctxt = HtmlParserCtxt::new();
2633 ctxt.options = options;
2634 if !encoding.is_null() {
2635 ctxt.encoding = unsafe { c_strdup(encoding) };
2636 }
2637 unsafe { html_parse_buffer(&mut ctxt, buffer, size) }
2638}
2639
2640pub(crate) unsafe fn parse_doc(
2651 cur: *const xmlChar,
2652 encoding: *const c_char,
2653 options: c_int,
2654) -> *mut _xmlDoc {
2655 if cur.is_null() {
2656 return ptr::null_mut();
2657 }
2658
2659 let len = unsafe { xml_strlen(cur) };
2660 let mut ctxt = HtmlParserCtxt::new();
2661 ctxt.options = options;
2662 if !encoding.is_null() {
2663 ctxt.encoding = unsafe { c_strdup(encoding) };
2664 }
2665
2666 unsafe { html_parse_buffer(&mut ctxt, cur as *const c_char, len as c_int) }
2667}
2668
2669#[allow(dead_code)]
2680pub(crate) unsafe fn create_file_parser_ctxt(
2681 filename: *const c_char,
2682 encoding: *const c_char,
2683) -> *mut c_void {
2684 if filename.is_null() {
2685 return ptr::null_mut();
2686 }
2687
2688 let total = size_of::<_xmlParserCtxt>() + size_of::<HtmlParserCtxt>();
2691 let mem = unsafe { xmlMallocZero(total) } as *mut u8;
2692 if mem.is_null() {
2693 return ptr::null_mut();
2694 }
2695
2696 let ctxt = mem.add(size_of::<_xmlParserCtxt>()) as *mut HtmlParserCtxt;
2697 unsafe {
2698 ptr::write(ctxt, HtmlParserCtxt::new());
2699 if !encoding.is_null() {
2700 (*ctxt).encoding = c_strdup(encoding);
2701 }
2702 (*(mem as *mut _xmlParserCtxt)).html = 1;
2703 }
2704
2705 mem as *mut c_void
2706}
2707
2708pub(crate) unsafe fn free_parser_ctxt(ctxt: *mut c_void) {
2718 if ctxt.is_null() {
2719 return;
2720 }
2721
2722 let state = (ctxt as *mut u8).add(size_of::<_xmlParserCtxt>()) as *mut HtmlParserCtxt;
2726 unsafe {
2727 if !(*state).input.is_null() {
2728 xmlFreeImpl((*state).input as *mut c_void);
2729 }
2730 if !(*state).filename.is_null() {
2731 xmlFreeImpl((*state).filename as *mut c_void);
2732 }
2733 if !(*state).encoding.is_null() {
2734 xmlFreeImpl((*state).encoding as *mut c_void);
2735 }
2736 xmlFreeImpl(ctxt);
2737 }
2738}
2739
2740#[allow(dead_code)]
2746pub(crate) const fn init_parser() {
2747 }
2750
2751#[allow(dead_code)]
2757pub(crate) const fn cleanup_parser() {
2758 }
2761
2762pub(crate) unsafe fn new_doc(version: *const xmlChar) -> *mut _xmlDoc {
2780 let doc = tree::new_doc(version);
2781 if doc.is_null() {
2782 return ptr::null_mut();
2783 }
2784
2785 unsafe {
2786 (*doc).type_ = XML_HTML_DOCUMENT_NODE as c_int;
2787 (*doc).properties = XML_DOC_WELLFORMED as c_int;
2788 }
2789
2790 doc
2791}
2792
2793pub(crate) unsafe fn new_doc_no_dtd(version: *const xmlChar) -> *mut _xmlDoc {
2806 let doc = tree::new_doc(version);
2807 if doc.is_null() {
2808 return ptr::null_mut();
2809 }
2810
2811 unsafe {
2812 (*doc).type_ = XML_HTML_DOCUMENT_NODE as c_int;
2813 (*doc).properties = XML_DOC_WELLFORMED as c_int;
2814 }
2815
2816 doc
2817}
2818
2819const HTML_VOID_ELEMENTS: &[&str] = &[
2825 "area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "param", "source",
2826 "track", "wbr", "frame",
2827];
2828
2829fn is_html_void(name: &str) -> bool {
2831 HTML_VOID_ELEMENTS
2832 .iter()
2833 .any(|v| v.eq_ignore_ascii_case(name))
2834}
2835
2836#[allow(dead_code)]
2838fn has_optional_end_tag(name: &str) -> bool {
2839 matches!(
2840 name.to_ascii_lowercase().as_str(),
2841 "p" | "li" | "tr" | "td" | "th" | "dt" | "dd"
2842 )
2843}
2844
2845unsafe fn html_write_quoted(buf: *mut _xmlBuffer, s: *const xmlChar) {
2851 if buf.is_null() || s.is_null() {
2852 return;
2853 }
2854 io::buf_ccat(buf, b'"');
2855 io::buf_cat(buf, s);
2856 io::buf_ccat(buf, b'"');
2857}
2858
2859fn trim_ascii_start(s: &[u8]) -> &[u8] {
2861 let start = s
2862 .iter()
2863 .position(|&b| !b.is_ascii_whitespace())
2864 .unwrap_or(s.len());
2865 &s[start..]
2866}
2867
2868unsafe fn html_serialize_text(buf: *mut _xmlBuffer, content: *const xmlChar, len: c_int) {
2880 if buf.is_null() || content.is_null() || len <= 0 {
2881 return;
2882 }
2883
2884 let mut i: c_int = 0;
2885 while i < len {
2886 let ch = unsafe { *content.add(i as usize) };
2887
2888 match ch {
2889 b'<' => {
2890 io::buf_add(buf, b"<" as *const u8, 4);
2891 }
2892 b'&' => {
2893 io::buf_add(buf, b"&" as *const u8, 5);
2894 }
2895 b'>' => {
2896 io::buf_add(buf, b">" as *const u8, 4);
2897 }
2898 _ => {
2899 io::buf_add(buf, &ch as *const u8, 1);
2900 }
2901 }
2902 i += 1;
2903 }
2904}
2905
2906unsafe fn html_serialize_attr_value(buf: *mut _xmlBuffer, value: *const xmlChar) {
2917 if buf.is_null() || value.is_null() {
2918 return;
2919 }
2920
2921 let len = unsafe { xml_strlen(value) as c_int };
2922 let mut i: c_int = 0;
2923 while i < len {
2924 let ch = unsafe { *value.add(i as usize) };
2925
2926 match ch {
2927 b'&' => {
2928 io::buf_add(buf, b"&" as *const u8, 5);
2929 }
2930 b'"' => {
2931 io::buf_add(buf, b""" as *const u8, 6);
2932 }
2933 _ => {
2934 io::buf_add(buf, &ch as *const u8, 1);
2935 }
2936 }
2937 i += 1;
2938 }
2939}
2940
2941unsafe fn html_head_has_meta(child: *mut _xmlNode) -> bool {
2959 let mut c = child;
2960 while !c.is_null() {
2961 if (*c).type_ == XML_ELEMENT_NODE as c_int && !(*c).name.is_null() {
2962 let nm = xmlstr_to_bytes((*c).name);
2963 if nm.eq_ignore_ascii_case(b"meta") {
2964 return true;
2965 }
2966 }
2967 c = (*c).next;
2968 }
2969 false
2970}
2971
2972pub(crate) unsafe fn serialize_node(
2983 node: *mut _xmlNode,
2984 buf: *mut _xmlBuffer,
2985 format: c_int,
2986 level: c_int,
2987) {
2988 unsafe { serialize_node_enc(node, buf, format, level, None) }
2989}
2990
2991pub(crate) unsafe fn serialize_node_enc(
3003 node: *mut _xmlNode,
3004 buf: *mut _xmlBuffer,
3005 format: c_int,
3006 level: c_int,
3007 encoding: Option<&[u8]>,
3008) {
3009 if node.is_null() || buf.is_null() {
3010 return;
3011 }
3012
3013 let n = unsafe { &*node };
3014
3015 match n.type_ {
3016 t if t == XML_ELEMENT_NODE as c_int => {
3017 let name = if n.name.is_null() {
3018 ""
3019 } else {
3020 unsafe { core::str::from_utf8(xmlstr_to_bytes(n.name)).unwrap_or("") }
3021 };
3022
3023 let is_void = is_html_void(name);
3024 let info = html_tag_lookup(name);
3029 let is_inline = info.is_none_or(|i| i.flags & HTML_INLINE != 0);
3030 let no_format = is_inline || name.starts_with('p');
3031
3032 io::buf_ccat(buf, b'<');
3034 if !n.ns.is_null() && !(*n.ns).prefix.is_null() {
3040 io::buf_cat(buf, (*n.ns).prefix);
3041 io::buf_ccat(buf, b':');
3042 }
3043 if !n.name.is_null() {
3044 io::buf_cat(buf, n.name);
3045 }
3046 if !n.nsDef.is_null() {
3047 let mut ns = n.nsDef;
3048 while !ns.is_null() {
3049 let nsp = unsafe { &*ns };
3050 let is_xml = !nsp.prefix.is_null() && xmlstr_to_bytes(nsp.prefix) == b"xml";
3054 if nsp.type_ == XML_LOCAL_NAMESPACE as c_int && !nsp.href.is_null() && !is_xml {
3055 io::buf_ccat(buf, b' ');
3056 if nsp.prefix.is_null() {
3057 io::buf_add(buf, b"xmlns=\"" as *const u8, 7);
3058 } else {
3059 io::buf_add(buf, b"xmlns:" as *const u8, 6);
3060 io::buf_cat(buf, nsp.prefix);
3061 io::buf_add(buf, b"=\"" as *const u8, 2);
3062 }
3063 html_serialize_attr_value(buf, nsp.href);
3064 io::buf_ccat(buf, b'\"');
3065 }
3066 ns = nsp.next;
3067 }
3068 }
3069
3070 let mut attr = n.properties;
3072 while !attr.is_null() {
3073 let a = unsafe { &*attr };
3074 io::buf_ccat(buf, b' ');
3075 if !a.name.is_null() {
3076 io::buf_cat(buf, a.name);
3077 }
3078
3079 if !a.children.is_null() {
3081 let child = unsafe { &*a.children };
3082 if child.type_ == XML_TEXT_NODE as c_int && !child.content.is_null() {
3083 io::buf_ccat(buf, b'=');
3084 io::buf_ccat(buf, b'"');
3085 html_serialize_attr_value(buf, child.content);
3086 io::buf_ccat(buf, b'"');
3087 }
3088 }
3089
3090 attr = a.next;
3091 }
3092
3093 let mut meta_bytes: Option<Vec<u8>> = None;
3101 if name.eq_ignore_ascii_case("head") && level == 1 {
3102 if let Some(enc) = encoding {
3103 let parent_is_html = !n.parent.is_null() && !(*n.parent).name.is_null() && {
3104 let pn =
3105 core::str::from_utf8(xmlstr_to_bytes((*n.parent).name)).unwrap_or("");
3106 pn.eq_ignore_ascii_case("html")
3107 };
3108 if parent_is_html && !html_head_has_meta(n.children) {
3109 meta_bytes = Some(enc.to_vec());
3110 }
3111 }
3112 }
3113 let meta_inserted = meta_bytes.is_some();
3114
3115 let has_children = !n.children.is_null();
3116 let first_child = if has_children {
3117 unsafe { (*n.children).type_ }
3118 } else {
3119 XML_TEXT_NODE as c_int
3120 };
3121 let first_is_text = first_child == XML_TEXT_NODE as c_int
3122 || first_child == XML_ENTITY_REF_NODE as c_int;
3123 let multi_child = (has_children && n.children != n.last) || meta_inserted;
3126
3127 if is_void {
3128 io::buf_ccat(buf, b'>');
3130 } else {
3134 io::buf_ccat(buf, b'>');
3136
3137 if format != 0 && !no_format && !first_is_text && multi_child {
3142 io::buf_ccat(buf, b'\n');
3143 }
3144
3145 if let Some(enc) = &meta_bytes {
3146 io::buf_add(buf, b"<meta charset=\"" as *const u8, 15);
3147 io::buf_add(buf, enc.as_ptr(), enc.len() as c_int);
3148 io::buf_add(buf, b"\">" as *const u8, 2);
3149 if format != 0 && has_children && !first_is_text && !name.starts_with('p') {
3152 io::buf_ccat(buf, b'\n');
3153 }
3154 }
3155
3156 let mut child = n.children;
3159 while !child.is_null() {
3160 serialize_node_enc(child, buf, format, level + 1, encoding);
3161 let next = unsafe { (*child).next };
3165 if format != 0 && !next.is_null() && !name.starts_with('p') {
3166 let nt = unsafe { (*next).type_ };
3167 if nt != XML_TEXT_NODE as c_int && nt != XML_ENTITY_REF_NODE as c_int {
3168 let cname = if (*child).name.is_null() {
3169 ""
3170 } else {
3171 unsafe {
3172 core::str::from_utf8(xmlstr_to_bytes((*child).name))
3173 .unwrap_or("")
3174 }
3175 };
3176 let cinfo = html_tag_lookup(cname);
3177 let c_inline = cinfo.is_none_or(|i| i.flags & HTML_INLINE != 0);
3178 if !c_inline {
3179 io::buf_ccat(buf, b'\n');
3180 }
3181 }
3182 }
3183 child = next;
3184 }
3185
3186 let last_child = if has_children {
3190 unsafe { (*n.last).type_ }
3191 } else {
3192 XML_ELEMENT_NODE as c_int
3193 };
3194 let last_is_text = last_child == XML_TEXT_NODE as c_int
3195 || last_child == XML_ENTITY_REF_NODE as c_int;
3196 if format != 0 && !no_format && !last_is_text && multi_child {
3197 io::buf_ccat(buf, b'\n');
3198 }
3199
3200 io::buf_add(buf, b"</" as *const u8, 2);
3202 if !n.ns.is_null() && !(*n.ns).prefix.is_null() {
3205 io::buf_cat(buf, (*n.ns).prefix);
3206 io::buf_ccat(buf, b':');
3207 }
3208 if !n.name.is_null() {
3209 io::buf_cat(buf, n.name);
3210 }
3211 io::buf_ccat(buf, b'>');
3212 }
3213 }
3214 t if t == XML_TEXT_NODE as c_int => {
3215 let parent_is_raw = !n.parent.is_null() && !(*n.parent).name.is_null() && {
3219 let pn = xmlstr_to_bytes((*n.parent).name);
3220 pn.eq_ignore_ascii_case(b"script") || pn.eq_ignore_ascii_case(b"style")
3221 };
3222 if parent_is_raw {
3223 io::buf_cat(buf, n.content);
3224 } else {
3225 html_serialize_text(buf, n.content, xml_strlen(n.content) as c_int);
3226 }
3227 }
3228 t if t == XML_CDATA_SECTION_NODE as c_int => {
3229 io::buf_add(buf, b"<![CDATA[" as *const u8, 9);
3230 html_serialize_text(buf, n.content, xml_strlen(n.content) as c_int);
3231 io::buf_add(buf, b"]]>" as *const u8, 3);
3232 }
3233 t if t == XML_COMMENT_NODE as c_int => {
3234 if format != 0 && level > 0 {
3235 io::buf_ccat(buf, b'\n');
3236 for _ in 0..level {
3237 io::buf_add(buf, b" " as *const u8, 2);
3238 }
3239 }
3240 io::buf_add(buf, b"<!--" as *const u8, 4);
3241 if !n.content.is_null() {
3242 io::buf_cat(buf, n.content);
3243 }
3244 io::buf_add(buf, b"-->" as *const u8, 3);
3245 }
3246 t if t == XML_PI_NODE as c_int => {
3247 if format != 0 && level > 0 {
3248 io::buf_ccat(buf, b'\n');
3249 for _ in 0..level {
3250 io::buf_add(buf, b" " as *const u8, 2);
3251 }
3252 }
3253 io::buf_add(buf, b"<?" as *const u8, 2);
3254 if !n.name.is_null() {
3255 io::buf_cat(buf, n.name);
3256 }
3257 if !n.content.is_null() && unsafe { *n.content != 0 } {
3258 io::buf_ccat(buf, b' ');
3259 io::buf_cat(buf, n.content);
3260 }
3261 io::buf_add(buf, b"?>" as *const u8, 2);
3262 }
3263 t if t == XML_DOCUMENT_NODE as c_int || t == XML_HTML_DOCUMENT_NODE as c_int => {
3264 let doc_ptr = n as *const _xmlNode as *mut _xmlDoc;
3267 let d = &*doc_ptr;
3268 if !d.intSubset.is_null() {
3269 let dtd = &*d.intSubset;
3270 io::buf_add(buf, b"<!DOCTYPE " as *const u8, 10);
3271 if !dtd.name.is_null() {
3272 io::buf_cat(buf, dtd.name);
3273 }
3274 if !dtd.ExternalID.is_null() {
3275 io::buf_add(buf, b" PUBLIC " as *const u8, 8);
3276 html_write_quoted(buf, dtd.ExternalID);
3277 io::buf_ccat(buf, b' ');
3278 html_write_quoted(buf, dtd.SystemID);
3279 } else if !dtd.SystemID.is_null() {
3280 io::buf_add(buf, b" SYSTEM " as *const u8, 8);
3281 html_write_quoted(buf, dtd.SystemID);
3282 }
3283 io::buf_ccat(buf, b'>');
3284 io::buf_ccat(buf, b'\n');
3285 }
3286 let mut child = n.children;
3289 while !child.is_null() {
3290 serialize_node_enc(child, buf, format, 0, encoding);
3291 child = unsafe { (*child).next };
3292 }
3293 io::buf_ccat(buf, b'\n');
3296 }
3297 _ => {
3298 if !n.content.is_null() {
3299 html_serialize_text(buf, n.content, xml_strlen(n.content) as c_int);
3300 }
3301 }
3302 }
3303}
3304
3305pub(crate) unsafe fn doc_dump(buf: *mut _xmlBuffer, doc: *mut _xmlDoc) -> c_int {
3312 if buf.is_null() || doc.is_null() {
3313 return -1;
3314 }
3315
3316 let before = io::buf_length(buf);
3317 serialize_node(doc as *mut _xmlNode, buf, 0, 0);
3318 let after = io::buf_length(buf);
3319
3320 if after < 0 || before < 0 {
3321 return -1;
3322 }
3323 after - before
3324}
3325
3326#[cfg(test)]
3331mod tests {
3332 use super::*;
3333
3334 use crate::xml::io;
3335
3336 #[allow(dead_code)]
3338 unsafe fn to_xmlstr(s: &[u8]) -> *mut xmlChar {
3339 bytes_to_xmlstr(s)
3340 }
3341
3342 unsafe fn html_doc_to_string(doc: *mut _xmlDoc) -> String {
3344 let buf = io::buf_create(-1);
3345 assert!(!buf.is_null());
3346 doc_dump(buf, doc);
3347 let content = io::buf_content(buf);
3348 let s = if !content.is_null() {
3349 let len = xml_strlen(content);
3350 let slice = slice::from_raw_parts(content, len);
3351 String::from_utf8_lossy(slice).to_string()
3352 } else {
3353 String::new()
3354 };
3355 io::buf_free(buf);
3356 s
3357 }
3358
3359 #[test]
3364 fn test_html_tag_lookup() {
3365 assert!(html_tag_lookup("html").is_some());
3367 assert!(html_tag_lookup("HTML").is_some()); assert!(html_tag_lookup("p").is_some());
3369 assert!(html_tag_lookup("br").is_some());
3370 assert!(html_tag_lookup("div").is_some());
3371 assert!(html_tag_lookup("script").is_some());
3372
3373 assert!(html_tag_lookup("custom").is_none());
3375 assert!(html_tag_lookup("my-element").is_none());
3376 }
3377
3378 #[test]
3379 fn test_tag_flags() {
3380 let br = html_tag_lookup("br").unwrap();
3381 assert!(br.flags & HTML_INLINE != 0);
3382 assert!(br.flags & HTML_EMPTY != 0);
3383
3384 let div = html_tag_lookup("div").unwrap();
3385 assert!(div.flags & HTML_BLOCK != 0);
3386 assert!(div.flags & HTML_VALID != 0);
3387
3388 let p = html_tag_lookup("p").unwrap();
3389 assert!(p.flags & HTML_NO_END != 0);
3390
3391 let meta = html_tag_lookup("meta").unwrap();
3392 assert!(meta.flags & HTML_HEAD != 0);
3393 assert!(meta.flags & HTML_EMPTY != 0);
3394 }
3395
3396 #[test]
3401 fn test_html_entity_lookup() {
3402 assert_eq!(html_entity_lookup("amp"), Some("&"));
3403 assert_eq!(html_entity_lookup("lt"), Some("<"));
3404 assert_eq!(html_entity_lookup("gt"), Some(">"));
3405 assert_eq!(html_entity_lookup("quot"), Some("\""));
3406 assert_eq!(html_entity_lookup("nbsp"), Some("\u{00a0}"));
3407 assert_eq!(html_entity_lookup("copy"), Some("\u{00a9}"));
3408 assert!(html_entity_lookup("unknown_entity").is_none());
3409 }
3410
3411 #[test]
3425 fn test_parse_basic_html() {
3426 unsafe {
3427 let html = b"<html><head><title>Test</title></head><body><p>Hello</p></body></html>\0";
3428 let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
3429 assert!(!doc.is_null());
3430
3431 let s = html_doc_to_string(doc);
3432 assert!(s.contains("<html>"));
3433 assert!(s.contains("<head>"));
3434 assert!(s.contains("<title>Test</title>"));
3435 assert!(s.contains("<body>"));
3436 assert!(s.contains("<p>Hello</p>"));
3437
3438 tree::free_doc(doc);
3439 }
3440 }
3441
3442 #[test]
3449 fn test_parse_empty_document() {
3450 unsafe {
3451 let html = b"\0";
3452 let doc = parse_memory(html.as_ptr() as *const c_char, 0);
3453 assert!(doc.is_null());
3454 }
3455 }
3456
3457 #[test]
3471 fn test_implicit_html_head_body() {
3472 unsafe {
3473 let html = b"<p>Hello</p>\0";
3475 let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
3476 assert!(!doc.is_null());
3477
3478 let s = html_doc_to_string(doc);
3479 assert!(s.contains("<html>"));
3481 assert!(s.contains("<body>"));
3483 assert!(s.contains("<p>Hello</p>"));
3485
3486 tree::free_doc(doc);
3487 }
3488 }
3489
3490 #[test]
3500 fn test_implicit_head_with_title() {
3501 unsafe {
3502 let html = b"<title>My Page</title>\0";
3504 let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
3505 assert!(!doc.is_null());
3506
3507 let s = html_doc_to_string(doc);
3508 assert!(s.contains("<html>"));
3509 assert!(s.contains("<head>"));
3510 assert!(s.contains("<title>My Page</title>"));
3511
3512 tree::free_doc(doc);
3513 }
3514 }
3515
3516 #[test]
3529 fn test_auto_close_p() {
3530 unsafe {
3531 let html = b"<p>First<p>Second</p>\0";
3533 let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
3534 assert!(!doc.is_null());
3535
3536 let s = html_doc_to_string(doc);
3537 let first_pos = s.find("First");
3539 let second_pos = s.find("Second");
3540 assert!(first_pos.is_some());
3541 assert!(second_pos.is_some());
3542
3543 tree::free_doc(doc);
3544 }
3545 }
3546
3547 #[test]
3556 fn test_auto_close_heading() {
3557 unsafe {
3558 let html = b"<h1>Title</h1><h2>Subtitle</h2>\0";
3560 let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
3561 assert!(!doc.is_null());
3562
3563 let s = html_doc_to_string(doc);
3564 assert!(s.contains("<h1>Title</h1>"));
3565 assert!(s.contains("<h2>Subtitle</h2>"));
3566
3567 tree::free_doc(doc);
3568 }
3569 }
3570
3571 #[test]
3584 fn test_void_elements() {
3585 unsafe {
3586 let html = b"<br><hr><img src=\"test.jpg\"><input type=\"text\">\0";
3587 let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
3588 assert!(!doc.is_null());
3589
3590 let s = html_doc_to_string(doc);
3591 assert!(s.contains("<br>"));
3592 assert!(s.contains("<hr>"));
3593 assert!(s.contains("<img"));
3594 assert!(s.contains("<input"));
3595
3596 assert!(!s.contains("</br>"));
3598 assert!(!s.contains("</hr>"));
3599 assert!(!s.contains("</img>"));
3600
3601 tree::free_doc(doc);
3602 }
3603 }
3604
3605 #[test]
3619 fn test_unquoted_attributes() {
3620 unsafe {
3621 let html = b"<div class=main id=content>Text</div>\0";
3622 let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
3623 assert!(!doc.is_null());
3624
3625 let s = html_doc_to_string(doc);
3626 assert!(s.contains("class=\"main\""));
3627 assert!(s.contains("id=\"content\""));
3628
3629 tree::free_doc(doc);
3630 }
3631 }
3632
3633 #[test]
3642 fn test_minimized_attributes() {
3643 unsafe {
3644 let html = b"<option selected disabled>Value</option>\0";
3645 let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
3646 assert!(!doc.is_null());
3647
3648 let s = html_doc_to_string(doc);
3649 assert!(s.contains("selected"));
3651 assert!(s.contains("disabled"));
3652
3653 tree::free_doc(doc);
3654 }
3655 }
3656
3657 #[test]
3670 fn test_html_entities() {
3671 unsafe {
3672 let html = b"<p>& < > " ©</p>\0";
3673 let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
3674 assert!(!doc.is_null());
3675
3676 let s = html_doc_to_string(doc);
3677 assert!(s.contains("&")); assert!(s.contains("<")); assert!(s.contains(">")); assert!(s.contains("\u{00a0}")); tree::free_doc(doc);
3685 }
3686 }
3687
3688 #[test]
3697 fn test_numeric_entities() {
3698 unsafe {
3699 let html = b"<p>A A</p>\0";
3701 let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
3702 assert!(!doc.is_null());
3703
3704 let s = html_doc_to_string(doc);
3705 assert!(s.contains('A'));
3706
3707 tree::free_doc(doc);
3708 }
3709 }
3710
3711 #[test]
3724 fn test_nested_elements() {
3725 unsafe {
3726 let html = b"<div><ul><li>Item 1</li><li>Item 2</li></ul></div>\0";
3727 let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
3728 assert!(!doc.is_null());
3729
3730 let s = html_doc_to_string(doc);
3731 assert!(s.contains("<div>"));
3732 assert!(s.contains("<ul>"));
3733 assert!(s.contains("<li>Item 1</li>"));
3734 assert!(s.contains("<li>Item 2</li>"));
3735
3736 tree::free_doc(doc);
3737 }
3738 }
3739
3740 #[test]
3753 fn test_missing_end_tags() {
3754 unsafe {
3755 let html = b"<p>Paragraph without closing<div>Another div\0";
3757 let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
3758 assert!(!doc.is_null());
3759
3760 let s = html_doc_to_string(doc);
3761 assert!(s.contains("Paragraph without closing"));
3762 assert!(s.contains("Another div"));
3763
3764 tree::free_doc(doc);
3765 }
3766 }
3767
3768 #[test]
3777 fn test_mismatched_case() {
3778 unsafe {
3779 let html = b"<HTML><HEAD><TITLE>Test</TITLE></HEAD><BODY><P>Hello</P></BODY></HTML>\0";
3780 let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
3781 assert!(!doc.is_null());
3782
3783 let s = html_doc_to_string(doc);
3784 assert!(s.contains("<HTML>"));
3786 assert!(s.contains("<HEAD>"));
3787 assert!(s.contains("<BODY>"));
3788 assert!(s.contains("<P>Hello</P>"));
3789
3790 tree::free_doc(doc);
3791 }
3792 }
3793
3794 #[test]
3803 fn test_nested_malformed() {
3804 unsafe {
3805 let html = b"<div><p><span><b>Deep text</div></p>\0";
3807 let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
3808 assert!(!doc.is_null());
3809
3810 let s = html_doc_to_string(doc);
3811 assert!(s.contains("Deep text"));
3812
3813 tree::free_doc(doc);
3814 }
3815 }
3816
3817 #[test]
3830 fn test_serialization_round_trip_simple() {
3831 unsafe {
3832 let original = b"<p>Hello World</p>\0";
3833 let doc = parse_memory(
3834 original.as_ptr() as *const c_char,
3835 (original.len() - 1) as c_int,
3836 );
3837 assert!(!doc.is_null());
3838
3839 let s = html_doc_to_string(doc);
3840 assert!(s.contains("Hello World"));
3841
3842 tree::free_doc(doc);
3843 }
3844 }
3845
3846 #[test]
3855 fn test_serialize_void_elements_no_self_close() {
3856 unsafe {
3857 let html = b"<br><hr><img src=\"test.png\">\0";
3858 let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
3859 assert!(!doc.is_null());
3860
3861 let s = html_doc_to_string(doc);
3862 assert!(!s.contains("<br/>"));
3864 assert!(!s.contains("<hr/>"));
3865
3866 tree::free_doc(doc);
3867 }
3868 }
3869
3870 #[test]
3883 fn test_script_content() {
3884 unsafe {
3885 let html = b"<script>var x = 1;</script>\0";
3887 let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
3888 assert!(!doc.is_null());
3889
3890 let s = html_doc_to_string(doc);
3891 assert!(s.contains("<script>"));
3892 assert!(s.contains("var x = 1;"));
3894
3895 let html2 = b"<script>if (a < b && c > d) { x(1); }</script>\0";
3898 let doc2 = parse_memory(html2.as_ptr() as *const c_char, (html2.len() - 1) as c_int);
3899 assert!(!doc2.is_null());
3900 let s2 = html_doc_to_string(doc2);
3901 assert!(
3902 s2.contains("if (a < b && c > d) { x(1); }"),
3903 "script content must be raw, got: {s2}"
3904 );
3905 assert!(
3906 !s2.contains("<"),
3907 "script content must not be escaped: {s2}"
3908 );
3909 tree::free_doc(doc2);
3910
3911 tree::free_doc(doc);
3912 }
3913 }
3914
3915 #[test]
3928 fn test_html_comment() {
3929 unsafe {
3930 let html = b"<html><!-- This is a comment --><body><p>Text</p></body></html>\0";
3931 let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
3932 assert!(!doc.is_null());
3933
3934 let s = html_doc_to_string(doc);
3935 assert!(s.contains("<!-- This is a comment -->"));
3936
3937 tree::free_doc(doc);
3938 }
3939 }
3940
3941 #[test]
3957 fn test_new_doc_creates_html_head_body() {
3958 unsafe {
3959 let doc = new_doc(ptr::null());
3960 assert!(!doc.is_null());
3961 assert_eq!((*doc).type_, XML_HTML_DOCUMENT_NODE as c_int);
3962
3963 let s = html_doc_to_string(doc);
3964 assert!(!s.contains("<html>"), "htmlNewDoc must not seed <html>");
3965 assert!(!s.contains("<head>"), "htmlNewDoc must not seed <head>");
3966 assert!(!s.contains("<body>"), "htmlNewDoc must not seed <body>");
3967
3968 tree::free_doc(doc);
3969 }
3970 }
3971
3972 #[test]
3982 fn test_new_doc_no_dtd() {
3983 unsafe {
3984 let doc = new_doc_no_dtd(ptr::null());
3985 assert!(!doc.is_null());
3986 assert_eq!((*doc).type_, XML_HTML_DOCUMENT_NODE as c_int);
3987
3988 let s = html_doc_to_string(doc);
3992 assert_eq!(s, "\n");
3993
3994 tree::free_doc(doc);
3995 }
3996 }
3997
3998 #[test]
4011 fn test_parsed_html_doc_flags() {
4012 unsafe {
4013 let doc = parse_memory(c"<html><body>x</body></html>".as_ptr(), 23);
4014 assert!(!doc.is_null());
4015 assert_eq!(
4016 (*doc).properties & (crate::abi::types::xmlDocProperties::XML_DOC_HTML as c_int),
4017 crate::abi::types::xmlDocProperties::XML_DOC_HTML as c_int,
4018 "html-parsed docs must carry XML_DOC_HTML"
4019 );
4020 assert_eq!(
4021 (*doc).standalone,
4022 1,
4023 "html-parsed docs default standalone=yes"
4024 );
4025 tree::free_doc(doc);
4026 }
4027 }
4028
4029 #[test]
4034 fn test_resolve_numeric_entity() {
4035 assert_eq!(resolve_numeric_entity("65", false), vec![b'A']);
4036 assert_eq!(resolve_numeric_entity("41", true), vec![b'A']);
4037 assert_eq!(resolve_numeric_entity("0", false), Vec::<u8>::new());
4038 }
4039
4040 #[test]
4041 fn test_resolve_entity_unknown() {
4042 let result = resolve_entity("unknown");
4043 assert_eq!(result, b"&unknown;");
4044 }
4045
4046 #[test]
4051 fn test_init_cleanup_parser() {
4052 init_parser();
4054 cleanup_parser();
4055 }
4056
4057 #[test]
4069 fn test_create_free_parser_ctxt() {
4070 unsafe {
4071 let ctxt =
4072 create_file_parser_ctxt(b"test.html\0" as *const u8 as *const c_char, ptr::null());
4073 assert!(!ctxt.is_null());
4074 free_parser_ctxt(ctxt);
4075 }
4076 }
4077
4078 #[test]
4091 fn test_complex_html_document() {
4092 unsafe {
4093 let html = b"<!DOCTYPE html>
4094<html>
4095<head>
4096 <meta charset=\"utf-8\">
4097 <title>Test Page</title>
4098 <link rel=\"stylesheet\" href=\"style.css\">
4099</head>
4100<body>
4101 <div id=\"main\">
4102 <h1>Title</h1>
4103 <p>First paragraph with <a href=\"link.html\">a link</a>.</p>
4104 <p>Second paragraph.</p>
4105 <ul>
4106 <li>Item 1</li>
4107 <li>Item 2</li>
4108 </ul>
4109 <br>
4110 <hr>
4111 <img src=\"image.jpg\" alt=\"An image\">
4112 </div>
4113 <script>alert('hello');</script>
4114</body>
4115</html>\0";
4116 let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
4117 assert!(!doc.is_null());
4118
4119 let s = html_doc_to_string(doc);
4120 assert!(s.contains("<html>"));
4121 assert!(s.contains("<head>"));
4122 assert!(s.contains("<title>Test Page</title>"));
4123 assert!(s.contains("<body>"));
4124 assert!(s.contains("<h1>Title</h1>"));
4125 assert!(s.contains("a link"));
4126 assert!(s.contains("Second paragraph"));
4127 assert!(s.contains("<br>"));
4128 assert!(s.contains("<hr>"));
4129 assert!(s.contains("<img"));
4130 assert!(s.contains("<script>"));
4131
4132 tree::free_doc(doc);
4133 }
4134 }
4135
4136 #[test]
4149 fn test_parse_doc() {
4150 unsafe {
4151 let html = b"<p>Hello from parse_doc</p>\0";
4152 let doc = parse_doc(html.as_ptr() as *const xmlChar, ptr::null(), 0);
4153 assert!(!doc.is_null());
4154
4155 let s = html_doc_to_string(doc);
4156 assert!(s.contains("Hello from parse_doc"));
4157
4158 tree::free_doc(doc);
4159 }
4160 }
4161
4162 #[test]
4175 fn test_table_element_auto_close() {
4176 unsafe {
4177 let html = b"<table><tr><td>Cell 1<td>Cell 2</td></tr></table>";
4178 let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
4179 assert!(!doc.is_null());
4180
4181 let s = html_doc_to_string(doc);
4182 assert!(s.contains("<td>Cell 1"));
4183 assert!(s.contains("<td>Cell 2"));
4184
4185 tree::free_doc(doc);
4186 }
4187 }
4188
4189 #[test]
4190 fn test_table_tr_auto_close() {
4191 unsafe {
4194 let html = b"<table><tr><td>1<td>2<tr><td>3</table>\0";
4195 let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
4196 assert!(!doc.is_null());
4197 let s = html_doc_to_string(doc);
4198 assert!(s.contains("</td></tr><tr>"));
4199 tree::free_doc(doc);
4200 }
4201 }
4202}