1use core::ffi::c_void;
15use core::ptr;
16use core::slice;
17use std::os::raw::{c_char, c_int};
18
19use crate::abi::allocator::{xmlFree, xmlMalloc, xmlMallocZero, xmlRealloc};
20use crate::abi::constants::XML_DEFAULT_VERSION;
21use crate::abi::structs::*;
22use crate::abi::types::xmlCharEncoding::XML_CHAR_ENCODING_UTF8;
23use crate::abi::types::xmlDocProperties::XML_DOC_WELLFORMED;
24use crate::abi::types::xmlElementType::*;
25use crate::abi::types::*;
26use crate::xml::io;
27use crate::xml::string::*;
28use crate::xml::tree;
29
30const HTML_INLINE: u32 = 0x1;
36const HTML_BLOCK: u32 = 0x2;
37const HTML_EMPTY: u32 = 0x4;
38#[allow(dead_code)]
39const HTML_DEPRECATED: u32 = 0x8;
40const HTML_OL: u32 = 0x10;
41const HTML_DL: u32 = 0x20;
42#[allow(dead_code)]
43const HTML_COMPACT: u32 = 0x40;
44const HTML_HEAD: u32 = 0x80;
45const HTML_BODY: u32 = 0x100;
46#[allow(dead_code)]
47const HTML_HEADSTRUCK: u32 = 0x200;
48const HTML_VALID: u32 = 0x400;
49const HTML_NO_END: u32 = 0x800; const HTML_IMPLIED: u32 = 0x1000; #[derive(Clone, Copy)]
54struct HtmlElementInfo {
55 name: &'static str,
56 flags: u32,
57}
58
59const HTML_ELEMENTS: &[HtmlElementInfo] = &[
62 HtmlElementInfo {
64 name: "br",
65 flags: HTML_INLINE | HTML_EMPTY | HTML_VALID,
66 },
67 HtmlElementInfo {
68 name: "hr",
69 flags: HTML_BLOCK | HTML_EMPTY | HTML_VALID,
70 },
71 HtmlElementInfo {
72 name: "img",
73 flags: HTML_INLINE | HTML_EMPTY | HTML_VALID,
74 },
75 HtmlElementInfo {
76 name: "input",
77 flags: HTML_INLINE | HTML_EMPTY | HTML_VALID,
78 },
79 HtmlElementInfo {
80 name: "meta",
81 flags: HTML_HEAD | HTML_EMPTY | HTML_VALID,
82 },
83 HtmlElementInfo {
84 name: "link",
85 flags: HTML_HEAD | HTML_EMPTY | HTML_VALID,
86 },
87 HtmlElementInfo {
88 name: "base",
89 flags: HTML_HEAD | HTML_EMPTY | HTML_VALID,
90 },
91 HtmlElementInfo {
92 name: "area",
93 flags: HTML_INLINE | HTML_EMPTY | HTML_VALID,
94 },
95 HtmlElementInfo {
96 name: "col",
97 flags: HTML_BLOCK | HTML_EMPTY | HTML_VALID,
98 },
99 HtmlElementInfo {
100 name: "embed",
101 flags: HTML_INLINE | HTML_EMPTY | HTML_VALID,
102 },
103 HtmlElementInfo {
104 name: "param",
105 flags: HTML_HEAD | HTML_EMPTY | HTML_VALID,
106 },
107 HtmlElementInfo {
108 name: "source",
109 flags: HTML_INLINE | HTML_EMPTY | HTML_VALID,
110 },
111 HtmlElementInfo {
112 name: "track",
113 flags: HTML_INLINE | HTML_EMPTY | HTML_VALID,
114 },
115 HtmlElementInfo {
116 name: "wbr",
117 flags: HTML_INLINE | HTML_EMPTY | HTML_VALID,
118 },
119 HtmlElementInfo {
121 name: "html",
122 flags: HTML_BLOCK | HTML_VALID,
123 },
124 HtmlElementInfo {
125 name: "head",
126 flags: HTML_HEAD | HTML_BLOCK | HTML_VALID,
127 },
128 HtmlElementInfo {
129 name: "body",
130 flags: HTML_BODY | HTML_BLOCK | HTML_VALID,
131 },
132 HtmlElementInfo {
133 name: "div",
134 flags: HTML_BLOCK | HTML_VALID,
135 },
136 HtmlElementInfo {
137 name: "p",
138 flags: HTML_BLOCK | HTML_VALID | HTML_NO_END,
139 },
140 HtmlElementInfo {
141 name: "h1",
142 flags: HTML_BLOCK | HTML_VALID,
143 },
144 HtmlElementInfo {
145 name: "h2",
146 flags: HTML_BLOCK | HTML_VALID,
147 },
148 HtmlElementInfo {
149 name: "h3",
150 flags: HTML_BLOCK | HTML_VALID,
151 },
152 HtmlElementInfo {
153 name: "h4",
154 flags: HTML_BLOCK | HTML_VALID,
155 },
156 HtmlElementInfo {
157 name: "h5",
158 flags: HTML_BLOCK | HTML_VALID,
159 },
160 HtmlElementInfo {
161 name: "h6",
162 flags: HTML_BLOCK | HTML_VALID,
163 },
164 HtmlElementInfo {
165 name: "ul",
166 flags: HTML_BLOCK | HTML_VALID | HTML_OL,
167 },
168 HtmlElementInfo {
169 name: "ol",
170 flags: HTML_BLOCK | HTML_VALID | HTML_OL,
171 },
172 HtmlElementInfo {
173 name: "li",
174 flags: HTML_BLOCK | HTML_VALID | HTML_NO_END,
175 },
176 HtmlElementInfo {
177 name: "dl",
178 flags: HTML_BLOCK | HTML_VALID | HTML_DL,
179 },
180 HtmlElementInfo {
181 name: "dt",
182 flags: HTML_BLOCK | HTML_VALID | HTML_NO_END,
183 },
184 HtmlElementInfo {
185 name: "dd",
186 flags: HTML_BLOCK | HTML_VALID | HTML_NO_END,
187 },
188 HtmlElementInfo {
189 name: "table",
190 flags: HTML_BLOCK | HTML_VALID,
191 },
192 HtmlElementInfo {
193 name: "tr",
194 flags: HTML_BLOCK | HTML_VALID | HTML_NO_END,
195 },
196 HtmlElementInfo {
197 name: "td",
198 flags: HTML_BLOCK | HTML_VALID | HTML_NO_END,
199 },
200 HtmlElementInfo {
201 name: "th",
202 flags: HTML_BLOCK | HTML_VALID | HTML_NO_END,
203 },
204 HtmlElementInfo {
205 name: "thead",
206 flags: HTML_BLOCK | HTML_VALID,
207 },
208 HtmlElementInfo {
209 name: "tbody",
210 flags: HTML_BLOCK | HTML_VALID,
211 },
212 HtmlElementInfo {
213 name: "tfoot",
214 flags: HTML_BLOCK | HTML_VALID,
215 },
216 HtmlElementInfo {
217 name: "colgroup",
218 flags: HTML_BLOCK | HTML_VALID,
219 },
220 HtmlElementInfo {
221 name: "caption",
222 flags: HTML_BLOCK | HTML_VALID,
223 },
224 HtmlElementInfo {
225 name: "form",
226 flags: HTML_BLOCK | HTML_VALID,
227 },
228 HtmlElementInfo {
229 name: "fieldset",
230 flags: HTML_BLOCK | HTML_VALID,
231 },
232 HtmlElementInfo {
233 name: "legend",
234 flags: HTML_BLOCK | HTML_VALID,
235 },
236 HtmlElementInfo {
237 name: "pre",
238 flags: HTML_BLOCK | HTML_VALID,
239 },
240 HtmlElementInfo {
241 name: "blockquote",
242 flags: HTML_BLOCK | HTML_VALID,
243 },
244 HtmlElementInfo {
245 name: "address",
246 flags: HTML_BLOCK | HTML_VALID,
247 },
248 HtmlElementInfo {
249 name: "center",
250 flags: HTML_BLOCK | HTML_VALID,
251 },
252 HtmlElementInfo {
253 name: "dir",
254 flags: HTML_BLOCK | HTML_VALID,
255 },
256 HtmlElementInfo {
257 name: "menu",
258 flags: HTML_BLOCK | HTML_VALID,
259 },
260 HtmlElementInfo {
261 name: "noscript",
262 flags: HTML_BLOCK | HTML_VALID,
263 },
264 HtmlElementInfo {
265 name: "frameset",
266 flags: HTML_BLOCK | HTML_VALID,
267 },
268 HtmlElementInfo {
269 name: "frame",
270 flags: HTML_BLOCK | HTML_EMPTY | HTML_VALID,
271 },
272 HtmlElementInfo {
273 name: "iframe",
274 flags: HTML_BLOCK | HTML_VALID,
275 },
276 HtmlElementInfo {
277 name: "noframes",
278 flags: HTML_BLOCK | HTML_VALID,
279 },
280 HtmlElementInfo {
282 name: "a",
283 flags: HTML_INLINE | HTML_VALID,
284 },
285 HtmlElementInfo {
286 name: "abbr",
287 flags: HTML_INLINE | HTML_VALID,
288 },
289 HtmlElementInfo {
290 name: "acronym",
291 flags: HTML_INLINE | HTML_VALID,
292 },
293 HtmlElementInfo {
294 name: "b",
295 flags: HTML_INLINE | HTML_VALID,
296 },
297 HtmlElementInfo {
298 name: "basefont",
299 flags: HTML_INLINE | HTML_EMPTY | HTML_VALID,
300 },
301 HtmlElementInfo {
302 name: "bdo",
303 flags: HTML_INLINE | HTML_VALID,
304 },
305 HtmlElementInfo {
306 name: "big",
307 flags: HTML_INLINE | HTML_VALID,
308 },
309 HtmlElementInfo {
310 name: "cite",
311 flags: HTML_INLINE | HTML_VALID,
312 },
313 HtmlElementInfo {
314 name: "code",
315 flags: HTML_INLINE | HTML_VALID,
316 },
317 HtmlElementInfo {
318 name: "dfn",
319 flags: HTML_INLINE | HTML_VALID,
320 },
321 HtmlElementInfo {
322 name: "em",
323 flags: HTML_INLINE | HTML_VALID,
324 },
325 HtmlElementInfo {
326 name: "font",
327 flags: HTML_INLINE | HTML_VALID,
328 },
329 HtmlElementInfo {
330 name: "i",
331 flags: HTML_INLINE | HTML_VALID,
332 },
333 HtmlElementInfo {
334 name: "kbd",
335 flags: HTML_INLINE | HTML_VALID,
336 },
337 HtmlElementInfo {
338 name: "label",
339 flags: HTML_INLINE | HTML_VALID,
340 },
341 HtmlElementInfo {
342 name: "map",
343 flags: HTML_INLINE | HTML_VALID,
344 },
345 HtmlElementInfo {
346 name: "nobr",
347 flags: HTML_INLINE | HTML_VALID,
348 },
349 HtmlElementInfo {
350 name: "object",
351 flags: HTML_INLINE | HTML_VALID,
352 },
353 HtmlElementInfo {
354 name: "q",
355 flags: HTML_INLINE | HTML_VALID,
356 },
357 HtmlElementInfo {
358 name: "rb",
359 flags: HTML_INLINE | HTML_VALID,
360 },
361 HtmlElementInfo {
362 name: "rbc",
363 flags: HTML_INLINE | HTML_VALID,
364 },
365 HtmlElementInfo {
366 name: "rp",
367 flags: HTML_INLINE | HTML_VALID,
368 },
369 HtmlElementInfo {
370 name: "rt",
371 flags: HTML_INLINE | HTML_VALID,
372 },
373 HtmlElementInfo {
374 name: "rtc",
375 flags: HTML_INLINE | HTML_VALID,
376 },
377 HtmlElementInfo {
378 name: "ruby",
379 flags: HTML_INLINE | HTML_VALID,
380 },
381 HtmlElementInfo {
382 name: "s",
383 flags: HTML_INLINE | HTML_VALID,
384 },
385 HtmlElementInfo {
386 name: "samp",
387 flags: HTML_INLINE | HTML_VALID,
388 },
389 HtmlElementInfo {
390 name: "select",
391 flags: HTML_INLINE | HTML_VALID,
392 },
393 HtmlElementInfo {
394 name: "small",
395 flags: HTML_INLINE | HTML_VALID,
396 },
397 HtmlElementInfo {
398 name: "span",
399 flags: HTML_INLINE | HTML_VALID,
400 },
401 HtmlElementInfo {
402 name: "strike",
403 flags: HTML_INLINE | HTML_VALID,
404 },
405 HtmlElementInfo {
406 name: "strong",
407 flags: HTML_INLINE | HTML_VALID,
408 },
409 HtmlElementInfo {
410 name: "sub",
411 flags: HTML_INLINE | HTML_VALID,
412 },
413 HtmlElementInfo {
414 name: "sup",
415 flags: HTML_INLINE | HTML_VALID,
416 },
417 HtmlElementInfo {
418 name: "textarea",
419 flags: HTML_INLINE | HTML_VALID,
420 },
421 HtmlElementInfo {
422 name: "tt",
423 flags: HTML_INLINE | HTML_VALID,
424 },
425 HtmlElementInfo {
426 name: "u",
427 flags: HTML_INLINE | HTML_VALID,
428 },
429 HtmlElementInfo {
430 name: "var",
431 flags: HTML_INLINE | HTML_VALID,
432 },
433 HtmlElementInfo {
435 name: "header",
436 flags: HTML_BLOCK | HTML_VALID,
437 },
438 HtmlElementInfo {
439 name: "footer",
440 flags: HTML_BLOCK | HTML_VALID,
441 },
442 HtmlElementInfo {
443 name: "nav",
444 flags: HTML_BLOCK | HTML_VALID,
445 },
446 HtmlElementInfo {
447 name: "article",
448 flags: HTML_BLOCK | HTML_VALID,
449 },
450 HtmlElementInfo {
451 name: "section",
452 flags: HTML_BLOCK | HTML_VALID,
453 },
454 HtmlElementInfo {
455 name: "aside",
456 flags: HTML_BLOCK | HTML_VALID,
457 },
458 HtmlElementInfo {
459 name: "main",
460 flags: HTML_BLOCK | HTML_VALID,
461 },
462 HtmlElementInfo {
463 name: "figure",
464 flags: HTML_BLOCK | HTML_VALID,
465 },
466 HtmlElementInfo {
467 name: "figcaption",
468 flags: HTML_BLOCK | HTML_VALID,
469 },
470 HtmlElementInfo {
471 name: "details",
472 flags: HTML_BLOCK | HTML_VALID,
473 },
474 HtmlElementInfo {
475 name: "summary",
476 flags: HTML_BLOCK | HTML_VALID,
477 },
478 HtmlElementInfo {
480 name: "script",
481 flags: HTML_HEAD | HTML_BLOCK | HTML_VALID,
482 },
483 HtmlElementInfo {
484 name: "style",
485 flags: HTML_HEAD | HTML_BLOCK | HTML_VALID,
486 },
487 HtmlElementInfo {
488 name: "title",
489 flags: HTML_HEAD | HTML_BLOCK | HTML_VALID,
490 },
491];
492
493fn html_tag_lookup(name: &str) -> Option<&'static HtmlElementInfo> {
496 let lower: Vec<u8> = name.bytes().map(|b| b.to_ascii_lowercase()).collect();
498 let lower_str = match core::str::from_utf8(&lower) {
499 Ok(s) => s,
500 Err(_) => return None,
501 };
502 HTML_ELEMENTS.iter().find(|info| info.name == lower_str)
503}
504
505const HTML_ENTITIES: &[(&str, &str)] = &[
512 ("nbsp", "\u{00a0}"),
513 ("lt", "<"),
514 ("gt", ">"),
515 ("amp", "&"),
516 ("quot", "\""),
517 ("apos", "'"),
518 ("copy", "\u{00a9}"),
519 ("reg", "\u{00ae}"),
520 ("amp", "&"),
521 ("iexcl", "\u{00a1}"),
522 ("cent", "\u{00a2}"),
523 ("pound", "\u{00a3}"),
524 ("curren", "\u{00a4}"),
525 ("yen", "\u{00a5}"),
526 ("brvbar", "\u{00a6}"),
527 ("sect", "\u{00a7}"),
528 ("uml", "\u{00a8}"),
529 ("ordf", "\u{00aa}"),
530 ("laquo", "\u{00ab}"),
531 ("not", "\u{00ac}"),
532 ("shy", "\u{00ad}"),
533 ("macr", "\u{00ae}"),
534 ("deg", "\u{00b0}"),
535 ("plusmn", "\u{00b1}"),
536 ("sup2", "\u{00b2}"),
537 ("sup3", "\u{00b3}"),
538 ("acute", "\u{00b4}"),
539 ("micro", "\u{00b5}"),
540 ("para", "\u{00b6}"),
541 ("middot", "\u{00b7}"),
542 ("cedil", "\u{00b8}"),
543 ("sup1", "\u{00b9}"),
544 ("ordm", "\u{00ba}"),
545 ("raquo", "\u{00bb}"),
546 ("frac14", "\u{00bc}"),
547 ("frac12", "\u{00bd}"),
548 ("frac34", "\u{00be}"),
549 ("iquest", "\u{00bf}"),
550 ("times", "\u{00d7}"),
551 ("divide", "\u{00f7}"),
552 ("ETH", "\u{00d0}"),
553 ("eth", "\u{00f0}"),
554 ("THORN", "\u{00de}"),
555 ("thorn", "\u{00fe}"),
556 ("AElig", "\u{00c6}"),
557 ("aelig", "\u{00e6}"),
558 ("OElig", "\u{0152}"),
559 ("oelig", "\u{0153}"),
560 ("Scaron", "\u{0160}"),
561 ("scaron", "\u{0161}"),
562 ("Yuml", "\u{0178}"),
563 ("circ", "\u{02c6}"),
564 ("tilde", "\u{02dc}"),
565 ("ensp", "\u{2002}"),
566 ("emsp", "\u{2003}"),
567 ("thinsp", "\u{2009}"),
568 ("zwnj", "\u{200c}"),
569 ("zwj", "\u{200d}"),
570 ("lrm", "\u{200e}"),
571 ("rlm", "\u{200f}"),
572 ("ndash", "\u{2013}"),
573 ("mdash", "\u{2014}"),
574 ("lsquo", "\u{2018}"),
575 ("rsquo", "\u{2019}"),
576 ("sbquo", "\u{201a}"),
577 ("ldquo", "\u{201c}"),
578 ("rdquo", "\u{201d}"),
579 ("bdquo", "\u{201e}"),
580 ("dagger", "\u{2020}"),
581 ("Dagger", "\u{2021}"),
582 ("bull", "\u{2022}"),
583 ("hellip", "\u{2026}"),
584 ("permil", "\u{2030}"),
585 ("prime", "\u{2032}"),
586 ("Prime", "\u{2033}"),
587 ("lsaquo", "\u{2039}"),
588 ("rsaquo", "\u{203a}"),
589 ("oline", "\u{203e}"),
590 ("euro", "\u{20ac}"),
591 ("trade", "\u{2122}"),
592 ("larr", "\u{2190}"),
593 ("uarr", "\u{2191}"),
594 ("rarr", "\u{2192}"),
595 ("darr", "\u{2193}"),
596 ("harr", "\u{2194}"),
597 ("crarr", "\u{21b5}"),
598 ("lceil", "\u{2308}"),
599 ("rceil", "\u{2309}"),
600 ("lfloor", "\u{230a}"),
601 ("rfloor", "\u{230b}"),
602 ("loz", "\u{25ca}"),
603 ("spades", "\u{2660}"),
604 ("clubs", "\u{2663}"),
605 ("hearts", "\u{2665}"),
606 ("diams", "\u{2666}"),
607 ("Alpha", "\u{0391}"),
608 ("Beta", "\u{0392}"),
609 ("Gamma", "\u{0393}"),
610 ("Delta", "\u{0394}"),
611 ("Epsilon", "\u{0395}"),
612 ("Zeta", "\u{0396}"),
613 ("Eta", "\u{0397}"),
614 ("Theta", "\u{0398}"),
615 ("Iota", "\u{0399}"),
616 ("Kappa", "\u{039a}"),
617 ("Lambda", "\u{039b}"),
618 ("Mu", "\u{039c}"),
619 ("Nu", "\u{039d}"),
620 ("Xi", "\u{039e}"),
621 ("Omicron", "\u{039f}"),
622 ("Pi", "\u{03a0}"),
623 ("Rho", "\u{03a1}"),
624 ("Sigma", "\u{03a3}"),
625 ("Tau", "\u{03a4}"),
626 ("Upsilon", "\u{03a5}"),
627 ("Phi", "\u{03a6}"),
628 ("Chi", "\u{03a7}"),
629 ("Psi", "\u{03a8}"),
630 ("Omega", "\u{03a9}"),
631 ("alpha", "\u{03b1}"),
632 ("beta", "\u{03b2}"),
633 ("gamma", "\u{03b3}"),
634 ("delta", "\u{03b4}"),
635 ("epsilon", "\u{03b5}"),
636 ("zeta", "\u{03b6}"),
637 ("eta", "\u{03b7}"),
638 ("theta", "\u{03b8}"),
639 ("iota", "\u{03b9}"),
640 ("kappa", "\u{03ba}"),
641 ("lambda", "\u{03bb}"),
642 ("mu", "\u{03bc}"),
643 ("nu", "\u{03bd}"),
644 ("xi", "\u{03be}"),
645 ("omicron", "\u{03bf}"),
646 ("pi", "\u{03c0}"),
647 ("rho", "\u{03c1}"),
648 ("sigmaf", "\u{03c2}"),
649 ("sigma", "\u{03c3}"),
650 ("tau", "\u{03c4}"),
651 ("upsilon", "\u{03c5}"),
652 ("phi", "\u{03c6}"),
653 ("chi", "\u{03c7}"),
654 ("psi", "\u{03c8}"),
655 ("omega", "\u{03c9}"),
656 ("thetasym", "\u{03d1}"),
657 ("upsih", "\u{03d2}"),
658 ("piv", "\u{03d6}"),
659];
660
661fn html_entity_lookup(name: &str) -> Option<&'static str> {
664 HTML_ENTITIES
665 .iter()
666 .find(|(n, _)| *n == name)
667 .map(|(_, v)| *v)
668}
669
670struct HtmlParserCtxt {
676 doc: *mut _xmlDoc,
678 current: *mut _xmlNode,
680 html: *mut _xmlNode,
682 head: *mut _xmlNode,
684 body: *mut _xmlNode,
686 in_head: bool,
688 in_body: bool,
690 html_created: bool,
692 head_created: bool,
694 body_created: bool,
696 seen_body_content: bool,
698 input: *mut u8,
700 input_pos: usize,
702 input_len: usize,
704 line: c_int,
706 err: bool,
708 filename: *mut c_char,
710 encoding: *mut c_char,
712}
713
714impl HtmlParserCtxt {
715 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 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
815unsafe fn get_parent_element(node: *mut _xmlNode) -> *mut _xmlNode {
818 if node.is_null() {
819 return ptr::null_mut();
820 }
821 let mut n = node;
822 loop {
823 let parent = unsafe { (*n).parent };
824 if parent.is_null() {
825 return ptr::null_mut();
826 }
827 let ptype = unsafe { (*parent).type_ };
828 if ptype == XML_ELEMENT_NODE as c_int
829 || ptype == XML_HTML_DOCUMENT_NODE as c_int
830 || ptype == XML_DOCUMENT_NODE as c_int
831 {
832 return parent;
833 }
834 n = parent;
835 }
836}
837
838unsafe fn auto_close_element(ctxt: &mut HtmlParserCtxt, tag_name: &str) {
841 let tag_lower: Vec<u8> = tag_name.bytes().map(|b| b.to_ascii_lowercase()).collect();
842 let tag_lower_str = match core::str::from_utf8(&tag_lower) {
843 Ok(s) => s,
844 Err(_) => return,
845 };
846
847 let info = html_tag_lookup(&tag_lower_str);
848
849 let mut current = ctxt.current;
850
851 let mut open_names: Vec<Vec<u8>> = Vec::new();
853 let mut cur = current;
854 while !cur.is_null() {
855 let ctype = unsafe { (*cur).type_ };
856 if ctype == XML_ELEMENT_NODE as c_int {
857 if !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 }
862 cur = unsafe { (*cur).parent };
863 }
864
865 if tag_lower_str == "p" || info.map_or(false, |i| i.flags & HTML_BLOCK != 0) {
867 let mut cur2 = current;
869 while !cur2.is_null() {
870 let ctype = unsafe { (*cur2).type_ };
871 if ctype == XML_ELEMENT_NODE as c_int {
872 if !unsafe { (*cur2).name.is_null() } {
873 let name_bytes = unsafe { xmlstr_to_bytes((*cur2).name) };
874 let name_str = core::str::from_utf8(name_bytes).unwrap_or("");
875 if name_str.eq_ignore_ascii_case("p") {
876 current = unsafe { (*cur2).parent };
878 break;
879 }
880 }
881 }
882 cur2 = unsafe { (*cur2).parent };
883 }
884 }
885
886 if is_heading(&tag_lower_str) {
888 let mut cur2 = current;
889 while !cur2.is_null() {
890 let ctype = unsafe { (*cur2).type_ };
891 if ctype == XML_ELEMENT_NODE as c_int {
892 if !unsafe { (*cur2).name.is_null() } {
893 let name_bytes = unsafe { xmlstr_to_bytes((*cur2).name) };
894 let name_str = core::str::from_utf8(name_bytes).unwrap_or("");
895 if is_heading(name_str) {
896 current = unsafe { (*cur2).parent };
897 break;
898 }
899 }
900 }
901 cur2 = unsafe { (*cur2).parent };
902 }
903 }
904
905 if tag_lower_str == "li" {
907 let mut cur2 = current;
908 while !cur2.is_null() {
909 let ctype = unsafe { (*cur2).type_ };
910 if ctype == XML_ELEMENT_NODE as c_int {
911 if !unsafe { (*cur2).name.is_null() } {
912 let name_bytes = unsafe { xmlstr_to_bytes((*cur2).name) };
913 let name_str = core::str::from_utf8(name_bytes).unwrap_or("");
914 if name_str.eq_ignore_ascii_case("li") {
915 current = unsafe { (*cur2).parent };
916 break;
917 }
918 }
919 }
920 cur2 = unsafe { (*cur2).parent };
921 }
922 }
923
924 if tag_lower_str == "dt" || tag_lower_str == "dd" {
926 let mut cur2 = current;
927 while !cur2.is_null() {
928 let ctype = unsafe { (*cur2).type_ };
929 if ctype == XML_ELEMENT_NODE as c_int {
930 if !unsafe { (*cur2).name.is_null() } {
931 let name_bytes = unsafe { xmlstr_to_bytes((*cur2).name) };
932 let name_str = core::str::from_utf8(name_bytes).unwrap_or("");
933 if name_str == "dt" || name_str == "dd" {
934 current = unsafe { (*cur2).parent };
935 break;
936 }
937 }
938 }
939 cur2 = unsafe { (*cur2).parent };
940 }
941 }
942
943 if tag_lower_str == "tr" {
948 let mut cur2 = current;
949 while !cur2.is_null() {
950 let ctype = unsafe { (*cur2).type_ };
951 if ctype == XML_ELEMENT_NODE as c_int {
952 if !unsafe { (*cur2).name.is_null() } {
953 let name_bytes = unsafe { xmlstr_to_bytes((*cur2).name) };
954 let name_str = core::str::from_utf8(name_bytes).unwrap_or("");
955 if name_str == "tr" || name_str == "td" || name_str == "th" {
956 current = unsafe { (*cur2).parent };
957 break;
958 }
959 }
960 }
961 cur2 = unsafe { (*cur2).parent };
962 }
963 } else if tag_lower_str == "td" || tag_lower_str == "th" {
964 let mut cur2 = current;
965 while !cur2.is_null() {
966 let ctype = unsafe { (*cur2).type_ };
967 if ctype == XML_ELEMENT_NODE as c_int {
968 if !unsafe { (*cur2).name.is_null() } {
969 let name_bytes = unsafe { xmlstr_to_bytes((*cur2).name) };
970 let name_str = core::str::from_utf8(name_bytes).unwrap_or("");
971 if name_str == "td" || name_str == "th" {
972 current = unsafe { (*cur2).parent };
973 break;
974 }
975 }
976 }
977 cur2 = unsafe { (*cur2).parent };
978 }
979 }
980
981 if matches!(tag_lower_str, "thead" | "tbody" | "tfoot") {
983 let mut cur2 = current;
984 while !cur2.is_null() {
985 let ctype = unsafe { (*cur2).type_ };
986 if ctype == XML_ELEMENT_NODE as c_int {
987 if !unsafe { (*cur2).name.is_null() } {
988 let name_bytes = unsafe { xmlstr_to_bytes((*cur2).name) };
989 let name_str = core::str::from_utf8(name_bytes).unwrap_or("");
990 if name_str == "thead" || name_str == "tbody" || name_str == "tfoot" {
991 current = unsafe { (*cur2).parent };
992 break;
993 }
994 }
995 }
996 cur2 = unsafe { (*cur2).parent };
997 }
998 }
999
1000 if tag_lower_str == "colgroup" {
1002 let mut cur2 = current;
1003 while !cur2.is_null() {
1004 let ctype = unsafe { (*cur2).type_ };
1005 if ctype == XML_ELEMENT_NODE as c_int {
1006 if !unsafe { (*cur2).name.is_null() } {
1007 let name_bytes = unsafe { xmlstr_to_bytes((*cur2).name) };
1008 let name_str = core::str::from_utf8(name_bytes).unwrap_or("");
1009 if name_str == "colgroup" {
1010 current = unsafe { (*cur2).parent };
1011 break;
1012 }
1013 }
1014 }
1015 cur2 = unsafe { (*cur2).parent };
1016 }
1017 }
1018
1019 if tag_lower_str == "caption" {
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 {
1025 if !unsafe { (*cur2).name.is_null() } {
1026 let name_bytes = unsafe { xmlstr_to_bytes((*cur2).name) };
1027 let name_str = core::str::from_utf8(name_bytes).unwrap_or("");
1028 if name_str == "caption" {
1029 current = unsafe { (*cur2).parent };
1030 break;
1031 }
1032 }
1033 }
1034 cur2 = unsafe { (*cur2).parent };
1035 }
1036 }
1037
1038 if tag_lower_str == "form" {
1040 let mut cur2 = current;
1041 while !cur2.is_null() {
1042 let ctype = unsafe { (*cur2).type_ };
1043 if ctype == XML_ELEMENT_NODE as c_int {
1044 if !unsafe { (*cur2).name.is_null() } {
1045 let name_bytes = unsafe { xmlstr_to_bytes((*cur2).name) };
1046 let name_str = core::str::from_utf8(name_bytes).unwrap_or("");
1047 if name_str.eq_ignore_ascii_case("form") {
1048 current = unsafe { (*cur2).parent };
1049 break;
1050 }
1051 }
1052 }
1053 cur2 = unsafe { (*cur2).parent };
1054 }
1055 }
1056
1057 ctxt.current = current;
1058}
1059
1060unsafe fn ensure_html(ctxt: &mut HtmlParserCtxt) -> *mut _xmlNode {
1066 if !ctxt.html.is_null() {
1067 return ctxt.html;
1068 }
1069
1070 let html_node = tree::new_node(ptr::null_mut(), b"html\0" as *const u8 as *const xmlChar);
1071 if html_node.is_null() {
1072 return ptr::null_mut();
1073 }
1074 unsafe {
1075 }
1078 ctxt.html = html_node;
1079 ctxt.html_created = true;
1080
1081 tree::add_child(ctxt.doc as *mut _xmlNode, html_node);
1083 ctxt.current = html_node;
1084
1085 html_node
1086}
1087
1088unsafe fn ensure_head(ctxt: &mut HtmlParserCtxt) -> *mut _xmlNode {
1090 if !ctxt.head.is_null() {
1091 return ctxt.head;
1092 }
1093
1094 ensure_html(ctxt);
1096
1097 let head_node = tree::new_node(ptr::null_mut(), b"head\0" as *const u8 as *const xmlChar);
1098 if head_node.is_null() {
1099 return ptr::null_mut();
1100 }
1101 ctxt.head = head_node;
1102 ctxt.head_created = true;
1103
1104 tree::add_child(ctxt.html, head_node);
1106 ctxt.current = head_node;
1107 ctxt.in_head = true;
1108
1109 head_node
1110}
1111
1112unsafe fn ensure_body(ctxt: &mut HtmlParserCtxt) -> *mut _xmlNode {
1114 if !ctxt.body.is_null() {
1115 return ctxt.body;
1116 }
1117
1118 ensure_html(ctxt);
1120
1121 let body_node = tree::new_node(ptr::null_mut(), b"body\0" as *const u8 as *const xmlChar);
1122 if body_node.is_null() {
1123 return ptr::null_mut();
1124 }
1125 ctxt.body = body_node;
1126 ctxt.body_created = true;
1127
1128 tree::add_child(ctxt.html, body_node);
1130 ctxt.current = body_node;
1131 ctxt.in_body = true;
1132
1133 body_node
1134}
1135
1136unsafe fn transition_to_body(ctxt: &mut HtmlParserCtxt) {
1138 if ctxt.in_head && !ctxt.seen_body_content {
1139 ctxt.seen_body_content = true;
1140 ctxt.in_head = false;
1141 ensure_body(ctxt);
1142 }
1143}
1144
1145struct HtmlAttr {
1151 name: Vec<u8>,
1152 value: Vec<u8>,
1153 quoted: bool,
1154}
1155
1156fn parse_attr_name(ctxt: &mut HtmlParserCtxt) -> Vec<u8> {
1158 let mut name = Vec::new();
1159 while let Some(ch) = ctxt.peek() {
1160 if ch == b'='
1161 || ch == b'>'
1162 || ch == b'/'
1163 || ch == b' '
1164 || ch == b'\t'
1165 || ch == b'\n'
1166 || ch == b'\r'
1167 {
1168 break;
1169 }
1170 name.push(ch);
1171 ctxt.next();
1172 }
1173 name
1174}
1175
1176fn parse_attr_value(ctxt: &mut HtmlParserCtxt) -> (Vec<u8>, bool) {
1178 ctxt.skip_whitespace();
1179
1180 let quote = match ctxt.peek() {
1181 Some(b'"') => {
1182 ctxt.next(); b'"'
1184 }
1185 Some(b'\'') => {
1186 ctxt.next(); b'\''
1188 }
1189 _ => {
1190 let value = ctxt.read_while(|ch| {
1192 ch != b'>' && ch != b' ' && ch != b'\t' && ch != b'\n' && ch != b'\r'
1193 });
1194 return (value, false);
1195 }
1196 };
1197
1198 let mut value = Vec::new();
1200 loop {
1201 match ctxt.next() {
1202 Some(ch) if ch == quote => break,
1203 Some(ch) => value.push(ch),
1204 None => break,
1205 }
1206 }
1207 (value, true)
1208}
1209
1210fn parse_attributes(ctxt: &mut HtmlParserCtxt) -> Vec<HtmlAttr> {
1212 let mut attrs = Vec::new();
1213
1214 loop {
1215 ctxt.skip_whitespace();
1216
1217 match ctxt.peek() {
1218 Some(b'>') | None => break,
1219 Some(b'/') => {
1220 if ctxt.peek_at(1) == Some(b'>') {
1222 break;
1223 }
1224 }
1226 _ => {}
1227 }
1228
1229 let name = parse_attr_name(ctxt);
1230 if name.is_empty() {
1231 break;
1232 }
1233
1234 ctxt.skip_whitespace();
1236 if ctxt.peek() == Some(b'=') {
1237 ctxt.next(); let (value, quoted) = parse_attr_value(ctxt);
1239 attrs.push(HtmlAttr {
1240 name,
1241 value,
1242 quoted,
1243 });
1244 } else {
1245 attrs.push(HtmlAttr {
1247 name,
1248 value: Vec::new(),
1249 quoted: false,
1250 });
1251 }
1252 }
1253
1254 attrs
1255}
1256
1257fn resolve_entity(name: &str) -> Vec<u8> {
1263 if let Some(replacement) = html_entity_lookup(name) {
1264 replacement.as_bytes().to_vec()
1265 } else {
1266 let mut result = Vec::new();
1268 result.push(b'&');
1269 result.extend_from_slice(name.as_bytes());
1270 result.push(b';');
1271 result
1272 }
1273}
1274
1275fn resolve_numeric_entity(value: &str, is_hex: bool) -> Vec<u8> {
1277 let codepoint = if is_hex {
1278 u32::from_str_radix(value, 16).unwrap_or(0xFFFD)
1279 } else {
1280 u32::from_str_radix(value, 10).unwrap_or(0xFFFD)
1281 };
1282
1283 if codepoint == 0 {
1284 return Vec::new();
1285 }
1286
1287 match char::from_u32(codepoint) {
1289 Some(c) => {
1290 let mut buf = [0u8; 4];
1291 let s = c.encode_utf8(&mut buf);
1292 s.as_bytes().to_vec()
1293 }
1294 None => vec![0xEF, 0xBF, 0xBD], }
1296}
1297
1298fn parse_entity(ctxt: &mut HtmlParserCtxt) -> Vec<u8> {
1301 match ctxt.peek() {
1303 Some(b'&') => {
1304 ctxt.next(); }
1306 _ => return vec![b'&'],
1307 }
1308
1309 if ctxt.peek() == Some(b'#') {
1311 ctxt.next(); let is_hex = ctxt.peek() == Some(b'x') || ctxt.peek() == Some(b'X');
1313 if is_hex {
1314 ctxt.next(); }
1316
1317 let digits = ctxt.read_while(|ch| {
1318 if is_hex {
1319 ch.is_ascii_hexdigit()
1320 } else {
1321 ch.is_ascii_digit()
1322 }
1323 });
1324
1325 let digits_str = core::str::from_utf8(&digits).unwrap_or("");
1326 if digits_str.is_empty() {
1327 let mut result = vec![b'&', b'#'];
1328 if is_hex {
1329 result.push(b'x');
1330 }
1331 return result;
1332 }
1333
1334 if ctxt.peek() == Some(b';') {
1336 ctxt.next();
1337 }
1338
1339 return resolve_numeric_entity(digits_str, is_hex);
1340 }
1341
1342 let name = ctxt.read_while(|ch| ch.is_ascii_alphanumeric() || ch == b'_' || ch == b'-');
1344 let name_str = core::str::from_utf8(&name).unwrap_or("");
1345
1346 if ctxt.peek() == Some(b';') {
1348 ctxt.next();
1349 }
1350
1351 resolve_entity(name_str)
1352}
1353
1354unsafe fn handle_text(ctxt: &mut HtmlParserCtxt, text: &[u8]) {
1356 if text.is_empty() {
1357 return;
1358 }
1359
1360 let parent = if ctxt.in_head {
1362 ctxt.head
1363 } else if ctxt.in_body || ctxt.body_created {
1364 ctxt.body
1365 } else if ctxt.html_created {
1366 ctxt.html
1367 } else {
1368 ctxt.doc as *mut _xmlNode
1369 };
1370
1371 let insertion_point = if ctxt.current.is_null() {
1372 parent
1373 } else {
1374 ctxt.current
1375 };
1376
1377 if insertion_point.is_null() {
1378 let text_node = tree::new_text(ptr::null_mut());
1380 if !text_node.is_null() {
1381 let content = bytes_to_xmlstr(text);
1383 if !content.is_null() {
1384 unsafe {
1385 (*text_node).content = content;
1386 }
1387 }
1388 tree::add_child(ctxt.doc as *mut _xmlNode, text_node);
1389 }
1390 return;
1391 }
1392
1393 if insertion_point == ctxt.doc as *mut _xmlNode {
1398 if text.iter().all(|b| b.is_ascii_whitespace()) {
1399 return;
1400 }
1401 let content = trim_ascii_start(text);
1402 let html_node = tree::new_node(ptr::null_mut(), bytes_to_xmlstr(b"html"));
1403 if !html_node.is_null() {
1404 let content_c = bytes_to_xmlstr(content);
1405 let text_node = tree::new_text(ptr::null_mut());
1406 if !text_node.is_null() {
1407 unsafe {
1408 (*text_node).content = content_c;
1409 }
1410 tree::add_child(html_node, text_node);
1411 }
1412 tree::add_child(ctxt.doc as *mut _xmlNode, html_node);
1413 ctxt.html = html_node;
1414 ctxt.html_created = true;
1415 ctxt.current = html_node;
1416 }
1417 return;
1418 }
1419
1420 let text_node = tree::new_text(ptr::null_mut());
1421 if text_node.is_null() {
1422 return;
1423 }
1424
1425 let content = bytes_to_xmlstr(text);
1427 if !content.is_null() {
1428 unsafe {
1429 (*text_node).content = content;
1430 }
1431 }
1432
1433 tree::add_child(insertion_point, text_node);
1434}
1435
1436unsafe fn handle_start_tag(ctxt: &mut HtmlParserCtxt, tag_name: &[u8], attrs: &[HtmlAttr]) {
1438 let tag_lower: Vec<u8> = tag_name.iter().map(|b| b.to_ascii_lowercase()).collect();
1439 let tag_str = core::str::from_utf8(&tag_lower).unwrap_or("");
1440
1441 let info = html_tag_lookup(tag_str);
1442
1443 let is_head_tag = info.map_or(false, |i| i.flags & HTML_HEAD != 0);
1445 let is_body_tag = info.map_or(false, |i| i.flags & HTML_BODY != 0);
1446 let is_empty = info.map_or(false, |i| i.flags & HTML_EMPTY != 0);
1447 let is_block = info.map_or(false, |i| i.flags & HTML_BLOCK != 0);
1448
1449 if tag_str == "html" {
1451 if !ctxt.html.is_null() && !ctxt.html_created {
1452 return;
1454 }
1455 if ctxt.html.is_null() {
1457 let html_node = tree::new_node(ptr::null_mut(), bytes_to_xmlstr(tag_name));
1458 if !html_node.is_null() {
1459 ctxt.html = html_node;
1460 ctxt.html_created = false; tree::add_child(ctxt.doc as *mut _xmlNode, html_node);
1462 ctxt.current = html_node;
1463 }
1464 } else {
1465 ctxt.current = ctxt.html;
1467 }
1468 return;
1469 }
1470
1471 if tag_str == "head" {
1472 if !ctxt.head.is_null() && !ctxt.head_created {
1473 return;
1475 }
1476 ensure_html(ctxt);
1478
1479 if ctxt.head.is_null() {
1480 let head_node = tree::new_node(ptr::null_mut(), bytes_to_xmlstr(tag_name));
1481 if !head_node.is_null() {
1482 ctxt.head = head_node;
1483 ctxt.head_created = false;
1484 tree::add_child(ctxt.html, head_node);
1485 ctxt.current = head_node;
1486 ctxt.in_head = true;
1487 }
1488 } else {
1489 ctxt.current = ctxt.head;
1490 ctxt.in_head = true;
1491 }
1492 return;
1493 }
1494
1495 if tag_str == "body" {
1496 if !ctxt.body.is_null() && !ctxt.body_created {
1497 return;
1499 }
1500 ensure_html(ctxt);
1502
1503 if ctxt.body.is_null() {
1504 let body_node = tree::new_node(ptr::null_mut(), bytes_to_xmlstr(tag_name));
1505 if !body_node.is_null() {
1506 ctxt.body = body_node;
1507 ctxt.body_created = false;
1508 tree::add_child(ctxt.html, body_node);
1509 ctxt.current = body_node;
1510 ctxt.in_body = true;
1511 ctxt.in_head = false;
1512 ctxt.seen_body_content = true;
1513 }
1514 } else {
1515 ctxt.current = ctxt.body;
1516 ctxt.in_body = true;
1517 ctxt.in_head = false;
1518 ctxt.seen_body_content = true;
1519 }
1520 return;
1521 }
1522
1523 if is_head_tag && !ctxt.seen_body_content {
1525 if ctxt.head.is_null() {
1526 ensure_head(ctxt);
1527 }
1528
1529 if is_empty {
1530 let node = tree::new_node(ptr::null_mut(), bytes_to_xmlstr(tag_name));
1532 if !node.is_null() {
1533 for attr in attrs {
1534 let name_c = bytes_to_xmlstr(&attr.name);
1535 let val_c = bytes_to_xmlstr(&attr.value);
1536 if !name_c.is_null() {
1537 tree::set_prop(node, name_c, val_c);
1538 xmlFree(name_c as *mut c_void);
1539 if !val_c.is_null() {
1540 xmlFree(val_c as *mut c_void);
1541 }
1542 }
1543 }
1544 tree::add_child(ctxt.current, node);
1545 }
1546 return;
1547 }
1548
1549 let node = tree::new_node(ptr::null_mut(), bytes_to_xmlstr(tag_name));
1550 if !node.is_null() {
1551 for attr in attrs {
1552 let name_c = bytes_to_xmlstr(&attr.name);
1553 let val_c = bytes_to_xmlstr(&attr.value);
1554 if !name_c.is_null() {
1555 tree::set_prop(node, name_c, val_c);
1556 xmlFree(name_c as *mut c_void);
1557 if !val_c.is_null() {
1558 xmlFree(val_c as *mut c_void);
1559 }
1560 }
1561 }
1562 tree::add_child(ctxt.current, node);
1563 ctxt.current = node;
1564 }
1565 return;
1566 }
1567
1568 if !is_head_tag || ctxt.seen_body_content {
1570 if !ctxt.seen_body_content {
1571 ctxt.seen_body_content = true;
1572 ctxt.in_head = false;
1573 if ctxt.body.is_null() {
1574 ensure_body(ctxt);
1575 } else {
1576 ctxt.current = ctxt.body;
1577 ctxt.in_body = true;
1578 }
1579 } else if ctxt.body.is_null() {
1580 ensure_body(ctxt);
1581 }
1582 }
1583
1584 if !ctxt.current.is_null() {
1586 auto_close_element(ctxt, tag_str);
1587 }
1588
1589 if is_empty {
1590 let node = tree::new_node(ptr::null_mut(), bytes_to_xmlstr(tag_name));
1592 if !node.is_null() {
1593 for attr in attrs {
1594 let name_c = bytes_to_xmlstr(&attr.name);
1595 let val_c = bytes_to_xmlstr(&attr.value);
1596 if !name_c.is_null() {
1597 tree::set_prop(node, name_c, val_c);
1598 xmlFree(name_c as *mut c_void);
1599 if !val_c.is_null() {
1600 xmlFree(val_c as *mut c_void);
1601 }
1602 }
1603 }
1604 let insertion_point = if ctxt.current.is_null() {
1605 ctxt.body
1606 } else {
1607 ctxt.current
1608 };
1609 if !insertion_point.is_null() {
1610 tree::add_child(insertion_point, node);
1611 }
1612 }
1613 return;
1614 }
1615
1616 let node = tree::new_node(ptr::null_mut(), bytes_to_xmlstr(tag_name));
1618 if !node.is_null() {
1619 for attr in attrs {
1620 let name_c = bytes_to_xmlstr(&attr.name);
1621 let val_c = bytes_to_xmlstr(&attr.value);
1622 if !name_c.is_null() {
1623 tree::set_prop(node, name_c, val_c);
1624 xmlFree(name_c as *mut c_void);
1625 if !val_c.is_null() {
1626 xmlFree(val_c as *mut c_void);
1627 }
1628 }
1629 }
1630
1631 let insertion_point = if ctxt.current.is_null() {
1632 if ctxt.in_body || ctxt.body_created {
1633 ctxt.body
1634 } else if ctxt.in_head || ctxt.head_created {
1635 ctxt.head
1636 } else if ctxt.html_created {
1637 ctxt.html
1638 } else {
1639 ctxt.doc as *mut _xmlNode
1640 }
1641 } else {
1642 ctxt.current
1643 };
1644
1645 if !insertion_point.is_null() {
1646 tree::add_child(insertion_point, node);
1647 ctxt.current = node;
1649 }
1650 }
1651}
1652
1653unsafe fn handle_end_tag(ctxt: &mut HtmlParserCtxt, tag_name: &[u8]) {
1655 let tag_lower: Vec<u8> = tag_name.iter().map(|b| b.to_ascii_lowercase()).collect();
1656 let tag_str = core::str::from_utf8(&tag_lower).unwrap_or("");
1657
1658 let info = html_tag_lookup(tag_str);
1659
1660 if info.map_or(false, |i| i.flags & HTML_EMPTY != 0) {
1663 return;
1664 }
1665
1666 if tag_str == "html" {
1667 ctxt.current = ctxt.doc as *mut _xmlNode;
1668 return;
1669 }
1670
1671 if tag_str == "head" {
1672 ctxt.in_head = false;
1673 ctxt.current = ctxt.html;
1674 return;
1675 }
1676
1677 if tag_str == "body" {
1678 ctxt.in_body = false;
1679 ctxt.current = ctxt.html;
1680 return;
1681 }
1682
1683 let mut cur = ctxt.current;
1685 while !cur.is_null() {
1686 let ctype = unsafe { (*cur).type_ };
1687 if ctype == XML_ELEMENT_NODE as c_int {
1688 if !unsafe { (*cur).name.is_null() } {
1689 let name_bytes = unsafe { xmlstr_to_bytes((*cur).name) };
1690 if name_bytes.eq_ignore_ascii_case(tag_name) {
1691 ctxt.current = unsafe { (*cur).parent };
1693 return;
1694 }
1695 }
1696 }
1697 cur = unsafe { (*cur).parent };
1698 }
1699
1700 }
1702
1703unsafe fn html_parse_buffer(
1713 ctxt: &mut HtmlParserCtxt,
1714 buffer: *const c_char,
1715 size: c_int,
1716) -> *mut _xmlDoc {
1717 if buffer.is_null() || size <= 0 {
1718 return ptr::null_mut();
1719 }
1720
1721 let doc = tree::new_doc(ptr::null());
1723 if doc.is_null() {
1724 return ptr::null_mut();
1725 }
1726 unsafe {
1727 (*doc).type_ = XML_HTML_DOCUMENT_NODE as c_int;
1728 if !(*doc).version.is_null() {
1731 crate::abi::allocator::xmlFree((*doc).version as *mut c_void);
1732 }
1733 (*doc).version = ptr::null_mut();
1734 (*doc).properties = XML_DOC_WELLFORMED as c_int;
1735 (*doc).standalone = 1;
1738 }
1739 let default_dtd = crate::xml::dtd::create_int_subset(
1745 doc,
1746 b"html\0" as *const u8 as *const xmlChar,
1747 b"-//W3C//DTD HTML 4.0 Transitional//EN\0" as *const u8 as *const xmlChar,
1748 b"http://www.w3.org/TR/REC-html40/loose.dtd\0" as *const u8 as *const xmlChar,
1749 );
1750 let _ = default_dtd;
1751 ctxt.doc = doc;
1752
1753 ctxt.input = buffer as *mut u8;
1755 ctxt.input_len = size as usize;
1756 ctxt.input_pos = 0;
1757 ctxt.line = 1;
1758
1759 loop {
1761 if ctxt.is_eof() {
1762 break;
1763 }
1764
1765 let ch = ctxt.peek().unwrap_or(0);
1766
1767 if ch == b'<' {
1768 ctxt.next(); if ctxt.peek() == Some(b'/') {
1772 ctxt.next(); let tag_name = ctxt.read_while(|ch| {
1774 ch != b'>' && ch != b' ' && ch != b'\t' && ch != b'\n' && ch != b'\r'
1775 });
1776
1777 while ctxt.peek() != Some(b'>') && !ctxt.is_eof() {
1779 ctxt.next();
1780 }
1781 if ctxt.peek() == Some(b'>') {
1782 ctxt.next(); }
1784
1785 if !tag_name.is_empty() {
1786 handle_end_tag(ctxt, &tag_name);
1787 }
1788 continue;
1789 }
1790
1791 if ctxt.peek() == Some(b'!')
1793 && ctxt.peek_at(1) == Some(b'-')
1794 && ctxt.peek_at(2) == Some(b'-')
1795 {
1796 ctxt.next(); ctxt.next(); ctxt.next(); let mut comment_content = Vec::new();
1802 loop {
1803 if ctxt.peek() == Some(b'-')
1804 && ctxt.peek_at(1) == Some(b'-')
1805 && ctxt.peek_at(2) == Some(b'>')
1806 {
1807 ctxt.next(); ctxt.next(); ctxt.next(); break;
1811 }
1812 match ctxt.next() {
1813 Some(ch) => comment_content.push(ch),
1814 None => break,
1815 }
1816 }
1817
1818 if !comment_content.is_empty() {
1820 let comment_node = tree::new_comment(bytes_to_xmlstr(&comment_content));
1821 if !comment_node.is_null() {
1822 let insertion_point = if !ctxt.current.is_null() {
1823 ctxt.current
1824 } else {
1825 ctxt.doc as *mut _xmlNode
1826 };
1827 tree::add_child(insertion_point, comment_node);
1828 }
1829 }
1830 continue;
1831 }
1832
1833 if ctxt.peek() == Some(b'!') {
1835 ctxt.next(); let rest = ctxt.read_while(|ch| ch != b'>');
1837 if ctxt.peek() == Some(b'>') {
1838 ctxt.next(); }
1840 continue;
1843 }
1844
1845 if ctxt.peek() == Some(b'?') {
1847 ctxt.next(); let mut pi_content = Vec::new();
1850 loop {
1851 if ctxt.peek() == Some(b'?') && ctxt.peek_at(1) == Some(b'>') {
1852 break;
1853 }
1854 match ctxt.next() {
1855 Some(ch) => pi_content.push(ch),
1856 None => break,
1857 }
1858 }
1859 if ctxt.peek() == Some(b'?') {
1861 ctxt.next();
1862 }
1863 if ctxt.peek() == Some(b'>') {
1864 ctxt.next();
1865 }
1866 if !pi_content.is_empty() {
1868 let mut parts = pi_content.splitn(2, |b| *b == b' ');
1870 let target = parts.next().unwrap_or(&pi_content);
1871 let value = parts.next().unwrap_or(b"");
1872
1873 let pi_node = tree::new_pi(bytes_to_xmlstr(target), bytes_to_xmlstr(value));
1874 if !pi_node.is_null() {
1875 let insertion_point = if !ctxt.current.is_null() {
1876 ctxt.current
1877 } else {
1878 ctxt.doc as *mut _xmlNode
1879 };
1880 tree::add_child(insertion_point, pi_node);
1881 }
1882 }
1883 continue;
1884 }
1885
1886 let tag_name = ctxt.read_while(|ch| {
1888 ch != b'>' && ch != b'/' && ch != b' ' && ch != b'\t' && ch != b'\n' && ch != b'\r'
1889 });
1890
1891 if tag_name.is_empty() {
1892 handle_text(ctxt, &[b'<']);
1894 continue;
1895 }
1896
1897 let attrs = parse_attributes(ctxt);
1899
1900 if ctxt.peek() == Some(b'/') {
1902 ctxt.next(); if ctxt.peek() == Some(b'>') {
1904 ctxt.next(); }
1906 } else if ctxt.peek() == Some(b'>') {
1907 ctxt.next(); }
1909
1910 let tag_lower: Vec<u8> = tag_name.iter().map(|b| b.to_ascii_lowercase()).collect();
1912 let tag_str = core::str::from_utf8(&tag_lower).unwrap_or("");
1913
1914 if tag_str == "script" || tag_str == "style" {
1915 let raw_node = tree::new_node(ptr::null_mut(), bytes_to_xmlstr(&tag_name));
1918 if !raw_node.is_null() {
1919 for attr in &attrs {
1920 let name_c = bytes_to_xmlstr(&attr.name);
1921 let val_c = bytes_to_xmlstr(&attr.value);
1922 if !name_c.is_null() {
1923 tree::set_prop(raw_node, name_c, val_c);
1924 xmlFree(name_c as *mut c_void);
1925 if !val_c.is_null() {
1926 xmlFree(val_c as *mut c_void);
1927 }
1928 }
1929 }
1930
1931 let insertion_point = if ctxt.current.is_null() {
1932 if ctxt.in_head {
1933 ensure_head(ctxt);
1934 ctxt.head
1935 } else {
1936 ensure_body(ctxt);
1937 ctxt.body
1938 }
1939 } else {
1940 ctxt.current
1941 };
1942
1943 if !insertion_point.is_null() {
1944 tree::add_child(insertion_point, raw_node);
1945
1946 let end_tag = format!("</{}", tag_str);
1948 let end_bytes = end_tag.as_bytes();
1949 let mut raw_text = Vec::new();
1950 let mut match_idx = 0;
1951
1952 loop {
1953 if ctxt.is_eof() {
1954 break;
1955 }
1956 let ch = ctxt.peek().unwrap();
1957 if ch.to_ascii_lowercase() == end_bytes[match_idx] {
1958 match_idx += 1;
1959 if match_idx == end_bytes.len() {
1960 if !raw_text.is_empty() {
1963 let text_node = tree::new_text(bytes_to_xmlstr(&raw_text));
1964 if !text_node.is_null() {
1965 tree::add_child(raw_node, text_node);
1966 }
1967 }
1968 ctxt.next(); let _suffix = ctxt.read_while(|ch| ch != b'>');
1972 if ctxt.peek() == Some(b'>') {
1973 ctxt.next();
1974 }
1975 ctxt.current = unsafe { (*raw_node).parent };
1977 break;
1978 }
1979 if match_idx == 1 {
1981 raw_text.push(ch);
1982 }
1983 ctxt.next();
1984 } else {
1985 if match_idx > 0 {
1987 match_idx = 0;
1989 }
1990 raw_text.push(ch);
1991 ctxt.next();
1992 }
1993 }
1994
1995 if match_idx < end_bytes.len() && !raw_text.is_empty() {
1997 let text_node = tree::new_text(bytes_to_xmlstr(&raw_text));
1998 if !text_node.is_null() {
1999 tree::add_child(raw_node, text_node);
2000 }
2001 ctxt.current = unsafe { (*raw_node).parent };
2002 }
2003 }
2004 }
2005 continue;
2006 }
2007
2008 handle_start_tag(ctxt, &tag_name, &attrs);
2010 } else {
2011 let mut text = Vec::new();
2013 loop {
2014 match ctxt.peek() {
2015 Some(b'<') => break,
2016 Some(b'&') => {
2017 let entity_text = parse_entity(ctxt);
2019 text.extend_from_slice(&entity_text);
2020 }
2021 Some(ch) => {
2022 text.push(ch);
2023 ctxt.next();
2024 }
2025 None => break,
2026 }
2027 }
2028
2029 if !text.is_empty() {
2030 handle_text(ctxt, &text);
2031 }
2032 }
2033 }
2034
2035 if ctxt.html.is_null() {
2037 ensure_html(ctxt);
2038 }
2039
2040 doc
2041}
2042
2043pub unsafe fn parse_file(filename: *const c_char, encoding: *const c_char) -> *mut _xmlDoc {
2058 if filename.is_null() {
2059 return ptr::null_mut();
2060 }
2061
2062 let filename_str = unsafe { std::ffi::CStr::from_ptr(filename) };
2064 let path = filename_str.to_str().unwrap_or("");
2065 let content = match std::fs::read(path) {
2066 Ok(data) => data,
2067 Err(_) => return ptr::null_mut(),
2068 };
2069
2070 let mut ctxt = HtmlParserCtxt::new();
2071 if !encoding.is_null() {
2072 let enc_cstr = unsafe { std::ffi::CStr::from_ptr(encoding) };
2073 ctxt.encoding = unsafe { c_strdup(encoding) };
2074 }
2075
2076 let doc = unsafe {
2077 html_parse_buffer(
2078 &mut ctxt,
2079 content.as_ptr() as *const c_char,
2080 content.len() as c_int,
2081 )
2082 };
2083
2084 if !doc.is_null() && !filename.is_null() {
2085 unsafe {
2086 (*doc).URL = c_strdup(filename) as *mut xmlChar;
2087 }
2088 }
2089
2090 doc
2091}
2092
2093pub unsafe fn parse_memory(buffer: *const c_char, size: c_int) -> *mut _xmlDoc {
2104 if buffer.is_null() || size <= 0 {
2105 return ptr::null_mut();
2106 }
2107
2108 let mut ctxt = HtmlParserCtxt::new();
2109 unsafe { html_parse_buffer(&mut ctxt, buffer, size) }
2110}
2111
2112pub(crate) unsafe fn parse_doc(cur: *const xmlChar, encoding: *const c_char) -> *mut _xmlDoc {
2123 if cur.is_null() {
2124 return ptr::null_mut();
2125 }
2126
2127 let len = unsafe { xml_strlen(cur) };
2128 let mut ctxt = HtmlParserCtxt::new();
2129 if !encoding.is_null() {
2130 ctxt.encoding = unsafe { c_strdup(encoding) };
2131 }
2132
2133 unsafe { html_parse_buffer(&mut ctxt, cur as *const c_char, len as c_int) }
2134}
2135
2136pub(crate) unsafe fn create_file_parser_ctxt(
2147 filename: *const c_char,
2148 encoding: *const c_char,
2149) -> *mut c_void {
2150 if filename.is_null() {
2151 return ptr::null_mut();
2152 }
2153
2154 let ctxt = unsafe { xmlMallocZero(size_of::<HtmlParserCtxt>() as usize) };
2155 if ctxt.is_null() {
2156 return ptr::null_mut();
2157 }
2158
2159 let ctxt = ctxt as *mut HtmlParserCtxt;
2160 unsafe {
2161 ptr::write(ctxt, HtmlParserCtxt::new());
2162 if !encoding.is_null() {
2163 (*ctxt).encoding = c_strdup(encoding);
2164 }
2165 }
2166
2167 ctxt as *mut c_void
2168}
2169
2170pub(crate) unsafe fn free_parser_ctxt(ctxt: *mut c_void) {
2180 if ctxt.is_null() {
2181 return;
2182 }
2183
2184 let ctxt = ctxt as *mut HtmlParserCtxt;
2185 unsafe {
2186 if !(*ctxt).filename.is_null() {
2187 xmlFree((*ctxt).filename as *mut c_void);
2188 }
2189 if !(*ctxt).encoding.is_null() {
2190 xmlFree((*ctxt).encoding as *mut c_void);
2191 }
2192 xmlFree(ctxt as *mut c_void);
2193 }
2194}
2195
2196pub(crate) fn init_parser() {
2202 }
2205
2206pub(crate) fn cleanup_parser() {
2212 }
2215
2216pub(crate) unsafe fn new_doc(version: *const xmlChar) -> *mut _xmlDoc {
2229 let doc = tree::new_doc(version);
2230 if doc.is_null() {
2231 return ptr::null_mut();
2232 }
2233
2234 unsafe {
2235 (*doc).type_ = XML_HTML_DOCUMENT_NODE as c_int;
2236 (*doc).properties = XML_DOC_WELLFORMED as c_int;
2237 }
2238
2239 let mut ctxt = HtmlParserCtxt::new();
2241 ctxt.doc = doc;
2242
2243 unsafe {
2244 ensure_html(&mut ctxt);
2245 ensure_head(&mut ctxt);
2246 ensure_body(&mut ctxt);
2247 }
2248
2249 doc
2250}
2251
2252pub(crate) unsafe fn new_doc_no_dtd(version: *const xmlChar) -> *mut _xmlDoc {
2265 let doc = tree::new_doc(version);
2266 if doc.is_null() {
2267 return ptr::null_mut();
2268 }
2269
2270 unsafe {
2271 (*doc).type_ = XML_HTML_DOCUMENT_NODE as c_int;
2272 (*doc).properties = XML_DOC_WELLFORMED as c_int;
2273 }
2274
2275 doc
2276}
2277
2278const HTML_VOID_ELEMENTS: &[&str] = &[
2284 "area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "param", "source",
2285 "track", "wbr", "frame",
2286];
2287
2288fn is_html_void(name: &str) -> bool {
2290 HTML_VOID_ELEMENTS
2291 .iter()
2292 .any(|v| v.eq_ignore_ascii_case(name))
2293}
2294
2295fn has_optional_end_tag(name: &str) -> bool {
2297 matches!(
2298 name.to_ascii_lowercase().as_str(),
2299 "p" | "li" | "tr" | "td" | "th" | "dt" | "dd"
2300 )
2301}
2302
2303unsafe fn html_write_quoted(buf: *mut _xmlBuffer, s: *const xmlChar) {
2309 if buf.is_null() || s.is_null() {
2310 return;
2311 }
2312 io::buf_ccat(buf, b'"');
2313 io::buf_cat(buf, s);
2314 io::buf_ccat(buf, b'"');
2315}
2316
2317fn trim_ascii_start(s: &[u8]) -> &[u8] {
2319 let start = s
2320 .iter()
2321 .position(|&b| !b.is_ascii_whitespace())
2322 .unwrap_or(s.len());
2323 &s[start..]
2324}
2325
2326unsafe fn html_serialize_text(buf: *mut _xmlBuffer, content: *const xmlChar, len: c_int) {
2327 if buf.is_null() || content.is_null() || len <= 0 {
2328 return;
2329 }
2330
2331 let mut i: c_int = 0;
2332 while i < len {
2333 let ch = unsafe { *content.add(i as usize) };
2334
2335 match ch {
2336 b'<' => {
2337 io::buf_add(buf, b"<" as *const u8, 4);
2338 }
2339 b'&' => {
2340 io::buf_add(buf, b"&" as *const u8, 5);
2341 }
2342 _ => {
2343 io::buf_add(buf, &ch as *const u8, 1);
2344 }
2345 }
2346 i += 1;
2347 }
2348}
2349
2350unsafe fn html_serialize_attr_value(buf: *mut _xmlBuffer, value: *const xmlChar) {
2354 if buf.is_null() || value.is_null() {
2355 return;
2356 }
2357
2358 let len = unsafe { xml_strlen(value) as c_int };
2359 let mut i: c_int = 0;
2360 while i < len {
2361 let ch = unsafe { *value.add(i as usize) };
2362
2363 match ch {
2364 b'&' => {
2365 io::buf_add(buf, b"&" as *const u8, 5);
2366 }
2367 b'"' => {
2368 io::buf_add(buf, b""" as *const u8, 6);
2369 }
2370 _ => {
2371 io::buf_add(buf, &ch as *const u8, 1);
2372 }
2373 }
2374 i += 1;
2375 }
2376}
2377
2378unsafe fn html_head_has_meta(child: *mut _xmlNode) -> bool {
2395 let mut c = child;
2396 while !c.is_null() {
2397 if (*c).type_ == XML_ELEMENT_NODE as c_int && !(*c).name.is_null() {
2398 let nm = xmlstr_to_bytes((*c).name);
2399 if nm.eq_ignore_ascii_case(b"meta") {
2400 return true;
2401 }
2402 }
2403 c = (*c).next;
2404 }
2405 false
2406}
2407
2408pub(crate) unsafe fn serialize_node(
2409 node: *mut _xmlNode,
2410 buf: *mut _xmlBuffer,
2411 format: c_int,
2412 level: c_int,
2413) {
2414 if node.is_null() || buf.is_null() {
2415 return;
2416 }
2417
2418 let n = unsafe { &*node };
2419
2420 match n.type_ {
2421 t if t == XML_ELEMENT_NODE as c_int => {
2422 let name = if n.name.is_null() {
2423 ""
2424 } else {
2425 unsafe { core::str::from_utf8(xmlstr_to_bytes(n.name)).unwrap_or("") }
2426 };
2427
2428 let is_void = is_html_void(name);
2429 let info = html_tag_lookup(name);
2434 let is_inline = info.map_or(true, |i| i.flags & HTML_INLINE != 0);
2435 let no_format = is_inline || name.starts_with('p');
2436
2437 io::buf_ccat(buf, b'<');
2439 if !n.name.is_null() {
2440 io::buf_cat(buf, n.name);
2441 }
2442
2443 let mut attr = n.properties;
2445 while !attr.is_null() {
2446 let a = unsafe { &*attr };
2447 io::buf_ccat(buf, b' ');
2448 if !a.name.is_null() {
2449 io::buf_cat(buf, a.name);
2450 }
2451
2452 if !a.children.is_null() {
2454 let child = unsafe { &*a.children };
2455 if child.type_ == XML_TEXT_NODE as c_int && !child.content.is_null() {
2456 io::buf_ccat(buf, b'=');
2457 io::buf_ccat(buf, b'"');
2458 html_serialize_attr_value(buf, child.content);
2459 io::buf_ccat(buf, b'"');
2460 }
2461 }
2462
2463 attr = a.next;
2464 }
2465
2466 let mut meta_bytes: Option<Vec<u8>> = None;
2473 if name.eq_ignore_ascii_case("head")
2474 && level == 1
2475 && !n.doc.is_null()
2476 && !(*n.doc).encoding.is_null()
2477 {
2478 let parent_is_html = !n.parent.is_null() && !(*n.parent).name.is_null() && {
2479 let pn = core::str::from_utf8(xmlstr_to_bytes((*n.parent).name)).unwrap_or("");
2480 pn.eq_ignore_ascii_case("html")
2481 };
2482 if parent_is_html && !html_head_has_meta(n.children) {
2483 meta_bytes = Some(xmlstr_to_bytes((*n.doc).encoding).to_vec());
2484 }
2485 }
2486 let meta_inserted = meta_bytes.is_some();
2487
2488 let has_children = !n.children.is_null();
2489 let first_child = if has_children {
2490 unsafe { (*n.children).type_ }
2491 } else {
2492 XML_TEXT_NODE as c_int
2493 };
2494 let first_is_text = first_child == XML_TEXT_NODE as c_int
2495 || first_child == XML_ENTITY_REF_NODE as c_int;
2496 let multi_child = (has_children && n.children != n.last) || meta_inserted;
2499
2500 if is_void {
2501 io::buf_ccat(buf, b'>');
2503 } else {
2507 io::buf_ccat(buf, b'>');
2509
2510 if format != 0 && !no_format && !first_is_text && multi_child {
2515 io::buf_ccat(buf, b'\n');
2516 }
2517
2518 if let Some(enc) = &meta_bytes {
2519 io::buf_add(buf, b"<meta charset=\"" as *const u8, 15);
2520 io::buf_add(buf, enc.as_ptr() as *const u8, enc.len() as c_int);
2521 io::buf_add(buf, b"\">" as *const u8, 2);
2522 if format != 0 && has_children && !first_is_text && !name.starts_with('p') {
2525 io::buf_ccat(buf, b'\n');
2526 }
2527 }
2528
2529 let mut child = n.children;
2532 while !child.is_null() {
2533 serialize_node(child, buf, format, level + 1);
2534 let next = unsafe { (*child).next };
2538 if format != 0 && !next.is_null() && !name.starts_with('p') {
2539 let nt = unsafe { (*next).type_ };
2540 if nt != XML_TEXT_NODE as c_int && nt != XML_ENTITY_REF_NODE as c_int {
2541 let cname = if (*child).name.is_null() {
2542 ""
2543 } else {
2544 unsafe {
2545 core::str::from_utf8(xmlstr_to_bytes((*child).name))
2546 .unwrap_or("")
2547 }
2548 };
2549 let cinfo = html_tag_lookup(cname);
2550 let c_inline = cinfo.map_or(true, |i| i.flags & HTML_INLINE != 0);
2551 if !c_inline {
2552 io::buf_ccat(buf, b'\n');
2553 }
2554 }
2555 }
2556 child = next;
2557 }
2558
2559 let last_child = if has_children {
2563 unsafe { (*n.last).type_ }
2564 } else {
2565 XML_ELEMENT_NODE as c_int
2566 };
2567 let last_is_text = last_child == XML_TEXT_NODE as c_int
2568 || last_child == XML_ENTITY_REF_NODE as c_int;
2569 if format != 0 && !no_format && !last_is_text && multi_child {
2570 io::buf_ccat(buf, b'\n');
2571 }
2572
2573 io::buf_add(buf, b"</" as *const u8, 2);
2575 if !n.name.is_null() {
2576 io::buf_cat(buf, n.name);
2577 }
2578 io::buf_ccat(buf, b'>');
2579 }
2580 }
2581 t if t == XML_TEXT_NODE as c_int => {
2582 html_serialize_text(buf, n.content, xml_strlen(n.content) as c_int);
2583 }
2584 t if t == XML_CDATA_SECTION_NODE as c_int => {
2585 io::buf_add(buf, b"<![CDATA[" as *const u8, 9);
2586 html_serialize_text(buf, n.content, xml_strlen(n.content) as c_int);
2587 io::buf_add(buf, b"]]>" as *const u8, 3);
2588 }
2589 t if t == XML_COMMENT_NODE as c_int => {
2590 if format != 0 && level > 0 {
2591 io::buf_ccat(buf, b'\n');
2592 for _ in 0..level {
2593 io::buf_add(buf, b" " as *const u8, 2);
2594 }
2595 }
2596 io::buf_add(buf, b"<!--" as *const u8, 4);
2597 if !n.content.is_null() {
2598 io::buf_cat(buf, n.content);
2599 }
2600 io::buf_add(buf, b"-->" as *const u8, 3);
2601 }
2602 t if t == XML_PI_NODE as c_int => {
2603 if format != 0 && level > 0 {
2604 io::buf_ccat(buf, b'\n');
2605 for _ in 0..level {
2606 io::buf_add(buf, b" " as *const u8, 2);
2607 }
2608 }
2609 io::buf_add(buf, b"<?" as *const u8, 2);
2610 if !n.name.is_null() {
2611 io::buf_cat(buf, n.name);
2612 }
2613 if !n.content.is_null() && unsafe { *n.content != 0 } {
2614 io::buf_ccat(buf, b' ');
2615 io::buf_cat(buf, n.content);
2616 }
2617 io::buf_add(buf, b"?>" as *const u8, 2);
2618 }
2619 t if t == XML_DOCUMENT_NODE as c_int || t == XML_HTML_DOCUMENT_NODE as c_int => {
2620 let doc_ptr = n as *const _xmlNode as *mut _xmlDoc;
2623 let d = &*doc_ptr;
2624 if !d.intSubset.is_null() {
2625 let dtd = &*d.intSubset;
2626 io::buf_add(buf, b"<!DOCTYPE " as *const u8, 10);
2627 if !dtd.name.is_null() {
2628 io::buf_cat(buf, dtd.name);
2629 }
2630 if !dtd.ExternalID.is_null() {
2631 io::buf_add(buf, b" PUBLIC " as *const u8, 8);
2632 html_write_quoted(buf, dtd.ExternalID);
2633 io::buf_ccat(buf, b' ');
2634 html_write_quoted(buf, dtd.SystemID);
2635 } else if !dtd.SystemID.is_null() {
2636 io::buf_add(buf, b" SYSTEM " as *const u8, 8);
2637 html_write_quoted(buf, dtd.SystemID);
2638 }
2639 io::buf_ccat(buf, b'>');
2640 io::buf_ccat(buf, b'\n');
2641 }
2642 let mut child = n.children;
2645 while !child.is_null() {
2646 serialize_node(child, buf, format, 0);
2647 child = unsafe { (*child).next };
2648 }
2649 io::buf_ccat(buf, b'\n');
2652 }
2653 _ => {
2654 if !n.content.is_null() {
2655 html_serialize_text(buf, n.content, xml_strlen(n.content) as c_int);
2656 }
2657 }
2658 }
2659}
2660
2661pub(crate) unsafe fn doc_dump(buf: *mut _xmlBuffer, doc: *mut _xmlDoc) -> c_int {
2668 if buf.is_null() || doc.is_null() {
2669 return -1;
2670 }
2671
2672 let before = io::buf_length(buf);
2673 serialize_node(doc as *mut _xmlNode, buf, 0, 0);
2674 let after = io::buf_length(buf);
2675
2676 if after < 0 || before < 0 {
2677 return -1;
2678 }
2679 after - before
2680}
2681
2682#[cfg(test)]
2687mod tests {
2688 use super::*;
2689 use crate::abi::allocator::xmlFree;
2690 use crate::xml::io;
2691
2692 unsafe fn to_xmlstr(s: &[u8]) -> *mut xmlChar {
2694 bytes_to_xmlstr(s)
2695 }
2696
2697 unsafe fn html_doc_to_string(doc: *mut _xmlDoc) -> String {
2699 let buf = io::buf_create(-1);
2700 assert!(!buf.is_null());
2701 doc_dump(buf, doc);
2702 let content = io::buf_content(buf);
2703 let s = if !content.is_null() {
2704 let len = xml_strlen(content);
2705 let slice = slice::from_raw_parts(content, len as usize);
2706 String::from_utf8_lossy(slice).to_string()
2707 } else {
2708 String::new()
2709 };
2710 io::buf_free(buf);
2711 s
2712 }
2713
2714 #[test]
2719 fn test_html_tag_lookup() {
2720 assert!(html_tag_lookup("html").is_some());
2722 assert!(html_tag_lookup("HTML").is_some()); assert!(html_tag_lookup("p").is_some());
2724 assert!(html_tag_lookup("br").is_some());
2725 assert!(html_tag_lookup("div").is_some());
2726 assert!(html_tag_lookup("script").is_some());
2727
2728 assert!(html_tag_lookup("custom").is_none());
2730 assert!(html_tag_lookup("my-element").is_none());
2731 }
2732
2733 #[test]
2734 fn test_tag_flags() {
2735 let br = html_tag_lookup("br").unwrap();
2736 assert!(br.flags & HTML_INLINE != 0);
2737 assert!(br.flags & HTML_EMPTY != 0);
2738
2739 let div = html_tag_lookup("div").unwrap();
2740 assert!(div.flags & HTML_BLOCK != 0);
2741 assert!(div.flags & HTML_VALID != 0);
2742
2743 let p = html_tag_lookup("p").unwrap();
2744 assert!(p.flags & HTML_NO_END != 0);
2745
2746 let meta = html_tag_lookup("meta").unwrap();
2747 assert!(meta.flags & HTML_HEAD != 0);
2748 assert!(meta.flags & HTML_EMPTY != 0);
2749 }
2750
2751 #[test]
2756 fn test_html_entity_lookup() {
2757 assert_eq!(html_entity_lookup("amp"), Some("&"));
2758 assert_eq!(html_entity_lookup("lt"), Some("<"));
2759 assert_eq!(html_entity_lookup("gt"), Some(">"));
2760 assert_eq!(html_entity_lookup("quot"), Some("\""));
2761 assert_eq!(html_entity_lookup("nbsp"), Some("\u{00a0}"));
2762 assert_eq!(html_entity_lookup("copy"), Some("\u{00a9}"));
2763 assert!(html_entity_lookup("unknown_entity").is_none());
2764 }
2765
2766 #[test]
2771 fn test_parse_basic_html() {
2772 unsafe {
2773 let html = b"<html><head><title>Test</title></head><body><p>Hello</p></body></html>\0";
2774 let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
2775 assert!(!doc.is_null());
2776
2777 let s = html_doc_to_string(doc);
2778 assert!(s.contains("<html>"));
2779 assert!(s.contains("<head>"));
2780 assert!(s.contains("<title>Test</title>"));
2781 assert!(s.contains("<body>"));
2782 assert!(s.contains("<p>Hello</p>"));
2783
2784 tree::free_doc(doc);
2785 }
2786 }
2787
2788 #[test]
2789 fn test_parse_empty_document() {
2790 unsafe {
2791 let html = b"\0";
2792 let doc = parse_memory(html.as_ptr() as *const c_char, 0);
2793 assert!(doc.is_null());
2794 }
2795 }
2796
2797 #[test]
2802 fn test_implicit_html_head_body() {
2803 unsafe {
2804 let html = b"<p>Hello</p>\0";
2806 let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
2807 assert!(!doc.is_null());
2808
2809 let s = html_doc_to_string(doc);
2810 assert!(s.contains("<html>"));
2812 assert!(s.contains("<body>"));
2814 assert!(s.contains("<p>Hello</p>"));
2816
2817 tree::free_doc(doc);
2818 }
2819 }
2820
2821 #[test]
2822 fn test_implicit_head_with_title() {
2823 unsafe {
2824 let html = b"<title>My Page</title>\0";
2826 let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
2827 assert!(!doc.is_null());
2828
2829 let s = html_doc_to_string(doc);
2830 assert!(s.contains("<html>"));
2831 assert!(s.contains("<head>"));
2832 assert!(s.contains("<title>My Page</title>"));
2833
2834 tree::free_doc(doc);
2835 }
2836 }
2837
2838 #[test]
2843 fn test_auto_close_p() {
2844 unsafe {
2845 let html = b"<p>First<p>Second</p>\0";
2847 let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
2848 assert!(!doc.is_null());
2849
2850 let s = html_doc_to_string(doc);
2851 let first_pos = s.find("First");
2853 let second_pos = s.find("Second");
2854 assert!(first_pos.is_some());
2855 assert!(second_pos.is_some());
2856
2857 tree::free_doc(doc);
2858 }
2859 }
2860
2861 #[test]
2862 fn test_auto_close_heading() {
2863 unsafe {
2864 let html = b"<h1>Title</h1><h2>Subtitle</h2>\0";
2866 let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
2867 assert!(!doc.is_null());
2868
2869 let s = html_doc_to_string(doc);
2870 assert!(s.contains("<h1>Title</h1>"));
2871 assert!(s.contains("<h2>Subtitle</h2>"));
2872
2873 tree::free_doc(doc);
2874 }
2875 }
2876
2877 #[test]
2882 fn test_void_elements() {
2883 unsafe {
2884 let html = b"<br><hr><img src=\"test.jpg\"><input type=\"text\">\0";
2885 let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
2886 assert!(!doc.is_null());
2887
2888 let s = html_doc_to_string(doc);
2889 assert!(s.contains("<br>"));
2890 assert!(s.contains("<hr>"));
2891 assert!(s.contains("<img"));
2892 assert!(s.contains("<input"));
2893
2894 assert!(!s.contains("</br>"));
2896 assert!(!s.contains("</hr>"));
2897 assert!(!s.contains("</img>"));
2898
2899 tree::free_doc(doc);
2900 }
2901 }
2902
2903 #[test]
2908 fn test_unquoted_attributes() {
2909 unsafe {
2910 let html = b"<div class=main id=content>Text</div>\0";
2911 let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
2912 assert!(!doc.is_null());
2913
2914 let s = html_doc_to_string(doc);
2915 assert!(s.contains("class=\"main\""));
2916 assert!(s.contains("id=\"content\""));
2917
2918 tree::free_doc(doc);
2919 }
2920 }
2921
2922 #[test]
2923 fn test_minimized_attributes() {
2924 unsafe {
2925 let html = b"<option selected disabled>Value</option>\0";
2926 let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
2927 assert!(!doc.is_null());
2928
2929 let s = html_doc_to_string(doc);
2930 assert!(s.contains("selected"));
2932 assert!(s.contains("disabled"));
2933
2934 tree::free_doc(doc);
2935 }
2936 }
2937
2938 #[test]
2943 fn test_html_entities() {
2944 unsafe {
2945 let html = b"<p>& < > " ©</p>\0";
2946 let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
2947 assert!(!doc.is_null());
2948
2949 let s = html_doc_to_string(doc);
2950 assert!(s.contains("&")); assert!(s.contains("<")); assert!(s.contains(">")); assert!(s.contains("\u{00a0}")); tree::free_doc(doc);
2958 }
2959 }
2960
2961 #[test]
2962 fn test_numeric_entities() {
2963 unsafe {
2964 let html = b"<p>A A</p>\0";
2966 let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
2967 assert!(!doc.is_null());
2968
2969 let s = html_doc_to_string(doc);
2970 assert!(s.contains('A'));
2971
2972 tree::free_doc(doc);
2973 }
2974 }
2975
2976 #[test]
2981 fn test_nested_elements() {
2982 unsafe {
2983 let html = b"<div><ul><li>Item 1</li><li>Item 2</li></ul></div>\0";
2984 let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
2985 assert!(!doc.is_null());
2986
2987 let s = html_doc_to_string(doc);
2988 assert!(s.contains("<div>"));
2989 assert!(s.contains("<ul>"));
2990 assert!(s.contains("<li>Item 1</li>"));
2991 assert!(s.contains("<li>Item 2</li>"));
2992
2993 tree::free_doc(doc);
2994 }
2995 }
2996
2997 #[test]
3002 fn test_missing_end_tags() {
3003 unsafe {
3004 let html = b"<p>Paragraph without closing<div>Another div\0";
3006 let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
3007 assert!(!doc.is_null());
3008
3009 let s = html_doc_to_string(doc);
3010 assert!(s.contains("Paragraph without closing"));
3011 assert!(s.contains("Another div"));
3012
3013 tree::free_doc(doc);
3014 }
3015 }
3016
3017 #[test]
3018 fn test_mismatched_case() {
3019 unsafe {
3020 let html = b"<HTML><HEAD><TITLE>Test</TITLE></HEAD><BODY><P>Hello</P></BODY></HTML>\0";
3021 let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
3022 assert!(!doc.is_null());
3023
3024 let s = html_doc_to_string(doc);
3025 assert!(s.contains("<HTML>"));
3027 assert!(s.contains("<HEAD>"));
3028 assert!(s.contains("<BODY>"));
3029 assert!(s.contains("<P>Hello</P>"));
3030
3031 tree::free_doc(doc);
3032 }
3033 }
3034
3035 #[test]
3036 fn test_nested_malformed() {
3037 unsafe {
3038 let html = b"<div><p><span><b>Deep text</div></p>\0";
3040 let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
3041 assert!(!doc.is_null());
3042
3043 let s = html_doc_to_string(doc);
3044 assert!(s.contains("Deep text"));
3045
3046 tree::free_doc(doc);
3047 }
3048 }
3049
3050 #[test]
3055 fn test_serialization_round_trip_simple() {
3056 unsafe {
3057 let original = b"<p>Hello World</p>\0";
3058 let doc = parse_memory(
3059 original.as_ptr() as *const c_char,
3060 (original.len() - 1) as c_int,
3061 );
3062 assert!(!doc.is_null());
3063
3064 let s = html_doc_to_string(doc);
3065 assert!(s.contains("Hello World"));
3066
3067 tree::free_doc(doc);
3068 }
3069 }
3070
3071 #[test]
3072 fn test_serialize_void_elements_no_self_close() {
3073 unsafe {
3074 let html = b"<br><hr><img src=\"test.png\">\0";
3075 let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
3076 assert!(!doc.is_null());
3077
3078 let s = html_doc_to_string(doc);
3079 assert!(!s.contains("<br/>"));
3081 assert!(!s.contains("<hr/>"));
3082
3083 tree::free_doc(doc);
3084 }
3085 }
3086
3087 #[test]
3092 fn test_script_content() {
3093 unsafe {
3094 let html = b"<script>var x = 1;</script>\0";
3096 let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
3097 assert!(!doc.is_null());
3098
3099 let s = html_doc_to_string(doc);
3100 assert!(s.contains("<script>"));
3101 assert!(s.contains("var x = 1;"));
3103
3104 tree::free_doc(doc);
3105 }
3106 }
3107
3108 #[test]
3113 fn test_html_comment() {
3114 unsafe {
3115 let html = b"<html><!-- This is a comment --><body><p>Text</p></body></html>\0";
3116 let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
3117 assert!(!doc.is_null());
3118
3119 let s = html_doc_to_string(doc);
3120 assert!(s.contains("<!-- This is a comment -->"));
3121
3122 tree::free_doc(doc);
3123 }
3124 }
3125
3126 #[test]
3131 fn test_new_doc_creates_html_head_body() {
3132 unsafe {
3133 let doc = new_doc(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);
3138 assert!(s.contains("<html>"));
3139 assert!(s.contains("<head>"));
3140 assert!(s.contains("<body>"));
3141
3142 tree::free_doc(doc);
3143 }
3144 }
3145
3146 #[test]
3147 fn test_new_doc_no_dtd() {
3148 unsafe {
3149 let doc = new_doc_no_dtd(ptr::null());
3150 assert!(!doc.is_null());
3151 assert_eq!((*doc).type_, XML_HTML_DOCUMENT_NODE as c_int);
3152
3153 let s = html_doc_to_string(doc);
3157 assert_eq!(s, "\n");
3158
3159 tree::free_doc(doc);
3160 }
3161 }
3162
3163 #[test]
3168 fn test_resolve_numeric_entity() {
3169 assert_eq!(resolve_numeric_entity("65", false), vec![b'A']);
3170 assert_eq!(resolve_numeric_entity("41", true), vec![b'A']);
3171 assert_eq!(resolve_numeric_entity("0", false), Vec::<u8>::new());
3172 }
3173
3174 #[test]
3175 fn test_resolve_entity_unknown() {
3176 let result = resolve_entity("unknown");
3177 assert_eq!(result, b"&unknown;");
3178 }
3179
3180 #[test]
3185 fn test_init_cleanup_parser() {
3186 init_parser();
3188 cleanup_parser();
3189 }
3190
3191 #[test]
3196 fn test_create_free_parser_ctxt() {
3197 unsafe {
3198 let ctxt =
3199 create_file_parser_ctxt(b"test.html\0" as *const u8 as *const c_char, ptr::null());
3200 assert!(!ctxt.is_null());
3201 free_parser_ctxt(ctxt);
3202 }
3203 }
3204
3205 #[test]
3210 fn test_complex_html_document() {
3211 unsafe {
3212 let html = b"<!DOCTYPE html>
3213<html>
3214<head>
3215 <meta charset=\"utf-8\">
3216 <title>Test Page</title>
3217 <link rel=\"stylesheet\" href=\"style.css\">
3218</head>
3219<body>
3220 <div id=\"main\">
3221 <h1>Title</h1>
3222 <p>First paragraph with <a href=\"link.html\">a link</a>.</p>
3223 <p>Second paragraph.</p>
3224 <ul>
3225 <li>Item 1</li>
3226 <li>Item 2</li>
3227 </ul>
3228 <br>
3229 <hr>
3230 <img src=\"image.jpg\" alt=\"An image\">
3231 </div>
3232 <script>alert('hello');</script>
3233</body>
3234</html>\0";
3235 let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
3236 assert!(!doc.is_null());
3237
3238 let s = html_doc_to_string(doc);
3239 assert!(s.contains("<html>"));
3240 assert!(s.contains("<head>"));
3241 assert!(s.contains("<title>Test Page</title>"));
3242 assert!(s.contains("<body>"));
3243 assert!(s.contains("<h1>Title</h1>"));
3244 assert!(s.contains("a link"));
3245 assert!(s.contains("Second paragraph"));
3246 assert!(s.contains("<br>"));
3247 assert!(s.contains("<hr>"));
3248 assert!(s.contains("<img"));
3249 assert!(s.contains("<script>"));
3250
3251 tree::free_doc(doc);
3252 }
3253 }
3254
3255 #[test]
3260 fn test_parse_doc() {
3261 unsafe {
3262 let html = b"<p>Hello from parse_doc</p>";
3263 let doc = parse_doc(html.as_ptr() as *const xmlChar, ptr::null());
3264 assert!(!doc.is_null());
3265
3266 let s = html_doc_to_string(doc);
3267 assert!(s.contains("Hello from parse_doc"));
3268
3269 tree::free_doc(doc);
3270 }
3271 }
3272
3273 #[test]
3278 fn test_table_element_auto_close() {
3279 unsafe {
3280 let html = b"<table><tr><td>Cell 1<td>Cell 2</td></tr></table>";
3281 let doc = parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
3282 assert!(!doc.is_null());
3283
3284 let s = html_doc_to_string(doc);
3285 assert!(s.contains("<td>Cell 1"));
3286 assert!(s.contains("<td>Cell 2"));
3287
3288 tree::free_doc(doc);
3289 }
3290 }
3291}