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