Skip to main content

libxml_rs/abi/
exports_html.rs

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