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