1use core::ffi::c_void;
15use core::ptr;
16use core::slice;
17use std::os::raw::{c_char, c_int};
18
19use crate::abi::allocator::{xmlFreeImpl, xmlMallocZero};
20use crate::abi::structs::*;
21use crate::abi::types::xmlDocProperties::XML_DOC_WELLFORMED;
22use crate::abi::types::xmlElementType::*;
23use crate::abi::types::*;
24use crate::xml::io;
25use crate::xml::string::*;
26use crate::xml::tree;
27
28const HTML_INLINE: u32 = 0x1;
34const HTML_BLOCK: u32 = 0x2;
35const HTML_EMPTY: u32 = 0x4;
36#[allow(dead_code)]
37const HTML_DEPRECATED: u32 = 0x8;
38const HTML_OL: u32 = 0x10;
39const HTML_DL: u32 = 0x20;
40#[allow(dead_code)]
41const HTML_COMPACT: u32 = 0x40;
42const HTML_HEAD: u32 = 0x80;
43const HTML_BODY: u32 = 0x100;
44#[allow(dead_code)]
45const HTML_HEADSTRUCK: u32 = 0x200;
46const HTML_VALID: u32 = 0x400;
47const HTML_NO_END: u32 = 0x800; #[allow(dead_code)]
49const HTML_IMPLIED: u32 = 0x1000; #[derive(Clone, Copy)]
53struct HtmlElementInfo {
54 name: &'static str,
55 flags: u32,
56}
57
58const HTML_ELEMENTS: &[HtmlElementInfo] = &[
61 HtmlElementInfo {
63 name: "br",
64 flags: HTML_INLINE | HTML_EMPTY | HTML_VALID,
65 },
66 HtmlElementInfo {
67 name: "hr",
68 flags: HTML_BLOCK | HTML_EMPTY | HTML_VALID,
69 },
70 HtmlElementInfo {
71 name: "img",
72 flags: HTML_INLINE | HTML_EMPTY | HTML_VALID,
73 },
74 HtmlElementInfo {
75 name: "input",
76 flags: HTML_INLINE | HTML_EMPTY | HTML_VALID,
77 },
78 HtmlElementInfo {
79 name: "meta",
80 flags: HTML_HEAD | HTML_EMPTY | HTML_VALID,
81 },
82 HtmlElementInfo {
83 name: "link",
84 flags: HTML_HEAD | HTML_EMPTY | HTML_VALID,
85 },
86 HtmlElementInfo {
87 name: "base",
88 flags: HTML_HEAD | HTML_EMPTY | HTML_VALID,
89 },
90 HtmlElementInfo {
91 name: "area",
92 flags: HTML_INLINE | HTML_EMPTY | HTML_VALID,
93 },
94 HtmlElementInfo {
95 name: "col",
96 flags: HTML_BLOCK | HTML_EMPTY | HTML_VALID,
97 },
98 HtmlElementInfo {
99 name: "embed",
100 flags: HTML_INLINE | HTML_EMPTY | HTML_VALID,
101 },
102 HtmlElementInfo {
103 name: "param",
104 flags: HTML_HEAD | HTML_EMPTY | HTML_VALID,
105 },
106 HtmlElementInfo {
107 name: "source",
108 flags: HTML_INLINE | HTML_EMPTY | HTML_VALID,
109 },
110 HtmlElementInfo {
111 name: "track",
112 flags: HTML_INLINE | HTML_EMPTY | HTML_VALID,
113 },
114 HtmlElementInfo {
115 name: "wbr",
116 flags: HTML_INLINE | HTML_EMPTY | HTML_VALID,
117 },
118 HtmlElementInfo {
120 name: "html",
121 flags: HTML_BLOCK | HTML_VALID,
122 },
123 HtmlElementInfo {
124 name: "head",
125 flags: HTML_HEAD | HTML_BLOCK | HTML_VALID,
126 },
127 HtmlElementInfo {
128 name: "body",
129 flags: HTML_BODY | HTML_BLOCK | HTML_VALID,
130 },
131 HtmlElementInfo {
132 name: "div",
133 flags: HTML_BLOCK | HTML_VALID,
134 },
135 HtmlElementInfo {
136 name: "p",
137 flags: HTML_BLOCK | HTML_VALID | HTML_NO_END,
138 },
139 HtmlElementInfo {
140 name: "h1",
141 flags: HTML_BLOCK | HTML_VALID,
142 },
143 HtmlElementInfo {
144 name: "h2",
145 flags: HTML_BLOCK | HTML_VALID,
146 },
147 HtmlElementInfo {
148 name: "h3",
149 flags: HTML_BLOCK | HTML_VALID,
150 },
151 HtmlElementInfo {
152 name: "h4",
153 flags: HTML_BLOCK | HTML_VALID,
154 },
155 HtmlElementInfo {
156 name: "h5",
157 flags: HTML_BLOCK | HTML_VALID,
158 },
159 HtmlElementInfo {
160 name: "h6",
161 flags: HTML_BLOCK | HTML_VALID,
162 },
163 HtmlElementInfo {
164 name: "ul",
165 flags: HTML_BLOCK | HTML_VALID | HTML_OL,
166 },
167 HtmlElementInfo {
168 name: "ol",
169 flags: HTML_BLOCK | HTML_VALID | HTML_OL,
170 },
171 HtmlElementInfo {
172 name: "li",
173 flags: HTML_BLOCK | HTML_VALID | HTML_NO_END,
174 },
175 HtmlElementInfo {
176 name: "dl",
177 flags: HTML_BLOCK | HTML_VALID | HTML_DL,
178 },
179 HtmlElementInfo {
180 name: "dt",
181 flags: HTML_BLOCK | HTML_VALID | HTML_NO_END,
182 },
183 HtmlElementInfo {
184 name: "dd",
185 flags: HTML_BLOCK | HTML_VALID | HTML_NO_END,
186 },
187 HtmlElementInfo {
188 name: "table",
189 flags: HTML_BLOCK | HTML_VALID,
190 },
191 HtmlElementInfo {
192 name: "tr",
193 flags: HTML_BLOCK | HTML_VALID | HTML_NO_END,
194 },
195 HtmlElementInfo {
196 name: "td",
197 flags: HTML_BLOCK | HTML_VALID | HTML_NO_END,
198 },
199 HtmlElementInfo {
200 name: "th",
201 flags: HTML_BLOCK | HTML_VALID | HTML_NO_END,
202 },
203 HtmlElementInfo {
204 name: "thead",
205 flags: HTML_BLOCK | HTML_VALID,
206 },
207 HtmlElementInfo {
208 name: "tbody",
209 flags: HTML_BLOCK | HTML_VALID,
210 },
211 HtmlElementInfo {
212 name: "tfoot",
213 flags: HTML_BLOCK | HTML_VALID,
214 },
215 HtmlElementInfo {
216 name: "colgroup",
217 flags: HTML_BLOCK | HTML_VALID,
218 },
219 HtmlElementInfo {
220 name: "caption",
221 flags: HTML_BLOCK | HTML_VALID,
222 },
223 HtmlElementInfo {
224 name: "form",
225 flags: HTML_BLOCK | HTML_VALID,
226 },
227 HtmlElementInfo {
228 name: "fieldset",
229 flags: HTML_BLOCK | HTML_VALID,
230 },
231 HtmlElementInfo {
232 name: "legend",
233 flags: HTML_BLOCK | HTML_VALID,
234 },
235 HtmlElementInfo {
236 name: "pre",
237 flags: HTML_BLOCK | HTML_VALID,
238 },
239 HtmlElementInfo {
240 name: "blockquote",
241 flags: HTML_BLOCK | HTML_VALID,
242 },
243 HtmlElementInfo {
244 name: "address",
245 flags: HTML_BLOCK | HTML_VALID,
246 },
247 HtmlElementInfo {
248 name: "center",
249 flags: HTML_BLOCK | HTML_VALID,
250 },
251 HtmlElementInfo {
252 name: "dir",
253 flags: HTML_BLOCK | HTML_VALID,
254 },
255 HtmlElementInfo {
256 name: "menu",
257 flags: HTML_BLOCK | HTML_VALID,
258 },
259 HtmlElementInfo {
260 name: "noscript",
261 flags: HTML_BLOCK | HTML_VALID,
262 },
263 HtmlElementInfo {
264 name: "frameset",
265 flags: HTML_BLOCK | HTML_VALID,
266 },
267 HtmlElementInfo {
268 name: "frame",
269 flags: HTML_BLOCK | HTML_EMPTY | HTML_VALID,
270 },
271 HtmlElementInfo {
272 name: "iframe",
273 flags: HTML_BLOCK | HTML_VALID,
274 },
275 HtmlElementInfo {
276 name: "noframes",
277 flags: HTML_BLOCK | HTML_VALID,
278 },
279 HtmlElementInfo {
281 name: "a",
282 flags: HTML_INLINE | HTML_VALID,
283 },
284 HtmlElementInfo {
285 name: "abbr",
286 flags: HTML_INLINE | HTML_VALID,
287 },
288 HtmlElementInfo {
289 name: "acronym",
290 flags: HTML_INLINE | HTML_VALID,
291 },
292 HtmlElementInfo {
293 name: "b",
294 flags: HTML_INLINE | HTML_VALID,
295 },
296 HtmlElementInfo {
297 name: "basefont",
298 flags: HTML_INLINE | HTML_EMPTY | HTML_VALID,
299 },
300 HtmlElementInfo {
301 name: "bdo",
302 flags: HTML_INLINE | HTML_VALID,
303 },
304 HtmlElementInfo {
305 name: "big",
306 flags: HTML_INLINE | HTML_VALID,
307 },
308 HtmlElementInfo {
309 name: "cite",
310 flags: HTML_INLINE | HTML_VALID,
311 },
312 HtmlElementInfo {
313 name: "code",
314 flags: HTML_INLINE | HTML_VALID,
315 },
316 HtmlElementInfo {
317 name: "dfn",
318 flags: HTML_INLINE | HTML_VALID,
319 },
320 HtmlElementInfo {
321 name: "em",
322 flags: HTML_INLINE | HTML_VALID,
323 },
324 HtmlElementInfo {
325 name: "font",
326 flags: HTML_INLINE | HTML_VALID,
327 },
328 HtmlElementInfo {
329 name: "i",
330 flags: HTML_INLINE | HTML_VALID,
331 },
332 HtmlElementInfo {
333 name: "kbd",
334 flags: HTML_INLINE | HTML_VALID,
335 },
336 HtmlElementInfo {
337 name: "label",
338 flags: HTML_INLINE | HTML_VALID,
339 },
340 HtmlElementInfo {
341 name: "map",
342 flags: HTML_INLINE | HTML_VALID,
343 },
344 HtmlElementInfo {
345 name: "nobr",
346 flags: HTML_INLINE | HTML_VALID,
347 },
348 HtmlElementInfo {
349 name: "object",
350 flags: HTML_INLINE | HTML_VALID,
351 },
352 HtmlElementInfo {
353 name: "q",
354 flags: HTML_INLINE | HTML_VALID,
355 },
356 HtmlElementInfo {
357 name: "rb",
358 flags: HTML_INLINE | HTML_VALID,
359 },
360 HtmlElementInfo {
361 name: "rbc",
362 flags: HTML_INLINE | HTML_VALID,
363 },
364 HtmlElementInfo {
365 name: "rp",
366 flags: HTML_INLINE | HTML_VALID,
367 },
368 HtmlElementInfo {
369 name: "rt",
370 flags: HTML_INLINE | HTML_VALID,
371 },
372 HtmlElementInfo {
373 name: "rtc",
374 flags: HTML_INLINE | HTML_VALID,
375 },
376 HtmlElementInfo {
377 name: "ruby",
378 flags: HTML_INLINE | HTML_VALID,
379 },
380 HtmlElementInfo {
381 name: "s",
382 flags: HTML_INLINE | HTML_VALID,
383 },
384 HtmlElementInfo {
385 name: "samp",
386 flags: HTML_INLINE | HTML_VALID,
387 },
388 HtmlElementInfo {
389 name: "select",
390 flags: HTML_INLINE | HTML_VALID,
391 },
392 HtmlElementInfo {
393 name: "small",
394 flags: HTML_INLINE | HTML_VALID,
395 },
396 HtmlElementInfo {
397 name: "span",
398 flags: HTML_INLINE | HTML_VALID,
399 },
400 HtmlElementInfo {
401 name: "strike",
402 flags: HTML_INLINE | HTML_VALID,
403 },
404 HtmlElementInfo {
405 name: "strong",
406 flags: HTML_INLINE | HTML_VALID,
407 },
408 HtmlElementInfo {
409 name: "sub",
410 flags: HTML_INLINE | HTML_VALID,
411 },
412 HtmlElementInfo {
413 name: "sup",
414 flags: HTML_INLINE | HTML_VALID,
415 },
416 HtmlElementInfo {
417 name: "textarea",
418 flags: HTML_INLINE | HTML_VALID,
419 },
420 HtmlElementInfo {
421 name: "tt",
422 flags: HTML_INLINE | HTML_VALID,
423 },
424 HtmlElementInfo {
425 name: "u",
426 flags: HTML_INLINE | HTML_VALID,
427 },
428 HtmlElementInfo {
429 name: "var",
430 flags: HTML_INLINE | HTML_VALID,
431 },
432 HtmlElementInfo {
434 name: "header",
435 flags: HTML_BLOCK | HTML_VALID,
436 },
437 HtmlElementInfo {
438 name: "footer",
439 flags: HTML_BLOCK | HTML_VALID,
440 },
441 HtmlElementInfo {
442 name: "nav",
443 flags: HTML_BLOCK | HTML_VALID,
444 },
445 HtmlElementInfo {
446 name: "article",
447 flags: HTML_BLOCK | HTML_VALID,
448 },
449 HtmlElementInfo {
450 name: "section",
451 flags: HTML_BLOCK | HTML_VALID,
452 },
453 HtmlElementInfo {
454 name: "aside",
455 flags: HTML_BLOCK | HTML_VALID,
456 },
457 HtmlElementInfo {
458 name: "main",
459 flags: HTML_BLOCK | HTML_VALID,
460 },
461 HtmlElementInfo {
462 name: "figure",
463 flags: HTML_BLOCK | HTML_VALID,
464 },
465 HtmlElementInfo {
466 name: "figcaption",
467 flags: HTML_BLOCK | HTML_VALID,
468 },
469 HtmlElementInfo {
470 name: "details",
471 flags: HTML_BLOCK | HTML_VALID,
472 },
473 HtmlElementInfo {
474 name: "summary",
475 flags: HTML_BLOCK | HTML_VALID,
476 },
477 HtmlElementInfo {
479 name: "script",
480 flags: HTML_HEAD | HTML_BLOCK | HTML_VALID,
481 },
482 HtmlElementInfo {
483 name: "style",
484 flags: HTML_HEAD | HTML_BLOCK | HTML_VALID,
485 },
486 HtmlElementInfo {
487 name: "title",
488 flags: HTML_HEAD | HTML_BLOCK | HTML_VALID,
489 },
490];
491
492fn html_tag_lookup(name: &str) -> Option<&'static HtmlElementInfo> {
495 let lower: Vec<u8> = name.bytes().map(|b| b.to_ascii_lowercase()).collect();
497 let lower_str = match core::str::from_utf8(&lower) {
498 Ok(s) => s,
499 Err(_) => return None,
500 };
501 HTML_ELEMENTS.iter().find(|info| info.name == lower_str)
502}
503
504const HTML_ENTITIES: &[(&str, &str)] = &[
511 ("nbsp", "\u{00a0}"),
512 ("lt", "<"),
513 ("gt", ">"),
514 ("amp", "&"),
515 ("quot", "\""),
516 ("apos", "'"),
517 ("copy", "\u{00a9}"),
518 ("reg", "\u{00ae}"),
519 ("amp", "&"),
520 ("iexcl", "\u{00a1}"),
521 ("cent", "\u{00a2}"),
522 ("pound", "\u{00a3}"),
523 ("curren", "\u{00a4}"),
524 ("yen", "\u{00a5}"),
525 ("brvbar", "\u{00a6}"),
526 ("sect", "\u{00a7}"),
527 ("uml", "\u{00a8}"),
528 ("ordf", "\u{00aa}"),
529 ("laquo", "\u{00ab}"),
530 ("not", "\u{00ac}"),
531 ("shy", "\u{00ad}"),
532 ("macr", "\u{00ae}"),
533 ("deg", "\u{00b0}"),
534 ("plusmn", "\u{00b1}"),
535 ("sup2", "\u{00b2}"),
536 ("sup3", "\u{00b3}"),
537 ("acute", "\u{00b4}"),
538 ("micro", "\u{00b5}"),
539 ("para", "\u{00b6}"),
540 ("middot", "\u{00b7}"),
541 ("cedil", "\u{00b8}"),
542 ("sup1", "\u{00b9}"),
543 ("ordm", "\u{00ba}"),
544 ("raquo", "\u{00bb}"),
545 ("frac14", "\u{00bc}"),
546 ("frac12", "\u{00bd}"),
547 ("frac34", "\u{00be}"),
548 ("iquest", "\u{00bf}"),
549 ("times", "\u{00d7}"),
550 ("divide", "\u{00f7}"),
551 ("ETH", "\u{00d0}"),
552 ("eth", "\u{00f0}"),
553 ("THORN", "\u{00de}"),
554 ("thorn", "\u{00fe}"),
555 ("AElig", "\u{00c6}"),
556 ("aelig", "\u{00e6}"),
557 ("OElig", "\u{0152}"),
558 ("oelig", "\u{0153}"),
559 ("Scaron", "\u{0160}"),
560 ("scaron", "\u{0161}"),
561 ("Yuml", "\u{0178}"),
562 ("circ", "\u{02c6}"),
563 ("tilde", "\u{02dc}"),
564 ("ensp", "\u{2002}"),
565 ("emsp", "\u{2003}"),
566 ("thinsp", "\u{2009}"),
567 ("zwnj", "\u{200c}"),
568 ("zwj", "\u{200d}"),
569 ("lrm", "\u{200e}"),
570 ("rlm", "\u{200f}"),
571 ("ndash", "\u{2013}"),
572 ("mdash", "\u{2014}"),
573 ("lsquo", "\u{2018}"),
574 ("rsquo", "\u{2019}"),
575 ("sbquo", "\u{201a}"),
576 ("ldquo", "\u{201c}"),
577 ("rdquo", "\u{201d}"),
578 ("bdquo", "\u{201e}"),
579 ("dagger", "\u{2020}"),
580 ("Dagger", "\u{2021}"),
581 ("bull", "\u{2022}"),
582 ("hellip", "\u{2026}"),
583 ("permil", "\u{2030}"),
584 ("prime", "\u{2032}"),
585 ("Prime", "\u{2033}"),
586 ("lsaquo", "\u{2039}"),
587 ("rsaquo", "\u{203a}"),
588 ("oline", "\u{203e}"),
589 ("euro", "\u{20ac}"),
590 ("trade", "\u{2122}"),
591 ("larr", "\u{2190}"),
592 ("uarr", "\u{2191}"),
593 ("rarr", "\u{2192}"),
594 ("darr", "\u{2193}"),
595 ("harr", "\u{2194}"),
596 ("crarr", "\u{21b5}"),
597 ("lceil", "\u{2308}"),
598 ("rceil", "\u{2309}"),
599 ("lfloor", "\u{230a}"),
600 ("rfloor", "\u{230b}"),
601 ("loz", "\u{25ca}"),
602 ("spades", "\u{2660}"),
603 ("clubs", "\u{2663}"),
604 ("hearts", "\u{2665}"),
605 ("diams", "\u{2666}"),
606 ("Alpha", "\u{0391}"),
607 ("Beta", "\u{0392}"),
608 ("Gamma", "\u{0393}"),
609 ("Delta", "\u{0394}"),
610 ("Epsilon", "\u{0395}"),
611 ("Zeta", "\u{0396}"),
612 ("Eta", "\u{0397}"),
613 ("Theta", "\u{0398}"),
614 ("Iota", "\u{0399}"),
615 ("Kappa", "\u{039a}"),
616 ("Lambda", "\u{039b}"),
617 ("Mu", "\u{039c}"),
618 ("Nu", "\u{039d}"),
619 ("Xi", "\u{039e}"),
620 ("Omicron", "\u{039f}"),
621 ("Pi", "\u{03a0}"),
622 ("Rho", "\u{03a1}"),
623 ("Sigma", "\u{03a3}"),
624 ("Tau", "\u{03a4}"),
625 ("Upsilon", "\u{03a5}"),
626 ("Phi", "\u{03a6}"),
627 ("Chi", "\u{03a7}"),
628 ("Psi", "\u{03a8}"),
629 ("Omega", "\u{03a9}"),
630 ("alpha", "\u{03b1}"),
631 ("beta", "\u{03b2}"),
632 ("gamma", "\u{03b3}"),
633 ("delta", "\u{03b4}"),
634 ("epsilon", "\u{03b5}"),
635 ("zeta", "\u{03b6}"),
636 ("eta", "\u{03b7}"),
637 ("theta", "\u{03b8}"),
638 ("iota", "\u{03b9}"),
639 ("kappa", "\u{03ba}"),
640 ("lambda", "\u{03bb}"),
641 ("mu", "\u{03bc}"),
642 ("nu", "\u{03bd}"),
643 ("xi", "\u{03be}"),
644 ("omicron", "\u{03bf}"),
645 ("pi", "\u{03c0}"),
646 ("rho", "\u{03c1}"),
647 ("sigmaf", "\u{03c2}"),
648 ("sigma", "\u{03c3}"),
649 ("tau", "\u{03c4}"),
650 ("upsilon", "\u{03c5}"),
651 ("phi", "\u{03c6}"),
652 ("chi", "\u{03c7}"),
653 ("psi", "\u{03c8}"),
654 ("omega", "\u{03c9}"),
655 ("thetasym", "\u{03d1}"),
656 ("upsih", "\u{03d2}"),
657 ("piv", "\u{03d6}"),
658];
659
660fn html_entity_lookup(name: &str) -> Option<&'static str> {
663 HTML_ENTITIES
664 .iter()
665 .find(|(n, _)| *n == name)
666 .map(|(_, v)| *v)
667}
668
669struct HtmlParserCtxt {
675 doc: *mut _xmlDoc,
677 current: *mut _xmlNode,
679 html: *mut _xmlNode,
681 head: *mut _xmlNode,
683 body: *mut _xmlNode,
685 in_head: bool,
687 in_body: bool,
689 html_created: bool,
691 head_created: bool,
693 body_created: bool,
695 seen_body_content: bool,
697 input: *mut u8,
699 input_pos: usize,
701 input_len: usize,
703 line: c_int,
705 #[allow(dead_code)]
707 err: bool,
708 filename: *mut c_char,
710 encoding: *mut c_char,
712}
713
714impl HtmlParserCtxt {
715 const fn new() -> Self {
716 HtmlParserCtxt {
717 doc: ptr::null_mut(),
718 current: ptr::null_mut(),
719 html: ptr::null_mut(),
720 head: ptr::null_mut(),
721 body: ptr::null_mut(),
722 in_head: false,
723 in_body: false,
724 html_created: false,
725 head_created: false,
726 body_created: false,
727 seen_body_content: false,
728 input: ptr::null_mut(),
729 input_pos: 0,
730 input_len: 0,
731 line: 1,
732 err: false,
733 filename: ptr::null_mut(),
734 encoding: ptr::null_mut(),
735 }
736 }
737
738 fn peek(&self) -> Option<u8> {
740 if self.input_pos < self.input_len {
741 unsafe { Some(*self.input.add(self.input_pos)) }
742 } else {
743 None
744 }
745 }
746
747 fn peek_at(&self, offset: usize) -> Option<u8> {
749 let pos = self.input_pos + offset;
750 if pos < self.input_len {
751 unsafe { Some(*self.input.add(pos)) }
752 } else {
753 None
754 }
755 }
756
757 fn next(&mut self) -> Option<u8> {
759 if self.input_pos < self.input_len {
760 let ch = unsafe { *self.input.add(self.input_pos) };
761 self.input_pos += 1;
762 if ch == b'\n' {
763 self.line += 1;
764 }
765 Some(ch)
766 } else {
767 None
768 }
769 }
770
771 fn skip_while<F: Fn(u8) -> bool>(&mut self, f: F) {
773 while let Some(ch) = self.peek() {
774 if f(ch) {
775 self.next();
776 } else {
777 break;
778 }
779 }
780 }
781
782 fn skip_whitespace(&mut self) {
784 self.skip_while(|ch| ch == b' ' || ch == b'\t' || ch == b'\n' || ch == b'\r');
785 }
786
787 const fn is_eof(&self) -> bool {
789 self.input_pos >= self.input_len
790 }
791
792 fn read_while<F: Fn(u8) -> bool>(&mut self, f: F) -> Vec<u8> {
794 let start = self.input_pos;
795 while let Some(ch) = self.peek() {
796 if f(ch) {
797 self.next();
798 } else {
799 break;
800 }
801 }
802 unsafe { slice::from_raw_parts(self.input.add(start), self.input_pos - start).to_vec() }
803 }
804}
805
806fn is_heading(name: &str) -> bool {
812 matches!(name, "h1" | "h2" | "h3" | "h4" | "h5" | "h6")
813}
814
815#[allow(dead_code)]
818unsafe fn get_parent_element(node: *mut _xmlNode) -> *mut _xmlNode {
819 if node.is_null() {
820 return ptr::null_mut();
821 }
822 let mut n = node;
823 loop {
824 let parent = unsafe { (*n).parent };
825 if parent.is_null() {
826 return ptr::null_mut();
827 }
828 let ptype = unsafe { (*parent).type_ };
829 if ptype == XML_ELEMENT_NODE as c_int
830 || ptype == XML_HTML_DOCUMENT_NODE as c_int
831 || ptype == XML_DOCUMENT_NODE as c_int
832 {
833 return parent;
834 }
835 n = parent;
836 }
837}
838
839unsafe fn auto_close_element(ctxt: &mut HtmlParserCtxt, tag_name: &str) {
842 let tag_lower: Vec<u8> = tag_name.bytes().map(|b| b.to_ascii_lowercase()).collect();
843 let tag_lower_str = match core::str::from_utf8(&tag_lower) {
844 Ok(s) => s,
845 Err(_) => return,
846 };
847
848 let info = html_tag_lookup(tag_lower_str);
849
850 let mut current = ctxt.current;
851
852 let mut open_names: Vec<Vec<u8>> = Vec::new();
854 let mut cur = current;
855 while !cur.is_null() {
856 let ctype = unsafe { (*cur).type_ };
857 if ctype == XML_ELEMENT_NODE as c_int && !unsafe { (*cur).name.is_null() } {
858 let name_bytes = unsafe { xmlstr_to_bytes((*cur).name) };
859 open_names.push(name_bytes.to_vec());
860 }
861 cur = unsafe { (*cur).parent };
862 }
863
864 if tag_lower_str == "p" || info.is_some_and(|i| i.flags & HTML_BLOCK != 0) {
866 let mut cur2 = current;
868 while !cur2.is_null() {
869 let ctype = unsafe { (*cur2).type_ };
870 if ctype == XML_ELEMENT_NODE as c_int && !unsafe { (*cur2).name.is_null() } {
871 let name_bytes = unsafe { xmlstr_to_bytes((*cur2).name) };
872 let name_str = core::str::from_utf8(name_bytes).unwrap_or("");
873 if name_str.eq_ignore_ascii_case("p") {
874 current = unsafe { (*cur2).parent };
876 break;
877 }
878 }
879 cur2 = unsafe { (*cur2).parent };
880 }
881 }
882
883 if is_heading(tag_lower_str) {
885 let mut cur2 = current;
886 while !cur2.is_null() {
887 let ctype = unsafe { (*cur2).type_ };
888 if ctype == XML_ELEMENT_NODE as c_int && !unsafe { (*cur2).name.is_null() } {
889 let name_bytes = unsafe { xmlstr_to_bytes((*cur2).name) };
890 let name_str = core::str::from_utf8(name_bytes).unwrap_or("");
891 if is_heading(name_str) {
892 current = unsafe { (*cur2).parent };
893 break;
894 }
895 }
896 cur2 = unsafe { (*cur2).parent };
897 }
898 }
899
900 if tag_lower_str == "li" {
902 let mut cur2 = current;
903 while !cur2.is_null() {
904 let ctype = unsafe { (*cur2).type_ };
905 if ctype == XML_ELEMENT_NODE as c_int && !unsafe { (*cur2).name.is_null() } {
906 let name_bytes = unsafe { xmlstr_to_bytes((*cur2).name) };
907 let name_str = core::str::from_utf8(name_bytes).unwrap_or("");
908 if name_str.eq_ignore_ascii_case("li") {
909 current = unsafe { (*cur2).parent };
910 break;
911 }
912 }
913 cur2 = unsafe { (*cur2).parent };
914 }
915 }
916
917 if tag_lower_str == "dt" || tag_lower_str == "dd" {
919 let mut cur2 = current;
920 while !cur2.is_null() {
921 let ctype = unsafe { (*cur2).type_ };
922 if ctype == XML_ELEMENT_NODE as c_int && !unsafe { (*cur2).name.is_null() } {
923 let name_bytes = unsafe { xmlstr_to_bytes((*cur2).name) };
924 let name_str = core::str::from_utf8(name_bytes).unwrap_or("");
925 if name_str == "dt" || name_str == "dd" {
926 current = unsafe { (*cur2).parent };
927 break;
928 }
929 }
930 cur2 = unsafe { (*cur2).parent };
931 }
932 }
933
934 if tag_lower_str == "tr" {
939 let mut cur2 = current;
940 while !cur2.is_null() {
941 let ctype = unsafe { (*cur2).type_ };
942 if ctype == XML_ELEMENT_NODE as c_int && !unsafe { (*cur2).name.is_null() } {
943 let name_bytes = unsafe { xmlstr_to_bytes((*cur2).name) };
944 let name_str = core::str::from_utf8(name_bytes).unwrap_or("");
945 if name_str == "tr" || name_str == "td" || name_str == "th" {
946 current = unsafe { (*cur2).parent };
947 break;
948 }
949 }
950 cur2 = unsafe { (*cur2).parent };
951 }
952 } else if tag_lower_str == "td" || tag_lower_str == "th" {
953 let mut cur2 = current;
954 while !cur2.is_null() {
955 let ctype = unsafe { (*cur2).type_ };
956 if ctype == XML_ELEMENT_NODE as c_int && !unsafe { (*cur2).name.is_null() } {
957 let name_bytes = unsafe { xmlstr_to_bytes((*cur2).name) };
958 let name_str = core::str::from_utf8(name_bytes).unwrap_or("");
959 if name_str == "td" || name_str == "th" {
960 current = unsafe { (*cur2).parent };
961 break;
962 }
963 }
964 cur2 = unsafe { (*cur2).parent };
965 }
966 }
967
968 if matches!(tag_lower_str, "thead" | "tbody" | "tfoot") {
970 let mut cur2 = current;
971 while !cur2.is_null() {
972 let ctype = unsafe { (*cur2).type_ };
973 if ctype == XML_ELEMENT_NODE as c_int && !unsafe { (*cur2).name.is_null() } {
974 let name_bytes = unsafe { xmlstr_to_bytes((*cur2).name) };
975 let name_str = core::str::from_utf8(name_bytes).unwrap_or("");
976 if name_str == "thead" || name_str == "tbody" || name_str == "tfoot" {
977 current = unsafe { (*cur2).parent };
978 break;
979 }
980 }
981 cur2 = unsafe { (*cur2).parent };
982 }
983 }
984
985 if tag_lower_str == "colgroup" {
987 let mut cur2 = current;
988 while !cur2.is_null() {
989 let ctype = unsafe { (*cur2).type_ };
990 if ctype == XML_ELEMENT_NODE as c_int && !unsafe { (*cur2).name.is_null() } {
991 let name_bytes = unsafe { xmlstr_to_bytes((*cur2).name) };
992 let name_str = core::str::from_utf8(name_bytes).unwrap_or("");
993 if name_str == "colgroup" {
994 current = unsafe { (*cur2).parent };
995 break;
996 }
997 }
998 cur2 = unsafe { (*cur2).parent };
999 }
1000 }
1001
1002 if tag_lower_str == "caption" {
1004 let mut cur2 = current;
1005 while !cur2.is_null() {
1006 let ctype = unsafe { (*cur2).type_ };
1007 if ctype == XML_ELEMENT_NODE as c_int && !unsafe { (*cur2).name.is_null() } {
1008 let name_bytes = unsafe { xmlstr_to_bytes((*cur2).name) };
1009 let name_str = core::str::from_utf8(name_bytes).unwrap_or("");
1010 if name_str == "caption" {
1011 current = unsafe { (*cur2).parent };
1012 break;
1013 }
1014 }
1015 cur2 = unsafe { (*cur2).parent };
1016 }
1017 }
1018
1019 if tag_lower_str == "form" {
1021 let mut cur2 = current;
1022 while !cur2.is_null() {
1023 let ctype = unsafe { (*cur2).type_ };
1024 if ctype == XML_ELEMENT_NODE as c_int && !unsafe { (*cur2).name.is_null() } {
1025 let name_bytes = unsafe { xmlstr_to_bytes((*cur2).name) };
1026 let name_str = core::str::from_utf8(name_bytes).unwrap_or("");
1027 if name_str.eq_ignore_ascii_case("form") {
1028 current = unsafe { (*cur2).parent };
1029 break;
1030 }
1031 }
1032 cur2 = unsafe { (*cur2).parent };
1033 }
1034 }
1035
1036 ctxt.current = current;
1037}
1038
1039unsafe fn ensure_html(ctxt: &mut HtmlParserCtxt) -> *mut _xmlNode {
1045 if !ctxt.html.is_null() {
1046 return ctxt.html;
1047 }
1048
1049 let html_node = tree::new_node(ptr::null_mut(), b"html\0" as *const u8 as *const xmlChar);
1050 if html_node.is_null() {
1051 return ptr::null_mut();
1052 }
1053 {
1054 }
1057 ctxt.html = html_node;
1058 ctxt.html_created = true;
1059
1060 tree::add_child(ctxt.doc as *mut _xmlNode, html_node);
1062 ctxt.current = html_node;
1063
1064 html_node
1065}
1066
1067unsafe fn ensure_head(ctxt: &mut HtmlParserCtxt) -> *mut _xmlNode {
1069 if !ctxt.head.is_null() {
1070 return ctxt.head;
1071 }
1072
1073 ensure_html(ctxt);
1075
1076 let head_node = tree::new_node(ptr::null_mut(), b"head\0" as *const u8 as *const xmlChar);
1077 if head_node.is_null() {
1078 return ptr::null_mut();
1079 }
1080 ctxt.head = head_node;
1081 ctxt.head_created = true;
1082
1083 tree::add_child(ctxt.html, head_node);
1085 ctxt.current = head_node;
1086 ctxt.in_head = true;
1087
1088 head_node
1089}
1090
1091unsafe fn ensure_body(ctxt: &mut HtmlParserCtxt) -> *mut _xmlNode {
1093 if !ctxt.body.is_null() {
1094 return ctxt.body;
1095 }
1096
1097 ensure_html(ctxt);
1099
1100 let body_node = tree::new_node(ptr::null_mut(), b"body\0" as *const u8 as *const xmlChar);
1101 if body_node.is_null() {
1102 return ptr::null_mut();
1103 }
1104 ctxt.body = body_node;
1105 ctxt.body_created = true;
1106
1107 tree::add_child(ctxt.html, body_node);
1109 ctxt.current = body_node;
1110 ctxt.in_body = true;
1111
1112 body_node
1113}
1114
1115#[allow(dead_code)]
1117unsafe fn transition_to_body(ctxt: &mut HtmlParserCtxt) {
1118 if ctxt.in_head && !ctxt.seen_body_content {
1119 ctxt.seen_body_content = true;
1120 ctxt.in_head = false;
1121 ensure_body(ctxt);
1122 }
1123}
1124
1125struct HtmlAttr {
1131 name: Vec<u8>,
1132 value: Vec<u8>,
1133 #[allow(dead_code)]
1134 quoted: bool,
1135}
1136
1137fn parse_attr_name(ctxt: &mut HtmlParserCtxt) -> Vec<u8> {
1139 let mut name = Vec::new();
1140 while let Some(ch) = ctxt.peek() {
1141 if ch == b'='
1142 || ch == b'>'
1143 || ch == b'/'
1144 || ch == b' '
1145 || ch == b'\t'
1146 || ch == b'\n'
1147 || ch == b'\r'
1148 {
1149 break;
1150 }
1151 name.push(ch);
1152 ctxt.next();
1153 }
1154 name
1155}
1156
1157fn parse_attr_value(ctxt: &mut HtmlParserCtxt) -> (Vec<u8>, bool) {
1159 ctxt.skip_whitespace();
1160
1161 let quote = match ctxt.peek() {
1162 Some(b'"') => {
1163 ctxt.next(); b'"'
1165 }
1166 Some(b'\'') => {
1167 ctxt.next(); b'\''
1169 }
1170 _ => {
1171 let value = ctxt.read_while(|ch| {
1173 ch != b'>' && ch != b' ' && ch != b'\t' && ch != b'\n' && ch != b'\r'
1174 });
1175 return (value, false);
1176 }
1177 };
1178
1179 let mut value = Vec::new();
1181 loop {
1182 match ctxt.next() {
1183 Some(ch) if ch == quote => break,
1184 Some(ch) => value.push(ch),
1185 None => break,
1186 }
1187 }
1188 (value, true)
1189}
1190
1191fn parse_attributes(ctxt: &mut HtmlParserCtxt) -> Vec<HtmlAttr> {
1193 let mut attrs = Vec::new();
1194
1195 loop {
1196 ctxt.skip_whitespace();
1197
1198 match ctxt.peek() {
1199 Some(b'>') | None => break,
1200 Some(b'/')
1201 if ctxt.peek_at(1) == Some(b'>') => {
1203 break;
1204 }
1205 _ => {}
1207 }
1208
1209 let name = parse_attr_name(ctxt);
1210 if name.is_empty() {
1211 break;
1212 }
1213
1214 ctxt.skip_whitespace();
1216 if ctxt.peek() == Some(b'=') {
1217 ctxt.next(); let (value, quoted) = parse_attr_value(ctxt);
1219 attrs.push(HtmlAttr {
1220 name,
1221 value,
1222 quoted,
1223 });
1224 } else {
1225 attrs.push(HtmlAttr {
1227 name,
1228 value: Vec::new(),
1229 quoted: false,
1230 });
1231 }
1232 }
1233
1234 attrs
1235}
1236
1237fn resolve_entity(name: &str) -> Vec<u8> {
1243 if let Some(replacement) = html_entity_lookup(name) {
1244 replacement.as_bytes().to_vec()
1245 } else {
1246 let mut result = Vec::new();
1248 result.push(b'&');
1249 result.extend_from_slice(name.as_bytes());
1250 result.push(b';');
1251 result
1252 }
1253}
1254
1255fn resolve_numeric_entity(value: &str, is_hex: bool) -> Vec<u8> {
1257 let codepoint = if is_hex {
1258 u32::from_str_radix(value, 16).unwrap_or(0xFFFD)
1259 } else {
1260 value.parse::<u32>().unwrap_or(0xFFFD)
1261 };
1262
1263 if codepoint == 0 {
1264 return Vec::new();
1265 }
1266
1267 match char::from_u32(codepoint) {
1269 Some(c) => {
1270 let mut buf = [0u8; 4];
1271 let s = c.encode_utf8(&mut buf);
1272 s.as_bytes().to_vec()
1273 }
1274 None => vec![0xEF, 0xBF, 0xBD], }
1276}
1277
1278fn parse_entity(ctxt: &mut HtmlParserCtxt) -> Vec<u8> {
1281 match ctxt.peek() {
1283 Some(b'&') => {
1284 ctxt.next(); }
1286 _ => return vec![b'&'],
1287 }
1288
1289 if ctxt.peek() == Some(b'#') {
1291 ctxt.next(); let is_hex = ctxt.peek() == Some(b'x') || ctxt.peek() == Some(b'X');
1293 if is_hex {
1294 ctxt.next(); }
1296
1297 let digits = ctxt.read_while(|ch| {
1298 if is_hex {
1299 ch.is_ascii_hexdigit()
1300 } else {
1301 ch.is_ascii_digit()
1302 }
1303 });
1304
1305 let digits_str = core::str::from_utf8(&digits).unwrap_or("");
1306 if digits_str.is_empty() {
1307 let mut result = vec![b'&', b'#'];
1308 if is_hex {
1309 result.push(b'x');
1310 }
1311 return result;
1312 }
1313
1314 if ctxt.peek() == Some(b';') {
1316 ctxt.next();
1317 }
1318
1319 return resolve_numeric_entity(digits_str, is_hex);
1320 }
1321
1322 let name = ctxt.read_while(|ch| ch.is_ascii_alphanumeric() || ch == b'_' || ch == b'-');
1324 let name_str = core::str::from_utf8(&name).unwrap_or("");
1325
1326 if ctxt.peek() == Some(b';') {
1328 ctxt.next();
1329 }
1330
1331 resolve_entity(name_str)
1332}
1333
1334unsafe fn handle_text(ctxt: &mut HtmlParserCtxt, text: &[u8]) {
1336 if text.is_empty() {
1337 return;
1338 }
1339
1340 let parent = if ctxt.in_head {
1342 ctxt.head
1343 } else if ctxt.in_body || ctxt.body_created {
1344 ctxt.body
1345 } else if ctxt.html_created {
1346 ctxt.html
1347 } else {
1348 ctxt.doc as *mut _xmlNode
1349 };
1350
1351 let insertion_point = if ctxt.current.is_null() {
1352 parent
1353 } else {
1354 ctxt.current
1355 };
1356
1357 if insertion_point.is_null() {
1358 let text_node = tree::new_text(ptr::null_mut());
1360 if !text_node.is_null() {
1361 let content = bytes_to_xmlstr(text);
1363 if !content.is_null() {
1364 unsafe {
1365 (*text_node).content = content;
1366 }
1367 }
1368 tree::add_child(ctxt.doc as *mut _xmlNode, text_node);
1369 }
1370 return;
1371 }
1372
1373 if insertion_point == ctxt.doc as *mut _xmlNode {
1378 if text.iter().all(|b| b.is_ascii_whitespace()) {
1379 return;
1380 }
1381 let content = trim_ascii_start(text);
1382 let html_node = tree::new_node(ptr::null_mut(), bytes_to_xmlstr(b"html"));
1383 if !html_node.is_null() {
1384 let content_c = bytes_to_xmlstr(content);
1385 let text_node = tree::new_text(ptr::null_mut());
1386 if !text_node.is_null() {
1387 unsafe {
1388 (*text_node).content = content_c;
1389 }
1390 tree::add_child(html_node, text_node);
1391 }
1392 tree::add_child(ctxt.doc as *mut _xmlNode, html_node);
1393 ctxt.html = html_node;
1394 ctxt.html_created = true;
1395 ctxt.current = html_node;
1396 }
1397 return;
1398 }
1399
1400 let text_node = tree::new_text(ptr::null_mut());
1401 if text_node.is_null() {
1402 return;
1403 }
1404
1405 let content = bytes_to_xmlstr(text);
1407 if !content.is_null() {
1408 unsafe {
1409 (*text_node).content = content;
1410 }
1411 }
1412
1413 tree::add_child(insertion_point, text_node);
1414}
1415
1416unsafe fn handle_start_tag(ctxt: &mut HtmlParserCtxt, tag_name: &[u8], attrs: &[HtmlAttr]) {
1418 let tag_lower: Vec<u8> = tag_name.iter().map(|b| b.to_ascii_lowercase()).collect();
1419 let tag_str = core::str::from_utf8(&tag_lower).unwrap_or("");
1420
1421 let info = html_tag_lookup(tag_str);
1422
1423 let is_head_tag = info.is_some_and(|i| i.flags & HTML_HEAD != 0);
1425 let _is_body_tag = info.is_some_and(|i| i.flags & HTML_BODY != 0);
1426 let is_empty = info.is_some_and(|i| i.flags & HTML_EMPTY != 0);
1427 let _is_block = info.is_some_and(|i| i.flags & HTML_BLOCK != 0);
1428
1429 if tag_str == "html" {
1431 if !ctxt.html.is_null() && !ctxt.html_created {
1432 return;
1434 }
1435 if ctxt.html.is_null() {
1437 let html_node = tree::new_node(ptr::null_mut(), bytes_to_xmlstr(tag_name));
1438 if !html_node.is_null() {
1439 ctxt.html = html_node;
1440 ctxt.html_created = false; tree::add_child(ctxt.doc as *mut _xmlNode, html_node);
1442 ctxt.current = html_node;
1443 }
1444 } else {
1445 ctxt.current = ctxt.html;
1447 }
1448 return;
1449 }
1450
1451 if tag_str == "head" {
1452 if !ctxt.head.is_null() && !ctxt.head_created {
1453 return;
1455 }
1456 ensure_html(ctxt);
1458
1459 if ctxt.head.is_null() {
1460 let head_node = tree::new_node(ptr::null_mut(), bytes_to_xmlstr(tag_name));
1461 if !head_node.is_null() {
1462 ctxt.head = head_node;
1463 ctxt.head_created = false;
1464 tree::add_child(ctxt.html, head_node);
1465 ctxt.current = head_node;
1466 ctxt.in_head = true;
1467 }
1468 } else {
1469 ctxt.current = ctxt.head;
1470 ctxt.in_head = true;
1471 }
1472 return;
1473 }
1474
1475 if tag_str == "body" {
1476 if !ctxt.body.is_null() && !ctxt.body_created {
1477 return;
1479 }
1480 ensure_html(ctxt);
1482
1483 if ctxt.body.is_null() {
1484 let body_node = tree::new_node(ptr::null_mut(), bytes_to_xmlstr(tag_name));
1485 if !body_node.is_null() {
1486 ctxt.body = body_node;
1487 ctxt.body_created = false;
1488 tree::add_child(ctxt.html, body_node);
1489 ctxt.current = body_node;
1490 ctxt.in_body = true;
1491 ctxt.in_head = false;
1492 ctxt.seen_body_content = true;
1493 }
1494 } else {
1495 ctxt.current = ctxt.body;
1496 ctxt.in_body = true;
1497 ctxt.in_head = false;
1498 ctxt.seen_body_content = true;
1499 }
1500 return;
1501 }
1502
1503 if is_head_tag && !ctxt.seen_body_content {
1505 if ctxt.head.is_null() {
1506 ensure_head(ctxt);
1507 }
1508
1509 if is_empty {
1510 let node = tree::new_node(ptr::null_mut(), bytes_to_xmlstr(tag_name));
1512 if !node.is_null() {
1513 for attr in attrs {
1514 let name_c = bytes_to_xmlstr(&attr.name);
1515 let val_c = bytes_to_xmlstr(&attr.value);
1516 if !name_c.is_null() {
1517 tree::set_prop(node, name_c, val_c);
1518 xmlFreeImpl(name_c as *mut c_void);
1519 if !val_c.is_null() {
1520 xmlFreeImpl(val_c as *mut c_void);
1521 }
1522 }
1523 }
1524 tree::add_child(ctxt.current, node);
1525 }
1526 return;
1527 }
1528
1529 let node = tree::new_node(ptr::null_mut(), bytes_to_xmlstr(tag_name));
1530 if !node.is_null() {
1531 for attr in attrs {
1532 let name_c = bytes_to_xmlstr(&attr.name);
1533 let val_c = bytes_to_xmlstr(&attr.value);
1534 if !name_c.is_null() {
1535 tree::set_prop(node, name_c, val_c);
1536 xmlFreeImpl(name_c as *mut c_void);
1537 if !val_c.is_null() {
1538 xmlFreeImpl(val_c as *mut c_void);
1539 }
1540 }
1541 }
1542 tree::add_child(ctxt.current, node);
1543 ctxt.current = node;
1544 }
1545 return;
1546 }
1547
1548 if !is_head_tag || ctxt.seen_body_content {
1550 if !ctxt.seen_body_content {
1551 ctxt.seen_body_content = true;
1552 ctxt.in_head = false;
1553 if ctxt.body.is_null() {
1554 ensure_body(ctxt);
1555 } else {
1556 ctxt.current = ctxt.body;
1557 ctxt.in_body = true;
1558 }
1559 } else if ctxt.body.is_null() {
1560 ensure_body(ctxt);
1561 }
1562 }
1563
1564 if !ctxt.current.is_null() {
1566 auto_close_element(ctxt, tag_str);
1567 }
1568
1569 if is_empty {
1570 let node = tree::new_node(ptr::null_mut(), bytes_to_xmlstr(tag_name));
1572 if !node.is_null() {
1573 for attr in attrs {
1574 let name_c = bytes_to_xmlstr(&attr.name);
1575 let val_c = bytes_to_xmlstr(&attr.value);
1576 if !name_c.is_null() {
1577 tree::set_prop(node, name_c, val_c);
1578 xmlFreeImpl(name_c as *mut c_void);
1579 if !val_c.is_null() {
1580 xmlFreeImpl(val_c as *mut c_void);
1581 }
1582 }
1583 }
1584 let insertion_point = if ctxt.current.is_null() {
1585 ctxt.body
1586 } else {
1587 ctxt.current
1588 };
1589 if !insertion_point.is_null() {
1590 tree::add_child(insertion_point, node);
1591 }
1592 }
1593 return;
1594 }
1595
1596 let node = tree::new_node(ptr::null_mut(), bytes_to_xmlstr(tag_name));
1598 if !node.is_null() {
1599 for attr in attrs {
1600 let name_c = bytes_to_xmlstr(&attr.name);
1601 let val_c = bytes_to_xmlstr(&attr.value);
1602 if !name_c.is_null() {
1603 tree::set_prop(node, name_c, val_c);
1604 xmlFreeImpl(name_c as *mut c_void);
1605 if !val_c.is_null() {
1606 xmlFreeImpl(val_c as *mut c_void);
1607 }
1608 }
1609 }
1610
1611 let insertion_point = if ctxt.current.is_null() {
1612 if ctxt.in_body || ctxt.body_created {
1613 ctxt.body
1614 } else if ctxt.in_head || ctxt.head_created {
1615 ctxt.head
1616 } else if ctxt.html_created {
1617 ctxt.html
1618 } else {
1619 ctxt.doc as *mut _xmlNode
1620 }
1621 } else {
1622 ctxt.current
1623 };
1624
1625 if !insertion_point.is_null() {
1626 tree::add_child(insertion_point, node);
1627 ctxt.current = node;
1629 }
1630 }
1631}
1632
1633unsafe fn handle_end_tag(ctxt: &mut HtmlParserCtxt, tag_name: &[u8]) {
1635 let tag_lower: Vec<u8> = tag_name.iter().map(|b| b.to_ascii_lowercase()).collect();
1636 let tag_str = core::str::from_utf8(&tag_lower).unwrap_or("");
1637
1638 let info = html_tag_lookup(tag_str);
1639
1640 if info.is_some_and(|i| i.flags & HTML_EMPTY != 0) {
1643 return;
1644 }
1645
1646 if tag_str == "html" {
1647 ctxt.current = ctxt.doc as *mut _xmlNode;
1648 return;
1649 }
1650
1651 if tag_str == "head" {
1652 ctxt.in_head = false;
1653 ctxt.current = ctxt.html;
1654 return;
1655 }
1656
1657 if tag_str == "body" {
1658 ctxt.in_body = false;
1659 ctxt.current = ctxt.html;
1660 return;
1661 }
1662
1663 let mut cur = ctxt.current;
1665 while !cur.is_null() {
1666 let ctype = unsafe { (*cur).type_ };
1667 if ctype == XML_ELEMENT_NODE as c_int && !unsafe { (*cur).name.is_null() } {
1668 let name_bytes = unsafe { xmlstr_to_bytes((*cur).name) };
1669 if name_bytes.eq_ignore_ascii_case(tag_name) {
1670 ctxt.current = unsafe { (*cur).parent };
1672 return;
1673 }
1674 }
1675 cur = unsafe { (*cur).parent };
1676 }
1677
1678 }
1680
1681unsafe fn html_parse_buffer(
1691 ctxt: &mut HtmlParserCtxt,
1692 buffer: *const c_char,
1693 size: c_int,
1694) -> *mut _xmlDoc {
1695 if buffer.is_null() || size <= 0 {
1696 return ptr::null_mut();
1697 }
1698
1699 let doc = tree::new_doc(ptr::null());
1701 if doc.is_null() {
1702 return ptr::null_mut();
1703 }
1704 unsafe {
1705 (*doc).type_ = XML_HTML_DOCUMENT_NODE as c_int;
1706 if !(*doc).version.is_null() {
1709 crate::abi::allocator::xmlFreeImpl((*doc).version as *mut c_void);
1710 }
1711 (*doc).version = ptr::null_mut();
1712 (*doc).properties = XML_DOC_WELLFORMED as c_int;
1713 (*doc).standalone = 1;
1716 }
1717 let default_dtd = crate::xml::dtd::create_int_subset(
1723 doc,
1724 b"html\0" as *const u8 as *const xmlChar,
1725 b"-//W3C//DTD HTML 4.0 Transitional//EN\0" as *const u8 as *const xmlChar,
1726 b"http://www.w3.org/TR/REC-html40/loose.dtd\0" as *const u8 as *const xmlChar,
1727 );
1728 let _ = default_dtd;
1729 ctxt.doc = doc;
1730
1731 ctxt.input = buffer as *mut u8;
1733 ctxt.input_len = size as usize;
1734 ctxt.input_pos = 0;
1735 ctxt.line = 1;
1736
1737 loop {
1739 if ctxt.is_eof() {
1740 break;
1741 }
1742
1743 let ch = ctxt.peek().unwrap_or(0);
1744
1745 if ch == b'<' {
1746 ctxt.next(); if ctxt.peek() == Some(b'/') {
1750 ctxt.next(); let tag_name = ctxt.read_while(|ch| {
1752 ch != b'>' && ch != b' ' && ch != b'\t' && ch != b'\n' && ch != b'\r'
1753 });
1754
1755 while ctxt.peek() != Some(b'>') && !ctxt.is_eof() {
1757 ctxt.next();
1758 }
1759 if ctxt.peek() == Some(b'>') {
1760 ctxt.next(); }
1762
1763 if !tag_name.is_empty() {
1764 handle_end_tag(ctxt, &tag_name);
1765 }
1766 continue;
1767 }
1768
1769 if ctxt.peek() == Some(b'!')
1771 && ctxt.peek_at(1) == Some(b'-')
1772 && ctxt.peek_at(2) == Some(b'-')
1773 {
1774 ctxt.next(); ctxt.next(); ctxt.next(); let mut comment_content = Vec::new();
1780 loop {
1781 if ctxt.peek() == Some(b'-')
1782 && ctxt.peek_at(1) == Some(b'-')
1783 && ctxt.peek_at(2) == Some(b'>')
1784 {
1785 ctxt.next(); ctxt.next(); ctxt.next(); break;
1789 }
1790 match ctxt.next() {
1791 Some(ch) => comment_content.push(ch),
1792 None => break,
1793 }
1794 }
1795
1796 if !comment_content.is_empty() {
1798 let comment_node = tree::new_comment(bytes_to_xmlstr(&comment_content));
1799 if !comment_node.is_null() {
1800 let insertion_point = if !ctxt.current.is_null() {
1801 ctxt.current
1802 } else {
1803 ctxt.doc as *mut _xmlNode
1804 };
1805 tree::add_child(insertion_point, comment_node);
1806 }
1807 }
1808 continue;
1809 }
1810
1811 if ctxt.peek() == Some(b'!') {
1813 ctxt.next(); let _rest = ctxt.read_while(|ch| ch != b'>');
1815 if ctxt.peek() == Some(b'>') {
1816 ctxt.next(); }
1818 continue;
1821 }
1822
1823 if ctxt.peek() == Some(b'?') {
1825 ctxt.next(); let mut pi_content = Vec::new();
1828 loop {
1829 if ctxt.peek() == Some(b'?') && ctxt.peek_at(1) == Some(b'>') {
1830 break;
1831 }
1832 match ctxt.next() {
1833 Some(ch) => pi_content.push(ch),
1834 None => break,
1835 }
1836 }
1837 if ctxt.peek() == Some(b'?') {
1839 ctxt.next();
1840 }
1841 if ctxt.peek() == Some(b'>') {
1842 ctxt.next();
1843 }
1844 if !pi_content.is_empty() {
1846 let mut parts = pi_content.splitn(2, |b| *b == b' ');
1848 let target = parts.next().unwrap_or(&pi_content);
1849 let value = parts.next().unwrap_or(b"");
1850
1851 let pi_node = tree::new_pi(bytes_to_xmlstr(target), bytes_to_xmlstr(value));
1852 if !pi_node.is_null() {
1853 let insertion_point = if !ctxt.current.is_null() {
1854 ctxt.current
1855 } else {
1856 ctxt.doc as *mut _xmlNode
1857 };
1858 tree::add_child(insertion_point, pi_node);
1859 }
1860 }
1861 continue;
1862 }
1863
1864 let tag_name = ctxt.read_while(|ch| {
1866 ch != b'>' && ch != b'/' && ch != b' ' && ch != b'\t' && ch != b'\n' && ch != b'\r'
1867 });
1868
1869 if tag_name.is_empty() {
1870 handle_text(ctxt, b"<");
1872 continue;
1873 }
1874
1875 let attrs = parse_attributes(ctxt);
1877
1878 if ctxt.peek() == Some(b'/') {
1880 ctxt.next(); if ctxt.peek() == Some(b'>') {
1882 ctxt.next(); }
1884 } else if ctxt.peek() == Some(b'>') {
1885 ctxt.next(); }
1887
1888 let tag_lower: Vec<u8> = tag_name.iter().map(|b| b.to_ascii_lowercase()).collect();
1890 let tag_str = core::str::from_utf8(&tag_lower).unwrap_or("");
1891
1892 if tag_str == "script" || tag_str == "style" {
1893 let raw_node = tree::new_node(ptr::null_mut(), bytes_to_xmlstr(&tag_name));
1896 if !raw_node.is_null() {
1897 for attr in &attrs {
1898 let name_c = bytes_to_xmlstr(&attr.name);
1899 let val_c = bytes_to_xmlstr(&attr.value);
1900 if !name_c.is_null() {
1901 tree::set_prop(raw_node, name_c, val_c);
1902 xmlFreeImpl(name_c as *mut c_void);
1903 if !val_c.is_null() {
1904 xmlFreeImpl(val_c as *mut c_void);
1905 }
1906 }
1907 }
1908
1909 let insertion_point = if ctxt.current.is_null() {
1910 if ctxt.in_head {
1911 ensure_head(ctxt);
1912 ctxt.head
1913 } else {
1914 ensure_body(ctxt);
1915 ctxt.body
1916 }
1917 } else {
1918 ctxt.current
1919 };
1920
1921 if !insertion_point.is_null() {
1922 tree::add_child(insertion_point, raw_node);
1923
1924 let end_tag = format!("</{}", tag_str);
1926 let end_bytes = end_tag.as_bytes();
1927 let mut raw_text = Vec::new();
1928 let mut match_idx = 0;
1929
1930 loop {
1931 if ctxt.is_eof() {
1932 break;
1933 }
1934 let ch = ctxt.peek().unwrap();
1935 if ch.to_ascii_lowercase() == end_bytes[match_idx] {
1936 match_idx += 1;
1937 if match_idx == end_bytes.len() {
1938 if !raw_text.is_empty() {
1941 let text_node = tree::new_text(bytes_to_xmlstr(&raw_text));
1942 if !text_node.is_null() {
1943 tree::add_child(raw_node, text_node);
1944 }
1945 }
1946 ctxt.next(); let _suffix = ctxt.read_while(|ch| ch != b'>');
1950 if ctxt.peek() == Some(b'>') {
1951 ctxt.next();
1952 }
1953 ctxt.current = unsafe { (*raw_node).parent };
1955 break;
1956 }
1957 if match_idx == 1 {
1959 raw_text.push(ch);
1960 }
1961 ctxt.next();
1962 } else {
1963 if match_idx > 0 {
1965 match_idx = 0;
1967 }
1968 raw_text.push(ch);
1969 ctxt.next();
1970 }
1971 }
1972
1973 if match_idx < end_bytes.len() && !raw_text.is_empty() {
1975 let text_node = tree::new_text(bytes_to_xmlstr(&raw_text));
1976 if !text_node.is_null() {
1977 tree::add_child(raw_node, text_node);
1978 }
1979 ctxt.current = unsafe { (*raw_node).parent };
1980 }
1981 }
1982 }
1983 continue;
1984 }
1985
1986 handle_start_tag(ctxt, &tag_name, &attrs);
1988 } else {
1989 let mut text = Vec::new();
1991 loop {
1992 match ctxt.peek() {
1993 Some(b'<') => break,
1994 Some(b'&') => {
1995 let entity_text = parse_entity(ctxt);
1997 text.extend_from_slice(&entity_text);
1998 }
1999 Some(ch) => {
2000 text.push(ch);
2001 ctxt.next();
2002 }
2003 None => break,
2004 }
2005 }
2006
2007 if !text.is_empty() {
2008 handle_text(ctxt, &text);
2009 }
2010 }
2011 }
2012
2013 if ctxt.html.is_null() {
2015 ensure_html(ctxt);
2016 }
2017
2018 doc
2019}
2020
2021pub unsafe fn parse_file(filename: *const c_char, encoding: *const c_char) -> *mut _xmlDoc {
2036 if filename.is_null() {
2037 return ptr::null_mut();
2038 }
2039
2040 let filename_str = unsafe { std::ffi::CStr::from_ptr(filename) };
2042 let path = filename_str.to_str().unwrap_or("");
2043 let content = match std::fs::read(path) {
2044 Ok(data) => data,
2045 Err(_) => return ptr::null_mut(),
2046 };
2047
2048 let mut ctxt = HtmlParserCtxt::new();
2049 if !encoding.is_null() {
2050 let _enc_cstr = unsafe { std::ffi::CStr::from_ptr(encoding) };
2051 ctxt.encoding = unsafe { c_strdup(encoding) };
2052 }
2053
2054 let doc = unsafe {
2055 html_parse_buffer(
2056 &mut ctxt,
2057 content.as_ptr() as *const c_char,
2058 content.len() as c_int,
2059 )
2060 };
2061
2062 if !doc.is_null() && !filename.is_null() {
2063 unsafe {
2064 (*doc).URL = c_strdup(filename) as *mut xmlChar;
2065 }
2066 }
2067
2068 doc
2069}
2070
2071pub unsafe fn parse_memory(buffer: *const c_char, size: c_int) -> *mut _xmlDoc {
2082 if buffer.is_null() || size <= 0 {
2083 return ptr::null_mut();
2084 }
2085
2086 let mut ctxt = HtmlParserCtxt::new();
2087 unsafe { html_parse_buffer(&mut ctxt, buffer, size) }
2088}
2089
2090pub(crate) unsafe fn parse_doc(cur: *const xmlChar, encoding: *const c_char) -> *mut _xmlDoc {
2101 if cur.is_null() {
2102 return ptr::null_mut();
2103 }
2104
2105 let len = unsafe { xml_strlen(cur) };
2106 let mut ctxt = HtmlParserCtxt::new();
2107 if !encoding.is_null() {
2108 ctxt.encoding = unsafe { c_strdup(encoding) };
2109 }
2110
2111 unsafe { html_parse_buffer(&mut ctxt, cur as *const c_char, len as c_int) }
2112}
2113
2114#[allow(dead_code)]
2125pub(crate) unsafe fn create_file_parser_ctxt(
2126 filename: *const c_char,
2127 encoding: *const c_char,
2128) -> *mut c_void {
2129 if filename.is_null() {
2130 return ptr::null_mut();
2131 }
2132
2133 let ctxt = unsafe { xmlMallocZero(size_of::<HtmlParserCtxt>() as usize) };
2134 if ctxt.is_null() {
2135 return ptr::null_mut();
2136 }
2137
2138 let ctxt = ctxt as *mut HtmlParserCtxt;
2139 unsafe {
2140 ptr::write(ctxt, HtmlParserCtxt::new());
2141 if !encoding.is_null() {
2142 (*ctxt).encoding = c_strdup(encoding);
2143 }
2144 }
2145
2146 ctxt as *mut c_void
2147}
2148
2149pub(crate) unsafe fn free_parser_ctxt(ctxt: *mut c_void) {
2159 if ctxt.is_null() {
2160 return;
2161 }
2162
2163 let ctxt = ctxt as *mut HtmlParserCtxt;
2164 unsafe {
2165 if !(*ctxt).filename.is_null() {
2166 xmlFreeImpl((*ctxt).filename as *mut c_void);
2167 }
2168 if !(*ctxt).encoding.is_null() {
2169 xmlFreeImpl((*ctxt).encoding as *mut c_void);
2170 }
2171 xmlFreeImpl(ctxt as *mut c_void);
2172 }
2173}
2174
2175#[allow(dead_code)]
2181pub(crate) const fn init_parser() {
2182 }
2185
2186#[allow(dead_code)]
2192pub(crate) const fn cleanup_parser() {
2193 }
2196
2197pub(crate) unsafe fn new_doc(version: *const xmlChar) -> *mut _xmlDoc {
2210 let doc = tree::new_doc(version);
2211 if doc.is_null() {
2212 return ptr::null_mut();
2213 }
2214
2215 unsafe {
2216 (*doc).type_ = XML_HTML_DOCUMENT_NODE as c_int;
2217 (*doc).properties = XML_DOC_WELLFORMED as c_int;
2218 }
2219
2220 let mut ctxt = HtmlParserCtxt::new();
2222 ctxt.doc = doc;
2223
2224 unsafe {
2225 ensure_html(&mut ctxt);
2226 ensure_head(&mut ctxt);
2227 ensure_body(&mut ctxt);
2228 }
2229
2230 doc
2231}
2232
2233pub(crate) unsafe fn new_doc_no_dtd(version: *const xmlChar) -> *mut _xmlDoc {
2246 let doc = tree::new_doc(version);
2247 if doc.is_null() {
2248 return ptr::null_mut();
2249 }
2250
2251 unsafe {
2252 (*doc).type_ = XML_HTML_DOCUMENT_NODE as c_int;
2253 (*doc).properties = XML_DOC_WELLFORMED as c_int;
2254 }
2255
2256 doc
2257}
2258
2259const HTML_VOID_ELEMENTS: &[&str] = &[
2265 "area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "param", "source",
2266 "track", "wbr", "frame",
2267];
2268
2269fn is_html_void(name: &str) -> bool {
2271 HTML_VOID_ELEMENTS
2272 .iter()
2273 .any(|v| v.eq_ignore_ascii_case(name))
2274}
2275
2276#[allow(dead_code)]
2278fn has_optional_end_tag(name: &str) -> bool {
2279 matches!(
2280 name.to_ascii_lowercase().as_str(),
2281 "p" | "li" | "tr" | "td" | "th" | "dt" | "dd"
2282 )
2283}
2284
2285unsafe fn html_write_quoted(buf: *mut _xmlBuffer, s: *const xmlChar) {
2291 if buf.is_null() || s.is_null() {
2292 return;
2293 }
2294 io::buf_ccat(buf, b'"');
2295 io::buf_cat(buf, s);
2296 io::buf_ccat(buf, b'"');
2297}
2298
2299fn trim_ascii_start(s: &[u8]) -> &[u8] {
2301 let start = s
2302 .iter()
2303 .position(|&b| !b.is_ascii_whitespace())
2304 .unwrap_or(s.len());
2305 &s[start..]
2306}
2307
2308unsafe fn html_serialize_text(buf: *mut _xmlBuffer, content: *const xmlChar, len: c_int) {
2309 if buf.is_null() || content.is_null() || len <= 0 {
2310 return;
2311 }
2312
2313 let mut i: c_int = 0;
2314 while i < len {
2315 let ch = unsafe { *content.add(i as usize) };
2316
2317 match ch {
2318 b'<' => {
2319 io::buf_add(buf, b"<" as *const u8, 4);
2320 }
2321 b'&' => {
2322 io::buf_add(buf, b"&" as *const u8, 5);
2323 }
2324 _ => {
2325 io::buf_add(buf, &ch as *const u8, 1);
2326 }
2327 }
2328 i += 1;
2329 }
2330}
2331
2332unsafe fn html_serialize_attr_value(buf: *mut _xmlBuffer, value: *const xmlChar) {
2336 if buf.is_null() || value.is_null() {
2337 return;
2338 }
2339
2340 let len = unsafe { xml_strlen(value) as c_int };
2341 let mut i: c_int = 0;
2342 while i < len {
2343 let ch = unsafe { *value.add(i as usize) };
2344
2345 match ch {
2346 b'&' => {
2347 io::buf_add(buf, b"&" as *const u8, 5);
2348 }
2349 b'"' => {
2350 io::buf_add(buf, b""" as *const u8, 6);
2351 }
2352 _ => {
2353 io::buf_add(buf, &ch as *const u8, 1);
2354 }
2355 }
2356 i += 1;
2357 }
2358}
2359
2360unsafe fn html_head_has_meta(child: *mut _xmlNode) -> bool {
2378 let mut c = child;
2379 while !c.is_null() {
2380 if (*c).type_ == XML_ELEMENT_NODE as c_int && !(*c).name.is_null() {
2381 let nm = xmlstr_to_bytes((*c).name);
2382 if nm.eq_ignore_ascii_case(b"meta") {
2383 return true;
2384 }
2385 }
2386 c = (*c).next;
2387 }
2388 false
2389}
2390
2391pub(crate) unsafe fn serialize_node(
2392 node: *mut _xmlNode,
2393 buf: *mut _xmlBuffer,
2394 format: c_int,
2395 level: c_int,
2396) {
2397 if node.is_null() || buf.is_null() {
2398 return;
2399 }
2400
2401 let n = unsafe { &*node };
2402
2403 match n.type_ {
2404 t if t == XML_ELEMENT_NODE as c_int => {
2405 let name = if n.name.is_null() {
2406 ""
2407 } else {
2408 unsafe { core::str::from_utf8(xmlstr_to_bytes(n.name)).unwrap_or("") }
2409 };
2410
2411 let is_void = is_html_void(name);
2412 let info = html_tag_lookup(name);
2417 let is_inline = info.is_none_or(|i| i.flags & HTML_INLINE != 0);
2418 let no_format = is_inline || name.starts_with('p');
2419
2420 io::buf_ccat(buf, b'<');
2422 if !n.name.is_null() {
2423 io::buf_cat(buf, n.name);
2424 }
2425
2426 let mut attr = n.properties;
2428 while !attr.is_null() {
2429 let a = unsafe { &*attr };
2430 io::buf_ccat(buf, b' ');
2431 if !a.name.is_null() {
2432 io::buf_cat(buf, a.name);
2433 }
2434
2435 if !a.children.is_null() {
2437 let child = unsafe { &*a.children };
2438 if child.type_ == XML_TEXT_NODE as c_int && !child.content.is_null() {
2439 io::buf_ccat(buf, b'=');
2440 io::buf_ccat(buf, b'"');
2441 html_serialize_attr_value(buf, child.content);
2442 io::buf_ccat(buf, b'"');
2443 }
2444 }
2445
2446 attr = a.next;
2447 }
2448
2449 let mut meta_bytes: Option<Vec<u8>> = None;
2456 if name.eq_ignore_ascii_case("head")
2457 && level == 1
2458 && !n.doc.is_null()
2459 && !(*n.doc).encoding.is_null()
2460 {
2461 let parent_is_html = !n.parent.is_null() && !(*n.parent).name.is_null() && {
2462 let pn = core::str::from_utf8(xmlstr_to_bytes((*n.parent).name)).unwrap_or("");
2463 pn.eq_ignore_ascii_case("html")
2464 };
2465 if parent_is_html && !html_head_has_meta(n.children) {
2466 meta_bytes = Some(xmlstr_to_bytes((*n.doc).encoding).to_vec());
2467 }
2468 }
2469 let meta_inserted = meta_bytes.is_some();
2470
2471 let has_children = !n.children.is_null();
2472 let first_child = if has_children {
2473 unsafe { (*n.children).type_ }
2474 } else {
2475 XML_TEXT_NODE as c_int
2476 };
2477 let first_is_text = first_child == XML_TEXT_NODE as c_int
2478 || first_child == XML_ENTITY_REF_NODE as c_int;
2479 let multi_child = (has_children && n.children != n.last) || meta_inserted;
2482
2483 if is_void {
2484 io::buf_ccat(buf, b'>');
2486 } else {
2490 io::buf_ccat(buf, b'>');
2492
2493 if format != 0 && !no_format && !first_is_text && multi_child {
2498 io::buf_ccat(buf, b'\n');
2499 }
2500
2501 if let Some(enc) = &meta_bytes {
2502 io::buf_add(buf, b"<meta charset=\"" as *const u8, 15);
2503 io::buf_add(buf, enc.as_ptr(), enc.len() as c_int);
2504 io::buf_add(buf, b"\">" as *const u8, 2);
2505 if format != 0 && has_children && !first_is_text && !name.starts_with('p') {
2508 io::buf_ccat(buf, b'\n');
2509 }
2510 }
2511
2512 let mut child = n.children;
2515 while !child.is_null() {
2516 serialize_node(child, buf, format, level + 1);
2517 let next = unsafe { (*child).next };
2521 if format != 0 && !next.is_null() && !name.starts_with('p') {
2522 let nt = unsafe { (*next).type_ };
2523 if nt != XML_TEXT_NODE as c_int && nt != XML_ENTITY_REF_NODE as c_int {
2524 let cname = if (*child).name.is_null() {
2525 ""
2526 } else {
2527 unsafe {
2528 core::str::from_utf8(xmlstr_to_bytes((*child).name))
2529 .unwrap_or("")
2530 }
2531 };
2532 let cinfo = html_tag_lookup(cname);
2533 let c_inline = cinfo.is_none_or(|i| i.flags & HTML_INLINE != 0);
2534 if !c_inline {
2535 io::buf_ccat(buf, b'\n');
2536 }
2537 }
2538 }
2539 child = next;
2540 }
2541
2542 let last_child = if has_children {
2546 unsafe { (*n.last).type_ }
2547 } else {
2548 XML_ELEMENT_NODE as c_int
2549 };
2550 let last_is_text = last_child == XML_TEXT_NODE as c_int
2551 || last_child == XML_ENTITY_REF_NODE as c_int;
2552 if format != 0 && !no_format && !last_is_text && multi_child {
2553 io::buf_ccat(buf, b'\n');
2554 }
2555
2556 io::buf_add(buf, b"</" as *const u8, 2);
2558 if !n.name.is_null() {
2559 io::buf_cat(buf, n.name);
2560 }
2561 io::buf_ccat(buf, b'>');
2562 }
2563 }
2564 t if t == XML_TEXT_NODE as c_int => {
2565 html_serialize_text(buf, n.content, xml_strlen(n.content) as c_int);
2566 }
2567 t if t == XML_CDATA_SECTION_NODE as c_int => {
2568 io::buf_add(buf, b"<![CDATA[" as *const u8, 9);
2569 html_serialize_text(buf, n.content, xml_strlen(n.content) as c_int);
2570 io::buf_add(buf, b"]]>" as *const u8, 3);
2571 }
2572 t if t == XML_COMMENT_NODE as c_int => {
2573 if format != 0 && level > 0 {
2574 io::buf_ccat(buf, b'\n');
2575 for _ in 0..level {
2576 io::buf_add(buf, b" " as *const u8, 2);
2577 }
2578 }
2579 io::buf_add(buf, b"<!--" as *const u8, 4);
2580 if !n.content.is_null() {
2581 io::buf_cat(buf, n.content);
2582 }
2583 io::buf_add(buf, b"-->" as *const u8, 3);
2584 }
2585 t if t == XML_PI_NODE as c_int => {
2586 if format != 0 && level > 0 {
2587 io::buf_ccat(buf, b'\n');
2588 for _ in 0..level {
2589 io::buf_add(buf, b" " as *const u8, 2);
2590 }
2591 }
2592 io::buf_add(buf, b"<?" as *const u8, 2);
2593 if !n.name.is_null() {
2594 io::buf_cat(buf, n.name);
2595 }
2596 if !n.content.is_null() && unsafe { *n.content != 0 } {
2597 io::buf_ccat(buf, b' ');
2598 io::buf_cat(buf, n.content);
2599 }
2600 io::buf_add(buf, b"?>" as *const u8, 2);
2601 }
2602 t if t == XML_DOCUMENT_NODE as c_int || t == XML_HTML_DOCUMENT_NODE as c_int => {
2603 let doc_ptr = n as *const _xmlNode as *mut _xmlDoc;
2606 let d = &*doc_ptr;
2607 if !d.intSubset.is_null() {
2608 let dtd = &*d.intSubset;
2609 io::buf_add(buf, b"<!DOCTYPE " as *const u8, 10);
2610 if !dtd.name.is_null() {
2611 io::buf_cat(buf, dtd.name);
2612 }
2613 if !dtd.ExternalID.is_null() {
2614 io::buf_add(buf, b" PUBLIC " as *const u8, 8);
2615 html_write_quoted(buf, dtd.ExternalID);
2616 io::buf_ccat(buf, b' ');
2617 html_write_quoted(buf, dtd.SystemID);
2618 } else if !dtd.SystemID.is_null() {
2619 io::buf_add(buf, b" SYSTEM " as *const u8, 8);
2620 html_write_quoted(buf, dtd.SystemID);
2621 }
2622 io::buf_ccat(buf, b'>');
2623 io::buf_ccat(buf, b'\n');
2624 }
2625 let mut child = n.children;
2628 while !child.is_null() {
2629 serialize_node(child, buf, format, 0);
2630 child = unsafe { (*child).next };
2631 }
2632 io::buf_ccat(buf, b'\n');
2635 }
2636 _ => {
2637 if !n.content.is_null() {
2638 html_serialize_text(buf, n.content, xml_strlen(n.content) as c_int);
2639 }
2640 }
2641 }
2642}
2643
2644pub(crate) unsafe fn doc_dump(buf: *mut _xmlBuffer, doc: *mut _xmlDoc) -> c_int {
2651 if buf.is_null() || doc.is_null() {
2652 return -1;
2653 }
2654
2655 let before = io::buf_length(buf);
2656 serialize_node(doc as *mut _xmlNode, buf, 0, 0);
2657 let after = io::buf_length(buf);
2658
2659 if after < 0 || before < 0 {
2660 return -1;
2661 }
2662 after - before
2663}
2664
2665#[cfg(test)]
2670mod tests {
2671 use super::*;
2672
2673 use crate::xml::io;
2674
2675 #[allow(dead_code)]
2677 unsafe fn to_xmlstr(s: &[u8]) -> *mut xmlChar {
2678 bytes_to_xmlstr(s)
2679 }
2680
2681 unsafe fn html_doc_to_string(doc: *mut _xmlDoc) -> String {
2683 let buf = io::buf_create(-1);
2684 assert!(!buf.is_null());
2685 doc_dump(buf, doc);
2686 let content = io::buf_content(buf);
2687 let s = if !content.is_null() {
2688 let len = xml_strlen(content);
2689 let slice = slice::from_raw_parts(content, len);
2690 String::from_utf8_lossy(slice).to_string()
2691 } else {
2692 String::new()
2693 };
2694 io::buf_free(buf);
2695 s
2696 }
2697
2698 #[test]
2703 fn test_html_tag_lookup() {
2704 assert!(html_tag_lookup("html").is_some());
2706 assert!(html_tag_lookup("HTML").is_some()); assert!(html_tag_lookup("p").is_some());
2708 assert!(html_tag_lookup("br").is_some());
2709 assert!(html_tag_lookup("div").is_some());
2710 assert!(html_tag_lookup("script").is_some());
2711
2712 assert!(html_tag_lookup("custom").is_none());
2714 assert!(html_tag_lookup("my-element").is_none());
2715 }
2716
2717 #[test]
2718 fn test_tag_flags() {
2719 let br = html_tag_lookup("br").unwrap();
2720 assert!(br.flags & HTML_INLINE != 0);
2721 assert!(br.flags & HTML_EMPTY != 0);
2722
2723 let div = html_tag_lookup("div").unwrap();
2724 assert!(div.flags & HTML_BLOCK != 0);
2725 assert!(div.flags & HTML_VALID != 0);
2726
2727 let p = html_tag_lookup("p").unwrap();
2728 assert!(p.flags & HTML_NO_END != 0);
2729
2730 let meta = html_tag_lookup("meta").unwrap();
2731 assert!(meta.flags & HTML_HEAD != 0);
2732 assert!(meta.flags & HTML_EMPTY != 0);
2733 }
2734
2735 #[test]
2740 fn test_html_entity_lookup() {
2741 assert_eq!(html_entity_lookup("amp"), Some("&"));
2742 assert_eq!(html_entity_lookup("lt"), Some("<"));
2743 assert_eq!(html_entity_lookup("gt"), Some(">"));
2744 assert_eq!(html_entity_lookup("quot"), Some("\""));
2745 assert_eq!(html_entity_lookup("nbsp"), Some("\u{00a0}"));
2746 assert_eq!(html_entity_lookup("copy"), Some("\u{00a9}"));
2747 assert!(html_entity_lookup("unknown_entity").is_none());
2748 }
2749
2750 #[test]
2755 fn test_parse_basic_html() {
2756 unsafe {
2757 let html = b"<html><head><title>Test</title></head><body><p>Hello</p></body></html>\0";
2758 let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
2759 assert!(!doc.is_null());
2760
2761 let s = html_doc_to_string(doc);
2762 assert!(s.contains("<html>"));
2763 assert!(s.contains("<head>"));
2764 assert!(s.contains("<title>Test</title>"));
2765 assert!(s.contains("<body>"));
2766 assert!(s.contains("<p>Hello</p>"));
2767
2768 tree::free_doc(doc);
2769 }
2770 }
2771
2772 #[test]
2773 fn test_parse_empty_document() {
2774 unsafe {
2775 let html = b"\0";
2776 let doc = parse_memory(html.as_ptr() as *const c_char, 0);
2777 assert!(doc.is_null());
2778 }
2779 }
2780
2781 #[test]
2786 fn test_implicit_html_head_body() {
2787 unsafe {
2788 let html = b"<p>Hello</p>\0";
2790 let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
2791 assert!(!doc.is_null());
2792
2793 let s = html_doc_to_string(doc);
2794 assert!(s.contains("<html>"));
2796 assert!(s.contains("<body>"));
2798 assert!(s.contains("<p>Hello</p>"));
2800
2801 tree::free_doc(doc);
2802 }
2803 }
2804
2805 #[test]
2806 fn test_implicit_head_with_title() {
2807 unsafe {
2808 let html = b"<title>My Page</title>\0";
2810 let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
2811 assert!(!doc.is_null());
2812
2813 let s = html_doc_to_string(doc);
2814 assert!(s.contains("<html>"));
2815 assert!(s.contains("<head>"));
2816 assert!(s.contains("<title>My Page</title>"));
2817
2818 tree::free_doc(doc);
2819 }
2820 }
2821
2822 #[test]
2827 fn test_auto_close_p() {
2828 unsafe {
2829 let html = b"<p>First<p>Second</p>\0";
2831 let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
2832 assert!(!doc.is_null());
2833
2834 let s = html_doc_to_string(doc);
2835 let first_pos = s.find("First");
2837 let second_pos = s.find("Second");
2838 assert!(first_pos.is_some());
2839 assert!(second_pos.is_some());
2840
2841 tree::free_doc(doc);
2842 }
2843 }
2844
2845 #[test]
2846 fn test_auto_close_heading() {
2847 unsafe {
2848 let html = b"<h1>Title</h1><h2>Subtitle</h2>\0";
2850 let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
2851 assert!(!doc.is_null());
2852
2853 let s = html_doc_to_string(doc);
2854 assert!(s.contains("<h1>Title</h1>"));
2855 assert!(s.contains("<h2>Subtitle</h2>"));
2856
2857 tree::free_doc(doc);
2858 }
2859 }
2860
2861 #[test]
2866 fn test_void_elements() {
2867 unsafe {
2868 let html = b"<br><hr><img src=\"test.jpg\"><input type=\"text\">\0";
2869 let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
2870 assert!(!doc.is_null());
2871
2872 let s = html_doc_to_string(doc);
2873 assert!(s.contains("<br>"));
2874 assert!(s.contains("<hr>"));
2875 assert!(s.contains("<img"));
2876 assert!(s.contains("<input"));
2877
2878 assert!(!s.contains("</br>"));
2880 assert!(!s.contains("</hr>"));
2881 assert!(!s.contains("</img>"));
2882
2883 tree::free_doc(doc);
2884 }
2885 }
2886
2887 #[test]
2892 fn test_unquoted_attributes() {
2893 unsafe {
2894 let html = b"<div class=main id=content>Text</div>\0";
2895 let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
2896 assert!(!doc.is_null());
2897
2898 let s = html_doc_to_string(doc);
2899 assert!(s.contains("class=\"main\""));
2900 assert!(s.contains("id=\"content\""));
2901
2902 tree::free_doc(doc);
2903 }
2904 }
2905
2906 #[test]
2907 fn test_minimized_attributes() {
2908 unsafe {
2909 let html = b"<option selected disabled>Value</option>\0";
2910 let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
2911 assert!(!doc.is_null());
2912
2913 let s = html_doc_to_string(doc);
2914 assert!(s.contains("selected"));
2916 assert!(s.contains("disabled"));
2917
2918 tree::free_doc(doc);
2919 }
2920 }
2921
2922 #[test]
2927 fn test_html_entities() {
2928 unsafe {
2929 let html = b"<p>& < > " ©</p>\0";
2930 let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
2931 assert!(!doc.is_null());
2932
2933 let s = html_doc_to_string(doc);
2934 assert!(s.contains("&")); assert!(s.contains("<")); assert!(s.contains(">")); assert!(s.contains("\u{00a0}")); tree::free_doc(doc);
2942 }
2943 }
2944
2945 #[test]
2946 fn test_numeric_entities() {
2947 unsafe {
2948 let html = b"<p>A A</p>\0";
2950 let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
2951 assert!(!doc.is_null());
2952
2953 let s = html_doc_to_string(doc);
2954 assert!(s.contains('A'));
2955
2956 tree::free_doc(doc);
2957 }
2958 }
2959
2960 #[test]
2965 fn test_nested_elements() {
2966 unsafe {
2967 let html = b"<div><ul><li>Item 1</li><li>Item 2</li></ul></div>\0";
2968 let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
2969 assert!(!doc.is_null());
2970
2971 let s = html_doc_to_string(doc);
2972 assert!(s.contains("<div>"));
2973 assert!(s.contains("<ul>"));
2974 assert!(s.contains("<li>Item 1</li>"));
2975 assert!(s.contains("<li>Item 2</li>"));
2976
2977 tree::free_doc(doc);
2978 }
2979 }
2980
2981 #[test]
2986 fn test_missing_end_tags() {
2987 unsafe {
2988 let html = b"<p>Paragraph without closing<div>Another div\0";
2990 let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
2991 assert!(!doc.is_null());
2992
2993 let s = html_doc_to_string(doc);
2994 assert!(s.contains("Paragraph without closing"));
2995 assert!(s.contains("Another div"));
2996
2997 tree::free_doc(doc);
2998 }
2999 }
3000
3001 #[test]
3002 fn test_mismatched_case() {
3003 unsafe {
3004 let html = b"<HTML><HEAD><TITLE>Test</TITLE></HEAD><BODY><P>Hello</P></BODY></HTML>\0";
3005 let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
3006 assert!(!doc.is_null());
3007
3008 let s = html_doc_to_string(doc);
3009 assert!(s.contains("<HTML>"));
3011 assert!(s.contains("<HEAD>"));
3012 assert!(s.contains("<BODY>"));
3013 assert!(s.contains("<P>Hello</P>"));
3014
3015 tree::free_doc(doc);
3016 }
3017 }
3018
3019 #[test]
3020 fn test_nested_malformed() {
3021 unsafe {
3022 let html = b"<div><p><span><b>Deep text</div></p>\0";
3024 let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
3025 assert!(!doc.is_null());
3026
3027 let s = html_doc_to_string(doc);
3028 assert!(s.contains("Deep text"));
3029
3030 tree::free_doc(doc);
3031 }
3032 }
3033
3034 #[test]
3039 fn test_serialization_round_trip_simple() {
3040 unsafe {
3041 let original = b"<p>Hello World</p>\0";
3042 let doc = parse_memory(
3043 original.as_ptr() as *const c_char,
3044 (original.len() - 1) as c_int,
3045 );
3046 assert!(!doc.is_null());
3047
3048 let s = html_doc_to_string(doc);
3049 assert!(s.contains("Hello World"));
3050
3051 tree::free_doc(doc);
3052 }
3053 }
3054
3055 #[test]
3056 fn test_serialize_void_elements_no_self_close() {
3057 unsafe {
3058 let html = b"<br><hr><img src=\"test.png\">\0";
3059 let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
3060 assert!(!doc.is_null());
3061
3062 let s = html_doc_to_string(doc);
3063 assert!(!s.contains("<br/>"));
3065 assert!(!s.contains("<hr/>"));
3066
3067 tree::free_doc(doc);
3068 }
3069 }
3070
3071 #[test]
3076 fn test_script_content() {
3077 unsafe {
3078 let html = b"<script>var x = 1;</script>\0";
3080 let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
3081 assert!(!doc.is_null());
3082
3083 let s = html_doc_to_string(doc);
3084 assert!(s.contains("<script>"));
3085 assert!(s.contains("var x = 1;"));
3087
3088 tree::free_doc(doc);
3089 }
3090 }
3091
3092 #[test]
3097 fn test_html_comment() {
3098 unsafe {
3099 let html = b"<html><!-- This is a comment --><body><p>Text</p></body></html>\0";
3100 let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
3101 assert!(!doc.is_null());
3102
3103 let s = html_doc_to_string(doc);
3104 assert!(s.contains("<!-- This is a comment -->"));
3105
3106 tree::free_doc(doc);
3107 }
3108 }
3109
3110 #[test]
3115 fn test_new_doc_creates_html_head_body() {
3116 unsafe {
3117 let doc = new_doc(ptr::null());
3118 assert!(!doc.is_null());
3119 assert_eq!((*doc).type_, XML_HTML_DOCUMENT_NODE as c_int);
3120
3121 let s = html_doc_to_string(doc);
3122 assert!(s.contains("<html>"));
3123 assert!(s.contains("<head>"));
3124 assert!(s.contains("<body>"));
3125
3126 tree::free_doc(doc);
3127 }
3128 }
3129
3130 #[test]
3131 fn test_new_doc_no_dtd() {
3132 unsafe {
3133 let doc = new_doc_no_dtd(ptr::null());
3134 assert!(!doc.is_null());
3135 assert_eq!((*doc).type_, XML_HTML_DOCUMENT_NODE as c_int);
3136
3137 let s = html_doc_to_string(doc);
3141 assert_eq!(s, "\n");
3142
3143 tree::free_doc(doc);
3144 }
3145 }
3146
3147 #[test]
3152 fn test_resolve_numeric_entity() {
3153 assert_eq!(resolve_numeric_entity("65", false), vec![b'A']);
3154 assert_eq!(resolve_numeric_entity("41", true), vec![b'A']);
3155 assert_eq!(resolve_numeric_entity("0", false), Vec::<u8>::new());
3156 }
3157
3158 #[test]
3159 fn test_resolve_entity_unknown() {
3160 let result = resolve_entity("unknown");
3161 assert_eq!(result, b"&unknown;");
3162 }
3163
3164 #[test]
3169 fn test_init_cleanup_parser() {
3170 init_parser();
3172 cleanup_parser();
3173 }
3174
3175 #[test]
3180 fn test_create_free_parser_ctxt() {
3181 unsafe {
3182 let ctxt =
3183 create_file_parser_ctxt(b"test.html\0" as *const u8 as *const c_char, ptr::null());
3184 assert!(!ctxt.is_null());
3185 free_parser_ctxt(ctxt);
3186 }
3187 }
3188
3189 #[test]
3194 fn test_complex_html_document() {
3195 unsafe {
3196 let html = b"<!DOCTYPE html>
3197<html>
3198<head>
3199 <meta charset=\"utf-8\">
3200 <title>Test Page</title>
3201 <link rel=\"stylesheet\" href=\"style.css\">
3202</head>
3203<body>
3204 <div id=\"main\">
3205 <h1>Title</h1>
3206 <p>First paragraph with <a href=\"link.html\">a link</a>.</p>
3207 <p>Second paragraph.</p>
3208 <ul>
3209 <li>Item 1</li>
3210 <li>Item 2</li>
3211 </ul>
3212 <br>
3213 <hr>
3214 <img src=\"image.jpg\" alt=\"An image\">
3215 </div>
3216 <script>alert('hello');</script>
3217</body>
3218</html>\0";
3219 let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
3220 assert!(!doc.is_null());
3221
3222 let s = html_doc_to_string(doc);
3223 assert!(s.contains("<html>"));
3224 assert!(s.contains("<head>"));
3225 assert!(s.contains("<title>Test Page</title>"));
3226 assert!(s.contains("<body>"));
3227 assert!(s.contains("<h1>Title</h1>"));
3228 assert!(s.contains("a link"));
3229 assert!(s.contains("Second paragraph"));
3230 assert!(s.contains("<br>"));
3231 assert!(s.contains("<hr>"));
3232 assert!(s.contains("<img"));
3233 assert!(s.contains("<script>"));
3234
3235 tree::free_doc(doc);
3236 }
3237 }
3238
3239 #[test]
3244 fn test_parse_doc() {
3245 unsafe {
3246 let html = b"<p>Hello from parse_doc</p>\0";
3247 let doc = parse_doc(html.as_ptr() as *const xmlChar, ptr::null());
3248 assert!(!doc.is_null());
3249
3250 let s = html_doc_to_string(doc);
3251 assert!(s.contains("Hello from parse_doc"));
3252
3253 tree::free_doc(doc);
3254 }
3255 }
3256
3257 #[test]
3262 fn test_table_element_auto_close() {
3263 unsafe {
3264 let html = b"<table><tr><td>Cell 1<td>Cell 2</td></tr></table>";
3265 let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
3266 assert!(!doc.is_null());
3267
3268 let s = html_doc_to_string(doc);
3269 assert!(s.contains("<td>Cell 1"));
3270 assert!(s.contains("<td>Cell 2"));
3271
3272 tree::free_doc(doc);
3273 }
3274 }
3275}