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