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