Skip to main content

citum_engine/processor/
setup.rs

1/*
2SPDX-License-Identifier: MIT OR Apache-2.0
3SPDX-FileCopyrightText: © 2023-2026 Bruce D'Arcus and Citum contributors
4*/
5
6//! Processor construction and configuration helpers.
7//!
8//! This module owns setup-time concerns for [`Processor`]: constructor paths,
9//! locale/config resolution, compound-set validation, and numeric citation
10//! number initialization. It intentionally does not contain citation or
11//! bibliography rendering logic.
12
13use super::Processor;
14use super::disambiguation::Disambiguator;
15use super::run_state::RunState;
16use crate::error::ProcessorError;
17use crate::reference::{Bibliography, CitationItem, Reference};
18use crate::values::ProcHints;
19use citum_schema::Style;
20use citum_schema::locale::Locale;
21use citum_schema::options::{
22    BibliographyPartitionKind, BibliographyPartitionMode, BibliographySortPartitioning, Config,
23    PunctuationConfig, SortingMultilingualMode, bibliography::BibliographyConfig,
24};
25use indexmap::IndexMap;
26use std::collections::HashMap;
27
28impl Default for Processor {
29    fn default() -> Self {
30        let compound_sets = IndexMap::new();
31        let (compound_set_by_ref, compound_member_index) =
32            Self::build_compound_set_indexes(&compound_sets);
33        Self {
34            style: Style::default(),
35            bibliography: Bibliography::default(),
36            locale: Locale::en_us(),
37            default_config: Config::default(),
38            hints: HashMap::new(),
39            compound_sets,
40            compound_set_by_ref,
41            compound_member_index,
42            show_semantics: true,
43            inject_ast_indices: false,
44            abbreviation_map: None,
45        }
46    }
47}
48
49impl Processor {
50    /// Fill unset style punctuation options from the active locale.
51    fn resolve_punctuation_defaults(&self, config: &mut Config) {
52        let punctuation = config
53            .punctuation
54            .get_or_insert_with(PunctuationConfig::default);
55        punctuation
56            .strong_terminal_comma_policy
57            .get_or_insert(self.locale.grammar_options.strong_terminal_comma_policy);
58        punctuation
59            .delimiter_suppressing_terminal_marks
60            .get_or_insert_with(|| {
61                self.locale
62                    .grammar_options
63                    .delimiter_suppressing_terminal_marks
64                    .clone()
65            });
66    }
67
68    /// Return whether applying locale punctuation defaults would change `config`.
69    fn punctuation_defaults_require_resolution(&self, config: &Config) -> bool {
70        let punctuation = config.punctuation.as_ref();
71        let policy_is_unset = punctuation
72            .and_then(|options| options.strong_terminal_comma_policy)
73            .is_none();
74        let marks_are_unset = punctuation
75            .and_then(|options| options.delimiter_suppressing_terminal_marks.as_ref())
76            .is_none();
77
78        (policy_is_unset
79            && self.locale.grammar_options.strong_terminal_comma_policy
80                != citum_schema::options::StrongTerminalCommaPolicy::default())
81            || (marks_are_unset
82                && self
83                    .locale
84                    .grammar_options
85                    .delimiter_suppressing_terminal_marks
86                    != "?!…")
87    }
88
89    /// Apply locale punctuation defaults only when they change the effective config.
90    fn with_punctuation_defaults<'a>(
91        &self,
92        config: std::borrow::Cow<'a, Config>,
93    ) -> std::borrow::Cow<'a, Config> {
94        if !self.punctuation_defaults_require_resolution(&config) {
95            return config;
96        }
97
98        let mut config = config.into_owned();
99        self.resolve_punctuation_defaults(&mut config);
100        std::borrow::Cow::Owned(config)
101    }
102
103    /// Core internal constructor path.
104    ///
105    /// Resolves the style presets before initializing the processor.
106    fn build_processor(
107        style: Style,
108        bibliography: Bibliography,
109        locale: Locale,
110        compound_sets: IndexMap<String, Vec<String>>,
111    ) -> Self {
112        let style = style.into_resolved();
113        Self::build_processor_pre_resolved(style, bibliography, locale, compound_sets)
114    }
115
116    /// Build a processor from an already-resolved style, skipping preset resolution.
117    ///
118    /// Use this when the style was cloned from a processor that has already
119    /// called `into_resolved()`, to avoid a second resolution pass that would
120    /// re-apply preset defaults and overwrite null-cleared fields.
121    pub(super) fn build_processor_pre_resolved(
122        style: Style,
123        bibliography: Bibliography,
124        locale: Locale,
125        compound_sets: IndexMap<String, Vec<String>>,
126    ) -> Self {
127        let (compound_set_by_ref, compound_member_index) =
128            Self::build_compound_set_indexes(&compound_sets);
129        let mut processor = Processor {
130            style,
131            bibliography,
132            locale,
133            default_config: Config::default(),
134            hints: HashMap::new(),
135            compound_sets,
136            compound_set_by_ref,
137            compound_member_index,
138            show_semantics: true,
139            inject_ast_indices: false,
140            abbreviation_map: None,
141        };
142
143        // Pre-calculate hints for disambiguation.
144        processor.hints = processor.calculate_hints();
145        processor
146    }
147
148    /// Validate compound sets against the bibliography.
149    fn try_validate_compound_sets(
150        bibliography: &Bibliography,
151        compound_sets: IndexMap<String, Vec<String>>,
152    ) -> Result<IndexMap<String, Vec<String>>, ProcessorError> {
153        super::validate_compound_sets(Some(compound_sets), bibliography)
154            .map(Option::unwrap_or_default)
155    }
156
157    /// Validate compound sets, falling back to an empty map on error.
158    fn validate_compound_sets_or_default(
159        bibliography: &Bibliography,
160        compound_sets: IndexMap<String, Vec<String>>,
161    ) -> IndexMap<String, Vec<String>> {
162        Self::try_validate_compound_sets(bibliography, compound_sets).unwrap_or_default()
163    }
164
165    /// Build flat reverse-lookup maps for compound sets.
166    ///
167    /// Maps reference IDs to their parent set ID and their 0-based position
168    /// within that set.
169    fn build_compound_set_indexes(
170        sets: &IndexMap<String, Vec<String>>,
171    ) -> (HashMap<String, String>, HashMap<String, usize>) {
172        let mut by_ref = HashMap::new();
173        let mut member_index = HashMap::new();
174        for (set_id, members) in sets {
175            for (idx, member) in members.iter().enumerate() {
176                by_ref.insert(member.clone(), set_id.clone());
177                member_index.insert(member.clone(), idx);
178            }
179        }
180        (by_ref, member_index)
181    }
182
183    /// Check whether the style uses note-based citations (footnotes/endnotes).
184    pub(crate) fn is_note_style(&self) -> bool {
185        self.get_config()
186            .processing
187            .as_ref()
188            .is_some_and(|processing| matches!(processing, citum_schema::options::Processing::Note))
189    }
190
191    /// Check whether the style uses numeric citation rendering.
192    fn is_numeric_style(&self) -> bool {
193        self.get_config()
194            .processing
195            .as_ref()
196            .is_some_and(|processing| {
197                matches!(processing, citum_schema::options::Processing::Numeric)
198            })
199    }
200
201    /// Check whether the style uses numeric bibliography rendering.
202    fn is_numeric_bibliography_style(&self) -> bool {
203        self.get_bibliography_config()
204            .processing
205            .as_ref()
206            .is_some_and(|processing| {
207                matches!(processing, citum_schema::options::Processing::Numeric)
208            })
209    }
210
211    /// Resolve the effective bibliography sort specification.
212    ///
213    /// Accounts for style overrides, processing-family preset defaults, and
214    /// (when neither applies) an explicit config-level `sort:`. The returned
215    /// `bool` is `true` only when the sort originates from that last,
216    /// explicit config-level step; the caller then applies the deterministic
217    /// entry-ID tiebreaker so config-driven sorts (`Processing::Numeric` /
218    /// `Custom` with an explicit `sort:`) stay oracle-fidelity-stable.
219    /// Styles with a processing-family preset default (`AuthorDate*`, `Note`,
220    /// `Label`) never reach the config-level step; those without any
221    /// `processing:` fall back to the default family (`AuthorDate`), whose
222    /// config carries the author-date sort preset.
223    fn resolved_bibliography_sort(&self) -> Option<(citum_schema::grouping::GroupSort, bool)> {
224        if let Some(sort_spec) = self
225            .style
226            .bibliography
227            .as_ref()
228            .and_then(|bibliography| bibliography.sort.as_ref())
229        {
230            return Some((sort_spec.resolve(), false));
231        }
232
233        let bibliography_config = self.get_bibliography_config();
234
235        if let Some(preset) = bibliography_config
236            .processing
237            .as_ref()
238            .and_then(citum_schema::options::Processing::default_bibliography_sort)
239        {
240            return Some((preset.group_sort(), false));
241        }
242
243        bibliography_config
244            .processing
245            .clone()
246            .unwrap_or_default()
247            .config()
248            .sort
249            .map(|sort_entry| (sort_entry.resolve().group_sort(), true))
250    }
251
252    /// Initialize numeric citation numbers from bibliography insertion order.
253    ///
254    /// citeproc-js registers all bibliography items before citation rendering in
255    /// the oracle workflow, so numeric labels are stable by reference registry
256    /// order rather than first-citation order.
257    ///
258    /// When the style declares an explicit bibliography sort, or the
259    /// processing family provides a bibliography default, citation numbers
260    /// must follow that resolved bibliography order.
261    pub(crate) fn initialize_numeric_citation_numbers(&self, run: &mut RunState) {
262        if !self.is_numeric_style() {
263            return;
264        }
265
266        self.initialize_numeric_numbers(run, self.sort_citation_number_order());
267    }
268
269    /// Initialize numeric bibliography numbers from resolved bibliography order.
270    pub(crate) fn initialize_numeric_bibliography_numbers(&self, run: &mut RunState) {
271        if !self.is_numeric_bibliography_style() {
272            return;
273        }
274
275        self.initialize_numeric_numbers(run, self.sort_bibliography_number_order());
276    }
277
278    /// Initialize citation numbers if the map is currently empty.
279    fn initialize_numeric_numbers(&self, run: &mut RunState, ordered_ids: Vec<String>) {
280        if !run
281            .citation_numbers
282            .read()
283            .unwrap_or_else(std::sync::PoisonError::into_inner)
284            .is_empty()
285        {
286            return;
287        }
288
289        self.initialize_numeric_citation_numbers_from_ordered_ids(run, ordered_ids);
290    }
291
292    /// Calculate the document-wide reference order for citation numbering.
293    fn sort_citation_number_order(&self) -> Vec<String> {
294        self.sort_references(self.bibliography.values().collect())
295            .into_iter()
296            .filter_map(citum_schema::reference::InputReference::id)
297            .map(String::from)
298            .collect()
299    }
300
301    /// Calculate the reference order for bibliography numbering.
302    fn sort_bibliography_number_order(&self) -> Vec<String> {
303        self.sort_references(self.bibliography.values().collect())
304            .into_iter()
305            .filter_map(citum_schema::reference::InputReference::id)
306            .map(String::from)
307            .collect()
308    }
309
310    /// Assign stable numeric labels to references based on a document order.
311    ///
312    /// Also populates compound groups for numeric styles that enable compound
313    /// numbering.
314    fn initialize_numeric_citation_numbers_from_ordered_ids(
315        &self,
316        run: &mut RunState,
317        ordered_ids: Vec<String>,
318    ) {
319        let mut numbers = run
320            .citation_numbers
321            .write()
322            .unwrap_or_else(std::sync::PoisonError::into_inner);
323        if !numbers.is_empty() {
324            return;
325        }
326
327        let compound_config = self.get_bibliography_options().compound_numeric.clone();
328
329        if compound_config.is_some() {
330            let mut set_first_seen: IndexMap<String, usize> = IndexMap::new();
331            let mut current_number = 1usize;
332            run.compound_groups.clear();
333
334            for ref_id in &ordered_ids {
335                if let Some(set_id) = self.compound_set_by_ref.get(ref_id) {
336                    if let Some(&number) = set_first_seen.get(set_id) {
337                        numbers.insert(ref_id.clone(), number);
338                    } else {
339                        set_first_seen.insert(set_id.clone(), current_number);
340                        if let Some(members) = self.compound_sets.get(set_id) {
341                            let present_members: Vec<String> = members
342                                .iter()
343                                .filter(|id| self.bibliography.contains_key(*id))
344                                .cloned()
345                                .collect();
346                            for member in &present_members {
347                                numbers.insert(member.clone(), current_number);
348                            }
349                            if present_members.len() > 1 {
350                                run.compound_groups.insert(current_number, present_members);
351                            }
352                        } else {
353                            numbers.insert(ref_id.clone(), current_number);
354                        }
355                        current_number += 1;
356                    }
357                } else if !numbers.contains_key(ref_id) {
358                    numbers.insert(ref_id.clone(), current_number);
359                    current_number += 1;
360                }
361            }
362        } else {
363            for (index, ref_id) in ordered_ids.into_iter().enumerate() {
364                numbers.insert(ref_id, index + 1);
365            }
366        }
367    }
368
369    /// Begin a new render run.
370    ///
371    /// Allocates fresh per-run state (citation numbers, cite-order tracking,
372    /// dynamic compound groups, first-note tracking) and performs numeric
373    /// citation-number pre-initialization from the processor's immutable
374    /// bibliography/style data. Registration methods (`&self, &mut RunState`)
375    /// populate the returned `RunState` in citation-processing order; call
376    /// [`RunState::finalize`] before rendering. See
377    /// `docs/specs/EXPLICIT_RENDER_RUN_STATE.md`.
378    #[must_use]
379    pub fn begin_run(&self) -> RunState {
380        let mut run = RunState::default();
381        self.initialize_numeric_citation_numbers(&mut run);
382        self.initialize_numeric_bibliography_numbers(&mut run);
383        run
384    }
385
386    /// Create a new processor with default English locale (en-US).
387    #[must_use]
388    pub fn new(style: Style, bibliography: Bibliography) -> Self {
389        Self::with_compound_sets(style, bibliography, IndexMap::new())
390    }
391
392    /// Create a new processor with explicit compound sets, returning an error for invalid sets.
393    ///
394    /// # Errors
395    ///
396    /// Returns an error when any compound set references unknown bibliography
397    /// entries or reuses the same member more than once.
398    pub fn try_with_compound_sets(
399        style: Style,
400        bibliography: Bibliography,
401        compound_sets: IndexMap<String, Vec<String>>,
402    ) -> Result<Self, ProcessorError> {
403        Self::try_with_locale_and_compound_sets(style, bibliography, Locale::en_us(), compound_sets)
404    }
405
406    /// Create a new processor with explicit compound sets.
407    ///
408    /// If `compound_sets` is invalid, this constructor ignores the supplied sets
409    /// and falls back to a processor without compound sets.
410    #[must_use]
411    pub fn with_compound_sets(
412        style: Style,
413        bibliography: Bibliography,
414        compound_sets: IndexMap<String, Vec<String>>,
415    ) -> Self {
416        let validated_sets = Self::validate_compound_sets_or_default(&bibliography, compound_sets);
417        Self::build_processor(style, bibliography, Locale::en_us(), validated_sets)
418    }
419
420    /// Create a new processor with a specified locale.
421    ///
422    /// The locale determines term translations and locale-specific formatting behavior.
423    #[must_use]
424    pub fn with_locale(style: Style, bibliography: Bibliography, locale: Locale) -> Self {
425        Self::with_locale_and_compound_sets(style, bibliography, locale, IndexMap::new())
426    }
427
428    /// Create a new processor with explicit locale and compound sets, returning
429    /// an error for invalid sets.
430    ///
431    /// # Errors
432    ///
433    /// Returns an error when any compound set references unknown bibliography
434    /// entries or reuses the same member more than once.
435    pub fn try_with_locale_and_compound_sets(
436        style: Style,
437        bibliography: Bibliography,
438        locale: Locale,
439        compound_sets: IndexMap<String, Vec<String>>,
440    ) -> Result<Self, ProcessorError> {
441        let validated_sets = Self::try_validate_compound_sets(&bibliography, compound_sets)?;
442        Ok(Self::build_processor(
443            style,
444            bibliography,
445            locale,
446            validated_sets,
447        ))
448    }
449
450    /// Create a new processor with a specified locale and explicit compound sets.
451    ///
452    /// The locale determines term translations and locale-specific formatting behavior.
453    ///
454    /// If `compound_sets` is invalid, this constructor ignores the supplied sets
455    /// and falls back to a processor without compound sets.
456    #[must_use]
457    pub fn with_locale_and_compound_sets(
458        style: Style,
459        bibliography: Bibliography,
460        locale: Locale,
461        compound_sets: IndexMap<String, Vec<String>>,
462    ) -> Self {
463        let validated_sets = Self::validate_compound_sets_or_default(&bibliography, compound_sets);
464        Self::build_processor(style, bibliography, locale, validated_sets)
465    }
466
467    /// Create a new processor, loading the locale from disk.
468    ///
469    /// Loads the locale specified in the style's `default_locale` field from the given directory,
470    /// falling back to en-US if not found or not specified.
471    #[must_use]
472    pub fn with_style_locale(
473        style: Style,
474        bibliography: Bibliography,
475        locales_dir: &std::path::Path,
476    ) -> Self {
477        let style = style.into_resolved();
478        let locale = if let Some(ref locale_id) = style.info.default_locale {
479            Locale::load(locale_id, locales_dir)
480        } else {
481            Locale::en_us()
482        };
483        Self::with_locale_and_compound_sets(style, bibliography, locale, IndexMap::new())
484    }
485
486    /// Return a copy of the processor that injects source template indices into semantic HTML.
487    #[must_use]
488    pub fn with_inject_ast_indices(mut self, inject_ast_indices: bool) -> Self {
489        self.inject_ast_indices = inject_ast_indices;
490        self
491    }
492
493    /// Enable or disable source template index injection for semantic HTML output.
494    pub fn set_inject_ast_indices(&mut self, inject_ast_indices: bool) {
495        self.inject_ast_indices = inject_ast_indices;
496    }
497
498    /// Return the global style configuration.
499    pub fn get_config(&self) -> &Config {
500        self.style.options.as_ref().unwrap_or(&self.default_config)
501    }
502
503    /// Return merged config for citation rendering.
504    ///
505    /// Combines global style options with citation-specific overrides, borrowing
506    /// the global configuration when no merge or locale resolution is required.
507    pub fn get_citation_config(&self) -> std::borrow::Cow<'_, Config> {
508        let base = self.get_config();
509        let config = match self
510            .style
511            .citation
512            .as_ref()
513            .and_then(|citation| citation.options.as_ref())
514        {
515            Some(citation_options) => std::borrow::Cow::Owned(
516                citation_options
517                    .merged_with_raw(base, self.style.scoped_raw_options.citation.as_ref()),
518            ),
519            None => std::borrow::Cow::Borrowed(base),
520        };
521        self.with_punctuation_defaults(config)
522    }
523
524    /// Return merged shared config for bibliography rendering.
525    ///
526    /// Combines global shared style options with bibliography-local shared overrides,
527    /// borrowing the global configuration when no merge or locale resolution is required.
528    pub fn get_bibliography_config(&self) -> std::borrow::Cow<'_, Config> {
529        let base = self.get_config();
530        let config = match self
531            .style
532            .bibliography
533            .as_ref()
534            .and_then(|bibliography| bibliography.options.as_ref())
535        {
536            Some(bibliography_options) => std::borrow::Cow::Owned(
537                bibliography_options
538                    .merged_with_raw(base, self.style.scoped_raw_options.bibliography.as_ref()),
539            ),
540            None => std::borrow::Cow::Borrowed(base),
541        };
542        self.with_punctuation_defaults(config)
543    }
544
545    /// Return effective bibliography-only configuration.
546    pub fn get_bibliography_options(&self) -> std::borrow::Cow<'_, BibliographyConfig> {
547        match self
548            .style
549            .bibliography
550            .as_ref()
551            .and_then(|bibliography| bibliography.options.as_ref())
552        {
553            Some(bibliography_options) => {
554                std::borrow::Cow::Owned(bibliography_options.to_bibliography_config())
555            }
556            None => std::borrow::Cow::Owned(BibliographyConfig::default()),
557        }
558    }
559
560    /// Sort references according to the style's bibliography sort specification.
561    ///
562    /// Uses style-specified sort keys (author, title, issued, etc.) and sort order.
563    pub fn sort_references<'a>(&self, references: Vec<&'a Reference>) -> Vec<&'a Reference> {
564        let bibliography_config = self.get_bibliography_config();
565        let mut sorted_refs = match self.resolved_bibliography_sort() {
566            Some((sort_spec, true)) => {
567                let mut sorter = crate::sorting::ReferenceSorter::with_bibliography_config(
568                    &self.locale,
569                    &bibliography_config,
570                );
571                if let Some(spec) = self.style.bibliography.as_ref() {
572                    sorter = sorter.with_bibliography_spec(spec);
573                }
574                sorter.sort_references_with_id_tiebreak(references, &sort_spec)
575            }
576            Some((sort_spec, false)) => {
577                let mut sorter = crate::sorting::ReferenceSorter::with_bibliography_config(
578                    &self.locale,
579                    &bibliography_config,
580                );
581                if let Some(spec) = self.style.bibliography.as_ref() {
582                    sorter = sorter.with_bibliography_spec(spec);
583                }
584                sorter.sort_references(references, &sort_spec)
585            }
586            None => references,
587        };
588
589        let bibliography_options = self.get_bibliography_options();
590        if let Some(partitioning) =
591            effective_sort_partitioning(&bibliography_options, &bibliography_config).as_ref()
592            && crate::sort_partitioning::should_sort_flat(partitioning)
593        {
594            crate::sort_partitioning::sort_by_partition(
595                sorted_refs.as_mut_slice(),
596                &self.locale,
597                partitioning,
598            );
599        }
600
601        sorted_refs
602    }
603
604    /// Sort citation items according to the style's citation sort specification.
605    pub fn sort_citation_items(
606        &self,
607        items: Vec<CitationItem>,
608        spec: &citum_schema::CitationSpec,
609    ) -> Vec<CitationItem> {
610        if let Some(sort_spec) = &spec.sort {
611            let items_with_refs: Vec<(CitationItem, Option<&Reference>)> = items
612                .into_iter()
613                .map(|item| {
614                    let reference = self.bibliography.get(&item.id);
615                    (item, reference)
616                })
617                .collect();
618
619            let resolved_sort = sort_spec.resolve();
620            let citation_config = self.get_citation_config();
621            let sorter = crate::sorting::ReferenceSorter::with_bibliography_config(
622                &self.locale,
623                &citation_config,
624            )
625            .with_citation_spec(spec);
626            let sorted =
627                sorter.sort_by_keys(items_with_refs, &resolved_sort.template, |item| item.1);
628
629            return sorted.into_iter().map(|(item, _reference)| item).collect();
630        }
631
632        items
633    }
634
635    /// Calculate disambiguation hints needed for the style.
636    ///
637    /// Analyzes the bibliography to determine which items need disambiguation
638    /// (year suffixes, etc.) and calculates hints for efficient rendering.
639    pub fn calculate_hints(&self) -> HashMap<String, ProcHints> {
640        let citation_config = self.get_citation_config();
641        let config = citation_config.as_ref();
642        let bibliography_config = self.get_bibliography_config();
643        let bibliography_sort = self.resolved_bibliography_sort();
644
645        let mut disambiguator = if let Some((resolved_sort, _id_tiebreak)) = &bibliography_sort {
646            Disambiguator::with_group_sort(
647                &self.bibliography,
648                config,
649                &bibliography_config,
650                &self.locale,
651                resolved_sort,
652            )
653        } else {
654            Disambiguator::new(
655                &self.bibliography,
656                config,
657                &bibliography_config,
658                &self.locale,
659            )
660        };
661
662        if let Some(citation_spec) = self.style.citation.as_ref() {
663            disambiguator = disambiguator.with_citation_spec(citation_spec);
664        }
665        if let Some(bibliography_spec) = self.style.bibliography.as_ref() {
666            disambiguator = disambiguator.with_bibliography_spec(bibliography_spec);
667        }
668
669        disambiguator.calculate_hints()
670    }
671}
672
673fn effective_sort_partitioning(
674    bibliography_options: &BibliographyConfig,
675    bibliography_config: &Config,
676) -> Option<BibliographySortPartitioning> {
677    if let Some(partitioning) = &bibliography_options.sort_partitioning {
678        return Some(partitioning.clone());
679    }
680
681    bibliography_config
682        .sorting
683        .as_ref()
684        .is_some_and(|sorting| {
685            sorting.effective_multilingual() == SortingMultilingualMode::PerScript
686        })
687        .then(|| BibliographySortPartitioning {
688            by: BibliographyPartitionKind::Script,
689            mode: BibliographyPartitionMode::SortOnly,
690            order: Vec::new(),
691            headings: HashMap::new(),
692            unknown_fields: Default::default(),
693        })
694}