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_cross_dso()
2755                .is_some()
2756            {
2757                crate::abi::exports_parser::call_loader_materialize(filename).ok()
2758            } else {
2759                let name = std::ffi::CStr::from_ptr(filename)
2760                    .to_string_lossy()
2761                    .into_owned();
2762                std::fs::read(name).ok()
2763            };
2764        let Some(bytes) = loaded else {
2765            // UPSTREAM-PARITY (parserInternals.c xmlCtxtErrIO via
2766            // htmlCreateFileParserCtxt -> xmlCtxtNewInputFromUrl): a failed
2767            // main-document load raises the I/O warning
2768            // "failed to load \"%s\": %s\n" (XML_IO_ENOENT, warning
2769            // level) through the parser channel BEFORE the NULL context is
2770            // returned — php's error handlers format it as
2771            // "loadHTMLFile(): I/O warning : ..." (DOMDocument_loadHTMLfile_error1).
2772            let msg = crate::abi::exports_parser::io_load_failure_message(filename);
2773            unsafe {
2774                crate::abi::exports_parser::emit_io_warning(
2775                    host as *mut crate::abi::structs::_xmlParserCtxt,
2776                    msg,
2777                );
2778            }
2779            crate::xml::html::free_parser_ctxt(host);
2780            return ptr::null_mut();
2781        };
2782        if bytes.is_empty() {
2783            crate::xml::html::free_parser_ctxt(host);
2784            return ptr::null_mut();
2785        }
2786        if !encoding.is_null() {
2787            let st = html_state(host);
2788            (*st).encoding = c_strdup(encoding);
2789        }
2790        html_ctxt_set_input(host, bytes.as_ptr() as *const c_char, bytes.len() as c_int);
2791        let st = html_state(host);
2792        if (*st).input.is_null() {
2793            crate::xml::html::free_parser_ctxt(host);
2794            return ptr::null_mut();
2795        }
2796        host
2797    }
2798}
2799
2800/// Reset a parser context.
2801///
2802/// # UPSTREAM-PARITY
2803///
2804/// ```c
2805/// void htmlCtxtReset(htmlParserCtxt *ctxt);
2806/// ```
2807#[no_mangle]
2808pub unsafe extern "C" fn htmlCtxtReset(ctxt: *mut c_void) {
2809    if ctxt.is_null() {
2810        return;
2811    }
2812    let st = unsafe { html_state(ctxt) };
2813    let c = ctxt as *mut _xmlParserCtxt;
2814    unsafe {
2815        if !(*st).input.is_null() {
2816            xmlFreeImpl((*st).input as *mut c_void);
2817        }
2818        (*st).input = ptr::null_mut();
2819        (*st).input_len = 0;
2820        (*st).input_pos = 0;
2821        (*st).doc = ptr::null_mut();
2822        (*st).options = 0;
2823        (*st).line = 1;
2824        (*st).err = false;
2825        // C-visible state: upstream htmlCtxtReset clears the input streams
2826        // and doc, resets errNo and wellFormed, and keeps the SAX handler
2827        // and userData.
2828        (*c).myDoc = ptr::null_mut();
2829        (*c).errNo = 0;
2830        (*c).wellFormed = 1;
2831        (*c).options = 0;
2832    }
2833}
2834
2835/// Applies the options to the parser context (upstream `htmlCtxtUseOptions`):
2836/// returns 0 when all options are known, else the set of unknown or
2837/// unimplemented options. The internal parser engine does not implement
2838/// option-driven behavior, so only the return value is observable.
2839///
2840/// # UPSTREAM-PARITY
2841///
2842/// ```c
2843/// int htmlCtxtUseOptions(htmlParserCtxt *ctxt, int options);
2844/// ```
2845#[no_mangle]
2846pub unsafe extern "C" fn htmlCtxtUseOptions(ctxt: *mut c_void, options: c_int) -> c_int {
2847    if ctxt.is_null() {
2848        return -1;
2849    }
2850    let c = ctxt as *mut _xmlParserCtxt;
2851    let st = unsafe { html_state(ctxt) };
2852    // Historic storage rule: some options can only be enabled.
2853    unsafe {
2854        (*c).options = ((*c).options & HTML_OPTIONS_KEEP_MASK) | (options & HTML_OPTIONS_ALL_MASK);
2855        (*st).options = (*c).options;
2856    }
2857    // Return the set of unknown/unimplemented options (XML_PARSE_NOENT is
2858    // accepted and ignored, matching upstream).
2859    options & !HTML_OPTIONS_ALL_MASK & !crate::abi::types::XML_PARSE_NOENT
2860}
2861
2862/// Parse an HTML document from the context's stored input and invoke the
2863/// SAX handlers.
2864///
2865/// # UPSTREAM-PARITY
2866///
2867/// ```c
2868/// int htmlParseDocument(htmlParserCtxt *ctxt);
2869/// ```
2870#[no_mangle]
2871pub unsafe extern "C" fn htmlParseDocument(ctxt: *mut c_void) -> c_int {
2872    if ctxt.is_null() {
2873        return -1;
2874    }
2875    let st = unsafe { html_state(ctxt) };
2876    if unsafe { (*st).input.is_null() } {
2877        return -1;
2878    }
2879    let doc = unsafe {
2880        html::parse_memory_enc(
2881            (*st).input as *const c_char,
2882            (*st).input_len as c_int,
2883            (*st).encoding,
2884            (*st).options,
2885        )
2886    };
2887    let c = ctxt as *mut _xmlParserCtxt;
2888    unsafe {
2889        (*st).doc = doc;
2890        (*c).myDoc = doc;
2891        if !doc.is_null() {
2892            (*c).wellFormed = 1;
2893        }
2894    }
2895    if doc.is_null() {
2896        -1
2897    } else {
2898        0
2899    }
2900}
2901
2902/// Parse a chunk of data. The last chunk must be marked with `terminate`; the
2903/// resulting document is stored in the context (opaque here) and returned by
2904/// `htmlCtxtParseDocument`-style entry points. The internal engine parses
2905/// incrementally only at the terminating chunk.
2906///
2907/// # UPSTREAM-PARITY
2908///
2909/// ```c
2910/// int htmlParseChunk(htmlParserCtxt *ctxt, const char *chunk, int size, int terminate);
2911/// ```
2912#[no_mangle]
2913pub unsafe extern "C" fn htmlParseChunk(
2914    ctxt: *mut c_void,
2915    chunk: *const c_char,
2916    size: c_int,
2917    terminate: c_int,
2918) -> c_int {
2919    if ctxt.is_null() || size < 0 || (size > 0 && chunk.is_null()) {
2920        return XML_ERR_ARGUMENT;
2921    }
2922    let st = unsafe { html_state(ctxt) };
2923    if unsafe { (*st).input.is_null() } {
2924        return XML_ERR_ARGUMENT;
2925    }
2926
2927    if size > 0 {
2928        let new_len = unsafe { (*st).input_len }.wrapping_add(size as usize);
2929        let nb = unsafe { xmlReallocImpl((*st).input as *mut c_void, new_len) } as *mut u8;
2930        if nb.is_null() {
2931            return XML_ERR_NO_MEMORY;
2932        }
2933        unsafe {
2934            ptr::copy_nonoverlapping(chunk as *const u8, nb.add((*st).input_len), size as usize);
2935            (*st).input = nb;
2936            (*st).input_len = new_len;
2937        }
2938    }
2939
2940    if terminate != 0 {
2941        let doc = unsafe {
2942            html::parse_memory_enc(
2943                (*st).input as *const c_char,
2944                (*st).input_len as c_int,
2945                (*st).encoding,
2946                (*st).options,
2947            )
2948        };
2949        let c = ctxt as *mut _xmlParserCtxt;
2950        unsafe {
2951            (*st).doc = doc;
2952            (*c).myDoc = doc;
2953            if !doc.is_null() {
2954                (*c).wellFormed = 1;
2955            }
2956            // The accumulated input is no longer needed.
2957            xmlFreeImpl((*st).input as *mut c_void);
2958            (*st).input = ptr::null_mut();
2959            (*st).input_len = 0;
2960        }
2961    }
2962    XML_ERR_OK
2963}
2964
2965/// Parse an HTML document and return the resulting document tree.
2966///
2967/// # UPSTREAM-PARITY
2968///
2969/// ```c
2970/// xmlDoc *htmlCtxtParseDocument(htmlParserCtxt *ctxt, xmlParserInput *input);
2971/// ```
2972#[no_mangle]
2973pub unsafe extern "C" fn htmlCtxtParseDocument(
2974    ctxt: *mut c_void,
2975    input: *mut _xmlParserInput,
2976) -> *mut _xmlDoc {
2977    if ctxt.is_null() || input.is_null() {
2978        return ptr::null_mut();
2979    }
2980    let cur = unsafe { (*input).cur };
2981    let end = unsafe { (*input).end };
2982    if cur.is_null() {
2983        return ptr::null_mut();
2984    }
2985    let len = (end as usize).wrapping_sub(cur as usize) as c_int;
2986    if len <= 0 {
2987        return ptr::null_mut();
2988    }
2989    let st = unsafe { html_state(ctxt) };
2990    let doc =
2991        unsafe { html::parse_memory_enc(cur as *const c_char, len, (*st).encoding, (*st).options) };
2992    let c = ctxt as *mut _xmlParserCtxt;
2993    unsafe {
2994        (*st).doc = doc;
2995        (*c).myDoc = doc;
2996        if !doc.is_null() {
2997            (*c).wellFormed = 1;
2998        }
2999    }
3000    doc
3001}
3002
3003// ═══════════════════════════════════════════════════════════════════════════════
3004// Convenience read APIs (htmlCtxtRead* / htmlRead* / htmlSAXParse*)
3005// ═══════════════════════════════════════════════════════════════════════════════
3006
3007/// Shared tail of the `htmlCtxtRead*` family: stash the parsed document in
3008/// the context and attach the URL.
3009unsafe fn html_ctxt_finish_read(
3010    ctxt: *mut c_void,
3011    doc: *mut _xmlDoc,
3012    url: *const c_char,
3013) -> *mut _xmlDoc {
3014    if ctxt.is_null() {
3015        return doc;
3016    }
3017    let st = unsafe { html_state(ctxt) };
3018    let c = ctxt as *mut _xmlParserCtxt;
3019    unsafe {
3020        (*st).doc = doc;
3021        (*c).myDoc = doc;
3022        if !doc.is_null() {
3023            (*c).wellFormed = 1;
3024        }
3025        if !doc.is_null() && !url.is_null() {
3026            (*doc).URL = c_strdup(url) as *mut xmlChar;
3027        }
3028    }
3029    doc
3030}
3031
3032/// Parse an HTML in-memory document and build a tree.
3033///
3034/// # UPSTREAM-PARITY
3035///
3036/// ```c
3037/// xmlDoc *htmlCtxtReadMemory(xmlParserCtxt *ctxt, const char *buffer, int size,
3038///                            const char *URL, const char *encoding, int options);
3039/// ```
3040#[no_mangle]
3041pub unsafe extern "C" fn htmlCtxtReadMemory(
3042    ctxt: *mut c_void,
3043    buffer: *const c_char,
3044    size: c_int,
3045    URL: *const c_char,
3046    encoding: *const c_char,
3047    options: c_int,
3048) -> *mut _xmlDoc {
3049    if ctxt.is_null() || size < 0 {
3050        return ptr::null_mut();
3051    }
3052    unsafe { htmlCtxtReset(ctxt) };
3053    unsafe { htmlCtxtUseOptions(ctxt, options) };
3054    let st = unsafe { html_state(ctxt) };
3055    let doc = unsafe { html::parse_memory_enc(buffer, size, encoding, (*st).options) };
3056    unsafe { html_ctxt_finish_read(ctxt, doc, URL) }
3057}
3058
3059/// Parse an HTML in-memory document and build a tree.
3060///
3061/// # UPSTREAM-PARITY
3062///
3063/// ```c
3064/// xmlDoc *htmlCtxtReadDoc(xmlParserCtxt *ctxt, const xmlChar *str,
3065///                         const char *URL, const char *encoding, int options);
3066/// ```
3067#[no_mangle]
3068pub unsafe extern "C" fn htmlCtxtReadDoc(
3069    ctxt: *mut c_void,
3070    str: *const xmlChar,
3071    URL: *const c_char,
3072    encoding: *const c_char,
3073    options: c_int,
3074) -> *mut _xmlDoc {
3075    if ctxt.is_null() {
3076        return ptr::null_mut();
3077    }
3078    unsafe { htmlCtxtReset(ctxt) };
3079    unsafe { htmlCtxtUseOptions(ctxt, options) };
3080    let st = unsafe { html_state(ctxt) };
3081    let doc = unsafe { html::parse_doc(str, encoding, (*st).options) };
3082    unsafe { html_ctxt_finish_read(ctxt, doc, URL) }
3083}
3084
3085/// Parse an HTML file from the filesystem, the network or a user-defined
3086/// resource loader and build a tree.
3087///
3088/// # UPSTREAM-PARITY
3089///
3090/// ```c
3091/// xmlDoc *htmlCtxtReadFile(xmlParserCtxt *ctxt, const char *filename,
3092///                          const char *encoding, int options);
3093/// ```
3094#[no_mangle]
3095pub unsafe extern "C" fn htmlCtxtReadFile(
3096    ctxt: *mut c_void,
3097    filename: *const c_char,
3098    encoding: *const c_char,
3099    options: c_int,
3100) -> *mut _xmlDoc {
3101    if ctxt.is_null() {
3102        return ptr::null_mut();
3103    }
3104    unsafe { htmlCtxtReset(ctxt) };
3105    unsafe { htmlCtxtUseOptions(ctxt, options) };
3106    let st = unsafe { html_state(ctxt) };
3107    let doc = unsafe { html::parse_file(filename, encoding, (*st).options) };
3108    unsafe { html_ctxt_finish_read(ctxt, doc, filename) }
3109}
3110
3111/// Read all data from an open file descriptor.
3112unsafe fn html_read_fd(fd: c_int) -> Vec<u8> {
3113    let mut buf = Vec::new();
3114    let mut tmp = [0u8; 4096];
3115    loop {
3116        let n = libc::read(fd, tmp.as_mut_ptr() as *mut c_void, tmp.len());
3117        if n <= 0 {
3118            break;
3119        }
3120        buf.extend_from_slice(&tmp[..n as usize]);
3121    }
3122    buf
3123}
3124
3125/// Read all data through an input callback.
3126unsafe fn html_read_io(ioread: Option<xmlInputReadCallback>, ioctx: *mut c_void) -> Vec<u8> {
3127    let mut buf = Vec::new();
3128    let mut tmp = [0u8; 4096];
3129    if let Some(read) = ioread {
3130        loop {
3131            let n = unsafe { read(ioctx, tmp.as_mut_ptr() as *mut c_char, tmp.len() as c_int) };
3132            if n <= 0 {
3133                break;
3134            }
3135            buf.extend_from_slice(&tmp[..n as usize]);
3136        }
3137    }
3138    buf
3139}
3140
3141/// Parse an HTML document from a file descriptor and build a tree.
3142///
3143/// # UPSTREAM-PARITY
3144///
3145/// ```c
3146/// xmlDoc *htmlCtxtReadFd(xmlParserCtxt *ctxt, int fd,
3147///                        const char *URL, const char *encoding, int options);
3148/// ```
3149#[no_mangle]
3150pub unsafe extern "C" fn htmlCtxtReadFd(
3151    ctxt: *mut c_void,
3152    fd: c_int,
3153    URL: *const c_char,
3154    encoding: *const c_char,
3155    options: c_int,
3156) -> *mut _xmlDoc {
3157    if ctxt.is_null() {
3158        return ptr::null_mut();
3159    }
3160    unsafe { htmlCtxtReset(ctxt) };
3161    unsafe { htmlCtxtUseOptions(ctxt, options) };
3162    let data = unsafe { html_read_fd(fd) };
3163    let st = unsafe { html_state(ctxt) };
3164    let doc = unsafe {
3165        html::parse_memory_enc(
3166            data.as_ptr() as *const c_char,
3167            data.len() as c_int,
3168            encoding,
3169            (*st).options,
3170        )
3171    };
3172    unsafe { html_ctxt_finish_read(ctxt, doc, URL) }
3173}
3174
3175/// Parse an HTML document from I/O functions and source and build a tree.
3176///
3177/// # UPSTREAM-PARITY
3178///
3179/// ```c
3180/// xmlDoc *htmlCtxtReadIO(xmlParserCtxt *ctxt, xmlInputReadCallback ioread,
3181///                        xmlInputCloseCallback ioclose, void *ioctx,
3182///                        const char *URL, const char *encoding, int options);
3183/// ```
3184#[no_mangle]
3185pub unsafe extern "C" fn htmlCtxtReadIO(
3186    ctxt: *mut c_void,
3187    ioread: Option<xmlInputReadCallback>,
3188    _ioclose: Option<xmlInputCloseCallback>,
3189    ioctx: *mut c_void,
3190    URL: *const c_char,
3191    encoding: *const c_char,
3192    options: c_int,
3193) -> *mut _xmlDoc {
3194    if ctxt.is_null() {
3195        return ptr::null_mut();
3196    }
3197    unsafe { htmlCtxtReset(ctxt) };
3198    unsafe { htmlCtxtUseOptions(ctxt, options) };
3199    let data = unsafe { html_read_io(ioread, ioctx) };
3200    let st = unsafe { html_state(ctxt) };
3201    let doc = unsafe {
3202        html::parse_memory_enc(
3203            data.as_ptr() as *const c_char,
3204            data.len() as c_int,
3205            encoding,
3206            (*st).options,
3207        )
3208    };
3209    unsafe { html_ctxt_finish_read(ctxt, doc, URL) }
3210}
3211
3212/// Convenience function to parse an HTML document from memory.
3213///
3214/// # UPSTREAM-PARITY
3215///
3216/// ```c
3217/// xmlDoc *htmlReadMemory(const char *buffer, int size, const char *url,
3218///                        const char *encoding, int options);
3219/// ```
3220#[no_mangle]
3221pub unsafe extern "C" fn htmlReadMemory(
3222    buffer: *const c_char,
3223    size: c_int,
3224    url: *const c_char,
3225    encoding: *const c_char,
3226    options: c_int,
3227) -> *mut _xmlDoc {
3228    if size < 0 {
3229        return ptr::null_mut();
3230    }
3231    let ctxt = unsafe { htmlNewParserCtxt() };
3232    if ctxt.is_null() {
3233        return ptr::null_mut();
3234    }
3235    let doc = unsafe { htmlCtxtReadMemory(ctxt, buffer, size, url, encoding, options) };
3236    unsafe { crate::xml::html::free_parser_ctxt(ctxt) };
3237    doc
3238}
3239
3240/// Convenience function to parse an HTML document from a zero-terminated
3241/// string.
3242///
3243/// # UPSTREAM-PARITY
3244///
3245/// ```c
3246/// xmlDoc *htmlReadDoc(const xmlChar *str, const char *url,
3247///                     const char *encoding, int options);
3248/// ```
3249#[no_mangle]
3250pub unsafe extern "C" fn htmlReadDoc(
3251    str: *const xmlChar,
3252    url: *const c_char,
3253    encoding: *const c_char,
3254    options: c_int,
3255) -> *mut _xmlDoc {
3256    let ctxt = unsafe { htmlNewParserCtxt() };
3257    if ctxt.is_null() {
3258        return ptr::null_mut();
3259    }
3260    let doc = unsafe { htmlCtxtReadDoc(ctxt, str, url, encoding, options) };
3261    unsafe { crate::xml::html::free_parser_ctxt(ctxt) };
3262    doc
3263}
3264
3265/// Convenience function to parse an HTML file from the filesystem, the
3266/// network or a global user-defined resource loader.
3267///
3268/// # UPSTREAM-PARITY
3269///
3270/// ```c
3271/// xmlDoc *htmlReadFile(const char *filename, const char *encoding, int options);
3272/// ```
3273#[no_mangle]
3274pub unsafe extern "C" fn htmlReadFile(
3275    filename: *const c_char,
3276    encoding: *const c_char,
3277    options: c_int,
3278) -> *mut _xmlDoc {
3279    let ctxt = unsafe { htmlNewParserCtxt() };
3280    if ctxt.is_null() {
3281        return ptr::null_mut();
3282    }
3283    let doc = unsafe { htmlCtxtReadFile(ctxt, filename, encoding, options) };
3284    unsafe { crate::xml::html::free_parser_ctxt(ctxt) };
3285    doc
3286}
3287
3288/// Convenience function to parse an HTML document from a file descriptor.
3289///
3290/// # UPSTREAM-PARITY
3291///
3292/// ```c
3293/// xmlDoc *htmlReadFd(int fd, const char *url, const char *encoding, int options);
3294/// ```
3295#[no_mangle]
3296pub unsafe extern "C" fn htmlReadFd(
3297    fd: c_int,
3298    url: *const c_char,
3299    encoding: *const c_char,
3300    options: c_int,
3301) -> *mut _xmlDoc {
3302    let ctxt = unsafe { htmlNewParserCtxt() };
3303    if ctxt.is_null() {
3304        return ptr::null_mut();
3305    }
3306    let doc = unsafe { htmlCtxtReadFd(ctxt, fd, url, encoding, options) };
3307    unsafe { crate::xml::html::free_parser_ctxt(ctxt) };
3308    doc
3309}
3310
3311/// Convenience function to parse an HTML document from I/O functions and
3312/// context.
3313///
3314/// # UPSTREAM-PARITY
3315///
3316/// ```c
3317/// xmlDoc *htmlReadIO(xmlInputReadCallback ioread, xmlInputCloseCallback ioclose,
3318///                    void *ioctx, const char *url, const char *encoding, int options);
3319/// ```
3320#[no_mangle]
3321pub unsafe extern "C" fn htmlReadIO(
3322    ioread: Option<xmlInputReadCallback>,
3323    ioclose: Option<xmlInputCloseCallback>,
3324    ioctx: *mut c_void,
3325    url: *const c_char,
3326    encoding: *const c_char,
3327    options: c_int,
3328) -> *mut _xmlDoc {
3329    let ctxt = unsafe { htmlNewParserCtxt() };
3330    if ctxt.is_null() {
3331        return ptr::null_mut();
3332    }
3333    let doc = unsafe { htmlCtxtReadIO(ctxt, ioread, ioclose, ioctx, url, encoding, options) };
3334    unsafe { crate::xml::html::free_parser_ctxt(ctxt) };
3335    doc
3336}
3337
3338/// Parse an HTML in-memory document. If sax is not NULL, use the SAX
3339/// callbacks to handle parse events; the internal engine is DOM-based, so a
3340/// non-NULL sax is accepted and ignored (documented divergence).
3341///
3342/// # UPSTREAM-PARITY
3343///
3344/// ```c
3345/// xmlDoc *htmlSAXParseDoc(const xmlChar *cur, const char *encoding,
3346///                         htmlSAXHandler *sax, void *userData);
3347/// ```
3348#[no_mangle]
3349pub unsafe extern "C" fn htmlSAXParseDoc(
3350    cur: *const xmlChar,
3351    encoding: *const c_char,
3352    sax: *mut _xmlSAXHandler,
3353    userData: *mut c_void,
3354) -> *mut _xmlDoc {
3355    if cur.is_null() {
3356        return ptr::null_mut();
3357    }
3358    let ctxt = unsafe { htmlNewSAXParserCtxt(sax, userData) };
3359    if ctxt.is_null() {
3360        return ptr::null_mut();
3361    }
3362    let doc = unsafe { htmlCtxtReadDoc(ctxt, cur, ptr::null(), encoding, 0) };
3363    unsafe { crate::xml::html::free_parser_ctxt(ctxt) };
3364    doc
3365}
3366
3367/// Parse an HTML file and build a tree. If sax is not NULL, use the SAX
3368/// callbacks to handle parse events; the internal engine is DOM-based, so a
3369/// non-NULL sax is accepted and ignored (documented divergence).
3370///
3371/// # UPSTREAM-PARITY
3372///
3373/// ```c
3374/// xmlDoc *htmlSAXParseFile(const char *filename, const char *encoding,
3375///                          htmlSAXHandler *sax, void *userData);
3376/// ```
3377#[no_mangle]
3378pub unsafe extern "C" fn htmlSAXParseFile(
3379    filename: *const c_char,
3380    encoding: *const c_char,
3381    sax: *mut _xmlSAXHandler,
3382    userData: *mut c_void,
3383) -> *mut _xmlDoc {
3384    let ctxt = unsafe { htmlNewSAXParserCtxt(sax, userData) };
3385    if ctxt.is_null() {
3386        return ptr::null_mut();
3387    }
3388    let doc = unsafe { htmlCtxtReadFile(ctxt, filename, encoding, 0) };
3389    unsafe { crate::xml::html::free_parser_ctxt(ctxt) };
3390    doc
3391}
3392
3393/// Upstream `htmlParseElement` parses one element from the context's input
3394/// stream. The internal engine exposes whole-document parsing only, so this
3395/// best-effort implementation parses the context's stored input and stashes
3396/// the document (deprecated internal function; no-op when no input is set).
3397///
3398/// # UPSTREAM-PARITY
3399///
3400/// ```c
3401/// void htmlParseElement(htmlParserCtxt *ctxt);
3402/// ```
3403#[no_mangle]
3404pub unsafe extern "C" fn htmlParseElement(ctxt: *mut c_void) {
3405    if ctxt.is_null() {
3406        return;
3407    }
3408    let st = unsafe { html_state(ctxt) };
3409    if unsafe { (*st).input.is_null() } {
3410        return;
3411    }
3412    let doc = unsafe {
3413        html::parse_memory_enc(
3414            (*st).input as *const c_char,
3415            (*st).input_len as c_int,
3416            (*st).encoding,
3417            (*st).options,
3418        )
3419    };
3420    unsafe {
3421        (*st).doc = doc;
3422        (*(ctxt as *mut _xmlParserCtxt)).myDoc = doc;
3423    }
3424}
3425
3426// ═══════════════════════════════════════════════════════════════════════════════
3427// Document creation
3428// ═══════════════════════════════════════════════════════════════════════════════
3429
3430/// Creates a new HTML document without a DTD node if `URI` and `publicId`
3431/// are NULL.
3432///
3433/// # UPSTREAM-PARITY
3434///
3435/// ```c
3436/// xmlDoc *htmlNewDocNoDtD(const xmlChar *URI, const xmlChar *ExternalID);
3437/// ```
3438#[no_mangle]
3439pub unsafe extern "C" fn htmlNewDocNoDtD(
3440    URI: *const xmlChar,
3441    publicId: *const xmlChar,
3442) -> *mut _xmlDoc {
3443    let doc = unsafe { html::new_doc_no_dtd(ptr::null()) };
3444    if doc.is_null() {
3445        return ptr::null_mut();
3446    }
3447    unsafe {
3448        // UPSTREAM-PARITY (HTMLparser.c htmlNewDocNoDtD): standalone=1,
3449        // charset=UTF-8, properties = XML_DOC_HTML | XML_DOC_USERBUILT.
3450        (*doc).standalone = 1;
3451        (*doc).charset = crate::abi::types::xmlCharEncoding::XML_CHAR_ENCODING_UTF8 as c_int;
3452        (*doc).properties = crate::abi::types::xmlDocProperties::XML_DOC_HTML as c_int
3453            | crate::abi::types::xmlDocProperties::XML_DOC_USERBUILT as c_int;
3454        if !publicId.is_null() || !URI.is_null() {
3455            let dtd = crate::xml::dtd::create_int_subset(
3456                doc,
3457                b"html\0" as *const u8 as *const xmlChar,
3458                publicId,
3459                URI,
3460            );
3461            if dtd.is_null() {
3462                tree::free_doc(doc);
3463                return ptr::null_mut();
3464            }
3465        }
3466    }
3467    doc
3468}
3469
3470/// Creates a new HTML document.
3471///
3472/// The document comes from the internal module's `html::new_doc` (the
3473/// crate's oracle-verified HTML module auto-creates the implicit
3474/// html/head/body skeleton, matching the crate's `htmlNewDoc` semantics);
3475/// the internal subset is attached exactly like upstream — the default HTML
3476/// 4.0 Transitional DTD when neither URI nor publicId is given, otherwise a
3477/// DTD with the supplied identifiers.
3478///
3479/// # UPSTREAM-PARITY
3480///
3481/// ```c
3482/// xmlDoc *htmlNewDoc(const xmlChar *URI, const xmlChar *ExternalID);
3483/// ```
3484#[no_mangle]
3485pub unsafe extern "C" fn htmlNewDoc(
3486    URI: *const xmlChar,
3487    ExternalID: *const xmlChar,
3488) -> *mut _xmlDoc {
3489    let doc = unsafe { html::new_doc(ptr::null()) };
3490    if doc.is_null() {
3491        return ptr::null_mut();
3492    }
3493    unsafe {
3494        // UPSTREAM-PARITY (HTMLparser.c htmlNewDocNoDtD): standalone=1,
3495        // charset=UTF-8, properties = XML_DOC_HTML | XML_DOC_USERBUILT.
3496        (*doc).standalone = 1;
3497        (*doc).charset = crate::abi::types::xmlCharEncoding::XML_CHAR_ENCODING_UTF8 as c_int;
3498        (*doc).properties = crate::abi::types::xmlDocProperties::XML_DOC_HTML as c_int
3499            | crate::abi::types::xmlDocProperties::XML_DOC_USERBUILT as c_int;
3500        if URI.is_null() && ExternalID.is_null() {
3501            let dtd = crate::xml::dtd::create_int_subset(
3502                doc,
3503                b"html\0" as *const u8 as *const xmlChar,
3504                b"-//W3C//DTD HTML 4.0 Transitional//EN\0" as *const u8 as *const xmlChar,
3505                b"http://www.w3.org/TR/REC-html40/loose.dtd\0" as *const u8 as *const xmlChar,
3506            );
3507            if dtd.is_null() {
3508                tree::free_doc(doc);
3509                return ptr::null_mut();
3510            }
3511        } else if !ExternalID.is_null() || !URI.is_null() {
3512            let dtd = crate::xml::dtd::create_int_subset(
3513                doc,
3514                b"html\0" as *const u8 as *const xmlChar,
3515                ExternalID,
3516                URI,
3517            );
3518            if dtd.is_null() {
3519                tree::free_doc(doc);
3520                return ptr::null_mut();
3521            }
3522        }
3523    }
3524    doc
3525}
3526
3527// ═══════════════════════════════════════════════════════════════════════════════
3528// Meta encoding (HTMLtree.c)
3529// ═══════════════════════════════════════════════════════════════════════════════
3530
3531/// Find the first child of `node` whose name matches (case-insensitive).
3532unsafe fn html_find_first_child(node: *mut _xmlNode, name: &[u8]) -> *mut _xmlNode {
3533    let mut c = unsafe { (*node).children };
3534    while !c.is_null() {
3535        let n = unsafe { &*c };
3536        if n.type_ == XML_ELEMENT_NODE as c_int
3537            && !n.name.is_null()
3538            && unsafe { xmlstr_to_bytes(n.name) }.eq_ignore_ascii_case(name)
3539        {
3540            return c;
3541        }
3542        c = unsafe { (*c).next };
3543    }
3544    ptr::null_mut()
3545}
3546
3547/// Locate the `<head>` element (upstream `htmlFindHead`): first `html` child
3548/// of the document, then first `head` child.
3549unsafe fn html_find_head(doc: *mut _xmlDoc) -> *mut _xmlNode {
3550    if doc.is_null() {
3551        return ptr::null_mut();
3552    }
3553    let html = unsafe { html_find_first_child(doc as *mut _xmlNode, b"html") };
3554    if html.is_null() {
3555        return ptr::null_mut();
3556    }
3557    unsafe { html_find_first_child(html, b"head") }
3558}
3559
3560/// Find the encoding-declaring attribute of a `meta` element
3561/// (upstream `htmlFindMetaEncodingAttr`). Returns `(attr, is_content_type)`.
3562unsafe fn html_find_meta_encoding_attr(elem: *mut _xmlNode) -> (*mut _xmlAttr, bool) {
3563    let n = unsafe { &*elem };
3564    if n.type_ != XML_ELEMENT_NODE as c_int || n.name.is_null() {
3565        return (ptr::null_mut(), false);
3566    }
3567    if !unsafe { xmlstr_to_bytes(n.name) }.eq_ignore_ascii_case(b"meta") {
3568        return (ptr::null_mut(), false);
3569    }
3570
3571    let mut content_attr: *mut _xmlAttr = ptr::null_mut();
3572    let mut is_content_type = false;
3573    let mut attr = n.properties;
3574    while !attr.is_null() {
3575        let a = unsafe { &*attr };
3576        if a.ns.is_null() && !a.name.is_null() {
3577            let nm = unsafe { xmlstr_to_bytes(a.name) };
3578            if nm.eq_ignore_ascii_case(b"charset") {
3579                return (attr, false);
3580            }
3581            if nm.eq_ignore_ascii_case(b"content") {
3582                content_attr = attr;
3583            }
3584            if nm.eq_ignore_ascii_case(b"http-equiv")
3585                && !a.children.is_null()
3586                && unsafe { (*(a.children)).type_ } == XML_TEXT_NODE as c_int
3587                && unsafe { (*(a.children)).next }.is_null()
3588                && !unsafe { (*(a.children)).content }.is_null()
3589                && unsafe { xmlstr_to_bytes((*(a.children)).content) }
3590                    .eq_ignore_ascii_case(b"Content-Type")
3591            {
3592                is_content_type = true;
3593            }
3594        }
3595        attr = unsafe { (*attr).next };
3596    }
3597    if is_content_type && !content_attr.is_null() {
3598        (content_attr, true)
3599    } else {
3600        (ptr::null_mut(), false)
3601    }
3602}
3603
3604/// Parse `charset=` out of a `content` attribute value (upstream
3605/// `htmlParseContentType`). Returns `(start, end, size)` offsets.
3606unsafe fn html_parse_content_type(val: *const xmlChar) -> Option<(usize, usize, usize)> {
3607    let bytes = unsafe { xmlstr_to_bytes(val) };
3608    let n = bytes.len();
3609    let at = |i: usize| -> u8 {
3610        if i < n {
3611            bytes[i]
3612        } else {
3613            0
3614        }
3615    };
3616
3617    let mut p = 0usize;
3618    loop {
3619        // Find 'c' or 'C'
3620        loop {
3621            let ch = at(p);
3622            if ch == b'c' || ch == b'C' {
3623                break;
3624            }
3625            if ch == 0 {
3626                return None;
3627            }
3628            p += 1;
3629        }
3630        p += 1;
3631
3632        // "harset" must follow (6 bytes, case-insensitive)
3633        let mut ok = true;
3634        for (k, want) in b"harset".iter().enumerate() {
3635            if at(p + k).to_ascii_lowercase() != *want {
3636                ok = false;
3637                break;
3638            }
3639        }
3640        if !ok {
3641            continue;
3642        }
3643        p += 6;
3644        while is_ws_html(at(p)) {
3645            p += 1;
3646        }
3647        if at(p) != b'=' {
3648            continue;
3649        }
3650        p += 1;
3651        while is_ws_html(at(p)) {
3652            p += 1;
3653        }
3654        if at(p) == 0 {
3655            return None;
3656        }
3657
3658        let (start, mut end): (usize, usize);
3659        if at(p) == b'"' || at(p) == b'\'' {
3660            let quote = at(p);
3661            p += 1;
3662            while is_ws_html(at(p)) {
3663                p += 1;
3664            }
3665            start = p;
3666            end = start;
3667            loop {
3668                if at(p) == 0 {
3669                    return None;
3670                }
3671                if !is_ws_html(at(p)) {
3672                    end = p + 1;
3673                }
3674                if at(p) == quote {
3675                    break;
3676                }
3677                p += 1;
3678            }
3679        } else {
3680            start = p;
3681            while at(p) != 0 && at(p) != b';' && !is_ws_html(at(p)) {
3682                p += 1;
3683            }
3684            end = p;
3685        }
3686        let size = n;
3687        return Some((start, end, size));
3688    }
3689}
3690
3691/// Look up an encoding declaration in the meta tags of the document.
3692///
3693/// The returned string points into attribute content (may contain trailing
3694/// garbage); it should be copied before modifying or freeing nodes —
3695/// upstream contract.
3696///
3697/// # UPSTREAM-PARITY
3698///
3699/// ```c
3700/// const xmlChar *htmlGetMetaEncoding(xmlDoc *doc);
3701/// ```
3702#[no_mangle]
3703pub unsafe extern "C" fn htmlGetMetaEncoding(doc: *mut _xmlDoc) -> *const xmlChar {
3704    let head = unsafe { html_find_head(doc) };
3705    if head.is_null() {
3706        return ptr::null();
3707    }
3708    let mut node = unsafe { (*head).children };
3709    while !node.is_null() {
3710        let (attr, is_content_type) = unsafe { html_find_meta_encoding_attr(node) };
3711        if !attr.is_null() {
3712            let a = unsafe { &*attr };
3713            let val = if !a.children.is_null()
3714                && unsafe { (*(a.children)).type_ } == XML_TEXT_NODE as c_int
3715                && unsafe { (*(a.children)).next }.is_null()
3716                && !unsafe { (*(a.children)).content }.is_null()
3717            {
3718                unsafe { (*(a.children)).content }
3719            } else {
3720                b"\0" as *const u8 as *const xmlChar
3721            };
3722            if !is_content_type {
3723                let bytes = unsafe { xmlstr_to_bytes(val) };
3724                let mut start = 0usize;
3725                while start < bytes.len() && is_ws_html(bytes[start]) {
3726                    start += 1;
3727                }
3728                return unsafe { val.add(start) };
3729            } else if let Some((start, _, _)) = unsafe { html_parse_content_type(val) } {
3730                return unsafe { val.add(start) };
3731            }
3732        }
3733        node = unsafe { (*node).next };
3734    }
3735    ptr::null()
3736}
3737
3738/// Build the updated charset value for an existing meta tag
3739/// (upstream `htmlUpdateMetaEncoding`).
3740unsafe fn html_update_meta_encoding(
3741    attr_value: *const xmlChar,
3742    start: usize,
3743    end: usize,
3744    size: usize,
3745    encoding: &[u8],
3746) -> *mut xmlChar {
3747    // The pseudo "HTML" encoding only produces ASCII.
3748    let enc: &[u8] = if encoding.eq_ignore_ascii_case(b"HTML") {
3749        b"ASCII"
3750    } else {
3751        encoding
3752    };
3753    let bytes = unsafe { xmlstr_to_bytes(attr_value) };
3754    let e = end.min(bytes.len()).min(size);
3755    let s = start.min(e);
3756    let total = size - (e - s) + enc.len();
3757    let new_val = xmlMallocImpl(total + 1) as *mut xmlChar;
3758    if new_val.is_null() {
3759        return ptr::null_mut();
3760    }
3761    unsafe {
3762        let mut p = new_val;
3763        ptr::copy_nonoverlapping(bytes.as_ptr(), p, s);
3764        p = p.add(s);
3765        ptr::copy_nonoverlapping(enc.as_ptr(), p, enc.len());
3766        p = p.add(enc.len());
3767        ptr::copy_nonoverlapping(bytes.as_ptr().add(e), p, size - e);
3768        *new_val.add(total) = 0;
3769    }
3770    new_val
3771}
3772
3773/// Replace the content of an attribute's single text child
3774/// (upstream `xmlNodeSetContent` on the attribute node).
3775unsafe fn html_set_attr_content(attr: *mut _xmlAttr, content: *const xmlChar) -> c_int {
3776    if attr.is_null() {
3777        return -1;
3778    }
3779    unsafe {
3780        if !(*attr).children.is_null() {
3781            tree::free_node_list((*attr).children);
3782            (*attr).children = ptr::null_mut();
3783            (*attr).last = ptr::null_mut();
3784        }
3785        let text = tree::new_text(content);
3786        if text.is_null() {
3787            return -1;
3788        }
3789        (*text).parent = attr as *mut _xmlNode;
3790        (*text).doc = (*attr).doc;
3791        (*attr).children = text;
3792        (*attr).last = text;
3793    }
3794    0
3795}
3796
3797/// Creates or updates a meta tag with an encoding declaration.
3798///
3799/// # UPSTREAM-PARITY
3800///
3801/// ```c
3802/// int htmlSetMetaEncoding(xmlDoc *doc, const xmlChar *encoding);
3803/// ```
3804#[no_mangle]
3805pub unsafe extern "C" fn htmlSetMetaEncoding(doc: *mut _xmlDoc, encoding: *const xmlChar) -> c_int {
3806    if encoding.is_null() {
3807        return 1;
3808    }
3809    let head = unsafe { html_find_head(doc) };
3810    if head.is_null() {
3811        return 1;
3812    }
3813    let enc_bytes = unsafe { xmlstr_to_bytes(encoding) }.to_vec();
3814
3815    let mut found = 0;
3816    let mut meta = unsafe { (*head).children };
3817    while !meta.is_null() {
3818        let (attr, is_content_type) = unsafe { html_find_meta_encoding_attr(meta) };
3819        if !attr.is_null() {
3820            let a = unsafe { &*attr };
3821            let val = if !a.children.is_null()
3822                && unsafe { (*(a.children)).type_ } == XML_TEXT_NODE as c_int
3823                && unsafe { (*(a.children)).next }.is_null()
3824                && !unsafe { (*(a.children)).content }.is_null()
3825            {
3826                unsafe { (*(a.children)).content }
3827            } else {
3828                b"\0" as *const u8 as *const xmlChar
3829            };
3830            found = 1;
3831            let off = if is_content_type {
3832                unsafe { html_parse_content_type(val) }
3833            } else {
3834                let bytes = unsafe { xmlstr_to_bytes(val) };
3835                let mut start = 0usize;
3836                let mut end = bytes.len();
3837                while start < end && is_ws_html(bytes[start]) {
3838                    start += 1;
3839                }
3840                while end > start && is_ws_html(bytes[end - 1]) {
3841                    end -= 1;
3842                }
3843                Some((start, end, bytes.len()))
3844            };
3845            if let Some((start, end, size)) = off {
3846                let new_val =
3847                    unsafe { html_update_meta_encoding(val, start, end, size, &enc_bytes) };
3848                if new_val.is_null() {
3849                    return -1;
3850                }
3851                let ret = unsafe { html_set_attr_content(attr, new_val) };
3852                unsafe { xmlFreeImpl(new_val as *mut c_void) };
3853                if ret < 0 {
3854                    return -1;
3855                }
3856            } else {
3857                return -1;
3858            }
3859        }
3860        meta = unsafe { (*meta).next };
3861    }
3862
3863    if found != 0 {
3864        return 0;
3865    }
3866
3867    // No meta found: create one and insert it as the first child of head.
3868    let meta_node =
3869        unsafe { tree::new_node(ptr::null_mut(), b"meta\0" as *const u8 as *const xmlChar) };
3870    if meta_node.is_null() {
3871        return -1;
3872    }
3873    unsafe {
3874        (*meta_node).doc = (*head).doc;
3875    }
3876    let prop = unsafe {
3877        tree::set_prop(
3878            meta_node,
3879            b"charset\0" as *const u8 as *const xmlChar,
3880            encoding,
3881        )
3882    };
3883    if prop.is_null() {
3884        unsafe { tree::free_node(meta_node) };
3885        return -1;
3886    }
3887    if unsafe { (*head).children }.is_null() {
3888        unsafe { tree::add_child(head, meta_node) };
3889    } else {
3890        unsafe { tree::add_sibling_before((*head).children, meta_node) };
3891    }
3892    0
3893}
3894
3895// ═══════════════════════════════════════════════════════════════════════════════
3896// Serialization (HTMLtree.c)
3897// ═══════════════════════════════════════════════════════════════════════════════
3898
3899/// Serialize `node` into a fresh `_xmlBuffer` via the internal HTML
3900/// serializer. Returns the buffer (caller frees with `io::buf_free`) or NULL.
3901unsafe fn html_serialize_to_buffer(node: *mut _xmlNode, format: c_int) -> *mut _xmlBuffer {
3902    let buf = io::buf_create(0);
3903    if buf.is_null() {
3904        return ptr::null_mut();
3905    }
3906    unsafe { html::serialize_node(node, buf, format, 0) };
3907    buf
3908}
3909
3910/// Whether an HTML save must serialize through the pseudo "HTML" output
3911/// encoding — upstream `htmlFindOutputEncoder` (HTMLtree.c) resolves the
3912/// encoding name for the convenience dumps, and a NULL name falls back to
3913/// "HTML". When "HTML" is in force the dumped UTF-8 is converted to ASCII
3914/// with HTML 4 named/numeric character references (`htmlUTF8ToHtml`,
3915/// HTMLparser.c). A declared non-NULL encoding gets a real output converter
3916/// upstream; the candidate has no output converters yet (Workstream 9 /
3917/// R-000157), so those names keep the raw UTF-8 pass-through.
3918///
3919/// # Safety
3920///
3921/// - `encoding` must be NULL or a valid NUL-terminated C string.
3922const unsafe fn html_pseudo_encoding_in_force(encoding: *const c_char) -> bool {
3923    if encoding.is_null() {
3924        return true;
3925    }
3926    let b = unsafe { std::ffi::CStr::from_ptr(encoding) }.to_bytes();
3927    b.eq_ignore_ascii_case(b"HTML")
3928}
3929
3930/// Append the ASCII + HTML-entity form of a UTF-8 dump to `out` — upstream
3931/// `htmlUTF8ToHtml` byte semantics: ASCII passes through unchanged; a
3932/// multi-byte sequence is looked up in the HTML 4.0 value table and written
3933/// as `&name;`, or as a decimal reference `&#N;` when it has no name. An
3934/// incomplete sequence at the very end of the dump is dropped, exactly like
3935/// the upstream stream converter when it never receives another chunk.
3936///
3937/// # Safety
3938///
3939/// - `out` must be a valid writable `_xmlBuffer`; `content` must be readable
3940///   for `len` bytes.
3941unsafe fn html_buf_append_html_ascii(out: *mut _xmlBuffer, content: *const xmlChar, len: usize) {
3942    let mut i = 0usize;
3943    while i < len {
3944        let d = unsafe { *content.add(i) };
3945        if d < 0x80 {
3946            // Copy the whole ASCII run in one write.
3947            let start = i;
3948            i += 1;
3949            while i < len && unsafe { *content.add(i) } < 0x80 {
3950                i += 1;
3951            }
3952            io::buf_add(out, content.add(start), (i - start) as c_int);
3953            continue;
3954        }
3955        let (mut c, seqlen) = if d < 0xE0 {
3956            ((d & 0x1F) as c_uint, 2usize)
3957        } else if d < 0xF0 {
3958            ((d & 0x0F) as c_uint, 3usize)
3959        } else {
3960            ((d & 0x07) as c_uint, 4usize)
3961        };
3962        if len - i < seqlen {
3963            // Truncated trailing sequence: dropped, matching the stream
3964            // converter (no further chunk ever arrives).
3965            break;
3966        }
3967        for k in 1..seqlen {
3968            let dd = unsafe { *content.add(i + k) };
3969            c = (c << 6) | ((dd & 0x3F) as c_uint);
3970        }
3971        i += seqlen;
3972        let ent = unsafe { html_entity_value_lookup_static(c) };
3973        if ent.is_null() {
3974            // Decimal reference: htmlUTF8ToHtml's snprintf(nbuf, "#%u", c).
3975            io::buf_ccat(out, b'&');
3976            io::buf_ccat(out, b'#');
3977            let mut digits = [0u8; 10];
3978            let mut n = 0usize;
3979            let mut v = c;
3980            if v == 0 {
3981                digits[0] = b'0';
3982                n = 1;
3983            }
3984            while v > 0 {
3985                digits[n] = b'0' + (v % 10) as u8;
3986                n += 1;
3987                v /= 10;
3988            }
3989            while n > 0 {
3990                n -= 1;
3991                io::buf_ccat(out, digits[n]);
3992            }
3993            io::buf_ccat(out, b';');
3994        } else {
3995            io::buf_ccat(out, b'&');
3996            io::buf_cat(out, (*ent).name as *const xmlChar);
3997            io::buf_ccat(out, b';');
3998        }
3999    }
4000}
4001
4002/// Return a buffer holding the final bytes of an HTML dump: when the pseudo
4003/// "HTML" output encoding is in force for `encoding`, a fresh buffer with the
4004/// ASCII + HTML-entity form of `buf`'s content (the original is freed);
4005/// otherwise `buf` itself. The caller frees the returned buffer in both
4006/// cases.
4007///
4008/// # Safety
4009///
4010/// - `buf` must be NULL or a valid `_xmlBuffer`; `encoding` must be NULL or a
4011///   valid NUL-terminated C string.
4012unsafe fn html_buf_apply_pseudo_encoding(
4013    buf: *mut _xmlBuffer,
4014    encoding: *const c_char,
4015) -> *mut _xmlBuffer {
4016    if buf.is_null() || !unsafe { html_pseudo_encoding_in_force(encoding) } {
4017        return buf;
4018    }
4019    let len = io::buf_length(buf);
4020    if len <= 0 {
4021        return buf;
4022    }
4023    let content = io::buf_content(buf);
4024    let conv = io::buf_create(0);
4025    if conv.is_null() {
4026        return buf;
4027    }
4028    unsafe { html_buf_append_html_ascii(conv, content, len as usize) };
4029    io::buf_free(buf);
4030    conv
4031}
4032
4033/// Serialize `node` into an `_xmlOutputBuffer` (writes the serialized bytes
4034/// through the output buffer's I/O channel).
4035unsafe fn html_serialize_to_obuf(obuf: *mut _xmlOutputBuffer, node: *mut _xmlNode, format: c_int) {
4036    unsafe { html_serialize_to_obuf_enc(obuf, node, format, None) }
4037}
4038
4039/// Serialize `node` into an `_xmlOutputBuffer` with an optional output
4040/// encoding (upstream `htmlNodeDumpInternal`'s `encoding` parameter, which
4041/// gates the `<meta charset>` insertion).
4042unsafe fn html_serialize_to_obuf_enc(
4043    obuf: *mut _xmlOutputBuffer,
4044    node: *mut _xmlNode,
4045    format: c_int,
4046    encoding: Option<&[u8]>,
4047) {
4048    if obuf.is_null() || node.is_null() {
4049        return;
4050    }
4051    let buf = if encoding.is_some() {
4052        let b = io::buf_create(0);
4053        if b.is_null() {
4054            return;
4055        }
4056        unsafe { html::serialize_node_enc(node, b, format, 0, encoding) };
4057        b
4058    } else {
4059        unsafe { html_serialize_to_buffer(node, format) }
4060    };
4061    if buf.is_null() {
4062        return;
4063    }
4064    let len = io::buf_length(buf);
4065    if len > 0 {
4066        let content = io::buf_content(buf);
4067        unsafe {
4068            io::output_buffer_write(obuf, len, content as *const c_char);
4069        }
4070    }
4071    io::buf_free(buf);
4072}
4073
4074/// Serialize an HTML node to an xmlBuffer. Always uses UTF-8.
4075///
4076/// # UPSTREAM-PARITY
4077///
4078/// ```c
4079/// int htmlNodeDump(xmlBuffer *buf, xmlDoc *doc, xmlNode *cur);
4080/// ```
4081#[no_mangle]
4082pub unsafe extern "C" fn htmlNodeDump(
4083    buf: *mut _xmlBuffer,
4084    _doc: *mut _xmlDoc,
4085    cur: *mut _xmlNode,
4086) -> c_int {
4087    if buf.is_null() || cur.is_null() {
4088        return -1;
4089    }
4090    let before = io::buf_length(buf);
4091    unsafe { html::serialize_node(cur, buf, 1, 0) };
4092    let after = io::buf_length(buf);
4093    if after < 0 || before < 0 {
4094        return -1;
4095    }
4096    after - before
4097}
4098
4099/// Serialize an HTML node to a FILE.
4100///
4101/// # UPSTREAM-PARITY
4102///
4103/// ```c
4104/// void htmlNodeDumpFile(FILE *out, xmlDoc *doc, xmlNode *cur);
4105/// ```
4106#[no_mangle]
4107pub unsafe extern "C" fn htmlNodeDumpFile(out: *mut c_void, doc: *mut _xmlDoc, cur: *mut _xmlNode) {
4108    unsafe { htmlNodeDumpFileFormat(out, doc, cur, ptr::null(), 1) };
4109}
4110
4111/// Serialize an HTML node to a FILE with encoding and format.
4112///
4113/// # UPSTREAM-PARITY
4114///
4115/// ```c
4116/// int htmlNodeDumpFileFormat(FILE *out, xmlDoc *doc, xmlNode *cur,
4117///                            const char *encoding, int format);
4118/// ```
4119#[no_mangle]
4120pub unsafe extern "C" fn htmlNodeDumpFileFormat(
4121    out: *mut c_void,
4122    _doc: *mut _xmlDoc,
4123    cur: *mut _xmlNode,
4124    _encoding: *const c_char,
4125    format: c_int,
4126) -> c_int {
4127    let obuf = io::output_buffer_create_file(out as *mut libc::FILE, ptr::null_mut());
4128    if obuf.is_null() {
4129        return -1;
4130    }
4131    unsafe { html_serialize_to_obuf(obuf, cur, format) };
4132    io::output_buffer_close(obuf)
4133}
4134
4135/// Serialize an HTML node to an output buffer.
4136///
4137/// # UPSTREAM-PARITY
4138///
4139/// ```c
4140/// void htmlNodeDumpOutput(xmlOutputBuffer *buf, xmlDoc *doc, xmlNode *cur,
4141///                         const char *encoding);
4142/// ```
4143#[no_mangle]
4144pub unsafe extern "C" fn htmlNodeDumpOutput(
4145    buf: *mut _xmlOutputBuffer,
4146    _doc: *mut _xmlDoc,
4147    cur: *mut _xmlNode,
4148    _encoding: *const c_char,
4149) {
4150    unsafe { html_serialize_to_obuf(buf, cur, 1) };
4151}
4152
4153/// Serialize an HTML node to an output buffer with format.
4154///
4155/// # UPSTREAM-PARITY
4156///
4157/// ```c
4158/// void htmlNodeDumpFormatOutput(xmlOutputBuffer *buf, xmlDoc *doc, xmlNode *cur,
4159///                               const char *encoding, int format);
4160/// ```
4161#[no_mangle]
4162pub unsafe extern "C" fn htmlNodeDumpFormatOutput(
4163    buf: *mut _xmlOutputBuffer,
4164    _doc: *mut _xmlDoc,
4165    cur: *mut _xmlNode,
4166    _encoding: *const c_char,
4167    format: c_int,
4168) {
4169    unsafe { html_serialize_to_obuf(buf, cur, format) };
4170}
4171
4172/// Serialize an HTML document to an output buffer.
4173///
4174/// # UPSTREAM-PARITY
4175///
4176/// ```c
4177/// void htmlDocContentDumpOutput(xmlOutputBuffer *buf, xmlDoc *cur,
4178///                               const char *encoding);
4179/// ```
4180#[no_mangle]
4181pub unsafe extern "C" fn htmlDocContentDumpOutput(
4182    buf: *mut _xmlOutputBuffer,
4183    cur: *mut _xmlDoc,
4184    _encoding: *const c_char,
4185) {
4186    unsafe { html_serialize_to_obuf(buf, cur as *mut _xmlNode, 1) };
4187}
4188
4189/// Serialize an HTML document to an output buffer with format.
4190///
4191/// # UPSTREAM-PARITY
4192///
4193/// ```c
4194/// void htmlDocContentDumpFormatOutput(xmlOutputBuffer *buf, xmlDoc *cur,
4195///                                     const char *encoding, int format);
4196/// ```
4197#[no_mangle]
4198pub unsafe extern "C" fn htmlDocContentDumpFormatOutput(
4199    buf: *mut _xmlOutputBuffer,
4200    cur: *mut _xmlDoc,
4201    _encoding: *const c_char,
4202    format: c_int,
4203) {
4204    unsafe { html_serialize_to_obuf(buf, cur as *mut _xmlNode, format) };
4205}
4206
4207/// Serialize an HTML document to memory, also returning the size of the
4208/// result. The caller frees `mem` with `xmlFree`. The output is UTF-8
4209/// (upstream converts to the document encoding).
4210///
4211/// # UPSTREAM-PARITY
4212///
4213/// ```c
4214/// void htmlDocDumpMemoryFormat(xmlDoc *cur, xmlChar **mem, int *size, int format);
4215/// ```
4216#[no_mangle]
4217pub unsafe extern "C" fn htmlDocDumpMemoryFormat(
4218    cur: *mut _xmlDoc,
4219    mem: *mut *mut xmlChar,
4220    size: *mut c_int,
4221    format: c_int,
4222) {
4223    if mem.is_null() || size.is_null() {
4224        return;
4225    }
4226    unsafe {
4227        *mem = ptr::null_mut();
4228        *size = 0;
4229    }
4230    if cur.is_null() {
4231        return;
4232    }
4233    let buf = unsafe { html_serialize_to_buffer(cur as *mut _xmlNode, format) };
4234    if buf.is_null() {
4235        return;
4236    }
4237    // UPSTREAM-PARITY (HTMLtree.c htmlDocDumpMemoryFormat): the output
4238    // buffer is created with the encoding resolved from the document — a
4239    // NULL doc->encoding falls back to the pseudo "HTML" handler whose
4240    // converter (htmlUTF8ToHtml) re-emits every non-ASCII character as an
4241    // HTML 4 named entity (&nbsp;, &eacute;) or a decimal reference. This is
4242    // the step that re-prints `&nbsp;` in DOMDocument::saveHTML() for docs
4243    // loaded without a declared encoding (ext/dom dom005).
4244    let buf = unsafe { html_buf_apply_pseudo_encoding(buf, (*cur).encoding as *const c_char) };
4245    if buf.is_null() {
4246        return;
4247    }
4248    let len = io::buf_length(buf);
4249    if len > 0 {
4250        let content = io::buf_content(buf);
4251        unsafe {
4252            *mem = xml_strndup(content, len as usize);
4253            if !(*mem).is_null() {
4254                *size = len;
4255            }
4256        }
4257    }
4258    io::buf_free(buf);
4259}
4260
4261/// Same as `htmlDocDumpMemoryFormat` with `format` set to 1.
4262///
4263/// # UPSTREAM-PARITY
4264///
4265/// ```c
4266/// void htmlDocDumpMemory(xmlDoc *cur, xmlChar **mem, int *size);
4267/// ```
4268#[no_mangle]
4269pub unsafe extern "C" fn htmlDocDumpMemory(
4270    cur: *mut _xmlDoc,
4271    mem: *mut *mut xmlChar,
4272    size: *mut c_int,
4273) {
4274    unsafe { htmlDocDumpMemoryFormat(cur, mem, size, 1) };
4275}
4276
4277/// Serialize an HTML document to an open FILE.
4278///
4279/// # UPSTREAM-PARITY
4280///
4281/// ```c
4282/// int htmlDocDump(FILE *f, xmlDoc *cur);
4283/// ```
4284#[no_mangle]
4285pub unsafe extern "C" fn htmlDocDump(f: *mut c_void, cur: *mut _xmlDoc) -> c_int {
4286    if f.is_null() || cur.is_null() {
4287        return -1;
4288    }
4289    let obuf = io::output_buffer_create_file(f as *mut libc::FILE, ptr::null_mut());
4290    if obuf.is_null() {
4291        return -1;
4292    }
4293    unsafe { html_serialize_to_obuf(obuf, cur as *mut _xmlNode, 1) };
4294    io::output_buffer_close(obuf)
4295}
4296
4297/// Serialize an HTML document to a file using a given encoding and format.
4298///
4299/// # UPSTREAM-PARITY
4300///
4301/// ```c
4302/// int htmlSaveFileFormat(const char *filename, xmlDoc *cur,
4303///                        const char *encoding, int format);
4304/// ```
4305#[no_mangle]
4306pub unsafe extern "C" fn htmlSaveFileFormat(
4307    filename: *const c_char,
4308    cur: *mut _xmlDoc,
4309    encoding: *const c_char,
4310    format: c_int,
4311) -> c_int {
4312    if cur.is_null() || filename.is_null() {
4313        return -1;
4314    }
4315    // UPSTREAM-PARITY (HTMLtree.c htmlSaveFileFormat): the caller's encoding
4316    // string is passed through to htmlNodeDumpInternal, which inserts a
4317    // <meta charset> when it is non-NULL. The file open funnels through the
4318    // registered default create-filename callback when present (php streams).
4319    let enc: Option<&[u8]> = if encoding.is_null() {
4320        None
4321    } else {
4322        let s = std::ffi::CStr::from_ptr(encoding);
4323        Some(s.to_bytes())
4324    };
4325    let obuf = io::output_buffer_create_filename_routed(filename, ptr::null_mut(), 0);
4326    if obuf.is_null() {
4327        // UPSTREAM-PARITY: a failed output buffer yields 0, not -1.
4328        return 0;
4329    }
4330    // UPSTREAM-PARITY (HTMLtree.c htmlSaveFileFormat): the output buffer is
4331    // created with htmlFindOutputEncoder(caller-encoding) — NULL (php
4332    // saveHTMLFile passes htmlGetMetaEncoding, NULL for meta-less docs)
4333    // falls back to the pseudo "HTML" encoder (ASCII + HTML entities).
4334    let buf = if enc.is_some() {
4335        let b = io::buf_create(0);
4336        if b.is_null() {
4337            io::output_buffer_close(obuf);
4338            return 0;
4339        }
4340        unsafe { html::serialize_node_enc(cur as *mut _xmlNode, b, format, 0, enc) };
4341        b
4342    } else {
4343        unsafe { html_serialize_to_buffer(cur as *mut _xmlNode, format) }
4344    };
4345    let buf = unsafe { html_buf_apply_pseudo_encoding(buf, encoding) };
4346    if buf.is_null() {
4347        io::output_buffer_close(obuf);
4348        return 0;
4349    }
4350    let len = io::buf_length(buf);
4351    if len > 0 {
4352        let content = io::buf_content(buf);
4353        unsafe {
4354            io::output_buffer_write(obuf, len, content as *const c_char);
4355        }
4356    }
4357    io::buf_free(buf);
4358    io::output_buffer_close(obuf)
4359}
4360
4361/// Same as `htmlSaveFileFormat` with `encoding` set to NULL and `format` set
4362/// to 1.
4363///
4364/// # UPSTREAM-PARITY
4365///
4366/// ```c
4367/// int htmlSaveFile(const char *filename, xmlDoc *cur);
4368/// ```
4369#[no_mangle]
4370pub unsafe extern "C" fn htmlSaveFile(filename: *const c_char, cur: *mut _xmlDoc) -> c_int {
4371    unsafe { htmlSaveFileFormat(filename, cur, ptr::null(), 1) }
4372}
4373
4374/// Same as `htmlSaveFileFormat` with `format` set to 1.
4375///
4376/// # UPSTREAM-PARITY
4377///
4378/// ```c
4379/// int htmlSaveFileEnc(const char *filename, xmlDoc *cur, const char *encoding);
4380/// ```
4381#[no_mangle]
4382pub unsafe extern "C" fn htmlSaveFileEnc(
4383    filename: *const c_char,
4384    cur: *mut _xmlDoc,
4385    encoding: *const c_char,
4386) -> c_int {
4387    unsafe { htmlSaveFileFormat(filename, cur, encoding, 1) }
4388}
4389
4390/// Set the options on an HTML parser context, clearing unset options
4391/// (upstream HTMLparser.c `htmlCtxtSetOptions` — the 2.14+ variant with
4392/// keepMask 0). Returns the set of unknown/unimplemented options.
4393///
4394/// # SAFETY
4395///
4396/// `ctxt` must be a valid HTML parser context or NULL.
4397#[no_mangle]
4398pub unsafe extern "C" fn htmlCtxtSetOptions(ctxt: *mut c_void, options: c_int) -> c_int {
4399    if ctxt.is_null() {
4400        return -1;
4401    }
4402    let c = ctxt as *mut _xmlParserCtxt;
4403    let st = unsafe { html_state(ctxt) };
4404    unsafe {
4405        (*c).options = options & HTML_OPTIONS_ALL_MASK;
4406        (*st).options = (*c).options;
4407    }
4408    options & !HTML_OPTIONS_ALL_MASK & !crate::abi::types::XML_PARSE_NOENT
4409}
4410
4411/// Take a block of UTF-8 chars and convert it to ASCII + HTML entities
4412/// (upstream HTMLparser.c `htmlUTF8ToHtml`; deprecated internal helper).
4413///
4414/// Returns the number of bytes written or an xmlCharEncError code
4415/// (XML_ENC_ERR_INTERNAL on NULL args, XML_ENC_ERR_SUCCESS for NULL input).
4416///
4417/// # SAFETY
4418///
4419/// `out`/`in` must be valid buffers for `*outlen`/`*inlen` bytes.
4420#[no_mangle]
4421pub unsafe extern "C" fn htmlUTF8ToHtml(
4422    out: *mut u8,
4423    outlen: *mut c_int,
4424    input: *const u8,
4425    inlen: *mut c_int,
4426) -> c_int {
4427    // xmlCharEncError values (encoding.h).
4428    const XML_ENC_ERR_INTERNAL: c_int = -1;
4429    const XML_ENC_ERR_SUCCESS: c_int = 0;
4430    const XML_ENC_ERR_SPACE: c_int = -2;
4431    unsafe {
4432        if out.is_null() || outlen.is_null() || inlen.is_null() {
4433            return XML_ENC_ERR_INTERNAL;
4434        }
4435        if input.is_null() {
4436            *outlen = 0;
4437            *inlen = 0;
4438            return XML_ENC_ERR_SUCCESS;
4439        }
4440        let mut in_pos: usize = 0;
4441        let mut out_pos: usize = 0;
4442        let in_end = *inlen as usize;
4443        let out_cap = *outlen as usize;
4444        let mut ret = XML_ENC_ERR_SPACE;
4445        while in_pos < in_end {
4446            let d = *input.add(in_pos);
4447            if d < 0x80 {
4448                if out_pos >= out_cap {
4449                    break;
4450                }
4451                *out.add(out_pos) = d;
4452                out_pos += 1;
4453                in_pos += 1;
4454                continue;
4455            }
4456            let (mut c, seqlen) = if d < 0xE0 {
4457                ((d & 0x1F) as u32, 2usize)
4458            } else if d < 0xF0 {
4459                ((d & 0x0F) as u32, 3usize)
4460            } else {
4461                ((d & 0x07) as u32, 4usize)
4462            };
4463            if in_end - in_pos < seqlen {
4464                break;
4465            }
4466            for i in 1..seqlen {
4467                let dd = *input.add(in_pos + i);
4468                c = (c << 6) | ((dd & 0x3F) as u32);
4469            }
4470            let ent = htmlEntityValueLookup(c);
4471            let mut nbuf = [0u8; 16];
4472            let cp: *const u8;
4473            let mut owned_len: usize = 0;
4474            if ent.is_null() {
4475                let s = format!("#{}", c);
4476                let bytes = s.as_bytes();
4477                nbuf[..bytes.len()].copy_from_slice(bytes);
4478                cp = nbuf.as_ptr();
4479                owned_len = bytes.len();
4480            } else {
4481                cp = (*ent).name as *const u8;
4482                let mut l = 0;
4483                while *cp.add(l) != 0 {
4484                    l += 1;
4485                }
4486                owned_len = l;
4487            }
4488            let len = owned_len;
4489            if out_cap - out_pos < len + 2 {
4490                break;
4491            }
4492            *out.add(out_pos) = b'&';
4493            out_pos += 1;
4494            core::ptr::copy_nonoverlapping(cp, out.add(out_pos), len);
4495            out_pos += len;
4496            *out.add(out_pos) = b';';
4497            out_pos += 1;
4498            in_pos += seqlen;
4499        }
4500        ret = out_pos as c_int;
4501        *outlen = out_pos as c_int;
4502        *inlen = in_pos as c_int;
4503        ret
4504    }
4505}
4506
4507#[cfg(test)]
4508mod tests {
4509    use super::*;
4510    use crate::xml::string::{xml_strdup, xml_strlen};
4511
4512    /// Helper: run the pseudo-HTML transcode over `input` bytes and return
4513    /// the resulting ASCII string.
4514    unsafe fn transcode(input: &[u8]) -> String {
4515        let buf = io::buf_create(0);
4516        assert!(!buf.is_null());
4517        unsafe { html_buf_append_html_ascii(buf, input.as_ptr(), input.len()) };
4518        let content = io::buf_content(buf);
4519        let len = xml_strlen(content);
4520        let s = String::from_utf8_lossy(core::slice::from_raw_parts(content, len)).to_string();
4521        io::buf_free(buf);
4522        s
4523    }
4524
4525    /// Phase 14.3 (dom S1 / html-save): the html convenience memory dump must
4526    /// serialize through the pseudo "HTML" output encoding when the document
4527    /// has no declared encoding — every non-ASCII character is re-emitted as
4528    /// an HTML 4 named entity (`&nbsp;`, `&eacute;`) or a decimal reference
4529    /// (upstream htmlFindOutputEncoder's "HTML" fallback + htmlUTF8ToHtml).
4530    #[test]
4531    fn test_pseudo_html_transcode_semantics() {
4532        unsafe {
4533            // ASCII passes through untouched (the serializer's own
4534            // &lt;/&amp;/&gt; escapes must never be double-escaped).
4535            assert_eq!(transcode(b"a&lt;b&amp;c&gt;d"), "a&lt;b&amp;c&gt;d");
4536            // nbsp (U+00A0) and eacute (U+00E9) map to named entities.
4537            assert_eq!(transcode("a\u{a0}b\u{e9}c".as_bytes()), "a&nbsp;b&eacute;c");
4538            // U+2603 (snowman) has no HTML 4 name: decimal reference.
4539            assert_eq!(transcode("\u{2603}".as_bytes()), "&#9731;");
4540            // A truncated trailing sequence is dropped (stream semantics).
4541            assert_eq!(transcode(b"x\xc2"), "x");
4542        }
4543    }
4544
4545    /// End-to-end: htmlDocDumpMemoryFormat on a meta-less html document (its
4546    /// `encoding` is NULL) re-emits parsed non-ASCII text as HTML 4 named
4547    /// references (`&nbsp;`, `&eacute;`); the same document with a declared
4548    /// UTF-8 encoding keeps raw UTF-8. The source carries raw UTF-8 bytes for
4549    /// the non-ASCII text (the parse-side resolution of `&eacute;`-style
4550    /// references is a separate engine rule; this guard owns the save layer).
4551    #[test]
4552    fn test_html_doc_dump_memory_pseudo_encoding() {
4553        unsafe {
4554            let html = b"<html><body><p>a&nbsp;b&#160;c\xc3\xa9d</p></body></html>\0";
4555            // NOTE: the \xc3\xa9 bytes above are the UTF-8 form of `é`.
4556            let doc = html::parse_memory(html.as_ptr() as *const c_char, (html.len() - 1) as c_int);
4557            assert!(!doc.is_null());
4558            assert!(
4559                (*doc).encoding.is_null(),
4560                "a meta-less html parse must leave doc->encoding NULL"
4561            );
4562
4563            let mut mem: *mut xmlChar = ptr::null_mut();
4564            let mut size: c_int = 0;
4565            htmlDocDumpMemoryFormat(doc, &mut mem, &mut size, 0);
4566            assert!(!mem.is_null());
4567            assert!(size > 0);
4568            let s = String::from_utf8_lossy(core::slice::from_raw_parts(
4569                mem as *const u8,
4570                size as usize,
4571            ))
4572            .to_string();
4573            assert!(s.contains("a&nbsp;b&nbsp;c&eacute;d"), "got: {s}");
4574            assert!(
4575                !s.contains('\u{a0}'),
4576                "no raw U+00A0 may survive the pseudo-HTML dump: got: {s}"
4577            );
4578            xmlFreeImpl(mem as *mut c_void);
4579
4580            // Declared UTF-8 encoding: no converter upstream, raw bytes out.
4581            (*doc).encoding = xml_strdup(b"UTF-8\0" as *const u8 as *const xmlChar);
4582            let mut mem2: *mut xmlChar = ptr::null_mut();
4583            let mut size2: c_int = 0;
4584            htmlDocDumpMemoryFormat(doc, &mut mem2, &mut size2, 0);
4585            assert!(!mem2.is_null());
4586            let s2 = String::from_utf8_lossy(core::slice::from_raw_parts(
4587                mem2 as *const u8,
4588                size2 as usize,
4589            ))
4590            .to_string();
4591            assert!(s2.contains("a\u{a0}b\u{a0}c\u{e9}d"), "got: {s2}");
4592            assert!(!s2.contains("&nbsp;"), "got: {s2}");
4593            xmlFreeImpl(mem2 as *mut c_void);
4594
4595            tree::free_doc(doc);
4596        }
4597    }
4598}