Skip to main content

libxml_rs/abi/
exports_html.rs

1//! exports_html — HTML C ABI family (HTMLparser.h, HTMLtree.h, SAX2.h).
2//!
3//! Implements the 57 `html*` entry points of the libxml2 HTML subsystem
4//! (family closure 11.1-I) with exact upstream signatures:
5//!
6//! - Parser contexts: `htmlNewParserCtxt`, `htmlNewSAXParserCtxt`,
7//!   `htmlCreateMemoryParserCtxt`, `htmlCreatePushParserCtxt`,
8//!   `htmlCtxtReset`, `htmlCtxtUseOptions`, `htmlCtxtParseDocument`,
9//!   `htmlParseDocument`, `htmlParseChunk`, `htmlCtxtReadMemory/File/Fd/IO/Doc`,
10//!   `htmlReadMemory/File/Fd/IO/Doc`, `htmlSAXParseDoc`, `htmlSAXParseFile`.
11//! - Entities/encoding: `htmlEncodeEntities`, `htmlDecodeEntities`,
12//!   `htmlEntityLookup`, `htmlEntityValueLookup`, `htmlGetMetaEncoding`,
13//!   `htmlSetMetaEncoding`, `htmlIsBooleanAttr`, `htmlIsScriptAttribute`.
14//! - Tree: `htmlNewDoc`, `htmlNewDocNoDtD`, `htmlDocDump`,
15//!   `htmlDocDumpMemory(Format)`, `htmlDocContentDumpOutput(FormatOutput)`,
16//!   `htmlNodeDump(File/FileFormat/Output/FormatOutput)`,
17//!   `htmlSaveFile(Enc/Format)`.
18//! - Element rules: `htmlAutoCloseTag`, `htmlElementAllowedHere`,
19//!   `htmlElementStatusHere`, `htmlNodeStatus`, `htmlIsAutoClosed`,
20//!   `htmlHandleOmittedElem`, `htmlInitAutoClose`, `htmlTagLookup`,
21//!   `htmlAttrAllowed`.
22//! - Misc: `htmlDefaultSAXHandlerInit`, `htmlParseElement`,
23//!   `htmlParseEntityRef`, `htmlParseCharRef`.
24//!
25//! Semantics follow archaeology/libxml2-git (HTMLparser.c, HTMLtree.c,
26//! SAX2.c, legacy.c). The document-parsing entry points wrap the internal
27//! module `src/xml/html/mod.rs` (`parse_memory` / `parse_file` / `parse_doc` /
28//! `serialize_node` / `new_doc*`), which is the oracle-verified engine used by
29//! `xmllint --html`.
30//!
31//! The `htmlParserCtxt` type is opaque at this boundary (`*mut c_void`):
32//! structs.rs has no `_htmlParserCtxt` mirror. Contexts created here are
33//! freed by the already-exported `htmlFreeParserCtxt`
34//! (`crate::xml::html::free_parser_ctxt`), so the context struct mirrors the
35//! internal `HtmlParserCtxt` field layout through `filename`/`encoding`
36//! (the only fields that function touches) before appending ABI state.
37
38#![allow(
39    missing_docs,
40    non_snake_case,
41    non_camel_case_types,
42    non_upper_case_globals
43)]
44#![allow(unused_variables)]
45#![allow(private_interfaces)]
46#![allow(unused_assignments)]
47#![allow(unused_unsafe)]
48#![allow(clippy::missing_safety_doc)]
49#![allow(clippy::not_unsafe_ptr_arg_deref)]
50
51use core::ffi::c_void;
52use core::ptr;
53use core::sync::atomic::{AtomicBool, AtomicI32, Ordering};
54use std::mem::size_of;
55use std::os::raw::{c_char, c_int, c_uint};
56
57use crate::abi::allocator::{xmlFree, xmlMalloc, xmlMallocZero, xmlRealloc};
58use crate::abi::callbacks::{xmlInputCloseCallback, xmlInputReadCallback};
59use crate::abi::structs::*;
60use crate::abi::types::xmlChar;
61use crate::abi::types::xmlCharEncoding;
62use crate::abi::types::xmlElementType::*;
63use crate::xml::html;
64use crate::xml::io;
65use crate::xml::string::{c_strdup, xml_strcmp, xml_strlen, xml_strndup, xmlstr_to_bytes};
66use crate::xml::tree;
67
68// ═══════════════════════════════════════════════════════════════════════════════
69// HTML parser options / status / error constants (HTMLparser.h)
70// ═══════════════════════════════════════════════════════════════════════════════
71
72const HTML_PARSE_RECOVER: c_int = 1 << 0;
73const HTML_PARSE_NODEFDTD: c_int = 1 << 2;
74const HTML_PARSE_NOERROR: c_int = 1 << 5;
75const HTML_PARSE_NOWARNING: c_int = 1 << 6;
76const HTML_PARSE_PEDANTIC: c_int = 1 << 7;
77const HTML_PARSE_NOBLANKS: c_int = 1 << 8;
78const HTML_PARSE_NONET: c_int = 1 << 11;
79const HTML_PARSE_NOIMPLIED: c_int = 1 << 13;
80const HTML_PARSE_COMPACT: c_int = 1 << 16;
81const HTML_PARSE_HUGE: c_int = 1 << 19;
82const HTML_PARSE_IGNORE_ENC: c_int = 1 << 21;
83const HTML_PARSE_BIG_LINES: c_int = 1 << 22;
84const HTML_PARSE_HTML5: c_int = 1 << 26;
85
86/// Options that `htmlCtxtUseOptions` can only enable, never clear.
87const HTML_OPTIONS_KEEP_MASK: c_int = HTML_PARSE_NODEFDTD
88    | HTML_PARSE_NOERROR
89    | HTML_PARSE_NOWARNING
90    | HTML_PARSE_NOIMPLIED
91    | HTML_PARSE_COMPACT
92    | HTML_PARSE_HUGE
93    | HTML_PARSE_IGNORE_ENC
94    | HTML_PARSE_BIG_LINES;
95
96/// All options the HTML parser recognizes (upstream `htmlCtxtSetOptionsInternal`).
97const HTML_OPTIONS_ALL_MASK: c_int = HTML_PARSE_RECOVER
98    | HTML_PARSE_HTML5
99    | HTML_PARSE_NODEFDTD
100    | HTML_PARSE_NOERROR
101    | HTML_PARSE_NOWARNING
102    | HTML_PARSE_PEDANTIC
103    | HTML_PARSE_NOBLANKS
104    | HTML_PARSE_NONET
105    | HTML_PARSE_NOIMPLIED
106    | HTML_PARSE_COMPACT
107    | HTML_PARSE_HUGE
108    | HTML_PARSE_IGNORE_ENC
109    | HTML_PARSE_BIG_LINES;
110
111/// `xmlParserErrors` values used by the HTML parser ABI.
112const XML_ERR_OK: c_int = 0;
113const XML_ERR_NO_MEMORY: c_int = 2;
114const XML_ERR_ARGUMENT: c_int = 115;
115
116/// `htmlStatus` (HTMLparser.h, deprecated content model).
117const HTML_VALID: c_int = 0x4;
118
119/// HTML element data modes (include/private/html.h).
120const DATA_NEUTRAL: c_int = 0;
121const DATA_RCDATA: c_int = 1;
122const DATA_RAWTEXT: c_int = 2;
123const DATA_PLAINTEXT: c_int = 3;
124const DATA_SCRIPT: c_int = 4;
125
126/// HTML whitespace predicate `IS_WS_HTML` (include/private/html.h).
127#[inline]
128fn is_ws_html(c: u8) -> bool {
129    c == 0x20 || (c >= 0x09 && c <= 0x0d && c != 0x0b)
130}
131
132// ═══════════════════════════════════════════════════════════════════════════════
133// htmlElemDesc / htmlAttributeDesc mirrors (HTMLparser.h)
134// ═══════════════════════════════════════════════════════════════════════════════
135
136/// Rust mirror of `struct _htmlElemDesc` (HTMLparser.h) with the upstream
137/// field layout. Only `name`, `endTag`, `empty`, `isinline` and `desc` are
138/// read by the exported functions; the deprecated pointer fields are kept
139/// NULL for layout parity.
140#[repr(C)]
141pub struct _htmlElemDesc {
142    pub name: *const c_char,
143    pub startTag: c_char,
144    pub endTag: c_char,
145    pub saveEndTag: c_char,
146    pub empty: c_char,
147    pub depr: c_char,
148    pub dtd: c_char,
149    pub isinline: c_char,
150    pub desc: *const c_char,
151    pub subelts: *const *const c_char,
152    pub defaultsubelt: *const c_char,
153    pub attrs_opt: *const *const c_char,
154    pub attrs_depr: *const *const c_char,
155    pub attrs_req: *const *const c_char,
156    pub dataMode: c_int,
157}
158
159// SAFETY: the struct only contains pointers into static, immutable tables.
160unsafe impl Sync for _htmlElemDesc {}
161unsafe impl Send for _htmlElemDesc {}
162
163/// Build a static `_htmlElemDesc` from the (name, startTag, endTag,
164/// saveEndTag, empty, depr, dtd, isinline, desc, dataMode) tuple used by the
165/// upstream `html40ElementTable`.
166macro_rules! elem {
167    ($name:literal, $startTag:expr, $endTag:expr, $saveEndTag:expr, $empty:expr, $depr:expr, $dtd:expr, $isinline:expr, $desc:literal, $dataMode:expr) => {
168        _htmlElemDesc {
169            name: concat!($name, "\0").as_ptr() as *const c_char,
170            startTag: $startTag,
171            endTag: $endTag,
172            saveEndTag: $saveEndTag,
173            empty: $empty,
174            depr: $depr,
175            dtd: $dtd,
176            isinline: $isinline,
177            desc: concat!($desc, "\0").as_ptr() as *const c_char,
178            subelts: ptr::null(),
179            defaultsubelt: ptr::null(),
180            attrs_opt: ptr::null(),
181            attrs_depr: ptr::null(),
182            attrs_req: ptr::null(),
183            dataMode: $dataMode,
184        }
185    };
186}
187
188/// The HTML 4.01 element table, faithfully ported from
189/// archaeology/libxml2-git/HTMLparser.c `html40ElementTable`.
190static HTML40_ELEMENTS: &[_htmlElemDesc] = &[
191    elem!("a", 0, 0, 0, 0, 0, 0, 1, "anchor ", DATA_NEUTRAL),
192    elem!(
193        "abbr",
194        0,
195        0,
196        0,
197        0,
198        0,
199        0,
200        1,
201        "abbreviated form",
202        DATA_NEUTRAL
203    ),
204    elem!("acronym", 0, 0, 0, 0, 0, 0, 1, "", DATA_NEUTRAL),
205    elem!(
206        "address",
207        0,
208        0,
209        0,
210        0,
211        0,
212        0,
213        0,
214        "information on author ",
215        DATA_NEUTRAL
216    ),
217    elem!("applet", 0, 0, 0, 0, 1, 1, 2, "java applet ", DATA_NEUTRAL),
218    elem!(
219        "area",
220        0,
221        2,
222        2,
223        1,
224        0,
225        0,
226        0,
227        "client-side image map area ",
228        DATA_NEUTRAL
229    ),
230    elem!("b", 0, 3, 0, 0, 0, 0, 1, "bold text style", DATA_NEUTRAL),
231    elem!(
232        "base",
233        0,
234        2,
235        2,
236        1,
237        0,
238        0,
239        0,
240        "document base uri ",
241        DATA_NEUTRAL
242    ),
243    elem!(
244        "basefont",
245        0,
246        2,
247        2,
248        1,
249        1,
250        1,
251        1,
252        "base font size ",
253        DATA_NEUTRAL
254    ),
255    elem!(
256        "bdo",
257        0,
258        0,
259        0,
260        0,
261        0,
262        0,
263        1,
264        "i18n bidi over-ride ",
265        DATA_NEUTRAL
266    ),
267    elem!("bgsound", 0, 0, 2, 1, 0, 0, 0, "", DATA_NEUTRAL),
268    elem!("big", 0, 3, 0, 0, 0, 0, 1, "large text style", DATA_NEUTRAL),
269    elem!(
270        "blockquote",
271        0,
272        0,
273        0,
274        0,
275        0,
276        0,
277        0,
278        "long quotation ",
279        DATA_NEUTRAL
280    ),
281    elem!("body", 1, 1, 0, 0, 0, 0, 0, "document body ", DATA_NEUTRAL),
282    elem!(
283        "br",
284        0,
285        2,
286        2,
287        1,
288        0,
289        0,
290        1,
291        "forced line break ",
292        DATA_NEUTRAL
293    ),
294    elem!("button", 0, 0, 0, 0, 0, 0, 2, "push button ", DATA_NEUTRAL),
295    elem!(
296        "caption",
297        0,
298        0,
299        0,
300        0,
301        0,
302        0,
303        0,
304        "table caption ",
305        DATA_NEUTRAL
306    ),
307    elem!(
308        "center",
309        0,
310        3,
311        0,
312        0,
313        1,
314        1,
315        0,
316        "shorthand for div align=center ",
317        DATA_NEUTRAL
318    ),
319    elem!("cite", 0, 0, 0, 0, 0, 0, 1, "citation", DATA_NEUTRAL),
320    elem!(
321        "code",
322        0,
323        0,
324        0,
325        0,
326        0,
327        0,
328        1,
329        "computer code fragment",
330        DATA_NEUTRAL
331    ),
332    elem!("col", 0, 2, 2, 1, 0, 0, 0, "table column ", DATA_NEUTRAL),
333    elem!(
334        "colgroup",
335        0,
336        1,
337        0,
338        0,
339        0,
340        0,
341        0,
342        "table column group ",
343        DATA_NEUTRAL
344    ),
345    elem!(
346        "dd",
347        0,
348        1,
349        0,
350        0,
351        0,
352        0,
353        0,
354        "definition description ",
355        DATA_NEUTRAL
356    ),
357    elem!("del", 0, 0, 0, 0, 0, 0, 2, "deleted text ", DATA_NEUTRAL),
358    elem!(
359        "dfn",
360        0,
361        0,
362        0,
363        0,
364        0,
365        0,
366        1,
367        "instance definition",
368        DATA_NEUTRAL
369    ),
370    elem!("dir", 0, 0, 0, 0, 1, 1, 0, "directory list", DATA_NEUTRAL),
371    elem!(
372        "div",
373        0,
374        0,
375        0,
376        0,
377        0,
378        0,
379        0,
380        "generic language/style container",
381        DATA_NEUTRAL
382    ),
383    elem!("dl", 0, 0, 0, 0, 0, 0, 0, "definition list ", DATA_NEUTRAL),
384    elem!("dt", 0, 1, 0, 0, 0, 0, 0, "definition term ", DATA_NEUTRAL),
385    elem!("em", 0, 3, 0, 0, 0, 0, 1, "emphasis", DATA_NEUTRAL),
386    elem!(
387        "embed",
388        0,
389        1,
390        2,
391        1,
392        1,
393        1,
394        1,
395        "generic embedded object ",
396        DATA_NEUTRAL
397    ),
398    elem!(
399        "fieldset",
400        0,
401        0,
402        0,
403        0,
404        0,
405        0,
406        0,
407        "form control group ",
408        DATA_NEUTRAL
409    ),
410    elem!(
411        "font",
412        0,
413        3,
414        0,
415        0,
416        1,
417        1,
418        1,
419        "local change to font ",
420        DATA_NEUTRAL
421    ),
422    elem!(
423        "form",
424        0,
425        0,
426        0,
427        0,
428        0,
429        0,
430        0,
431        "interactive form ",
432        DATA_NEUTRAL
433    ),
434    elem!("frame", 0, 2, 2, 1, 0, 2, 0, "subwindow ", DATA_NEUTRAL),
435    elem!(
436        "frameset",
437        0,
438        0,
439        0,
440        0,
441        0,
442        2,
443        0,
444        "window subdivision",
445        DATA_NEUTRAL
446    ),
447    elem!("h1", 0, 0, 0, 0, 0, 0, 0, "heading ", DATA_NEUTRAL),
448    elem!("h2", 0, 0, 0, 0, 0, 0, 0, "heading ", DATA_NEUTRAL),
449    elem!("h3", 0, 0, 0, 0, 0, 0, 0, "heading ", DATA_NEUTRAL),
450    elem!("h4", 0, 0, 0, 0, 0, 0, 0, "heading ", DATA_NEUTRAL),
451    elem!("h5", 0, 0, 0, 0, 0, 0, 0, "heading ", DATA_NEUTRAL),
452    elem!("h6", 0, 0, 0, 0, 0, 0, 0, "heading ", DATA_NEUTRAL),
453    elem!("head", 1, 1, 0, 0, 0, 0, 0, "document head ", DATA_NEUTRAL),
454    elem!("hr", 0, 2, 2, 1, 0, 0, 0, "horizontal rule ", DATA_NEUTRAL),
455    elem!(
456        "html",
457        1,
458        1,
459        0,
460        0,
461        0,
462        0,
463        0,
464        "document root element ",
465        DATA_NEUTRAL
466    ),
467    elem!("i", 0, 3, 0, 0, 0, 0, 1, "italic text style", DATA_NEUTRAL),
468    elem!(
469        "iframe",
470        0,
471        0,
472        0,
473        0,
474        0,
475        1,
476        2,
477        "inline subwindow ",
478        DATA_RAWTEXT
479    ),
480    elem!("img", 0, 2, 2, 1, 0, 0, 1, "embedded image ", DATA_NEUTRAL),
481    elem!("input", 0, 2, 2, 1, 0, 0, 1, "form control ", DATA_NEUTRAL),
482    elem!("ins", 0, 0, 0, 0, 0, 0, 2, "inserted text", DATA_NEUTRAL),
483    elem!(
484        "isindex",
485        0,
486        2,
487        2,
488        1,
489        1,
490        1,
491        0,
492        "single line prompt ",
493        DATA_NEUTRAL
494    ),
495    elem!(
496        "kbd",
497        0,
498        0,
499        0,
500        0,
501        0,
502        0,
503        1,
504        "text to be entered by the user",
505        DATA_NEUTRAL
506    ),
507    elem!("keygen", 0, 0, 2, 1, 0, 0, 0, "", DATA_NEUTRAL),
508    elem!(
509        "label",
510        0,
511        0,
512        0,
513        0,
514        0,
515        0,
516        1,
517        "form field label text ",
518        DATA_NEUTRAL
519    ),
520    elem!(
521        "legend",
522        0,
523        0,
524        0,
525        0,
526        0,
527        0,
528        0,
529        "fieldset legend ",
530        DATA_NEUTRAL
531    ),
532    elem!("li", 0, 1, 1, 0, 0, 0, 0, "list item ", DATA_NEUTRAL),
533    elem!(
534        "link",
535        0,
536        2,
537        2,
538        1,
539        0,
540        0,
541        0,
542        "a media-independent link ",
543        DATA_NEUTRAL
544    ),
545    elem!(
546        "map",
547        0,
548        0,
549        0,
550        0,
551        0,
552        0,
553        2,
554        "client-side image map ",
555        DATA_NEUTRAL
556    ),
557    elem!("menu", 0, 0, 0, 0, 1, 1, 0, "menu list ", DATA_NEUTRAL),
558    elem!(
559        "meta",
560        0,
561        2,
562        2,
563        1,
564        0,
565        0,
566        0,
567        "generic metainformation ",
568        DATA_NEUTRAL
569    ),
570    elem!("noembed", 0, 0, 0, 0, 0, 0, 0, "", DATA_RAWTEXT),
571    elem!(
572        "noframes",
573        0,
574        0,
575        0,
576        0,
577        0,
578        2,
579        0,
580        "alternate content container for non frame-based rendering ",
581        DATA_RAWTEXT
582    ),
583    elem!(
584        "noscript",
585        0,
586        0,
587        0,
588        0,
589        0,
590        0,
591        0,
592        "alternate content container for non script-based rendering ",
593        DATA_NEUTRAL
594    ),
595    elem!(
596        "object",
597        0,
598        0,
599        0,
600        0,
601        0,
602        0,
603        2,
604        "generic embedded object ",
605        DATA_NEUTRAL
606    ),
607    elem!("ol", 0, 0, 0, 0, 0, 0, 0, "ordered list ", DATA_NEUTRAL),
608    elem!(
609        "optgroup",
610        0,
611        0,
612        0,
613        0,
614        0,
615        0,
616        0,
617        "option group ",
618        DATA_NEUTRAL
619    ),
620    elem!(
621        "option",
622        0,
623        1,
624        0,
625        0,
626        0,
627        0,
628        0,
629        "selectable choice ",
630        DATA_NEUTRAL
631    ),
632    elem!("p", 0, 1, 0, 0, 0, 0, 0, "paragraph ", DATA_NEUTRAL),
633    elem!(
634        "param",
635        0,
636        2,
637        2,
638        1,
639        0,
640        0,
641        0,
642        "named property value ",
643        DATA_NEUTRAL
644    ),
645    elem!("plaintext", 0, 0, 0, 0, 0, 0, 0, "", DATA_PLAINTEXT),
646    elem!(
647        "pre",
648        0,
649        0,
650        0,
651        0,
652        0,
653        0,
654        0,
655        "preformatted text ",
656        DATA_NEUTRAL
657    ),
658    elem!(
659        "q",
660        0,
661        0,
662        0,
663        0,
664        0,
665        0,
666        1,
667        "short inline quotation ",
668        DATA_NEUTRAL
669    ),
670    elem!(
671        "s",
672        0,
673        3,
674        0,
675        0,
676        1,
677        1,
678        1,
679        "strike-through text style",
680        DATA_NEUTRAL
681    ),
682    elem!(
683        "samp",
684        0,
685        0,
686        0,
687        0,
688        0,
689        0,
690        1,
691        "sample program output, scripts, etc.",
692        DATA_NEUTRAL
693    ),
694    elem!(
695        "script",
696        0,
697        0,
698        0,
699        0,
700        0,
701        0,
702        2,
703        "script statements ",
704        DATA_SCRIPT
705    ),
706    elem!(
707        "select",
708        0,
709        0,
710        0,
711        0,
712        0,
713        0,
714        1,
715        "option selector ",
716        DATA_NEUTRAL
717    ),
718    elem!(
719        "small",
720        0,
721        3,
722        0,
723        0,
724        0,
725        0,
726        1,
727        "small text style",
728        DATA_NEUTRAL
729    ),
730    elem!("source", 0, 0, 2, 1, 0, 0, 0, "", DATA_NEUTRAL),
731    elem!(
732        "span",
733        0,
734        0,
735        0,
736        0,
737        0,
738        0,
739        1,
740        "generic language/style container ",
741        DATA_NEUTRAL
742    ),
743    elem!(
744        "strike",
745        0,
746        3,
747        0,
748        0,
749        1,
750        1,
751        1,
752        "strike-through text",
753        DATA_NEUTRAL
754    ),
755    elem!(
756        "strong",
757        0,
758        3,
759        0,
760        0,
761        0,
762        0,
763        1,
764        "strong emphasis",
765        DATA_NEUTRAL
766    ),
767    elem!("style", 0, 0, 0, 0, 0, 0, 0, "style info ", DATA_RAWTEXT),
768    elem!("sub", 0, 3, 0, 0, 0, 0, 1, "subscript", DATA_NEUTRAL),
769    elem!("sup", 0, 3, 0, 0, 0, 0, 1, "superscript ", DATA_NEUTRAL),
770    elem!("table", 0, 0, 0, 0, 0, 0, 0, "", DATA_NEUTRAL),
771    elem!("tbody", 1, 0, 0, 0, 0, 0, 0, "table body ", DATA_NEUTRAL),
772    elem!("td", 0, 0, 0, 0, 0, 0, 0, "table data cell", DATA_NEUTRAL),
773    elem!(
774        "textarea",
775        0,
776        0,
777        0,
778        0,
779        0,
780        0,
781        1,
782        "multi-line text field ",
783        DATA_RCDATA
784    ),
785    elem!("tfoot", 0, 1, 0, 0, 0, 0, 0, "table footer ", DATA_NEUTRAL),
786    elem!("th", 0, 1, 0, 0, 0, 0, 0, "table header cell", DATA_NEUTRAL),
787    elem!("thead", 0, 1, 0, 0, 0, 0, 0, "table header ", DATA_NEUTRAL),
788    elem!("title", 0, 0, 0, 0, 0, 0, 0, "document title ", DATA_RCDATA),
789    elem!("tr", 0, 0, 0, 0, 0, 0, 0, "table row ", DATA_NEUTRAL),
790    elem!("track", 0, 0, 2, 1, 0, 0, 0, "", DATA_NEUTRAL),
791    elem!(
792        "tt",
793        0,
794        3,
795        0,
796        0,
797        0,
798        0,
799        1,
800        "teletype or monospaced text style",
801        DATA_NEUTRAL
802    ),
803    elem!(
804        "u",
805        0,
806        3,
807        0,
808        0,
809        1,
810        1,
811        1,
812        "underlined text style",
813        DATA_NEUTRAL
814    ),
815    elem!("ul", 0, 0, 0, 0, 0, 0, 0, "unordered list ", DATA_NEUTRAL),
816    elem!(
817        "var",
818        0,
819        0,
820        0,
821        0,
822        0,
823        0,
824        1,
825        "instance of a variable or program argument",
826        DATA_NEUTRAL
827    ),
828    elem!("wbr", 0, 0, 2, 1, 0, 0, 0, "", DATA_NEUTRAL),
829    elem!("xmp", 0, 0, 0, 0, 0, 0, 1, "", DATA_RAWTEXT),
830];
831
832/// Lookup the HTML tag in the element table.
833///
834/// Upstream `htmlTagLookup` binary-searches the sorted `html40ElementTable`
835/// with `xmlStrcasecmp`; the same tag set is searched here case-insensitively
836/// with a linear scan (same results, no sort order required).
837///
838/// # UPSTREAM-PARITY
839///
840/// ```c
841/// const htmlElemDesc *htmlTagLookup(const xmlChar *tag);
842/// ```
843#[no_mangle]
844pub unsafe extern "C" fn htmlTagLookup(tag: *const xmlChar) -> *const _htmlElemDesc {
845    if tag.is_null() {
846        return ptr::null();
847    }
848    let bytes = unsafe { xmlstr_to_bytes(tag) };
849    for e in HTML40_ELEMENTS {
850        let name = unsafe { core::ffi::CStr::from_ptr(e.name) }.to_bytes();
851        if bytes.eq_ignore_ascii_case(name) {
852            return e as *const _htmlElemDesc;
853        }
854    }
855    ptr::null()
856}
857
858// ═══════════════════════════════════════════════════════════════════════════════
859// HTML entity table (HTMLparser.c html40EntitiesTable)
860// ═══════════════════════════════════════════════════════════════════════════════
861
862/// Rust mirror of `struct _htmlEntityDesc` (HTMLparser.h).
863#[repr(C)]
864pub struct _htmlEntityDesc {
865    pub value: c_uint,
866    pub name: *const c_char,
867    pub desc: *const c_char,
868}
869
870// SAFETY: the struct only contains pointers into static, immutable tables.
871unsafe impl Sync for _htmlEntityDesc {}
872unsafe impl Send for _htmlEntityDesc {}
873
874/// Build a static `_htmlEntityDesc`.
875macro_rules! ent {
876    ($value:expr, $name:literal, $desc:literal) => {
877        _htmlEntityDesc {
878            value: $value,
879            name: concat!($name, "\0").as_ptr() as *const c_char,
880            desc: concat!($desc, "\0").as_ptr() as *const c_char,
881        }
882    };
883}
884
885/// The HTML 4.0 predefined entities table, faithfully ported from
886/// archaeology/libxml2-git/HTMLparser.c `html40EntitiesTable` (sorted by
887/// value, matching upstream).
888static HTML40_ENTITIES: &[_htmlEntityDesc] = &[
889    ent!(34, "quot", "quotation mark = APL quote, U+0022 ISOnum"),
890    ent!(38, "amp", "ampersand, U+0026 ISOnum"),
891    ent!(39, "apos", "single quote"),
892    ent!(60, "lt", "less-than sign, U+003C ISOnum"),
893    ent!(62, "gt", "greater-than sign, U+003E ISOnum"),
894    ent!(
895        160,
896        "nbsp",
897        "no-break space = non-breaking space, U+00A0 ISOnum"
898    ),
899    ent!(161, "iexcl", "inverted exclamation mark, U+00A1 ISOnum"),
900    ent!(162, "cent", "cent sign, U+00A2 ISOnum"),
901    ent!(163, "pound", "pound sign, U+00A3 ISOnum"),
902    ent!(164, "curren", "currency sign, U+00A4 ISOnum"),
903    ent!(165, "yen", "yen sign = yuan sign, U+00A5 ISOnum"),
904    ent!(
905        166,
906        "brvbar",
907        "broken bar = broken vertical bar, U+00A6 ISOnum"
908    ),
909    ent!(167, "sect", "section sign, U+00A7 ISOnum"),
910    ent!(168, "uml", "diaeresis = spacing diaeresis, U+00A8 ISOdia"),
911    ent!(169, "copy", "copyright sign, U+00A9 ISOnum"),
912    ent!(170, "ordf", "feminine ordinal indicator, U+00AA ISOnum"),
913    ent!(
914        171,
915        "laquo",
916        "left-pointing double angle quotation mark = left pointing guillemet, U+00AB ISOnum"
917    ),
918    ent!(172, "not", "not sign, U+00AC ISOnum"),
919    ent!(
920        173,
921        "shy",
922        "soft hyphen = discretionary hyphen, U+00AD ISOnum"
923    ),
924    ent!(
925        174,
926        "reg",
927        "registered sign = registered trade mark sign, U+00AE ISOnum"
928    ),
929    ent!(
930        175,
931        "macr",
932        "macron = spacing macron = overline = APL overbar, U+00AF ISOdia"
933    ),
934    ent!(176, "deg", "degree sign, U+00B0 ISOnum"),
935    ent!(
936        177,
937        "plusmn",
938        "plus-minus sign = plus-or-minus sign, U+00B1 ISOnum"
939    ),
940    ent!(
941        178,
942        "sup2",
943        "superscript two = superscript digit two = squared, U+00B2 ISOnum"
944    ),
945    ent!(
946        179,
947        "sup3",
948        "superscript three = superscript digit three = cubed, U+00B3 ISOnum"
949    ),
950    ent!(180, "acute", "acute accent = spacing acute, U+00B4 ISOdia"),
951    ent!(181, "micro", "micro sign, U+00B5 ISOnum"),
952    ent!(182, "para", "pilcrow sign = paragraph sign, U+00B6 ISOnum"),
953    ent!(
954        183,
955        "middot",
956        "middle dot = Georgian comma Greek middle dot, U+00B7 ISOnum"
957    ),
958    ent!(184, "cedil", "cedilla = spacing cedilla, U+00B8 ISOdia"),
959    ent!(
960        185,
961        "sup1",
962        "superscript one = superscript digit one, U+00B9 ISOnum"
963    ),
964    ent!(186, "ordm", "masculine ordinal indicator, U+00BA ISOnum"),
965    ent!(
966        187,
967        "raquo",
968        "right-pointing double angle quotation mark right pointing guillemet, U+00BB ISOnum"
969    ),
970    ent!(
971        188,
972        "frac14",
973        "vulgar fraction one quarter = fraction one quarter, U+00BC ISOnum"
974    ),
975    ent!(
976        189,
977        "frac12",
978        "vulgar fraction one half = fraction one half, U+00BD ISOnum"
979    ),
980    ent!(
981        190,
982        "frac34",
983        "vulgar fraction three quarters = fraction three quarters, U+00BE ISOnum"
984    ),
985    ent!(
986        191,
987        "iquest",
988        "inverted question mark = turned question mark, U+00BF ISOnum"
989    ),
990    ent!(
991        192,
992        "Agrave",
993        "latin capital letter A with grave = latin capital letter A grave, U+00C0 ISOlat1"
994    ),
995    ent!(
996        193,
997        "Aacute",
998        "latin capital letter A with acute, U+00C1 ISOlat1"
999    ),
1000    ent!(
1001        194,
1002        "Acirc",
1003        "latin capital letter A with circumflex, U+00C2 ISOlat1"
1004    ),
1005    ent!(
1006        195,
1007        "Atilde",
1008        "latin capital letter A with tilde, U+00C3 ISOlat1"
1009    ),
1010    ent!(
1011        196,
1012        "Auml",
1013        "latin capital letter A with diaeresis, U+00C4 ISOlat1"
1014    ),
1015    ent!(
1016        197,
1017        "Aring",
1018        "latin capital letter A with ring above = latin capital letter A ring, U+00C5 ISOlat1"
1019    ),
1020    ent!(
1021        198,
1022        "AElig",
1023        "latin capital letter AE = latin capital ligature AE, U+00C6 ISOlat1"
1024    ),
1025    ent!(
1026        199,
1027        "Ccedil",
1028        "latin capital letter C with cedilla, U+00C7 ISOlat1"
1029    ),
1030    ent!(
1031        200,
1032        "Egrave",
1033        "latin capital letter E with grave, U+00C8 ISOlat1"
1034    ),
1035    ent!(
1036        201,
1037        "Eacute",
1038        "latin capital letter E with acute, U+00C9 ISOlat1"
1039    ),
1040    ent!(
1041        202,
1042        "Ecirc",
1043        "latin capital letter E with circumflex, U+00CA ISOlat1"
1044    ),
1045    ent!(
1046        203,
1047        "Euml",
1048        "latin capital letter E with diaeresis, U+00CB ISOlat1"
1049    ),
1050    ent!(
1051        204,
1052        "Igrave",
1053        "latin capital letter I with grave, U+00CC ISOlat1"
1054    ),
1055    ent!(
1056        205,
1057        "Iacute",
1058        "latin capital letter I with acute, U+00CD ISOlat1"
1059    ),
1060    ent!(
1061        206,
1062        "Icirc",
1063        "latin capital letter I with circumflex, U+00CE ISOlat1"
1064    ),
1065    ent!(
1066        207,
1067        "Iuml",
1068        "latin capital letter I with diaeresis, U+00CF ISOlat1"
1069    ),
1070    ent!(208, "ETH", "latin capital letter ETH, U+00D0 ISOlat1"),
1071    ent!(
1072        209,
1073        "Ntilde",
1074        "latin capital letter N with tilde, U+00D1 ISOlat1"
1075    ),
1076    ent!(
1077        210,
1078        "Ograve",
1079        "latin capital letter O with grave, U+00D2 ISOlat1"
1080    ),
1081    ent!(
1082        211,
1083        "Oacute",
1084        "latin capital letter O with acute, U+00D3 ISOlat1"
1085    ),
1086    ent!(
1087        212,
1088        "Ocirc",
1089        "latin capital letter O with circumflex, U+00D4 ISOlat1"
1090    ),
1091    ent!(
1092        213,
1093        "Otilde",
1094        "latin capital letter O with tilde, U+00D5 ISOlat1"
1095    ),
1096    ent!(
1097        214,
1098        "Ouml",
1099        "latin capital letter O with diaeresis, U+00D6 ISOlat1"
1100    ),
1101    ent!(215, "times", "multiplication sign, U+00D7 ISOnum"),
1102    ent!(
1103        216,
1104        "Oslash",
1105        "latin capital letter O with stroke latin capital letter O slash, U+00D8 ISOlat1"
1106    ),
1107    ent!(
1108        217,
1109        "Ugrave",
1110        "latin capital letter U with grave, U+00D9 ISOlat1"
1111    ),
1112    ent!(
1113        218,
1114        "Uacute",
1115        "latin capital letter U with acute, U+00DA ISOlat1"
1116    ),
1117    ent!(
1118        219,
1119        "Ucirc",
1120        "latin capital letter U with circumflex, U+00DB ISOlat1"
1121    ),
1122    ent!(
1123        220,
1124        "Uuml",
1125        "latin capital letter U with diaeresis, U+00DC ISOlat1"
1126    ),
1127    ent!(
1128        221,
1129        "Yacute",
1130        "latin capital letter Y with acute, U+00DD ISOlat1"
1131    ),
1132    ent!(222, "THORN", "latin capital letter THORN, U+00DE ISOlat1"),
1133    ent!(
1134        223,
1135        "szlig",
1136        "latin small letter sharp s = ess-zed, U+00DF ISOlat1"
1137    ),
1138    ent!(
1139        224,
1140        "agrave",
1141        "latin small letter a with grave = latin small letter a grave, U+00E0 ISOlat1"
1142    ),
1143    ent!(
1144        225,
1145        "aacute",
1146        "latin small letter a with acute, U+00E1 ISOlat1"
1147    ),
1148    ent!(
1149        226,
1150        "acirc",
1151        "latin small letter a with circumflex, U+00E2 ISOlat1"
1152    ),
1153    ent!(
1154        227,
1155        "atilde",
1156        "latin small letter a with tilde, U+00E3 ISOlat1"
1157    ),
1158    ent!(
1159        228,
1160        "auml",
1161        "latin small letter a with diaeresis, U+00E4 ISOlat1"
1162    ),
1163    ent!(
1164        229,
1165        "aring",
1166        "latin small letter a with ring above = latin small letter a ring, U+00E5 ISOlat1"
1167    ),
1168    ent!(
1169        230,
1170        "aelig",
1171        "latin small letter ae = latin small ligature ae, U+00E6 ISOlat1"
1172    ),
1173    ent!(
1174        231,
1175        "ccedil",
1176        "latin small letter c with cedilla, U+00E7 ISOlat1"
1177    ),
1178    ent!(
1179        232,
1180        "egrave",
1181        "latin small letter e with grave, U+00E8 ISOlat1"
1182    ),
1183    ent!(
1184        233,
1185        "eacute",
1186        "latin small letter e with acute, U+00E9 ISOlat1"
1187    ),
1188    ent!(
1189        234,
1190        "ecirc",
1191        "latin small letter e with circumflex, U+00EA ISOlat1"
1192    ),
1193    ent!(
1194        235,
1195        "euml",
1196        "latin small letter e with diaeresis, U+00EB ISOlat1"
1197    ),
1198    ent!(
1199        236,
1200        "igrave",
1201        "latin small letter i with grave, U+00EC ISOlat1"
1202    ),
1203    ent!(
1204        237,
1205        "iacute",
1206        "latin small letter i with acute, U+00ED ISOlat1"
1207    ),
1208    ent!(
1209        238,
1210        "icirc",
1211        "latin small letter i with circumflex, U+00EE ISOlat1"
1212    ),
1213    ent!(
1214        239,
1215        "iuml",
1216        "latin small letter i with diaeresis, U+00EF ISOlat1"
1217    ),
1218    ent!(240, "eth", "latin small letter eth, U+00F0 ISOlat1"),
1219    ent!(
1220        241,
1221        "ntilde",
1222        "latin small letter n with tilde, U+00F1 ISOlat1"
1223    ),
1224    ent!(
1225        242,
1226        "ograve",
1227        "latin small letter o with grave, U+00F2 ISOlat1"
1228    ),
1229    ent!(
1230        243,
1231        "oacute",
1232        "latin small letter o with acute, U+00F3 ISOlat1"
1233    ),
1234    ent!(
1235        244,
1236        "ocirc",
1237        "latin small letter o with circumflex, U+00F4 ISOlat1"
1238    ),
1239    ent!(
1240        245,
1241        "otilde",
1242        "latin small letter o with tilde, U+00F5 ISOlat1"
1243    ),
1244    ent!(
1245        246,
1246        "ouml",
1247        "latin small letter o with diaeresis, U+00F6 ISOlat1"
1248    ),
1249    ent!(247, "divide", "division sign, U+00F7 ISOnum"),
1250    ent!(
1251        248,
1252        "oslash",
1253        "latin small letter o with stroke, = latin small letter o slash, U+00F8 ISOlat1"
1254    ),
1255    ent!(
1256        249,
1257        "ugrave",
1258        "latin small letter u with grave, U+00F9 ISOlat1"
1259    ),
1260    ent!(
1261        250,
1262        "uacute",
1263        "latin small letter u with acute, U+00FA ISOlat1"
1264    ),
1265    ent!(
1266        251,
1267        "ucirc",
1268        "latin small letter u with circumflex, U+00FB ISOlat1"
1269    ),
1270    ent!(
1271        252,
1272        "uuml",
1273        "latin small letter u with diaeresis, U+00FC ISOlat1"
1274    ),
1275    ent!(
1276        253,
1277        "yacute",
1278        "latin small letter y with acute, U+00FD ISOlat1"
1279    ),
1280    ent!(
1281        254,
1282        "thorn",
1283        "latin small letter thorn with, U+00FE ISOlat1"
1284    ),
1285    ent!(
1286        255,
1287        "yuml",
1288        "latin small letter y with diaeresis, U+00FF ISOlat1"
1289    ),
1290    ent!(338, "OElig", "latin capital ligature OE, U+0152 ISOlat2"),
1291    ent!(339, "oelig", "latin small ligature oe, U+0153 ISOlat2"),
1292    ent!(
1293        352,
1294        "Scaron",
1295        "latin capital letter S with caron, U+0160 ISOlat2"
1296    ),
1297    ent!(
1298        353,
1299        "scaron",
1300        "latin small letter s with caron, U+0161 ISOlat2"
1301    ),
1302    ent!(
1303        376,
1304        "Yuml",
1305        "latin capital letter Y with diaeresis, U+0178 ISOlat2"
1306    ),
1307    ent!(
1308        402,
1309        "fnof",
1310        "latin small f with hook = function = florin, U+0192 ISOtech"
1311    ),
1312    ent!(
1313        710,
1314        "circ",
1315        "modifier letter circumflex accent, U+02C6 ISOpub"
1316    ),
1317    ent!(732, "tilde", "small tilde, U+02DC ISOdia"),
1318    ent!(913, "Alpha", "greek capital letter alpha, U+0391"),
1319    ent!(914, "Beta", "greek capital letter beta, U+0392"),
1320    ent!(915, "Gamma", "greek capital letter gamma, U+0393 ISOgrk3"),
1321    ent!(916, "Delta", "greek capital letter delta, U+0394 ISOgrk3"),
1322    ent!(917, "Epsilon", "greek capital letter epsilon, U+0395"),
1323    ent!(918, "Zeta", "greek capital letter zeta, U+0396"),
1324    ent!(919, "Eta", "greek capital letter eta, U+0397"),
1325    ent!(920, "Theta", "greek capital letter theta, U+0398 ISOgrk3"),
1326    ent!(921, "Iota", "greek capital letter iota, U+0399"),
1327    ent!(922, "Kappa", "greek capital letter kappa, U+039A"),
1328    ent!(923, "Lambda", "greek capital letter lambda, U+039B ISOgrk3"),
1329    ent!(924, "Mu", "greek capital letter mu, U+039C"),
1330    ent!(925, "Nu", "greek capital letter nu, U+039D"),
1331    ent!(926, "Xi", "greek capital letter xi, U+039E ISOgrk3"),
1332    ent!(927, "Omicron", "greek capital letter omicron, U+039F"),
1333    ent!(928, "Pi", "greek capital letter pi, U+03A0 ISOgrk3"),
1334    ent!(929, "Rho", "greek capital letter rho, U+03A1"),
1335    ent!(931, "Sigma", "greek capital letter sigma, U+03A3 ISOgrk3"),
1336    ent!(932, "Tau", "greek capital letter tau, U+03A4"),
1337    ent!(
1338        933,
1339        "Upsilon",
1340        "greek capital letter upsilon, U+03A5 ISOgrk3"
1341    ),
1342    ent!(934, "Phi", "greek capital letter phi, U+03A6 ISOgrk3"),
1343    ent!(935, "Chi", "greek capital letter chi, U+03A7"),
1344    ent!(936, "Psi", "greek capital letter psi, U+03A8 ISOgrk3"),
1345    ent!(937, "Omega", "greek capital letter omega, U+03A9 ISOgrk3"),
1346    ent!(945, "alpha", "greek small letter alpha, U+03B1 ISOgrk3"),
1347    ent!(946, "beta", "greek small letter beta, U+03B2 ISOgrk3"),
1348    ent!(947, "gamma", "greek small letter gamma, U+03B3 ISOgrk3"),
1349    ent!(948, "delta", "greek small letter delta, U+03B4 ISOgrk3"),
1350    ent!(949, "epsilon", "greek small letter epsilon, U+03B5 ISOgrk3"),
1351    ent!(950, "zeta", "greek small letter zeta, U+03B6 ISOgrk3"),
1352    ent!(951, "eta", "greek small letter eta, U+03B7 ISOgrk3"),
1353    ent!(952, "theta", "greek small letter theta, U+03B8 ISOgrk3"),
1354    ent!(953, "iota", "greek small letter iota, U+03B9 ISOgrk3"),
1355    ent!(954, "kappa", "greek small letter kappa, U+03BA ISOgrk3"),
1356    ent!(955, "lambda", "greek small letter lambda, U+03BB ISOgrk3"),
1357    ent!(956, "mu", "greek small letter mu, U+03BC ISOgrk3"),
1358    ent!(957, "nu", "greek small letter nu, U+03BD ISOgrk3"),
1359    ent!(958, "xi", "greek small letter xi, U+03BE ISOgrk3"),
1360    ent!(959, "omicron", "greek small letter omicron, U+03BF NEW"),
1361    ent!(960, "pi", "greek small letter pi, U+03C0 ISOgrk3"),
1362    ent!(961, "rho", "greek small letter rho, U+03C1 ISOgrk3"),
1363    ent!(
1364        962,
1365        "sigmaf",
1366        "greek small letter final sigma, U+03C2 ISOgrk3"
1367    ),
1368    ent!(963, "sigma", "greek small letter sigma, U+03C3 ISOgrk3"),
1369    ent!(964, "tau", "greek small letter tau, U+03C4 ISOgrk3"),
1370    ent!(965, "upsilon", "greek small letter upsilon, U+03C5 ISOgrk3"),
1371    ent!(966, "phi", "greek small letter phi, U+03C6 ISOgrk3"),
1372    ent!(967, "chi", "greek small letter chi, U+03C7 ISOgrk3"),
1373    ent!(968, "psi", "greek small letter psi, U+03C8 ISOgrk3"),
1374    ent!(969, "omega", "greek small letter omega, U+03C9 ISOgrk3"),
1375    ent!(
1376        977,
1377        "thetasym",
1378        "greek small letter theta symbol, U+03D1 NEW"
1379    ),
1380    ent!(978, "upsih", "greek upsilon with hook symbol, U+03D2 NEW"),
1381    ent!(982, "piv", "greek pi symbol, U+03D6 ISOgrk3"),
1382    ent!(8194, "ensp", "en space, U+2002 ISOpub"),
1383    ent!(8195, "emsp", "em space, U+2003 ISOpub"),
1384    ent!(8201, "thinsp", "thin space, U+2009 ISOpub"),
1385    ent!(8204, "zwnj", "zero width non-joiner, U+200C NEW RFC 2070"),
1386    ent!(8205, "zwj", "zero width joiner, U+200D NEW RFC 2070"),
1387    ent!(8206, "lrm", "left-to-right mark, U+200E NEW RFC 2070"),
1388    ent!(8207, "rlm", "right-to-left mark, U+200F NEW RFC 2070"),
1389    ent!(8211, "ndash", "en dash, U+2013 ISOpub"),
1390    ent!(8212, "mdash", "em dash, U+2014 ISOpub"),
1391    ent!(8216, "lsquo", "left single quotation mark, U+2018 ISOnum"),
1392    ent!(8217, "rsquo", "right single quotation mark, U+2019 ISOnum"),
1393    ent!(8218, "sbquo", "single low-9 quotation mark, U+201A NEW"),
1394    ent!(8220, "ldquo", "left double quotation mark, U+201C ISOnum"),
1395    ent!(8221, "rdquo", "right double quotation mark, U+201D ISOnum"),
1396    ent!(8222, "bdquo", "double low-9 quotation mark, U+201E NEW"),
1397    ent!(8224, "dagger", "dagger, U+2020 ISOpub"),
1398    ent!(8225, "Dagger", "double dagger, U+2021 ISOpub"),
1399    ent!(8226, "bull", "bullet = black small circle, U+2022 ISOpub"),
1400    ent!(
1401        8230,
1402        "hellip",
1403        "horizontal ellipsis = three dot leader, U+2026 ISOpub"
1404    ),
1405    ent!(8240, "permil", "per mille sign, U+2030 ISOtech"),
1406    ent!(8242, "prime", "prime = minutes = feet, U+2032 ISOtech"),
1407    ent!(
1408        8243,
1409        "Prime",
1410        "double prime = seconds = inches, U+2033 ISOtech"
1411    ),
1412    ent!(
1413        8249,
1414        "lsaquo",
1415        "single left-pointing angle quotation mark, U+2039 ISO proposed"
1416    ),
1417    ent!(
1418        8250,
1419        "rsaquo",
1420        "single right-pointing angle quotation mark, U+203A ISO proposed"
1421    ),
1422    ent!(8254, "oline", "overline = spacing overscore, U+203E NEW"),
1423    ent!(8260, "frasl", "fraction slash, U+2044 NEW"),
1424    ent!(8364, "euro", "euro sign, U+20AC NEW"),
1425    ent!(
1426        8465,
1427        "image",
1428        "blackletter capital I = imaginary part, U+2111 ISOamso"
1429    ),
1430    ent!(
1431        8472,
1432        "weierp",
1433        "script capital P = power set = Weierstrass p, U+2118 ISOamso"
1434    ),
1435    ent!(
1436        8476,
1437        "real",
1438        "blackletter capital R = real part symbol, U+211C ISOamso"
1439    ),
1440    ent!(8482, "trade", "trade mark sign, U+2122 ISOnum"),
1441    ent!(
1442        8501,
1443        "alefsym",
1444        "alef symbol = first transfinite cardinal, U+2135 NEW"
1445    ),
1446    ent!(8592, "larr", "leftwards arrow, U+2190 ISOnum"),
1447    ent!(8593, "uarr", "upwards arrow, U+2191 ISOnum"),
1448    ent!(8594, "rarr", "rightwards arrow, U+2192 ISOnum"),
1449    ent!(8595, "darr", "downwards arrow, U+2193 ISOnum"),
1450    ent!(8596, "harr", "left right arrow, U+2194 ISOamsa"),
1451    ent!(
1452        8629,
1453        "crarr",
1454        "downwards arrow with corner leftwards = carriage return, U+21B5 NEW"
1455    ),
1456    ent!(8656, "lArr", "leftwards double arrow, U+21D0 ISOtech"),
1457    ent!(8657, "uArr", "upwards double arrow, U+21D1 ISOamsa"),
1458    ent!(8658, "rArr", "rightwards double arrow, U+21D2 ISOtech"),
1459    ent!(8659, "dArr", "downwards double arrow, U+21D3 ISOamsa"),
1460    ent!(8660, "hArr", "left right double arrow, U+21D4 ISOamsa"),
1461    ent!(8704, "forall", "for all, U+2200 ISOtech"),
1462    ent!(8706, "part", "partial differential, U+2202 ISOtech"),
1463    ent!(8707, "exist", "there exists, U+2203 ISOtech"),
1464    ent!(
1465        8709,
1466        "empty",
1467        "empty set = null set = diameter, U+2205 ISOamso"
1468    ),
1469    ent!(8711, "nabla", "nabla = backward difference, U+2207 ISOtech"),
1470    ent!(8712, "isin", "element of, U+2208 ISOtech"),
1471    ent!(8713, "notin", "not an element of, U+2209 ISOtech"),
1472    ent!(8715, "ni", "contains as member, U+220B ISOtech"),
1473    ent!(8719, "prod", "n-ary product = product sign, U+220F ISOamsb"),
1474    ent!(8721, "sum", "n-ary summation, U+2211 ISOamsb"),
1475    ent!(8722, "minus", "minus sign, U+2212 ISOtech"),
1476    ent!(8727, "lowast", "asterisk operator, U+2217 ISOtech"),
1477    ent!(8730, "radic", "square root = radical sign, U+221A ISOtech"),
1478    ent!(8733, "prop", "proportional to, U+221D ISOtech"),
1479    ent!(8734, "infin", "infinity, U+221E ISOtech"),
1480    ent!(8736, "ang", "angle, U+2220 ISOamso"),
1481    ent!(8743, "and", "logical and = wedge, U+2227 ISOtech"),
1482    ent!(8744, "or", "logical or = vee, U+2228 ISOtech"),
1483    ent!(8745, "cap", "intersection = cap, U+2229 ISOtech"),
1484    ent!(8746, "cup", "union = cup, U+222A ISOtech"),
1485    ent!(8747, "int", "integral, U+222B ISOtech"),
1486    ent!(8756, "there4", "therefore, U+2234 ISOtech"),
1487    ent!(
1488        8764,
1489        "sim",
1490        "tilde operator = varies with = similar to, U+223C ISOtech"
1491    ),
1492    ent!(8773, "cong", "approximately equal to, U+2245 ISOtech"),
1493    ent!(
1494        8776,
1495        "asymp",
1496        "almost equal to = asymptotic to, U+2248 ISOamsr"
1497    ),
1498    ent!(8800, "ne", "not equal to, U+2260 ISOtech"),
1499    ent!(8801, "equiv", "identical to, U+2261 ISOtech"),
1500    ent!(8804, "le", "less-than or equal to, U+2264 ISOtech"),
1501    ent!(8805, "ge", "greater-than or equal to, U+2265 ISOtech"),
1502    ent!(8834, "sub", "subset of, U+2282 ISOtech"),
1503    ent!(8835, "sup", "superset of, U+2283 ISOtech"),
1504    ent!(8836, "nsub", "not a subset of, U+2284 ISOamsn"),
1505    ent!(8838, "sube", "subset of or equal to, U+2286 ISOtech"),
1506    ent!(8839, "supe", "superset of or equal to, U+2287 ISOtech"),
1507    ent!(8853, "oplus", "circled plus = direct sum, U+2295 ISOamsb"),
1508    ent!(
1509        8855,
1510        "otimes",
1511        "circled times = vector product, U+2297 ISOamsb"
1512    ),
1513    ent!(
1514        8869,
1515        "perp",
1516        "up tack = orthogonal to = perpendicular, U+22A5 ISOtech"
1517    ),
1518    ent!(8901, "sdot", "dot operator, U+22C5 ISOamsb"),
1519    ent!(8968, "lceil", "left ceiling = apl upstile, U+2308 ISOamsc"),
1520    ent!(8969, "rceil", "right ceiling, U+2309 ISOamsc"),
1521    ent!(8970, "lfloor", "left floor = apl downstile, U+230A ISOamsc"),
1522    ent!(8971, "rfloor", "right floor, U+230B ISOamsc"),
1523    ent!(
1524        9001,
1525        "lang",
1526        "left-pointing angle bracket = bra, U+2329 ISOtech"
1527    ),
1528    ent!(
1529        9002,
1530        "rang",
1531        "right-pointing angle bracket = ket, U+232A ISOtech"
1532    ),
1533    ent!(9674, "loz", "lozenge, U+25CA ISOpub"),
1534    ent!(9824, "spades", "black spade suit, U+2660 ISOpub"),
1535    ent!(9827, "clubs", "black club suit = shamrock, U+2663 ISOpub"),
1536    ent!(
1537        9829,
1538        "hearts",
1539        "black heart suit = valentine, U+2665 ISOpub"
1540    ),
1541    ent!(9830, "diams", "black diamond suit, U+2666 ISOpub"),
1542];
1543
1544/// Lookup the given entity in the entities table.
1545///
1546/// Upstream scans linearly with `xmlStrEqual`; the same table is scanned
1547/// here.
1548///
1549/// # UPSTREAM-PARITY
1550///
1551/// ```c
1552/// const htmlEntityDesc *htmlEntityLookup(const xmlChar *name);
1553/// ```
1554#[no_mangle]
1555pub unsafe extern "C" fn htmlEntityLookup(name: *const xmlChar) -> *const _htmlEntityDesc {
1556    if name.is_null() {
1557        return ptr::null();
1558    }
1559    let bytes = unsafe { xmlstr_to_bytes(name) };
1560    for e in HTML40_ENTITIES {
1561        let ename = unsafe { core::ffi::CStr::from_ptr(e.name) }.to_bytes();
1562        if bytes == ename {
1563            return e as *const _htmlEntityDesc;
1564        }
1565    }
1566    ptr::null()
1567}
1568
1569/// Lookup the given entity by unicode value.
1570///
1571/// Upstream binary-searches the value-sorted table; the same (sorted) table
1572/// is scanned linearly here.
1573///
1574/// # UPSTREAM-PARITY
1575///
1576/// ```c
1577/// const htmlEntityDesc *htmlEntityValueLookup(unsigned int value);
1578/// ```
1579#[no_mangle]
1580pub unsafe extern "C" fn htmlEntityValueLookup(value: c_uint) -> *const _htmlEntityDesc {
1581    for e in HTML40_ENTITIES {
1582        if e.value == value {
1583            return e as *const _htmlEntityDesc;
1584        }
1585    }
1586    ptr::null()
1587}
1588
1589/// Lookup helper used by `htmlEncodeEntities` (avoids the extern entry point
1590/// in the hot path; identical semantics).
1591#[inline]
1592unsafe fn html_entity_value_lookup_static(value: c_uint) -> *const _htmlEntityDesc {
1593    for e in HTML40_ENTITIES {
1594        if e.value == value {
1595            return e as *const _htmlEntityDesc;
1596        }
1597    }
1598    ptr::null()
1599}
1600
1601// ═══════════════════════════════════════════════════════════════════════════════
1602// Auto-close element rules (HTMLparser.c htmlStartClose / htmlScriptAttributes)
1603// ═══════════════════════════════════════════════════════════════════════════════
1604
1605/// Start tags that imply the end of the current element
1606/// (archaeology/libxml2-git/HTMLparser.c `htmlStartClose`). Each pair
1607/// `(old, new)` means: starting element `new` implicitly closes `old`.
1608static HTML_START_CLOSE: &[(&str, &str)] = &[
1609    ("a", "a"),
1610    ("a", "fieldset"),
1611    ("a", "table"),
1612    ("a", "td"),
1613    ("a", "th"),
1614    ("address", "dd"),
1615    ("address", "dl"),
1616    ("address", "dt"),
1617    ("address", "form"),
1618    ("address", "li"),
1619    ("address", "ul"),
1620    ("b", "center"),
1621    ("b", "p"),
1622    ("b", "td"),
1623    ("b", "th"),
1624    ("big", "p"),
1625    ("caption", "col"),
1626    ("caption", "colgroup"),
1627    ("caption", "tbody"),
1628    ("caption", "tfoot"),
1629    ("caption", "thead"),
1630    ("caption", "tr"),
1631    ("col", "col"),
1632    ("col", "colgroup"),
1633    ("col", "tbody"),
1634    ("col", "tfoot"),
1635    ("col", "thead"),
1636    ("col", "tr"),
1637    ("colgroup", "colgroup"),
1638    ("colgroup", "tbody"),
1639    ("colgroup", "tfoot"),
1640    ("colgroup", "thead"),
1641    ("colgroup", "tr"),
1642    ("dd", "dt"),
1643    ("dir", "dd"),
1644    ("dir", "dl"),
1645    ("dir", "dt"),
1646    ("dir", "form"),
1647    ("dir", "ul"),
1648    ("dl", "form"),
1649    ("dl", "li"),
1650    ("dt", "dd"),
1651    ("dt", "dl"),
1652    ("font", "center"),
1653    ("font", "td"),
1654    ("font", "th"),
1655    ("form", "form"),
1656    ("h1", "fieldset"),
1657    ("h1", "form"),
1658    ("h1", "li"),
1659    ("h1", "p"),
1660    ("h1", "table"),
1661    ("h2", "fieldset"),
1662    ("h2", "form"),
1663    ("h2", "li"),
1664    ("h2", "p"),
1665    ("h2", "table"),
1666    ("h3", "fieldset"),
1667    ("h3", "form"),
1668    ("h3", "li"),
1669    ("h3", "p"),
1670    ("h3", "table"),
1671    ("h4", "fieldset"),
1672    ("h4", "form"),
1673    ("h4", "li"),
1674    ("h4", "p"),
1675    ("h4", "table"),
1676    ("h5", "fieldset"),
1677    ("h5", "form"),
1678    ("h5", "li"),
1679    ("h5", "p"),
1680    ("h5", "table"),
1681    ("h6", "fieldset"),
1682    ("h6", "form"),
1683    ("h6", "li"),
1684    ("h6", "p"),
1685    ("h6", "table"),
1686    ("head", "a"),
1687    ("head", "abbr"),
1688    ("head", "acronym"),
1689    ("head", "address"),
1690    ("head", "b"),
1691    ("head", "bdo"),
1692    ("head", "big"),
1693    ("head", "blockquote"),
1694    ("head", "body"),
1695    ("head", "br"),
1696    ("head", "center"),
1697    ("head", "cite"),
1698    ("head", "code"),
1699    ("head", "dd"),
1700    ("head", "dfn"),
1701    ("head", "dir"),
1702    ("head", "div"),
1703    ("head", "dl"),
1704    ("head", "dt"),
1705    ("head", "em"),
1706    ("head", "fieldset"),
1707    ("head", "font"),
1708    ("head", "form"),
1709    ("head", "frameset"),
1710    ("head", "h1"),
1711    ("head", "h2"),
1712    ("head", "h3"),
1713    ("head", "h4"),
1714    ("head", "h5"),
1715    ("head", "h6"),
1716    ("head", "hr"),
1717    ("head", "i"),
1718    ("head", "iframe"),
1719    ("head", "img"),
1720    ("head", "kbd"),
1721    ("head", "li"),
1722    ("head", "listing"),
1723    ("head", "map"),
1724    ("head", "menu"),
1725    ("head", "ol"),
1726    ("head", "p"),
1727    ("head", "pre"),
1728    ("head", "q"),
1729    ("head", "s"),
1730    ("head", "samp"),
1731    ("head", "small"),
1732    ("head", "span"),
1733    ("head", "strike"),
1734    ("head", "strong"),
1735    ("head", "sub"),
1736    ("head", "sup"),
1737    ("head", "table"),
1738    ("head", "tt"),
1739    ("head", "u"),
1740    ("head", "ul"),
1741    ("head", "var"),
1742    ("head", "xmp"),
1743    ("hr", "form"),
1744    ("i", "center"),
1745    ("i", "p"),
1746    ("i", "td"),
1747    ("i", "th"),
1748    ("legend", "fieldset"),
1749    ("li", "li"),
1750    ("link", "body"),
1751    ("link", "frameset"),
1752    ("listing", "dd"),
1753    ("listing", "dl"),
1754    ("listing", "dt"),
1755    ("listing", "fieldset"),
1756    ("listing", "form"),
1757    ("listing", "li"),
1758    ("listing", "table"),
1759    ("listing", "ul"),
1760    ("menu", "dd"),
1761    ("menu", "dl"),
1762    ("menu", "dt"),
1763    ("menu", "form"),
1764    ("menu", "ul"),
1765    ("ol", "form"),
1766    ("option", "optgroup"),
1767    ("option", "option"),
1768    ("p", "address"),
1769    ("p", "blockquote"),
1770    ("p", "body"),
1771    ("p", "caption"),
1772    ("p", "center"),
1773    ("p", "col"),
1774    ("p", "colgroup"),
1775    ("p", "dd"),
1776    ("p", "dir"),
1777    ("p", "div"),
1778    ("p", "dl"),
1779    ("p", "dt"),
1780    ("p", "fieldset"),
1781    ("p", "form"),
1782    ("p", "frameset"),
1783    ("p", "h1"),
1784    ("p", "h2"),
1785    ("p", "h3"),
1786    ("p", "h4"),
1787    ("p", "h5"),
1788    ("p", "h6"),
1789    ("p", "head"),
1790    ("p", "hr"),
1791    ("p", "li"),
1792    ("p", "listing"),
1793    ("p", "menu"),
1794    ("p", "ol"),
1795    ("p", "p"),
1796    ("p", "pre"),
1797    ("p", "table"),
1798    ("p", "tbody"),
1799    ("p", "td"),
1800    ("p", "tfoot"),
1801    ("p", "th"),
1802    ("p", "title"),
1803    ("p", "tr"),
1804    ("p", "ul"),
1805    ("p", "xmp"),
1806    ("pre", "dd"),
1807    ("pre", "dl"),
1808    ("pre", "dt"),
1809    ("pre", "fieldset"),
1810    ("pre", "form"),
1811    ("pre", "li"),
1812    ("pre", "table"),
1813    ("pre", "ul"),
1814    ("s", "p"),
1815    ("script", "noscript"),
1816    ("small", "p"),
1817    ("span", "td"),
1818    ("span", "th"),
1819    ("strike", "p"),
1820    ("style", "body"),
1821    ("style", "frameset"),
1822    ("tbody", "tbody"),
1823    ("tbody", "tfoot"),
1824    ("td", "tbody"),
1825    ("td", "td"),
1826    ("td", "tfoot"),
1827    ("td", "th"),
1828    ("td", "tr"),
1829    ("tfoot", "tbody"),
1830    ("th", "tbody"),
1831    ("th", "td"),
1832    ("th", "tfoot"),
1833    ("th", "th"),
1834    ("th", "tr"),
1835    ("thead", "tbody"),
1836    ("thead", "tfoot"),
1837    ("title", "body"),
1838    ("title", "frameset"),
1839    ("tr", "tbody"),
1840    ("tr", "tfoot"),
1841    ("tr", "tr"),
1842    ("tt", "p"),
1843    ("u", "p"),
1844    ("u", "td"),
1845    ("u", "th"),
1846    ("ul", "address"),
1847    ("ul", "form"),
1848    ("ul", "menu"),
1849    ("ul", "pre"),
1850    ("xmp", "dd"),
1851    ("xmp", "dl"),
1852    ("xmp", "dt"),
1853    ("xmp", "fieldset"),
1854    ("xmp", "form"),
1855    ("xmp", "li"),
1856    ("xmp", "table"),
1857    ("xmp", "ul"),
1858];
1859
1860/// Checks whether the new tag is one of the registered valid tags for
1861/// closing old (upstream `htmlCheckAutoClose`). Exact, case-sensitive
1862/// byte comparison, like the upstream `strcmp`-based binary search.
1863unsafe fn html_check_auto_close(newtag: *const xmlChar, oldtag: *const xmlChar) -> bool {
1864    if newtag.is_null() || oldtag.is_null() {
1865        return false;
1866    }
1867    let new_bytes = unsafe { xmlstr_to_bytes(newtag) };
1868    let old_bytes = unsafe { xmlstr_to_bytes(oldtag) };
1869    HTML_START_CLOSE
1870        .iter()
1871        .any(|(old, new)| old.as_bytes() == old_bytes && new.as_bytes() == new_bytes)
1872}
1873
1874/// The HTML DTD allows a tag to implicitly close other tags. This function
1875/// checks if the element or one of its children would auto-close the given
1876/// tag.
1877///
1878/// # UPSTREAM-PARITY
1879///
1880/// ```c
1881/// int htmlAutoCloseTag(xmlDoc *doc, const xmlChar *name, xmlNode *elem);
1882/// ```
1883#[no_mangle]
1884pub unsafe extern "C" fn htmlAutoCloseTag(
1885    _doc: *mut _xmlDoc,
1886    name: *const xmlChar,
1887    elem: *mut _xmlNode,
1888) -> c_int {
1889    if elem.is_null() {
1890        return 1;
1891    }
1892    let n = unsafe { &*elem };
1893    if n.name.is_null() {
1894        // Upstream compares against elem->name; a nameless node cannot
1895        // match, fall through to the children scan.
1896    } else if unsafe { xml_strcmp(name, n.name) } == 0 {
1897        return 0;
1898    }
1899    if unsafe { html_check_auto_close(n.name, name) } {
1900        return 1;
1901    }
1902    let mut child = n.children;
1903    while !child.is_null() {
1904        if unsafe { htmlAutoCloseTag(_doc, name, child) } != 0 {
1905            return 1;
1906        }
1907        child = unsafe { (*child).next };
1908    }
1909    0
1910}
1911
1912/// The HTML DTD allows a tag to implicitly close other tags. This function
1913/// checks if a tag is auto-closed by one of its children.
1914///
1915/// # UPSTREAM-PARITY
1916///
1917/// ```c
1918/// int htmlIsAutoClosed(xmlDoc *doc, xmlNode *elem);
1919/// ```
1920#[no_mangle]
1921pub unsafe extern "C" fn htmlIsAutoClosed(doc: *mut _xmlDoc, elem: *mut _xmlNode) -> c_int {
1922    if elem.is_null() {
1923        return 1;
1924    }
1925    let n = unsafe { &*elem };
1926    let mut child = n.children;
1927    while !child.is_null() {
1928        if unsafe { htmlAutoCloseTag(doc, n.name, child) } != 0 {
1929            return 1;
1930        }
1931        child = unsafe { (*child).next };
1932    }
1933    0
1934}
1935
1936/// The list of HTML attributes which are of content type %Script;
1937/// (HTMLparser.c `htmlScriptAttributes`).
1938static HTML_SCRIPT_ATTRIBUTES: &[&str] = &[
1939    "onclick",
1940    "ondblclick",
1941    "onmousedown",
1942    "onmouseup",
1943    "onmouseover",
1944    "onmousemove",
1945    "onmouseout",
1946    "onkeypress",
1947    "onkeydown",
1948    "onkeyup",
1949    "onload",
1950    "onunload",
1951    "onfocus",
1952    "onblur",
1953    "onsubmit",
1954    "onreset",
1955    "onchange",
1956    "onselect",
1957];
1958
1959/// Check if an attribute is of content type Script. All script attributes
1960/// start with 'on'.
1961///
1962/// # UPSTREAM-PARITY
1963///
1964/// ```c
1965/// int htmlIsScriptAttribute(const xmlChar *name);
1966/// ```
1967#[no_mangle]
1968pub unsafe extern "C" fn htmlIsScriptAttribute(name: *const xmlChar) -> c_int {
1969    if name.is_null() {
1970        return 0;
1971    }
1972    let bytes = unsafe { xmlstr_to_bytes(name) };
1973    if bytes.len() < 3 || bytes[0] != b'o' || bytes[1] != b'n' {
1974        return 0;
1975    }
1976    for cand in HTML_SCRIPT_ATTRIBUTES {
1977        if bytes == cand.as_bytes() {
1978            return 1;
1979        }
1980    }
1981    0
1982}
1983
1984// ═══════════════════════════════════════════════════════════════════════════════
1985// Element-rule stubs (upstream 2.14+ deprecated content-model functions)
1986// ═══════════════════════════════════════════════════════════════════════════════
1987
1988/// # UPSTREAM-PARITY
1989///
1990/// ```c
1991/// int htmlElementAllowedHere(const htmlElemDesc *parent, const xmlChar *elt);
1992/// ```
1993///
1994/// Upstream is a deprecated stub returning 1 unconditionally.
1995#[no_mangle]
1996pub unsafe extern "C" fn htmlElementAllowedHere(
1997    _parent: *const _htmlElemDesc,
1998    _elt: *const xmlChar,
1999) -> c_int {
2000    1
2001}
2002
2003/// # UPSTREAM-PARITY
2004///
2005/// ```c
2006/// htmlStatus htmlElementStatusHere(const htmlElemDesc *parent, const htmlElemDesc *elt);
2007/// ```
2008///
2009/// Upstream is a deprecated stub returning HTML_VALID unconditionally.
2010#[no_mangle]
2011pub unsafe extern "C" fn htmlElementStatusHere(
2012    _parent: *const _htmlElemDesc,
2013    _elt: *const _htmlElemDesc,
2014) -> c_int {
2015    HTML_VALID
2016}
2017
2018/// # UPSTREAM-PARITY
2019///
2020/// ```c
2021/// htmlStatus htmlAttrAllowed(const htmlElemDesc *elt, const xmlChar *attr, int legacy);
2022/// ```
2023///
2024/// Upstream is a deprecated stub returning HTML_VALID unconditionally.
2025#[no_mangle]
2026pub unsafe extern "C" fn htmlAttrAllowed(
2027    _elt: *const _htmlElemDesc,
2028    _attr: *const xmlChar,
2029    _legacy: c_int,
2030) -> c_int {
2031    HTML_VALID
2032}
2033
2034/// # UPSTREAM-PARITY
2035///
2036/// ```c
2037/// htmlStatus htmlNodeStatus(xmlNode *node, int legacy);
2038/// ```
2039///
2040/// Upstream is a deprecated stub returning HTML_VALID unconditionally.
2041#[no_mangle]
2042pub unsafe extern "C" fn htmlNodeStatus(_node: *mut _xmlNode, _legacy: c_int) -> c_int {
2043    HTML_VALID
2044}
2045
2046// ═══════════════════════════════════════════════════════════════════════════════
2047// Encoding helpers
2048// ═══════════════════════════════════════════════════════════════════════════════
2049
2050/// Take a block of UTF-8 chars in and try to convert it to an ASCII plus
2051/// HTML entities block of chars out. Ported from
2052/// archaeology/libxml2-git/HTMLparser.c `htmlEncodeEntities`.
2053///
2054/// Returns 0 if success, -2 if the transcoding fails, or -1 otherwise.
2055/// `inlen` after return is the number of octets consumed; `outlen` the
2056/// number of octets produced.
2057///
2058/// # UPSTREAM-PARITY
2059///
2060/// ```c
2061/// int htmlEncodeEntities(unsigned char *out, int *outlen,
2062///                        const unsigned char *in, int *inlen, int quoteChar);
2063/// ```
2064#[no_mangle]
2065pub unsafe extern "C" fn htmlEncodeEntities(
2066    out: *mut u8,
2067    outlen: *mut c_int,
2068    input: *const u8,
2069    inlen: *mut c_int,
2070    quoteChar: c_int,
2071) -> c_int {
2072    if out.is_null() || outlen.is_null() || inlen.is_null() || input.is_null() {
2073        return -1;
2074    }
2075    let outend = (out as usize).wrapping_add((*outlen).max(0) as usize);
2076    let inend = (input as usize).wrapping_add((*inlen).max(0) as usize);
2077    let mut in_ptr = input as usize;
2078    let mut out_ptr = out as usize;
2079    let mut processed = in_ptr;
2080
2081    while in_ptr < inend {
2082        let mut c: c_uint;
2083        let d: c_uint;
2084        let mut trailing: c_int;
2085
2086        d = unsafe { *(in_ptr as *const u8) as c_uint };
2087        in_ptr += 1;
2088        if d < 0x80 {
2089            c = d;
2090            trailing = 0;
2091        } else if d < 0xC0 {
2092            // trailing byte in leading position
2093            *outlen = (out_ptr - out as usize) as c_int;
2094            *inlen = (processed - input as usize) as c_int;
2095            return -2;
2096        } else if d < 0xE0 {
2097            c = d & 0x1F;
2098            trailing = 1;
2099        } else if d < 0xF0 {
2100            c = d & 0x0F;
2101            trailing = 2;
2102        } else if d < 0xF8 {
2103            c = d & 0x07;
2104            trailing = 3;
2105        } else {
2106            // no chance for this in Ascii
2107            *outlen = (out_ptr - out as usize) as c_int;
2108            *inlen = (processed - input as usize) as c_int;
2109            return -2;
2110        }
2111
2112        if inend - in_ptr < trailing as usize {
2113            break;
2114        }
2115
2116        while trailing > 0 {
2117            let t = unsafe { *(in_ptr as *const u8) as c_uint };
2118            in_ptr += 1;
2119            if (t & 0xC0) != 0x80 {
2120                *outlen = (out_ptr - out as usize) as c_int;
2121                *inlen = (processed - input as usize) as c_int;
2122                return -2;
2123            }
2124            c = (c << 6) | (t & 0x3F);
2125            trailing -= 1;
2126        }
2127
2128        // assertion: c is a single UTF-4 value
2129        if (c < 0x80)
2130            && (c != quoteChar as c_uint)
2131            && (c != b'&' as c_uint)
2132            && (c != b'<' as c_uint)
2133            && (c != b'>' as c_uint)
2134        {
2135            if out_ptr >= outend {
2136                break;
2137            }
2138            unsafe { *(out_ptr as *mut u8) = c as u8 };
2139            out_ptr += 1;
2140        } else {
2141            let ent = unsafe { html_entity_value_lookup_static(c) };
2142            let mut nbuf = [0u8; 16];
2143            let (cp, len): (*const u8, usize) = if ent.is_null() {
2144                // snprintf(nbuf, sizeof(nbuf), "#%u", c)
2145                nbuf[0] = b'#';
2146                let mut i = 1usize;
2147                let mut digits = [0u8; 10];
2148                let mut nd = 0usize;
2149                let mut v = c;
2150                if v == 0 {
2151                    digits[0] = b'0';
2152                    nd = 1;
2153                }
2154                while v > 0 {
2155                    digits[nd] = b'0' + (v % 10) as u8;
2156                    nd += 1;
2157                    v /= 10;
2158                }
2159                while nd > 0 {
2160                    nd -= 1;
2161                    nbuf[i] = digits[nd];
2162                    i += 1;
2163                }
2164                (nbuf.as_ptr(), i)
2165            } else {
2166                (unsafe { (*ent).name } as *const u8, unsafe {
2167                    xml_strlen((*ent).name as *const xmlChar)
2168                })
2169            };
2170            if outend - out_ptr < len + 2 {
2171                break;
2172            }
2173            unsafe {
2174                *(out_ptr as *mut u8) = b'&';
2175                ptr::copy_nonoverlapping(cp, (out_ptr + 1) as *mut u8, len);
2176                *((out_ptr + 1 + len) as *mut u8) = b';';
2177            }
2178            out_ptr += len + 2;
2179        }
2180        processed = in_ptr;
2181    }
2182
2183    *outlen = (out_ptr - out as usize) as c_int;
2184    *inlen = (processed - input as usize) as c_int;
2185    0
2186}
2187
2188/// Substitute the HTML entities by their value.
2189///
2190/// DEPRECATED in upstream: since 2.13.0 the function lives in legacy.c and
2191/// emits a one-time diagnostic before returning NULL (oracle-verified
2192/// behavior).
2193///
2194/// # UPSTREAM-PARITY
2195///
2196/// ```c
2197/// xmlChar *htmlDecodeEntities(htmlParserCtxtPtr ctxt, int len,
2198///                             xmlChar end, xmlChar end2, xmlChar end3);
2199/// ```
2200#[no_mangle]
2201pub unsafe extern "C" fn htmlDecodeEntities(
2202    _ctxt: *mut c_void,
2203    _len: c_int,
2204    _end: xmlChar,
2205    _end2: xmlChar,
2206    _end3: xmlChar,
2207) -> *mut xmlChar {
2208    static DEPRECATED: AtomicBool = AtomicBool::new(false);
2209    if !DEPRECATED.swap(true, Ordering::Relaxed) {
2210        // Match the oracle: one-time "deprecated" diagnostic on stderr.
2211        let msg = b"htmlDecodeEntities() deprecated function reached\n";
2212        unsafe {
2213            libc::fwrite(
2214                msg.as_ptr() as *const c_void,
2215                1,
2216                msg.len(),
2217                libc::fdopen(2, b"w\0" as *const u8 as *const c_char) as *mut libc::FILE,
2218            );
2219        }
2220    }
2221    ptr::null_mut()
2222}
2223
2224/// Determine if a given attribute is a boolean attribute (HTMLtree.c
2225/// `htmlIsBooleanAttr`): ported decision tree over the XSLT 1.0 16.2
2226/// minimized-form attributes.
2227///
2228/// # UPSTREAM-PARITY
2229///
2230/// ```c
2231/// int htmlIsBooleanAttr(const xmlChar *name);
2232/// ```
2233#[no_mangle]
2234pub unsafe extern "C" fn htmlIsBooleanAttr(name: *const xmlChar) -> c_int {
2235    if name.is_null() {
2236        return 0;
2237    }
2238    let b = unsafe { xmlstr_to_bytes(name) };
2239    if b.is_empty() {
2240        return 0;
2241    }
2242    let mut i = 0usize;
2243    let mut suffix: Option<&'static [u8]> = None;
2244    match b[i].to_ascii_lowercase() {
2245        b'c' => {
2246            i += 1;
2247            match b.get(i).map(|&x| x.to_ascii_lowercase()) {
2248                Some(b'h') => suffix = Some(b"ecked"),
2249                Some(b'o') => suffix = Some(b"mpact"),
2250                _ => {}
2251            }
2252        }
2253        b'd' => {
2254            i += 1;
2255            match b.get(i).map(|&x| x.to_ascii_lowercase()) {
2256                Some(b'e') => {
2257                    i += 1;
2258                    match b.get(i).map(|&x| x.to_ascii_lowercase()) {
2259                        Some(b'c') => suffix = Some(b"lare"),
2260                        Some(b'f') => suffix = Some(b"er"),
2261                        _ => {}
2262                    }
2263                }
2264                Some(b'i') => suffix = Some(b"sabled"),
2265                _ => {}
2266            }
2267        }
2268        b'i' => suffix = Some(b"smap"),
2269        b'm' => suffix = Some(b"ultiple"),
2270        b'n' => {
2271            i += 1;
2272            if b.get(i).map(|&x| x.to_ascii_lowercase()) == Some(b'o') {
2273                i += 1;
2274                match b.get(i).map(|&x| x.to_ascii_lowercase()) {
2275                    Some(b'h') => suffix = Some(b"ref"),
2276                    Some(b'r') => suffix = Some(b"esize"),
2277                    Some(b's') => suffix = Some(b"hade"),
2278                    Some(b'w') => suffix = Some(b"rap"),
2279                    _ => {}
2280                }
2281            }
2282        }
2283        b'r' => suffix = Some(b"eadonly"),
2284        b's' => suffix = Some(b"elected"),
2285        _ => {}
2286    }
2287    let Some(suffix) = suffix else {
2288        return 0;
2289    };
2290    if b.len() == i + 1 + suffix.len() && b[i + 1..].eq_ignore_ascii_case(suffix) {
2291        1
2292    } else {
2293        0
2294    }
2295}
2296
2297// ═══════════════════════════════════════════════════════════════════════════════
2298// Global switches / initializers
2299// ═══════════════════════════════════════════════════════════════════════════════
2300
2301/// Global `htmlOmittedDefaultValue` mirroring the upstream static in
2302/// HTMLparser.c (initialized to 1).
2303static HTML_OMITTED_DEFAULT_VALUE: AtomicI32 = AtomicI32::new(1);
2304
2305/// Set and return the previous value for handling HTML omitted tags.
2306///
2307/// # UPSTREAM-PARITY
2308///
2309/// ```c
2310/// int htmlHandleOmittedElem(int val);
2311/// ```
2312#[no_mangle]
2313pub unsafe extern "C" fn htmlHandleOmittedElem(val: c_int) -> c_int {
2314    HTML_OMITTED_DEFAULT_VALUE.swap(val, Ordering::Relaxed)
2315}
2316
2317/// Upstream `htmlInitAutoClose` is a deprecated no-op.
2318///
2319/// # UPSTREAM-PARITY
2320///
2321/// ```c
2322/// void htmlInitAutoClose(void);
2323/// ```
2324#[no_mangle]
2325pub unsafe extern "C" fn htmlInitAutoClose() {
2326    // Deprecated no-op (HTMLparser.c).
2327}
2328
2329/// Initialize the htmlDefaultSAXHandler global (upstream SAX2.c
2330/// `htmlDefaultSAXHandlerInit`). The candidate's `htmlDefaultSAXHandler`
2331/// data symbol (data_globals.rs) is initialized statically, so this is a
2332/// no-op for ABI compatibility — same convention as `xmlDefaultSAXHandlerInit`.
2333///
2334/// # UPSTREAM-PARITY
2335///
2336/// ```c
2337/// void htmlDefaultSAXHandlerInit(void);
2338/// ```
2339#[no_mangle]
2340pub unsafe extern "C" fn htmlDefaultSAXHandlerInit() {
2341    // The exported htmlDefaultSAXHandler global is statically initialized.
2342}
2343
2344// ═══════════════════════════════════════════════════════════════════════════════
2345// Deprecated parser entry points (upstream stubs)
2346// ═══════════════════════════════════════════════════════════════════════════════
2347
2348/// Upstream `htmlParseEntityRef` is a deprecated stub returning NULL.
2349///
2350/// # UPSTREAM-PARITY
2351///
2352/// ```c
2353/// const htmlEntityDesc *htmlParseEntityRef(htmlParserCtxt *ctxt, const xmlChar **str);
2354/// ```
2355#[no_mangle]
2356pub unsafe extern "C" fn htmlParseEntityRef(
2357    _ctxt: *mut c_void,
2358    _str: *mut *const xmlChar,
2359) -> *const _htmlEntityDesc {
2360    ptr::null()
2361}
2362
2363/// Upstream `htmlParseCharRef` is a deprecated stub returning 0.
2364///
2365/// # UPSTREAM-PARITY
2366///
2367/// ```c
2368/// int htmlParseCharRef(htmlParserCtxt *ctxt);
2369/// ```
2370#[no_mangle]
2371pub unsafe extern "C" fn htmlParseCharRef(_ctxt: *mut c_void) -> c_int {
2372    0
2373}
2374
2375// ═══════════════════════════════════════════════════════════════════════════════
2376// HTML parser contexts
2377// ═══════════════════════════════════════════════════════════════════════════════
2378
2379/// Opaque HTML parser context.
2380///
2381/// # Layout contract
2382///
2383/// The exported `htmlFreeParserCtxt` (src/abi/exports_xml2.rs) routes to
2384/// `crate::xml::html::free_parser_ctxt`, which interprets the pointer as the
2385/// internal `HtmlParserCtxt` and frees its `filename`/`encoding` fields and
2386/// the block itself. The field prefix below therefore mirrors
2387/// `HtmlParserCtxt`'s declaration **exactly** (same field types, same order,
2388/// same default Rust representation — NOT `repr(C)`), which places
2389/// `filename`/`encoding` at the same byte offsets as the internal struct
2390/// (64 / 72 on 64-bit; verified empirically). The trailing fields are ABI
2391/// state that the internal module never touches.
2392struct HtmlOpaqueCtxt {
2393    // ── prefix mirroring xml::html::HtmlParserCtxt ──────────────────────
2394    doc: *mut _xmlDoc,
2395    current: *mut _xmlNode,
2396    html: *mut _xmlNode,
2397    head: *mut _xmlNode,
2398    body: *mut _xmlNode,
2399    in_head: bool,
2400    in_body: bool,
2401    html_created: bool,
2402    head_created: bool,
2403    body_created: bool,
2404    seen_body_content: bool,
2405    /// Accumulated push/input buffer (owned by the context).
2406    input: *mut u8,
2407    input_pos: usize,
2408    input_len: usize,
2409    line: c_int,
2410    err: bool,
2411    filename: *mut c_char,
2412    encoding: *mut c_char,
2413    // ── ABI state (not touched by the internal module) ──────────────────
2414    options: c_int,
2415    sax: *mut _xmlSAXHandler,
2416    user_data: *mut c_void,
2417}
2418
2419/// Allocate a zero-initialized HTML parser context with the internal
2420/// `HtmlParserCtxt`-compatible prefix. Freed by `htmlFreeParserCtxt`.
2421unsafe fn html_ctxt_alloc() -> *mut HtmlOpaqueCtxt {
2422    let mem = xmlMallocZero(size_of::<HtmlOpaqueCtxt>()) as *mut HtmlOpaqueCtxt;
2423    if mem.is_null() {
2424        return ptr::null_mut();
2425    }
2426    unsafe {
2427        ptr::write(
2428            mem,
2429            HtmlOpaqueCtxt {
2430                doc: ptr::null_mut(),
2431                current: ptr::null_mut(),
2432                html: ptr::null_mut(),
2433                head: ptr::null_mut(),
2434                body: ptr::null_mut(),
2435                in_head: false,
2436                in_body: false,
2437                html_created: false,
2438                head_created: false,
2439                body_created: false,
2440                seen_body_content: false,
2441                input: ptr::null_mut(),
2442                input_pos: 0,
2443                input_len: 0,
2444                line: 1,
2445                err: false,
2446                filename: ptr::null_mut(),
2447                encoding: ptr::null_mut(),
2448                options: 0,
2449                sax: ptr::null_mut(),
2450                user_data: ptr::null_mut(),
2451            },
2452        );
2453    }
2454    mem
2455}
2456
2457/// Store a copy of `buffer` (len bytes) as the context's input buffer.
2458unsafe fn html_ctxt_set_input(ctxt: *mut HtmlOpaqueCtxt, buffer: *const c_char, size: c_int) {
2459    if buffer.is_null() || size <= 0 {
2460        return;
2461    }
2462    let len = size as usize;
2463    let nb = xmlMalloc(len) as *mut u8;
2464    if nb.is_null() {
2465        return;
2466    }
2467    unsafe {
2468        ptr::copy_nonoverlapping(buffer as *const u8, nb, len);
2469        (*ctxt).input = nb;
2470        (*ctxt).input_len = len;
2471        (*ctxt).input_pos = 0;
2472    }
2473}
2474
2475/// Allocate and initialize a new HTML SAX parser context.
2476///
2477/// # UPSTREAM-PARITY
2478///
2479/// ```c
2480/// htmlParserCtxt *htmlNewSAXParserCtxt(const htmlSAXHandler *sax, void *userData);
2481/// ```
2482#[no_mangle]
2483pub unsafe extern "C" fn htmlNewSAXParserCtxt(
2484    sax: *const _xmlSAXHandler,
2485    userData: *mut c_void,
2486) -> *mut c_void {
2487    let ctxt = unsafe { html_ctxt_alloc() };
2488    if ctxt.is_null() {
2489        return ptr::null_mut();
2490    }
2491    unsafe {
2492        (*ctxt).sax = sax as *mut _xmlSAXHandler;
2493        (*ctxt).user_data = userData;
2494    }
2495    ctxt as *mut c_void
2496}
2497
2498/// Allocate and initialize a new HTML parser context.
2499///
2500/// # UPSTREAM-PARITY
2501///
2502/// ```c
2503/// htmlParserCtxt *htmlNewParserCtxt(void);
2504/// ```
2505#[no_mangle]
2506pub unsafe extern "C" fn htmlNewParserCtxt() -> *mut c_void {
2507    unsafe { htmlNewSAXParserCtxt(ptr::null(), ptr::null_mut()) }
2508}
2509
2510/// Create a parser context for an HTML in-memory document. The input buffer
2511/// must not contain any terminating null bytes.
2512///
2513/// # UPSTREAM-PARITY
2514///
2515/// ```c
2516/// htmlParserCtxt *htmlCreateMemoryParserCtxt(const char *buffer, int size);
2517/// ```
2518#[no_mangle]
2519pub unsafe extern "C" fn htmlCreateMemoryParserCtxt(
2520    buffer: *const c_char,
2521    size: c_int,
2522) -> *mut c_void {
2523    if buffer.is_null() || size <= 0 {
2524        return ptr::null_mut();
2525    }
2526    let ctxt = unsafe { html_ctxt_alloc() };
2527    if ctxt.is_null() {
2528        return ptr::null_mut();
2529    }
2530    unsafe { html_ctxt_set_input(ctxt, buffer, size) };
2531    if unsafe { (*ctxt).input.is_null() } {
2532        unsafe { crate::xml::html::free_parser_ctxt(ctxt as *mut c_void) };
2533        return ptr::null_mut();
2534    }
2535    ctxt as *mut c_void
2536}
2537
2538/// Create a parser context for using the HTML parser in push mode.
2539///
2540/// # UPSTREAM-PARITY
2541///
2542/// ```c
2543/// htmlParserCtxt *htmlCreatePushParserCtxt(htmlSAXHandler *sax, void *user_data,
2544///                                          const char *chunk, int size,
2545///                                          const char *filename, xmlCharEncoding enc);
2546/// ```
2547#[no_mangle]
2548pub unsafe extern "C" fn htmlCreatePushParserCtxt(
2549    sax: *mut _xmlSAXHandler,
2550    user_data: *mut c_void,
2551    chunk: *const c_char,
2552    size: c_int,
2553    filename: *const c_char,
2554    _enc: xmlCharEncoding,
2555) -> *mut c_void {
2556    let ctxt = unsafe { html_ctxt_alloc() };
2557    if ctxt.is_null() {
2558        return ptr::null_mut();
2559    }
2560    unsafe {
2561        (*ctxt).sax = sax;
2562        (*ctxt).user_data = user_data;
2563        if !filename.is_null() {
2564            (*ctxt).filename = c_strdup(filename);
2565        }
2566        if size > 0 && !chunk.is_null() {
2567            html_ctxt_set_input(ctxt, chunk, size);
2568        } else {
2569            // Upstream always creates a push input; allocate an (empty)
2570            // buffer so htmlParseChunk sees a valid input.
2571            let nb = xmlMalloc(1) as *mut u8;
2572            if !nb.is_null() {
2573                (*ctxt).input = nb;
2574                (*ctxt).input_len = 0;
2575                (*ctxt).input_pos = 0;
2576            }
2577        }
2578    }
2579    ctxt as *mut c_void
2580}
2581
2582/// Reset a parser context.
2583///
2584/// # UPSTREAM-PARITY
2585///
2586/// ```c
2587/// void htmlCtxtReset(htmlParserCtxt *ctxt);
2588/// ```
2589#[no_mangle]
2590pub unsafe extern "C" fn htmlCtxtReset(ctxt: *mut c_void) {
2591    if ctxt.is_null() {
2592        return;
2593    }
2594    let c = ctxt as *mut HtmlOpaqueCtxt;
2595    unsafe {
2596        if !(*c).input.is_null() {
2597            xmlFree((*c).input as *mut c_void);
2598        }
2599        (*c).input = ptr::null_mut();
2600        (*c).input_len = 0;
2601        (*c).input_pos = 0;
2602        (*c).doc = ptr::null_mut();
2603        (*c).options = 0;
2604        (*c).line = 1;
2605        (*c).err = false;
2606    }
2607}
2608
2609/// Applies the options to the parser context (upstream `htmlCtxtUseOptions`):
2610/// returns 0 when all options are known, else the set of unknown or
2611/// unimplemented options. The internal parser engine does not implement
2612/// option-driven behavior, so only the return value is observable.
2613///
2614/// # UPSTREAM-PARITY
2615///
2616/// ```c
2617/// int htmlCtxtUseOptions(htmlParserCtxt *ctxt, int options);
2618/// ```
2619#[no_mangle]
2620pub unsafe extern "C" fn htmlCtxtUseOptions(ctxt: *mut c_void, options: c_int) -> c_int {
2621    if ctxt.is_null() {
2622        return -1;
2623    }
2624    let c = ctxt as *mut HtmlOpaqueCtxt;
2625    // Historic storage rule: some options can only be enabled.
2626    unsafe {
2627        (*c).options = ((*c).options & HTML_OPTIONS_KEEP_MASK) | (options & HTML_OPTIONS_ALL_MASK);
2628    }
2629    // Return the set of unknown/unimplemented options (XML_PARSE_NOENT is
2630    // accepted and ignored, matching upstream).
2631    options & !HTML_OPTIONS_ALL_MASK & !crate::abi::types::XML_PARSE_NOENT
2632}
2633
2634/// Parse an HTML document from the context's stored input and invoke the
2635/// SAX handlers.
2636///
2637/// # UPSTREAM-PARITY
2638///
2639/// ```c
2640/// int htmlParseDocument(htmlParserCtxt *ctxt);
2641/// ```
2642#[no_mangle]
2643pub unsafe extern "C" fn htmlParseDocument(ctxt: *mut c_void) -> c_int {
2644    if ctxt.is_null() {
2645        return -1;
2646    }
2647    let c = ctxt as *mut HtmlOpaqueCtxt;
2648    if unsafe { (*c).input.is_null() } {
2649        return -1;
2650    }
2651    let doc = unsafe { html::parse_memory((*c).input as *const c_char, (*c).input_len as c_int) };
2652    unsafe { (*c).doc = doc };
2653    if doc.is_null() {
2654        -1
2655    } else {
2656        0
2657    }
2658}
2659
2660/// Parse a chunk of data. The last chunk must be marked with `terminate`; the
2661/// resulting document is stored in the context (opaque here) and returned by
2662/// `htmlCtxtParseDocument`-style entry points. The internal engine parses
2663/// incrementally only at the terminating chunk.
2664///
2665/// # UPSTREAM-PARITY
2666///
2667/// ```c
2668/// int htmlParseChunk(htmlParserCtxt *ctxt, const char *chunk, int size, int terminate);
2669/// ```
2670#[no_mangle]
2671pub unsafe extern "C" fn htmlParseChunk(
2672    ctxt: *mut c_void,
2673    chunk: *const c_char,
2674    size: c_int,
2675    terminate: c_int,
2676) -> c_int {
2677    if ctxt.is_null() || size < 0 || (size > 0 && chunk.is_null()) {
2678        return XML_ERR_ARGUMENT;
2679    }
2680    let c = ctxt as *mut HtmlOpaqueCtxt;
2681    if unsafe { (*c).input.is_null() } {
2682        return XML_ERR_ARGUMENT;
2683    }
2684
2685    if size > 0 {
2686        let new_len = unsafe { (*c).input_len }.wrapping_add(size as usize);
2687        let nb = unsafe { xmlRealloc((*c).input as *mut c_void, new_len) } as *mut u8;
2688        if nb.is_null() {
2689            return XML_ERR_NO_MEMORY;
2690        }
2691        unsafe {
2692            ptr::copy_nonoverlapping(chunk as *const u8, nb.add((*c).input_len), size as usize);
2693            (*c).input = nb;
2694            (*c).input_len = new_len;
2695        }
2696    }
2697
2698    if terminate != 0 {
2699        let doc =
2700            unsafe { html::parse_memory((*c).input as *const c_char, (*c).input_len as c_int) };
2701        unsafe {
2702            (*c).doc = doc;
2703            // The accumulated input is no longer needed.
2704            xmlFree((*c).input as *mut c_void);
2705            (*c).input = ptr::null_mut();
2706            (*c).input_len = 0;
2707        }
2708    }
2709    XML_ERR_OK
2710}
2711
2712/// Parse an HTML document and return the resulting document tree.
2713///
2714/// # UPSTREAM-PARITY
2715///
2716/// ```c
2717/// xmlDoc *htmlCtxtParseDocument(htmlParserCtxt *ctxt, xmlParserInput *input);
2718/// ```
2719#[no_mangle]
2720pub unsafe extern "C" fn htmlCtxtParseDocument(
2721    ctxt: *mut c_void,
2722    input: *mut _xmlParserInput,
2723) -> *mut _xmlDoc {
2724    if ctxt.is_null() || input.is_null() {
2725        return ptr::null_mut();
2726    }
2727    let cur = unsafe { (*input).cur };
2728    let end = unsafe { (*input).end };
2729    if cur.is_null() {
2730        return ptr::null_mut();
2731    }
2732    let len = (end as usize).wrapping_sub(cur as usize) as c_int;
2733    if len <= 0 {
2734        return ptr::null_mut();
2735    }
2736    let doc = unsafe { html::parse_memory(cur as *const c_char, len) };
2737    let c = ctxt as *mut HtmlOpaqueCtxt;
2738    unsafe {
2739        (*c).doc = doc;
2740    }
2741    doc
2742}
2743
2744// ═══════════════════════════════════════════════════════════════════════════════
2745// Convenience read APIs (htmlCtxtRead* / htmlRead* / htmlSAXParse*)
2746// ═══════════════════════════════════════════════════════════════════════════════
2747
2748/// Shared tail of the `htmlCtxtRead*` family: stash the parsed document in
2749/// the context and attach the URL.
2750unsafe fn html_ctxt_finish_read(
2751    ctxt: *mut c_void,
2752    doc: *mut _xmlDoc,
2753    url: *const c_char,
2754) -> *mut _xmlDoc {
2755    if ctxt.is_null() {
2756        return doc;
2757    }
2758    let c = ctxt as *mut HtmlOpaqueCtxt;
2759    unsafe {
2760        (*c).doc = doc;
2761        if !doc.is_null() && !url.is_null() {
2762            (*doc).URL = c_strdup(url) as *mut xmlChar;
2763        }
2764    }
2765    doc
2766}
2767
2768/// Parse an HTML in-memory document and build a tree.
2769///
2770/// # UPSTREAM-PARITY
2771///
2772/// ```c
2773/// xmlDoc *htmlCtxtReadMemory(xmlParserCtxt *ctxt, const char *buffer, int size,
2774///                            const char *URL, const char *encoding, int options);
2775/// ```
2776#[no_mangle]
2777pub unsafe extern "C" fn htmlCtxtReadMemory(
2778    ctxt: *mut c_void,
2779    buffer: *const c_char,
2780    size: c_int,
2781    URL: *const c_char,
2782    _encoding: *const c_char,
2783    options: c_int,
2784) -> *mut _xmlDoc {
2785    if ctxt.is_null() || size < 0 {
2786        return ptr::null_mut();
2787    }
2788    unsafe { htmlCtxtReset(ctxt) };
2789    unsafe { htmlCtxtUseOptions(ctxt, options) };
2790    let doc = unsafe { html::parse_memory(buffer, size) };
2791    unsafe { html_ctxt_finish_read(ctxt, doc, URL) }
2792}
2793
2794/// Parse an HTML in-memory document and build a tree.
2795///
2796/// # UPSTREAM-PARITY
2797///
2798/// ```c
2799/// xmlDoc *htmlCtxtReadDoc(xmlParserCtxt *ctxt, const xmlChar *str,
2800///                         const char *URL, const char *encoding, int options);
2801/// ```
2802#[no_mangle]
2803pub unsafe extern "C" fn htmlCtxtReadDoc(
2804    ctxt: *mut c_void,
2805    str: *const xmlChar,
2806    URL: *const c_char,
2807    encoding: *const c_char,
2808    options: c_int,
2809) -> *mut _xmlDoc {
2810    if ctxt.is_null() {
2811        return ptr::null_mut();
2812    }
2813    unsafe { htmlCtxtReset(ctxt) };
2814    unsafe { htmlCtxtUseOptions(ctxt, options) };
2815    let doc = unsafe { html::parse_doc(str, encoding) };
2816    unsafe { html_ctxt_finish_read(ctxt, doc, URL) }
2817}
2818
2819/// Parse an HTML file from the filesystem, the network or a user-defined
2820/// resource loader and build a tree.
2821///
2822/// # UPSTREAM-PARITY
2823///
2824/// ```c
2825/// xmlDoc *htmlCtxtReadFile(xmlParserCtxt *ctxt, const char *filename,
2826///                          const char *encoding, int options);
2827/// ```
2828#[no_mangle]
2829pub unsafe extern "C" fn htmlCtxtReadFile(
2830    ctxt: *mut c_void,
2831    filename: *const c_char,
2832    encoding: *const c_char,
2833    options: c_int,
2834) -> *mut _xmlDoc {
2835    if ctxt.is_null() {
2836        return ptr::null_mut();
2837    }
2838    unsafe { htmlCtxtReset(ctxt) };
2839    unsafe { htmlCtxtUseOptions(ctxt, options) };
2840    let doc = unsafe { html::parse_file(filename, encoding) };
2841    unsafe { html_ctxt_finish_read(ctxt, doc, filename) }
2842}
2843
2844/// Read all data from an open file descriptor.
2845unsafe fn html_read_fd(fd: c_int) -> Vec<u8> {
2846    let mut buf = Vec::new();
2847    let mut tmp = [0u8; 4096];
2848    loop {
2849        let n = libc::read(fd, tmp.as_mut_ptr() as *mut c_void, tmp.len());
2850        if n <= 0 {
2851            break;
2852        }
2853        buf.extend_from_slice(&tmp[..n as usize]);
2854    }
2855    buf
2856}
2857
2858/// Read all data through an input callback.
2859unsafe fn html_read_io(ioread: Option<xmlInputReadCallback>, ioctx: *mut c_void) -> Vec<u8> {
2860    let mut buf = Vec::new();
2861    let mut tmp = [0u8; 4096];
2862    if let Some(read) = ioread {
2863        loop {
2864            let n = unsafe { read(ioctx, tmp.as_mut_ptr() as *mut c_char, tmp.len() as c_int) };
2865            if n <= 0 {
2866                break;
2867            }
2868            buf.extend_from_slice(&tmp[..n as usize]);
2869        }
2870    }
2871    buf
2872}
2873
2874/// Parse an HTML document from a file descriptor and build a tree.
2875///
2876/// # UPSTREAM-PARITY
2877///
2878/// ```c
2879/// xmlDoc *htmlCtxtReadFd(xmlParserCtxt *ctxt, int fd,
2880///                        const char *URL, const char *encoding, int options);
2881/// ```
2882#[no_mangle]
2883pub unsafe extern "C" fn htmlCtxtReadFd(
2884    ctxt: *mut c_void,
2885    fd: c_int,
2886    URL: *const c_char,
2887    _encoding: *const c_char,
2888    options: c_int,
2889) -> *mut _xmlDoc {
2890    if ctxt.is_null() {
2891        return ptr::null_mut();
2892    }
2893    unsafe { htmlCtxtReset(ctxt) };
2894    unsafe { htmlCtxtUseOptions(ctxt, options) };
2895    let data = unsafe { html_read_fd(fd) };
2896    let doc = unsafe { html::parse_memory(data.as_ptr() as *const c_char, data.len() as c_int) };
2897    unsafe { html_ctxt_finish_read(ctxt, doc, URL) }
2898}
2899
2900/// Parse an HTML document from I/O functions and source and build a tree.
2901///
2902/// # UPSTREAM-PARITY
2903///
2904/// ```c
2905/// xmlDoc *htmlCtxtReadIO(xmlParserCtxt *ctxt, xmlInputReadCallback ioread,
2906///                        xmlInputCloseCallback ioclose, void *ioctx,
2907///                        const char *URL, const char *encoding, int options);
2908/// ```
2909#[no_mangle]
2910pub unsafe extern "C" fn htmlCtxtReadIO(
2911    ctxt: *mut c_void,
2912    ioread: Option<xmlInputReadCallback>,
2913    _ioclose: Option<xmlInputCloseCallback>,
2914    ioctx: *mut c_void,
2915    URL: *const c_char,
2916    _encoding: *const c_char,
2917    options: c_int,
2918) -> *mut _xmlDoc {
2919    if ctxt.is_null() {
2920        return ptr::null_mut();
2921    }
2922    unsafe { htmlCtxtReset(ctxt) };
2923    unsafe { htmlCtxtUseOptions(ctxt, options) };
2924    let data = unsafe { html_read_io(ioread, ioctx) };
2925    let doc = unsafe { html::parse_memory(data.as_ptr() as *const c_char, data.len() as c_int) };
2926    unsafe { html_ctxt_finish_read(ctxt, doc, URL) }
2927}
2928
2929/// Convenience function to parse an HTML document from memory.
2930///
2931/// # UPSTREAM-PARITY
2932///
2933/// ```c
2934/// xmlDoc *htmlReadMemory(const char *buffer, int size, const char *url,
2935///                        const char *encoding, int options);
2936/// ```
2937#[no_mangle]
2938pub unsafe extern "C" fn htmlReadMemory(
2939    buffer: *const c_char,
2940    size: c_int,
2941    url: *const c_char,
2942    encoding: *const c_char,
2943    options: c_int,
2944) -> *mut _xmlDoc {
2945    if size < 0 {
2946        return ptr::null_mut();
2947    }
2948    let ctxt = unsafe { htmlNewParserCtxt() };
2949    if ctxt.is_null() {
2950        return ptr::null_mut();
2951    }
2952    let doc = unsafe { htmlCtxtReadMemory(ctxt, buffer, size, url, encoding, options) };
2953    unsafe { crate::xml::html::free_parser_ctxt(ctxt) };
2954    doc
2955}
2956
2957/// Convenience function to parse an HTML document from a zero-terminated
2958/// string.
2959///
2960/// # UPSTREAM-PARITY
2961///
2962/// ```c
2963/// xmlDoc *htmlReadDoc(const xmlChar *str, const char *url,
2964///                     const char *encoding, int options);
2965/// ```
2966#[no_mangle]
2967pub unsafe extern "C" fn htmlReadDoc(
2968    str: *const xmlChar,
2969    url: *const c_char,
2970    encoding: *const c_char,
2971    options: c_int,
2972) -> *mut _xmlDoc {
2973    let ctxt = unsafe { htmlNewParserCtxt() };
2974    if ctxt.is_null() {
2975        return ptr::null_mut();
2976    }
2977    let doc = unsafe { htmlCtxtReadDoc(ctxt, str, url, encoding, options) };
2978    unsafe { crate::xml::html::free_parser_ctxt(ctxt) };
2979    doc
2980}
2981
2982/// Convenience function to parse an HTML file from the filesystem, the
2983/// network or a global user-defined resource loader.
2984///
2985/// # UPSTREAM-PARITY
2986///
2987/// ```c
2988/// xmlDoc *htmlReadFile(const char *filename, const char *encoding, int options);
2989/// ```
2990#[no_mangle]
2991pub unsafe extern "C" fn htmlReadFile(
2992    filename: *const c_char,
2993    encoding: *const c_char,
2994    options: c_int,
2995) -> *mut _xmlDoc {
2996    let ctxt = unsafe { htmlNewParserCtxt() };
2997    if ctxt.is_null() {
2998        return ptr::null_mut();
2999    }
3000    let doc = unsafe { htmlCtxtReadFile(ctxt, filename, encoding, options) };
3001    unsafe { crate::xml::html::free_parser_ctxt(ctxt) };
3002    doc
3003}
3004
3005/// Convenience function to parse an HTML document from a file descriptor.
3006///
3007/// # UPSTREAM-PARITY
3008///
3009/// ```c
3010/// xmlDoc *htmlReadFd(int fd, const char *url, const char *encoding, int options);
3011/// ```
3012#[no_mangle]
3013pub unsafe extern "C" fn htmlReadFd(
3014    fd: c_int,
3015    url: *const c_char,
3016    encoding: *const c_char,
3017    options: c_int,
3018) -> *mut _xmlDoc {
3019    let ctxt = unsafe { htmlNewParserCtxt() };
3020    if ctxt.is_null() {
3021        return ptr::null_mut();
3022    }
3023    let doc = unsafe { htmlCtxtReadFd(ctxt, fd, url, encoding, options) };
3024    unsafe { crate::xml::html::free_parser_ctxt(ctxt) };
3025    doc
3026}
3027
3028/// Convenience function to parse an HTML document from I/O functions and
3029/// context.
3030///
3031/// # UPSTREAM-PARITY
3032///
3033/// ```c
3034/// xmlDoc *htmlReadIO(xmlInputReadCallback ioread, xmlInputCloseCallback ioclose,
3035///                    void *ioctx, const char *url, const char *encoding, int options);
3036/// ```
3037#[no_mangle]
3038pub unsafe extern "C" fn htmlReadIO(
3039    ioread: Option<xmlInputReadCallback>,
3040    ioclose: Option<xmlInputCloseCallback>,
3041    ioctx: *mut c_void,
3042    url: *const c_char,
3043    encoding: *const c_char,
3044    options: c_int,
3045) -> *mut _xmlDoc {
3046    let ctxt = unsafe { htmlNewParserCtxt() };
3047    if ctxt.is_null() {
3048        return ptr::null_mut();
3049    }
3050    let doc = unsafe { htmlCtxtReadIO(ctxt, ioread, ioclose, ioctx, url, encoding, options) };
3051    unsafe { crate::xml::html::free_parser_ctxt(ctxt) };
3052    doc
3053}
3054
3055/// Parse an HTML in-memory document. If sax is not NULL, use the SAX
3056/// callbacks to handle parse events; the internal engine is DOM-based, so a
3057/// non-NULL sax is accepted and ignored (documented divergence).
3058///
3059/// # UPSTREAM-PARITY
3060///
3061/// ```c
3062/// xmlDoc *htmlSAXParseDoc(const xmlChar *cur, const char *encoding,
3063///                         htmlSAXHandler *sax, void *userData);
3064/// ```
3065#[no_mangle]
3066pub unsafe extern "C" fn htmlSAXParseDoc(
3067    cur: *const xmlChar,
3068    encoding: *const c_char,
3069    sax: *mut _xmlSAXHandler,
3070    userData: *mut c_void,
3071) -> *mut _xmlDoc {
3072    if cur.is_null() {
3073        return ptr::null_mut();
3074    }
3075    let ctxt = unsafe { htmlNewSAXParserCtxt(sax, userData) };
3076    if ctxt.is_null() {
3077        return ptr::null_mut();
3078    }
3079    let doc = unsafe { htmlCtxtReadDoc(ctxt, cur, ptr::null(), encoding, 0) };
3080    unsafe { crate::xml::html::free_parser_ctxt(ctxt) };
3081    doc
3082}
3083
3084/// Parse an HTML file and build a tree. If sax is not NULL, use the SAX
3085/// callbacks to handle parse events; the internal engine is DOM-based, so a
3086/// non-NULL sax is accepted and ignored (documented divergence).
3087///
3088/// # UPSTREAM-PARITY
3089///
3090/// ```c
3091/// xmlDoc *htmlSAXParseFile(const char *filename, const char *encoding,
3092///                          htmlSAXHandler *sax, void *userData);
3093/// ```
3094#[no_mangle]
3095pub unsafe extern "C" fn htmlSAXParseFile(
3096    filename: *const c_char,
3097    encoding: *const c_char,
3098    sax: *mut _xmlSAXHandler,
3099    userData: *mut c_void,
3100) -> *mut _xmlDoc {
3101    let ctxt = unsafe { htmlNewSAXParserCtxt(sax, userData) };
3102    if ctxt.is_null() {
3103        return ptr::null_mut();
3104    }
3105    let doc = unsafe { htmlCtxtReadFile(ctxt, filename, encoding, 0) };
3106    unsafe { crate::xml::html::free_parser_ctxt(ctxt) };
3107    doc
3108}
3109
3110/// Upstream `htmlParseElement` parses one element from the context's input
3111/// stream. The internal engine exposes whole-document parsing only, so this
3112/// best-effort implementation parses the context's stored input and stashes
3113/// the document (deprecated internal function; no-op when no input is set).
3114///
3115/// # UPSTREAM-PARITY
3116///
3117/// ```c
3118/// void htmlParseElement(htmlParserCtxt *ctxt);
3119/// ```
3120#[no_mangle]
3121pub unsafe extern "C" fn htmlParseElement(ctxt: *mut c_void) {
3122    if ctxt.is_null() {
3123        return;
3124    }
3125    let c = ctxt as *mut HtmlOpaqueCtxt;
3126    if unsafe { (*c).input.is_null() } {
3127        return;
3128    }
3129    let doc = unsafe { html::parse_memory((*c).input as *const c_char, (*c).input_len as c_int) };
3130    unsafe {
3131        (*c).doc = doc;
3132    }
3133}
3134
3135// ═══════════════════════════════════════════════════════════════════════════════
3136// Document creation
3137// ═══════════════════════════════════════════════════════════════════════════════
3138
3139/// Creates a new HTML document without a DTD node if `URI` and `publicId`
3140/// are NULL.
3141///
3142/// # UPSTREAM-PARITY
3143///
3144/// ```c
3145/// xmlDoc *htmlNewDocNoDtD(const xmlChar *URI, const xmlChar *ExternalID);
3146/// ```
3147#[no_mangle]
3148pub unsafe extern "C" fn htmlNewDocNoDtD(
3149    URI: *const xmlChar,
3150    publicId: *const xmlChar,
3151) -> *mut _xmlDoc {
3152    let doc = unsafe { html::new_doc_no_dtd(ptr::null()) };
3153    if doc.is_null() {
3154        return ptr::null_mut();
3155    }
3156    unsafe {
3157        // UPSTREAM-PARITY (HTMLparser.c htmlNewDocNoDtD): standalone=1,
3158        // charset=UTF-8, properties = XML_DOC_HTML | XML_DOC_USERBUILT.
3159        (*doc).standalone = 1;
3160        (*doc).charset = crate::abi::types::xmlCharEncoding::XML_CHAR_ENCODING_UTF8 as c_int;
3161        (*doc).properties = crate::abi::types::xmlDocProperties::XML_DOC_HTML as c_int
3162            | crate::abi::types::xmlDocProperties::XML_DOC_USERBUILT as c_int;
3163        if !publicId.is_null() || !URI.is_null() {
3164            let dtd = crate::xml::dtd::create_int_subset(
3165                doc,
3166                b"html\0" as *const u8 as *const xmlChar,
3167                publicId,
3168                URI,
3169            );
3170            if dtd.is_null() {
3171                tree::free_doc(doc);
3172                return ptr::null_mut();
3173            }
3174        }
3175    }
3176    doc
3177}
3178
3179/// Creates a new HTML document.
3180///
3181/// The document comes from the internal module's `html::new_doc` (the
3182/// crate's oracle-verified HTML module auto-creates the implicit
3183/// html/head/body skeleton, matching the crate's `htmlNewDoc` semantics);
3184/// the internal subset is attached exactly like upstream — the default HTML
3185/// 4.0 Transitional DTD when neither URI nor publicId is given, otherwise a
3186/// DTD with the supplied identifiers.
3187///
3188/// # UPSTREAM-PARITY
3189///
3190/// ```c
3191/// xmlDoc *htmlNewDoc(const xmlChar *URI, const xmlChar *ExternalID);
3192/// ```
3193#[no_mangle]
3194pub unsafe extern "C" fn htmlNewDoc(
3195    URI: *const xmlChar,
3196    ExternalID: *const xmlChar,
3197) -> *mut _xmlDoc {
3198    let doc = unsafe { html::new_doc(ptr::null()) };
3199    if doc.is_null() {
3200        return ptr::null_mut();
3201    }
3202    unsafe {
3203        // UPSTREAM-PARITY (HTMLparser.c htmlNewDocNoDtD): standalone=1,
3204        // charset=UTF-8, properties = XML_DOC_HTML | XML_DOC_USERBUILT.
3205        (*doc).standalone = 1;
3206        (*doc).charset = crate::abi::types::xmlCharEncoding::XML_CHAR_ENCODING_UTF8 as c_int;
3207        (*doc).properties = crate::abi::types::xmlDocProperties::XML_DOC_HTML as c_int
3208            | crate::abi::types::xmlDocProperties::XML_DOC_USERBUILT as c_int;
3209        if URI.is_null() && ExternalID.is_null() {
3210            let dtd = crate::xml::dtd::create_int_subset(
3211                doc,
3212                b"html\0" as *const u8 as *const xmlChar,
3213                b"-//W3C//DTD HTML 4.0 Transitional//EN\0" as *const u8 as *const xmlChar,
3214                b"http://www.w3.org/TR/REC-html40/loose.dtd\0" as *const u8 as *const xmlChar,
3215            );
3216            if dtd.is_null() {
3217                tree::free_doc(doc);
3218                return ptr::null_mut();
3219            }
3220        } else if !ExternalID.is_null() || !URI.is_null() {
3221            let dtd = crate::xml::dtd::create_int_subset(
3222                doc,
3223                b"html\0" as *const u8 as *const xmlChar,
3224                ExternalID,
3225                URI,
3226            );
3227            if dtd.is_null() {
3228                tree::free_doc(doc);
3229                return ptr::null_mut();
3230            }
3231        }
3232    }
3233    doc
3234}
3235
3236// ═══════════════════════════════════════════════════════════════════════════════
3237// Meta encoding (HTMLtree.c)
3238// ═══════════════════════════════════════════════════════════════════════════════
3239
3240/// Find the first child of `node` whose name matches (case-insensitive).
3241unsafe fn html_find_first_child(node: *mut _xmlNode, name: &[u8]) -> *mut _xmlNode {
3242    let mut c = unsafe { (*node).children };
3243    while !c.is_null() {
3244        let n = unsafe { &*c };
3245        if n.type_ == XML_ELEMENT_NODE as c_int
3246            && !n.name.is_null()
3247            && unsafe { xmlstr_to_bytes(n.name) }.eq_ignore_ascii_case(name)
3248        {
3249            return c;
3250        }
3251        c = unsafe { (*c).next };
3252    }
3253    ptr::null_mut()
3254}
3255
3256/// Locate the `<head>` element (upstream `htmlFindHead`): first `html` child
3257/// of the document, then first `head` child.
3258unsafe fn html_find_head(doc: *mut _xmlDoc) -> *mut _xmlNode {
3259    if doc.is_null() {
3260        return ptr::null_mut();
3261    }
3262    let html = unsafe { html_find_first_child(doc as *mut _xmlNode, b"html") };
3263    if html.is_null() {
3264        return ptr::null_mut();
3265    }
3266    unsafe { html_find_first_child(html, b"head") }
3267}
3268
3269/// Find the encoding-declaring attribute of a `meta` element
3270/// (upstream `htmlFindMetaEncodingAttr`). Returns `(attr, is_content_type)`.
3271unsafe fn html_find_meta_encoding_attr(elem: *mut _xmlNode) -> (*mut _xmlAttr, bool) {
3272    let n = unsafe { &*elem };
3273    if n.type_ != XML_ELEMENT_NODE as c_int || n.name.is_null() {
3274        return (ptr::null_mut(), false);
3275    }
3276    if !unsafe { xmlstr_to_bytes(n.name) }.eq_ignore_ascii_case(b"meta") {
3277        return (ptr::null_mut(), false);
3278    }
3279
3280    let mut content_attr: *mut _xmlAttr = ptr::null_mut();
3281    let mut is_content_type = false;
3282    let mut attr = n.properties;
3283    while !attr.is_null() {
3284        let a = unsafe { &*attr };
3285        if a.ns.is_null() && !a.name.is_null() {
3286            let nm = unsafe { xmlstr_to_bytes(a.name) };
3287            if nm.eq_ignore_ascii_case(b"charset") {
3288                return (attr, false);
3289            }
3290            if nm.eq_ignore_ascii_case(b"content") {
3291                content_attr = attr;
3292            }
3293            if nm.eq_ignore_ascii_case(b"http-equiv")
3294                && !a.children.is_null()
3295                && unsafe { (*(a.children)).type_ } == XML_TEXT_NODE as c_int
3296                && unsafe { (*(a.children)).next }.is_null()
3297                && !unsafe { (*(a.children)).content }.is_null()
3298                && unsafe { xmlstr_to_bytes((*(a.children)).content) }
3299                    .eq_ignore_ascii_case(b"Content-Type")
3300            {
3301                is_content_type = true;
3302            }
3303        }
3304        attr = unsafe { (*attr).next };
3305    }
3306    if is_content_type && !content_attr.is_null() {
3307        (content_attr, true)
3308    } else {
3309        (ptr::null_mut(), false)
3310    }
3311}
3312
3313/// Parse `charset=` out of a `content` attribute value (upstream
3314/// `htmlParseContentType`). Returns `(start, end, size)` offsets.
3315unsafe fn html_parse_content_type(val: *const xmlChar) -> Option<(usize, usize, usize)> {
3316    let bytes = unsafe { xmlstr_to_bytes(val) };
3317    let n = bytes.len();
3318    let at = |i: usize| -> u8 {
3319        if i < n {
3320            bytes[i]
3321        } else {
3322            0
3323        }
3324    };
3325
3326    let mut p = 0usize;
3327    loop {
3328        // Find 'c' or 'C'
3329        loop {
3330            let ch = at(p);
3331            if ch == b'c' || ch == b'C' {
3332                break;
3333            }
3334            if ch == 0 {
3335                return None;
3336            }
3337            p += 1;
3338        }
3339        p += 1;
3340
3341        // "harset" must follow (6 bytes, case-insensitive)
3342        let mut ok = true;
3343        for (k, want) in b"harset".iter().enumerate() {
3344            if at(p + k).to_ascii_lowercase() != *want {
3345                ok = false;
3346                break;
3347            }
3348        }
3349        if !ok {
3350            continue;
3351        }
3352        p += 6;
3353        while is_ws_html(at(p)) {
3354            p += 1;
3355        }
3356        if at(p) != b'=' {
3357            continue;
3358        }
3359        p += 1;
3360        while is_ws_html(at(p)) {
3361            p += 1;
3362        }
3363        if at(p) == 0 {
3364            return None;
3365        }
3366
3367        let (start, mut end): (usize, usize);
3368        if at(p) == b'"' || at(p) == b'\'' {
3369            let quote = at(p);
3370            p += 1;
3371            while is_ws_html(at(p)) {
3372                p += 1;
3373            }
3374            start = p;
3375            end = start;
3376            loop {
3377                if at(p) == 0 {
3378                    return None;
3379                }
3380                if !is_ws_html(at(p)) {
3381                    end = p + 1;
3382                }
3383                if at(p) == quote {
3384                    break;
3385                }
3386                p += 1;
3387            }
3388        } else {
3389            start = p;
3390            while at(p) != 0 && at(p) != b';' && !is_ws_html(at(p)) {
3391                p += 1;
3392            }
3393            end = p;
3394        }
3395        let size = n;
3396        return Some((start, end, size));
3397    }
3398}
3399
3400/// Look up an encoding declaration in the meta tags of the document.
3401///
3402/// The returned string points into attribute content (may contain trailing
3403/// garbage); it should be copied before modifying or freeing nodes —
3404/// upstream contract.
3405///
3406/// # UPSTREAM-PARITY
3407///
3408/// ```c
3409/// const xmlChar *htmlGetMetaEncoding(xmlDoc *doc);
3410/// ```
3411#[no_mangle]
3412pub unsafe extern "C" fn htmlGetMetaEncoding(doc: *mut _xmlDoc) -> *const xmlChar {
3413    let head = unsafe { html_find_head(doc) };
3414    if head.is_null() {
3415        return ptr::null();
3416    }
3417    let mut node = unsafe { (*head).children };
3418    while !node.is_null() {
3419        let (attr, is_content_type) = unsafe { html_find_meta_encoding_attr(node) };
3420        if !attr.is_null() {
3421            let a = unsafe { &*attr };
3422            let val = if !a.children.is_null()
3423                && unsafe { (*(a.children)).type_ } == XML_TEXT_NODE as c_int
3424                && unsafe { (*(a.children)).next }.is_null()
3425                && !unsafe { (*(a.children)).content }.is_null()
3426            {
3427                unsafe { (*(a.children)).content }
3428            } else {
3429                b"\0" as *const u8 as *const xmlChar
3430            };
3431            if !is_content_type {
3432                let bytes = unsafe { xmlstr_to_bytes(val) };
3433                let mut start = 0usize;
3434                while start < bytes.len() && is_ws_html(bytes[start]) {
3435                    start += 1;
3436                }
3437                return unsafe { val.add(start) };
3438            } else if let Some((start, _, _)) = unsafe { html_parse_content_type(val) } {
3439                return unsafe { val.add(start) };
3440            }
3441        }
3442        node = unsafe { (*node).next };
3443    }
3444    ptr::null()
3445}
3446
3447/// Build the updated charset value for an existing meta tag
3448/// (upstream `htmlUpdateMetaEncoding`).
3449unsafe fn html_update_meta_encoding(
3450    attr_value: *const xmlChar,
3451    start: usize,
3452    end: usize,
3453    size: usize,
3454    encoding: &[u8],
3455) -> *mut xmlChar {
3456    // The pseudo "HTML" encoding only produces ASCII.
3457    let enc: &[u8] = if encoding.eq_ignore_ascii_case(b"HTML") {
3458        b"ASCII"
3459    } else {
3460        encoding
3461    };
3462    let bytes = unsafe { xmlstr_to_bytes(attr_value) };
3463    let e = end.min(bytes.len()).min(size);
3464    let s = start.min(e);
3465    let total = size - (e - s) + enc.len();
3466    let new_val = xmlMalloc(total + 1) as *mut xmlChar;
3467    if new_val.is_null() {
3468        return ptr::null_mut();
3469    }
3470    unsafe {
3471        let mut p = new_val;
3472        ptr::copy_nonoverlapping(bytes.as_ptr(), p, s);
3473        p = p.add(s);
3474        ptr::copy_nonoverlapping(enc.as_ptr(), p, enc.len());
3475        p = p.add(enc.len());
3476        ptr::copy_nonoverlapping(bytes.as_ptr().add(e), p, size - e);
3477        *new_val.add(total) = 0;
3478    }
3479    new_val
3480}
3481
3482/// Replace the content of an attribute's single text child
3483/// (upstream `xmlNodeSetContent` on the attribute node).
3484unsafe fn html_set_attr_content(attr: *mut _xmlAttr, content: *const xmlChar) -> c_int {
3485    if attr.is_null() {
3486        return -1;
3487    }
3488    unsafe {
3489        if !(*attr).children.is_null() {
3490            tree::free_node_list((*attr).children);
3491            (*attr).children = ptr::null_mut();
3492            (*attr).last = ptr::null_mut();
3493        }
3494        let text = tree::new_text(content);
3495        if text.is_null() {
3496            return -1;
3497        }
3498        (*text).parent = attr as *mut _xmlNode;
3499        (*text).doc = (*attr).doc;
3500        (*attr).children = text;
3501        (*attr).last = text;
3502    }
3503    0
3504}
3505
3506/// Creates or updates a meta tag with an encoding declaration.
3507///
3508/// # UPSTREAM-PARITY
3509///
3510/// ```c
3511/// int htmlSetMetaEncoding(xmlDoc *doc, const xmlChar *encoding);
3512/// ```
3513#[no_mangle]
3514pub unsafe extern "C" fn htmlSetMetaEncoding(doc: *mut _xmlDoc, encoding: *const xmlChar) -> c_int {
3515    if encoding.is_null() {
3516        return 1;
3517    }
3518    let head = unsafe { html_find_head(doc) };
3519    if head.is_null() {
3520        return 1;
3521    }
3522    let enc_bytes = unsafe { xmlstr_to_bytes(encoding) }.to_vec();
3523
3524    let mut found = 0;
3525    let mut meta = unsafe { (*head).children };
3526    while !meta.is_null() {
3527        let (attr, is_content_type) = unsafe { html_find_meta_encoding_attr(meta) };
3528        if !attr.is_null() {
3529            let a = unsafe { &*attr };
3530            let val = if !a.children.is_null()
3531                && unsafe { (*(a.children)).type_ } == XML_TEXT_NODE as c_int
3532                && unsafe { (*(a.children)).next }.is_null()
3533                && !unsafe { (*(a.children)).content }.is_null()
3534            {
3535                unsafe { (*(a.children)).content }
3536            } else {
3537                b"\0" as *const u8 as *const xmlChar
3538            };
3539            found = 1;
3540            let off = if is_content_type {
3541                unsafe { html_parse_content_type(val) }
3542            } else {
3543                let bytes = unsafe { xmlstr_to_bytes(val) };
3544                let mut start = 0usize;
3545                let mut end = bytes.len();
3546                while start < end && is_ws_html(bytes[start]) {
3547                    start += 1;
3548                }
3549                while end > start && is_ws_html(bytes[end - 1]) {
3550                    end -= 1;
3551                }
3552                Some((start, end, bytes.len()))
3553            };
3554            if let Some((start, end, size)) = off {
3555                let new_val =
3556                    unsafe { html_update_meta_encoding(val, start, end, size, &enc_bytes) };
3557                if new_val.is_null() {
3558                    return -1;
3559                }
3560                let ret = unsafe { html_set_attr_content(attr, new_val) };
3561                unsafe { xmlFree(new_val as *mut c_void) };
3562                if ret < 0 {
3563                    return -1;
3564                }
3565            } else {
3566                return -1;
3567            }
3568        }
3569        meta = unsafe { (*meta).next };
3570    }
3571
3572    if found != 0 {
3573        return 0;
3574    }
3575
3576    // No meta found: create one and insert it as the first child of head.
3577    let meta_node =
3578        unsafe { tree::new_node(ptr::null_mut(), b"meta\0" as *const u8 as *const xmlChar) };
3579    if meta_node.is_null() {
3580        return -1;
3581    }
3582    unsafe {
3583        (*meta_node).doc = (*head).doc;
3584    }
3585    let prop = unsafe {
3586        tree::set_prop(
3587            meta_node,
3588            b"charset\0" as *const u8 as *const xmlChar,
3589            encoding,
3590        )
3591    };
3592    if prop.is_null() {
3593        unsafe { tree::free_node(meta_node) };
3594        return -1;
3595    }
3596    if unsafe { (*head).children }.is_null() {
3597        unsafe { tree::add_child(head, meta_node) };
3598    } else {
3599        unsafe { tree::add_sibling_before((*head).children, meta_node) };
3600    }
3601    0
3602}
3603
3604// ═══════════════════════════════════════════════════════════════════════════════
3605// Serialization (HTMLtree.c)
3606// ═══════════════════════════════════════════════════════════════════════════════
3607
3608/// Serialize `node` into a fresh `_xmlBuffer` via the internal HTML
3609/// serializer. Returns the buffer (caller frees with `io::buf_free`) or NULL.
3610unsafe fn html_serialize_to_buffer(node: *mut _xmlNode, format: c_int) -> *mut _xmlBuffer {
3611    let buf = io::buf_create(0);
3612    if buf.is_null() {
3613        return ptr::null_mut();
3614    }
3615    unsafe { html::serialize_node(node, buf, format, 0) };
3616    buf
3617}
3618
3619/// Serialize `node` into an `_xmlOutputBuffer` (writes the serialized bytes
3620/// through the output buffer's I/O channel).
3621unsafe fn html_serialize_to_obuf(obuf: *mut _xmlOutputBuffer, node: *mut _xmlNode, format: c_int) {
3622    if obuf.is_null() || node.is_null() {
3623        return;
3624    }
3625    let buf = unsafe { html_serialize_to_buffer(node, format) };
3626    if buf.is_null() {
3627        return;
3628    }
3629    let len = io::buf_length(buf);
3630    if len > 0 {
3631        let content = io::buf_content(buf);
3632        unsafe {
3633            io::output_buffer_write(obuf, len, content as *const c_char);
3634        }
3635    }
3636    io::buf_free(buf);
3637}
3638
3639/// Serialize an HTML node to an xmlBuffer. Always uses UTF-8.
3640///
3641/// # UPSTREAM-PARITY
3642///
3643/// ```c
3644/// int htmlNodeDump(xmlBuffer *buf, xmlDoc *doc, xmlNode *cur);
3645/// ```
3646#[no_mangle]
3647pub unsafe extern "C" fn htmlNodeDump(
3648    buf: *mut _xmlBuffer,
3649    _doc: *mut _xmlDoc,
3650    cur: *mut _xmlNode,
3651) -> c_int {
3652    if buf.is_null() || cur.is_null() {
3653        return -1;
3654    }
3655    let before = io::buf_length(buf);
3656    unsafe { html::serialize_node(cur, buf, 1, 0) };
3657    let after = io::buf_length(buf);
3658    if after < 0 || before < 0 {
3659        return -1;
3660    }
3661    after - before
3662}
3663
3664/// Serialize an HTML node to a FILE.
3665///
3666/// # UPSTREAM-PARITY
3667///
3668/// ```c
3669/// void htmlNodeDumpFile(FILE *out, xmlDoc *doc, xmlNode *cur);
3670/// ```
3671#[no_mangle]
3672pub unsafe extern "C" fn htmlNodeDumpFile(out: *mut c_void, doc: *mut _xmlDoc, cur: *mut _xmlNode) {
3673    unsafe { htmlNodeDumpFileFormat(out, doc, cur, ptr::null(), 1) };
3674}
3675
3676/// Serialize an HTML node to a FILE with encoding and format.
3677///
3678/// # UPSTREAM-PARITY
3679///
3680/// ```c
3681/// int htmlNodeDumpFileFormat(FILE *out, xmlDoc *doc, xmlNode *cur,
3682///                            const char *encoding, int format);
3683/// ```
3684#[no_mangle]
3685pub unsafe extern "C" fn htmlNodeDumpFileFormat(
3686    out: *mut c_void,
3687    _doc: *mut _xmlDoc,
3688    cur: *mut _xmlNode,
3689    _encoding: *const c_char,
3690    format: c_int,
3691) -> c_int {
3692    let obuf = io::output_buffer_create_file(out as *mut libc::FILE, ptr::null_mut());
3693    if obuf.is_null() {
3694        return -1;
3695    }
3696    unsafe { html_serialize_to_obuf(obuf, cur, format) };
3697    io::output_buffer_close(obuf)
3698}
3699
3700/// Serialize an HTML node to an output buffer.
3701///
3702/// # UPSTREAM-PARITY
3703///
3704/// ```c
3705/// void htmlNodeDumpOutput(xmlOutputBuffer *buf, xmlDoc *doc, xmlNode *cur,
3706///                         const char *encoding);
3707/// ```
3708#[no_mangle]
3709pub unsafe extern "C" fn htmlNodeDumpOutput(
3710    buf: *mut _xmlOutputBuffer,
3711    _doc: *mut _xmlDoc,
3712    cur: *mut _xmlNode,
3713    _encoding: *const c_char,
3714) {
3715    unsafe { html_serialize_to_obuf(buf, cur, 1) };
3716}
3717
3718/// Serialize an HTML node to an output buffer with format.
3719///
3720/// # UPSTREAM-PARITY
3721///
3722/// ```c
3723/// void htmlNodeDumpFormatOutput(xmlOutputBuffer *buf, xmlDoc *doc, xmlNode *cur,
3724///                               const char *encoding, int format);
3725/// ```
3726#[no_mangle]
3727pub unsafe extern "C" fn htmlNodeDumpFormatOutput(
3728    buf: *mut _xmlOutputBuffer,
3729    _doc: *mut _xmlDoc,
3730    cur: *mut _xmlNode,
3731    _encoding: *const c_char,
3732    format: c_int,
3733) {
3734    unsafe { html_serialize_to_obuf(buf, cur, format) };
3735}
3736
3737/// Serialize an HTML document to an output buffer.
3738///
3739/// # UPSTREAM-PARITY
3740///
3741/// ```c
3742/// void htmlDocContentDumpOutput(xmlOutputBuffer *buf, xmlDoc *cur,
3743///                               const char *encoding);
3744/// ```
3745#[no_mangle]
3746pub unsafe extern "C" fn htmlDocContentDumpOutput(
3747    buf: *mut _xmlOutputBuffer,
3748    cur: *mut _xmlDoc,
3749    _encoding: *const c_char,
3750) {
3751    unsafe { html_serialize_to_obuf(buf, cur as *mut _xmlNode, 1) };
3752}
3753
3754/// Serialize an HTML document to an output buffer with format.
3755///
3756/// # UPSTREAM-PARITY
3757///
3758/// ```c
3759/// void htmlDocContentDumpFormatOutput(xmlOutputBuffer *buf, xmlDoc *cur,
3760///                                     const char *encoding, int format);
3761/// ```
3762#[no_mangle]
3763pub unsafe extern "C" fn htmlDocContentDumpFormatOutput(
3764    buf: *mut _xmlOutputBuffer,
3765    cur: *mut _xmlDoc,
3766    _encoding: *const c_char,
3767    format: c_int,
3768) {
3769    unsafe { html_serialize_to_obuf(buf, cur as *mut _xmlNode, format) };
3770}
3771
3772/// Serialize an HTML document to memory, also returning the size of the
3773/// result. The caller frees `mem` with `xmlFree`. The output is UTF-8
3774/// (upstream converts to the document encoding).
3775///
3776/// # UPSTREAM-PARITY
3777///
3778/// ```c
3779/// void htmlDocDumpMemoryFormat(xmlDoc *cur, xmlChar **mem, int *size, int format);
3780/// ```
3781#[no_mangle]
3782pub unsafe extern "C" fn htmlDocDumpMemoryFormat(
3783    cur: *mut _xmlDoc,
3784    mem: *mut *mut xmlChar,
3785    size: *mut c_int,
3786    format: c_int,
3787) {
3788    if mem.is_null() || size.is_null() {
3789        return;
3790    }
3791    unsafe {
3792        *mem = ptr::null_mut();
3793        *size = 0;
3794    }
3795    if cur.is_null() {
3796        return;
3797    }
3798    let buf = unsafe { html_serialize_to_buffer(cur as *mut _xmlNode, format) };
3799    if buf.is_null() {
3800        return;
3801    }
3802    let len = io::buf_length(buf);
3803    if len > 0 {
3804        let content = io::buf_content(buf);
3805        unsafe {
3806            *mem = xml_strndup(content, len as usize);
3807            if !(*mem).is_null() {
3808                *size = len;
3809            }
3810        }
3811    }
3812    io::buf_free(buf);
3813}
3814
3815/// Same as `htmlDocDumpMemoryFormat` with `format` set to 1.
3816///
3817/// # UPSTREAM-PARITY
3818///
3819/// ```c
3820/// void htmlDocDumpMemory(xmlDoc *cur, xmlChar **mem, int *size);
3821/// ```
3822#[no_mangle]
3823pub unsafe extern "C" fn htmlDocDumpMemory(
3824    cur: *mut _xmlDoc,
3825    mem: *mut *mut xmlChar,
3826    size: *mut c_int,
3827) {
3828    unsafe { htmlDocDumpMemoryFormat(cur, mem, size, 1) };
3829}
3830
3831/// Serialize an HTML document to an open FILE.
3832///
3833/// # UPSTREAM-PARITY
3834///
3835/// ```c
3836/// int htmlDocDump(FILE *f, xmlDoc *cur);
3837/// ```
3838#[no_mangle]
3839pub unsafe extern "C" fn htmlDocDump(f: *mut c_void, cur: *mut _xmlDoc) -> c_int {
3840    if f.is_null() || cur.is_null() {
3841        return -1;
3842    }
3843    let obuf = io::output_buffer_create_file(f as *mut libc::FILE, ptr::null_mut());
3844    if obuf.is_null() {
3845        return -1;
3846    }
3847    unsafe { html_serialize_to_obuf(obuf, cur as *mut _xmlNode, 1) };
3848    io::output_buffer_close(obuf)
3849}
3850
3851/// Serialize an HTML document to a file using a given encoding and format.
3852///
3853/// # UPSTREAM-PARITY
3854///
3855/// ```c
3856/// int htmlSaveFileFormat(const char *filename, xmlDoc *cur,
3857///                        const char *encoding, int format);
3858/// ```
3859#[no_mangle]
3860pub unsafe extern "C" fn htmlSaveFileFormat(
3861    filename: *const c_char,
3862    cur: *mut _xmlDoc,
3863    _encoding: *const c_char,
3864    format: c_int,
3865) -> c_int {
3866    if cur.is_null() || filename.is_null() {
3867        return -1;
3868    }
3869    let obuf = io::output_buffer_create_filename(filename, ptr::null_mut(), 0);
3870    if obuf.is_null() {
3871        // UPSTREAM-PARITY: a failed output buffer yields 0, not -1.
3872        return 0;
3873    }
3874    unsafe { html_serialize_to_obuf(obuf, cur as *mut _xmlNode, format) };
3875    io::output_buffer_close(obuf)
3876}
3877
3878/// Same as `htmlSaveFileFormat` with `encoding` set to NULL and `format` set
3879/// to 1.
3880///
3881/// # UPSTREAM-PARITY
3882///
3883/// ```c
3884/// int htmlSaveFile(const char *filename, xmlDoc *cur);
3885/// ```
3886#[no_mangle]
3887pub unsafe extern "C" fn htmlSaveFile(filename: *const c_char, cur: *mut _xmlDoc) -> c_int {
3888    unsafe { htmlSaveFileFormat(filename, cur, ptr::null(), 1) }
3889}
3890
3891/// Same as `htmlSaveFileFormat` with `format` set to 1.
3892///
3893/// # UPSTREAM-PARITY
3894///
3895/// ```c
3896/// int htmlSaveFileEnc(const char *filename, xmlDoc *cur, const char *encoding);
3897/// ```
3898#[no_mangle]
3899pub unsafe extern "C" fn htmlSaveFileEnc(
3900    filename: *const c_char,
3901    cur: *mut _xmlDoc,
3902    encoding: *const c_char,
3903) -> c_int {
3904    unsafe { htmlSaveFileFormat(filename, cur, encoding, 1) }
3905}