Skip to main content

citum_schema_style/options/
mod.rs

1/*
2SPDX-License-Identifier: MIT OR Apache-2.0
3SPDX-FileCopyrightText: © 2023-2026 Bruce D'Arcus and Citum contributors
4*/
5
6//! Style configuration options.
7
8pub mod bibliography;
9pub mod contributors;
10pub mod dates;
11pub mod integral_name_memory;
12pub mod localization;
13pub mod locators;
14pub mod multilingual;
15pub mod processing;
16pub mod scoped;
17pub mod substitute;
18
19pub use bibliography::{
20    ArticleJournalBibliographyConfig, ArticleJournalNoPageFallback, BibliographyConfig,
21    BibliographyPartitionHeading, BibliographyPartitionKind, BibliographyPartitionMode,
22    BibliographySortPartitioning, SubsequentAuthorSubstituteRule,
23};
24pub use contributors::{
25    AndOptions, AndOtherOptions, ContributorConfig, ContributorConfigEntry, DelimiterPrecedesLast,
26    DemoteNonDroppingParticle, DisplayAsSort, NameForm, RoleLabelPreset, RoleOptions,
27    RoleOptionsEntry, RoleRendering, ShortenListOptions,
28};
29pub use dates::{DateConfig, DateConfigEntry};
30pub use integral_name_memory::{
31    IntegralNameContexts, IntegralNameMemoryConfig, IntegralNameScope, OrgAbbreviationMemoryConfig,
32    ResolvedIntegralNameMemoryConfig, ResolvedOrgAbbreviationMemoryConfig, ShortNameDisplay,
33    SubsequentNameForm,
34};
35pub use localization::{Localize, MonthFormat, Scope};
36pub use locators::{
37    LabelForm, LabelRepeat, LocatorConfig, LocatorConfigEntry, LocatorKindConfig, LocatorPattern,
38    LocatorPreset, TypeClass,
39};
40pub use multilingual::{MultilingualConfig, MultilingualMode, ScriptConfig};
41pub use processing::{
42    CitationSortPolicy, Disambiguation, GivennameRule, Group, LabelConfig, LabelParams,
43    LabelPreset, Processing, ProcessingCustom, Sort, SortEntry, SortKey, SortSpec,
44};
45pub use scoped::{
46    BibliographyLabelMode, BibliographyLabelWrap, CitationGroupDelimiter, DatePosition, LabelWrap,
47    RepeatedAuthorRendering, TitleTerminator,
48};
49pub use substitute::{Substitute, SubstituteConfig, SubstituteKey};
50
51use crate::template::DelimiterPunctuation;
52#[cfg(feature = "schema")]
53use schemars::JsonSchema;
54use serde::{Deserialize, Serialize};
55use std::collections::HashMap;
56
57/// Top-level style configuration.
58#[derive(Debug, Default, PartialEq, Clone, Serialize)]
59#[cfg_attr(feature = "schema", derive(JsonSchema))]
60#[serde(rename_all = "kebab-case")]
61pub struct Config {
62    /// Substitution rules for missing data.
63    #[serde(skip_serializing_if = "Option::is_none")]
64    pub substitute: Option<SubstituteConfig>,
65    /// Processing mode (author-date, numeric, etc.).
66    #[serde(skip_serializing_if = "Option::is_none")]
67    pub processing: Option<Processing>,
68    /// Style-level locale override ID loaded from `locales/overrides/<id>.*`.
69    ///
70    /// This patches the locale selected by `StyleInfo.default_locale` without
71    /// duplicating the full base locale. Runtime loading is limited to the
72    /// style-global config; nested citation or bibliography configs are ignored.
73    #[serde(skip_serializing_if = "Option::is_none")]
74    pub locale_override: Option<String>,
75    /// Localization settings.
76    #[serde(skip_serializing_if = "Option::is_none")]
77    pub localize: Option<Localize>,
78    /// Multilingual rendering defaults.
79    #[serde(skip_serializing_if = "Option::is_none")]
80    pub multilingual: Option<MultilingualConfig>,
81    /// Contributor formatting defaults. Accepts a preset name (e.g., "apa")
82    /// or explicit configuration.
83    #[serde(
84        skip_serializing_if = "Option::is_none",
85        deserialize_with = "deserialize_contributor_config",
86        default
87    )]
88    #[cfg_attr(feature = "schema", schemars(with = "Option<ContributorConfigEntry>"))]
89    pub contributors: Option<ContributorConfig>,
90    /// Date formatting defaults. Accepts a preset name (e.g., "long")
91    /// or explicit configuration.
92    #[serde(
93        skip_serializing_if = "Option::is_none",
94        deserialize_with = "deserialize_date_config",
95        default
96    )]
97    #[cfg_attr(feature = "schema", schemars(with = "Option<DateConfigEntry>"))]
98    pub dates: Option<DateConfig>,
99    /// Title formatting defaults. Accepts a preset name (e.g., "apa")
100    /// or explicit configuration.
101    #[serde(
102        skip_serializing_if = "Option::is_none",
103        deserialize_with = "deserialize_titles_config",
104        default
105    )]
106    #[cfg_attr(feature = "schema", schemars(with = "Option<TitlesConfigEntry>"))]
107    pub titles: Option<crate::options::titles::TitlesConfig>,
108    /// Locator rendering configuration. Accepts a preset name (e.g., "note")
109    /// or explicit configuration.
110    #[serde(
111        skip_serializing_if = "Option::is_none",
112        deserialize_with = "deserialize_locator_config",
113        default
114    )]
115    #[cfg_attr(feature = "schema", schemars(with = "Option<LocatorConfigEntry>"))]
116    pub locators: Option<LocatorConfig>,
117    /// Page range formatting (expanded, minimal, chicago).
118    #[serde(skip_serializing_if = "Option::is_none")]
119    pub page_range_format: Option<PageRangeFormat>,
120    /// Hyperlink configuration.
121    #[serde(skip_serializing_if = "Option::is_none")]
122    pub links: Option<LinksConfig>,
123    /// Whether to place periods/commas inside quotation marks.
124    /// true = American style ("text."), false = British style ("text".)
125    /// Defaults to false; en-US locale typically sets this to true.
126    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
127    pub punctuation_in_quote: bool,
128    /// Delimiter between volume/issue and pages for serial sources.
129    /// Processor adds trailing space when rendering.
130    /// Examples: Comma (APA ", "), Colon (Chicago ": ").
131    #[serde(skip_serializing_if = "Option::is_none")]
132    pub volume_pages_delimiter: Option<DelimiterPunctuation>,
133    /// Strip trailing periods from terms, labels, and abbreviated dates.
134    #[serde(skip_serializing_if = "Option::is_none", rename = "strip-periods")]
135    pub strip_periods: Option<bool>,
136    /// Document-level note marker placement and punctuation movement rules.
137    #[serde(skip_serializing_if = "Option::is_none")]
138    pub notes: Option<NoteConfig>,
139    /// Integral citation name-memory behavior.
140    #[serde(skip_serializing_if = "Option::is_none")]
141    pub integral_name_memory: Option<IntegralNameMemoryConfig>,
142    /// Organizational name abbreviation expansion policy.
143    #[serde(skip_serializing_if = "Option::is_none")]
144    pub org_abbreviation_memory: Option<OrgAbbreviationMemoryConfig>,
145    /// Custom user-defined fields for extensions.
146    #[serde(skip_serializing_if = "Option::is_none")]
147    pub custom: Option<HashMap<String, serde_json::Value>>,
148    /// Forward-compat: captures unknown keys when an older engine reads a
149    /// style produced by a newer schema. Empty by default; treated as a
150    /// SoftDegrade signal. See `docs/specs/FORWARD_COMPATIBILITY.md`.
151    #[serde(
152        flatten,
153        default,
154        skip_serializing_if = "std::collections::BTreeMap::is_empty"
155    )]
156    #[cfg_attr(feature = "schema", schemars(skip))]
157    pub unknown_fields: std::collections::BTreeMap<String, serde_yaml::Value>,
158}
159
160/// Citation-local option overrides.
161#[derive(Debug, Default, PartialEq, Clone, Serialize, Deserialize)]
162#[cfg_attr(feature = "schema", derive(JsonSchema))]
163#[serde(rename_all = "kebab-case")]
164pub struct CitationOptions {
165    /// Substitution rules for missing data.
166    #[serde(skip_serializing_if = "Option::is_none")]
167    pub substitute: Option<SubstituteConfig>,
168    /// Processing mode (author-date, numeric, etc.).
169    #[serde(skip_serializing_if = "Option::is_none")]
170    pub processing: Option<Processing>,
171    /// Localization settings.
172    #[serde(skip_serializing_if = "Option::is_none")]
173    pub localize: Option<Localize>,
174    /// Multilingual rendering defaults.
175    #[serde(skip_serializing_if = "Option::is_none")]
176    pub multilingual: Option<MultilingualConfig>,
177    /// Contributor formatting defaults.
178    #[serde(
179        skip_serializing_if = "Option::is_none",
180        deserialize_with = "deserialize_contributor_config",
181        default
182    )]
183    #[cfg_attr(feature = "schema", schemars(with = "Option<ContributorConfigEntry>"))]
184    pub contributors: Option<ContributorConfig>,
185    /// Date formatting defaults.
186    #[serde(
187        skip_serializing_if = "Option::is_none",
188        deserialize_with = "deserialize_date_config",
189        default
190    )]
191    #[cfg_attr(feature = "schema", schemars(with = "Option<DateConfigEntry>"))]
192    pub dates: Option<DateConfig>,
193    /// Title formatting defaults.
194    #[serde(
195        skip_serializing_if = "Option::is_none",
196        deserialize_with = "deserialize_titles_config",
197        default
198    )]
199    #[cfg_attr(feature = "schema", schemars(with = "Option<TitlesConfigEntry>"))]
200    pub titles: Option<crate::options::titles::TitlesConfig>,
201    /// Locator rendering configuration.
202    #[serde(
203        skip_serializing_if = "Option::is_none",
204        deserialize_with = "deserialize_locator_config",
205        default
206    )]
207    #[cfg_attr(feature = "schema", schemars(with = "Option<LocatorConfigEntry>"))]
208    pub locators: Option<LocatorConfig>,
209    /// Page range formatting (expanded, minimal, chicago).
210    #[serde(skip_serializing_if = "Option::is_none")]
211    pub page_range_format: Option<PageRangeFormat>,
212    /// Hyperlink configuration.
213    #[serde(skip_serializing_if = "Option::is_none")]
214    pub links: Option<LinksConfig>,
215    /// Whether to place periods/commas inside quotation marks.
216    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
217    pub punctuation_in_quote: bool,
218    /// Delimiter between volume/issue and pages for serial sources.
219    #[serde(skip_serializing_if = "Option::is_none")]
220    pub volume_pages_delimiter: Option<DelimiterPunctuation>,
221    /// Strip trailing periods from terms, labels, and abbreviated dates.
222    #[serde(skip_serializing_if = "Option::is_none", rename = "strip-periods")]
223    pub strip_periods: Option<bool>,
224    /// Document-level note marker placement and punctuation movement rules.
225    #[serde(skip_serializing_if = "Option::is_none")]
226    pub notes: Option<NoteConfig>,
227    /// Integral citation name-memory behavior.
228    #[serde(skip_serializing_if = "Option::is_none")]
229    pub integral_name_memory: Option<IntegralNameMemoryConfig>,
230    /// Organizational name abbreviation expansion policy.
231    #[serde(skip_serializing_if = "Option::is_none")]
232    pub org_abbreviation_memory: Option<OrgAbbreviationMemoryConfig>,
233    /// Label wrap policy applied to citation labels.
234    #[serde(skip_serializing_if = "Option::is_none")]
235    pub label_wrap: Option<LabelWrap>,
236    /// Delimiter between grouped citation items.
237    #[serde(skip_serializing_if = "Option::is_none")]
238    pub group_delimiter: Option<CitationGroupDelimiter>,
239    /// Custom user-defined fields for extensions.
240    #[serde(skip_serializing_if = "Option::is_none")]
241    pub custom: Option<HashMap<String, serde_json::Value>>,
242    /// Forward-compat: captures unknown keys when an older engine reads a
243    /// style produced by a newer schema. Empty by default; treated as a
244    /// SoftDegrade signal. See `docs/specs/FORWARD_COMPATIBILITY.md`.
245    #[serde(
246        flatten,
247        default,
248        skip_serializing_if = "std::collections::BTreeMap::is_empty"
249    )]
250    #[cfg_attr(feature = "schema", schemars(skip))]
251    pub unknown_fields: std::collections::BTreeMap<String, serde_yaml::Value>,
252}
253
254/// Bibliography-local option overrides.
255#[derive(Debug, Default, PartialEq, Clone, Serialize, Deserialize)]
256#[cfg_attr(feature = "schema", derive(JsonSchema))]
257#[serde(rename_all = "kebab-case")]
258pub struct BibliographyOptions {
259    /// Substitution rules for missing data.
260    #[serde(skip_serializing_if = "Option::is_none")]
261    pub substitute: Option<SubstituteConfig>,
262    /// Processing mode (author-date, numeric, etc.).
263    #[serde(skip_serializing_if = "Option::is_none")]
264    pub processing: Option<Processing>,
265    /// Localization settings.
266    #[serde(skip_serializing_if = "Option::is_none")]
267    pub localize: Option<Localize>,
268    /// Multilingual rendering defaults.
269    #[serde(skip_serializing_if = "Option::is_none")]
270    pub multilingual: Option<MultilingualConfig>,
271    /// Contributor formatting defaults.
272    #[serde(
273        skip_serializing_if = "Option::is_none",
274        deserialize_with = "deserialize_contributor_config",
275        default
276    )]
277    #[cfg_attr(feature = "schema", schemars(with = "Option<ContributorConfigEntry>"))]
278    pub contributors: Option<ContributorConfig>,
279    /// Date formatting defaults.
280    #[serde(
281        skip_serializing_if = "Option::is_none",
282        deserialize_with = "deserialize_date_config",
283        default
284    )]
285    #[cfg_attr(feature = "schema", schemars(with = "Option<DateConfigEntry>"))]
286    pub dates: Option<DateConfig>,
287    /// Title formatting defaults.
288    #[serde(
289        skip_serializing_if = "Option::is_none",
290        deserialize_with = "deserialize_titles_config",
291        default
292    )]
293    #[cfg_attr(feature = "schema", schemars(with = "Option<TitlesConfigEntry>"))]
294    pub titles: Option<crate::options::titles::TitlesConfig>,
295    /// Page range formatting (expanded, minimal, chicago).
296    #[serde(skip_serializing_if = "Option::is_none")]
297    pub page_range_format: Option<PageRangeFormat>,
298    /// Article-journal-specific bibliography policies.
299    #[serde(skip_serializing_if = "Option::is_none")]
300    pub article_journal: Option<ArticleJournalBibliographyConfig>,
301    /// String to substitute for repeating authors.
302    #[serde(skip_serializing_if = "Option::is_none")]
303    pub subsequent_author_substitute: Option<String>,
304    /// Rule for when to apply the substitute.
305    #[serde(skip_serializing_if = "Option::is_none")]
306    pub subsequent_author_substitute_rule: Option<SubsequentAuthorSubstituteRule>,
307    /// Whether to use a hanging indent.
308    #[serde(skip_serializing_if = "Option::is_none")]
309    pub hanging_indent: Option<bool>,
310    /// Suffix appended to each bibliography entry.
311    #[serde(skip_serializing_if = "Option::is_none")]
312    pub entry_suffix: Option<String>,
313    /// Separator between bibliography components.
314    #[serde(skip_serializing_if = "Option::is_none")]
315    pub separator: Option<String>,
316    /// Whether to suppress the trailing period after URLs/DOIs.
317    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
318    pub suppress_period_after_url: bool,
319    /// Configuration for compound numeric bibliography entries.
320    #[serde(skip_serializing_if = "Option::is_none")]
321    pub compound_numeric: Option<bibliography::CompoundNumericConfig>,
322    /// Partitioning policy for multilingual bibliography sorting and sections.
323    #[serde(skip_serializing_if = "Option::is_none")]
324    pub sort_partitioning: Option<bibliography::BibliographySortPartitioning>,
325    /// Hyperlink configuration.
326    #[serde(skip_serializing_if = "Option::is_none")]
327    pub links: Option<LinksConfig>,
328    /// Whether to place periods/commas inside quotation marks.
329    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
330    pub punctuation_in_quote: bool,
331    /// Delimiter between volume/issue and pages for serial sources.
332    #[serde(skip_serializing_if = "Option::is_none")]
333    pub volume_pages_delimiter: Option<DelimiterPunctuation>,
334    /// Bibliography label mode for label-bearing styles.
335    #[serde(skip_serializing_if = "Option::is_none")]
336    pub label_mode: Option<BibliographyLabelMode>,
337    /// Label wrap policy applied to bibliography labels.
338    #[serde(skip_serializing_if = "Option::is_none")]
339    pub label_wrap: Option<BibliographyLabelWrap>,
340    /// Placement of issued dates within bibliography entries.
341    #[serde(skip_serializing_if = "Option::is_none")]
342    pub date_position: Option<DatePosition>,
343    /// Terminator applied to primary-title bibliography components.
344    #[serde(skip_serializing_if = "Option::is_none")]
345    pub title_terminator: Option<TitleTerminator>,
346    /// Repeated-author rendering mode for bibliography entries.
347    #[serde(skip_serializing_if = "Option::is_none")]
348    pub repeated_author_rendering: Option<RepeatedAuthorRendering>,
349    /// Strip trailing periods from terms, labels, and abbreviated dates.
350    #[serde(skip_serializing_if = "Option::is_none", rename = "strip-periods")]
351    pub strip_periods: Option<bool>,
352    /// Custom user-defined fields for extensions.
353    #[serde(skip_serializing_if = "Option::is_none")]
354    pub custom: Option<HashMap<String, serde_json::Value>>,
355    /// Forward-compat: captures unknown keys when an older engine reads a
356    /// style produced by a newer schema. Empty by default; treated as a
357    /// SoftDegrade signal. See `docs/specs/FORWARD_COMPATIBILITY.md`.
358    #[serde(
359        flatten,
360        default,
361        skip_serializing_if = "std::collections::BTreeMap::is_empty"
362    )]
363    #[cfg_attr(feature = "schema", schemars(skip))]
364    pub unknown_fields: std::collections::BTreeMap<String, serde_yaml::Value>,
365}
366
367/// Document-level note marker placement rules.
368#[derive(Debug, Default, PartialEq, Clone, Serialize, Deserialize)]
369#[cfg_attr(feature = "schema", derive(JsonSchema))]
370#[serde(rename_all = "kebab-case")]
371pub struct NoteConfig {
372    /// Desired location of movable punctuation relative to closing quotation
373    /// marks when note markers are introduced.
374    #[serde(skip_serializing_if = "Option::is_none")]
375    pub punctuation: Option<NoteQuotePlacement>,
376    /// Desired location of the note marker relative to closing quotation marks.
377    #[serde(skip_serializing_if = "Option::is_none")]
378    pub number: Option<NoteNumberPlacement>,
379    /// Whether the note marker appears before or after the closest movable
380    /// punctuation mark.
381    #[serde(skip_serializing_if = "Option::is_none")]
382    pub order: Option<NoteMarkerOrder>,
383    /// Forward-compat: captures unknown keys when an older engine reads a
384    /// style produced by a newer schema. Empty by default; treated as a
385    /// SoftDegrade signal. See `docs/specs/FORWARD_COMPATIBILITY.md`.
386    #[serde(
387        flatten,
388        default,
389        skip_serializing_if = "std::collections::BTreeMap::is_empty"
390    )]
391    #[cfg_attr(feature = "schema", schemars(skip))]
392    pub unknown_fields: std::collections::BTreeMap<String, serde_yaml::Value>,
393}
394
395/// Controls where movable punctuation is placed relative to closing quotation marks.
396#[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize, Deserialize)]
397#[cfg_attr(feature = "schema", derive(JsonSchema))]
398#[serde(rename_all = "kebab-case")]
399pub enum NoteQuotePlacement {
400    /// Keep movable punctuation inside the closing quotation mark.
401    Inside,
402    /// Keep movable punctuation outside the closing quotation mark.
403    Outside,
404    /// Follow org-cite-style adaptive behavior: punctuation stays inside when
405    /// it is already flush with the closing quote, otherwise it is placed
406    /// outside.
407    Adaptive,
408}
409
410/// Controls where a footnote number marker is placed relative to closing quotation marks.
411#[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize, Deserialize)]
412#[cfg_attr(feature = "schema", derive(JsonSchema))]
413#[serde(rename_all = "kebab-case")]
414pub enum NoteNumberPlacement {
415    /// Place the note marker inside the closing quotation mark.
416    Inside,
417    /// Place the note marker outside the closing quotation mark.
418    Outside,
419    /// Place the note marker on the same side as the movable punctuation when
420    /// only one side has punctuation; otherwise default to outside.
421    Same,
422}
423
424/// Controls whether a note marker appears before or after adjacent movable punctuation.
425#[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize, Deserialize)]
426#[cfg_attr(feature = "schema", derive(JsonSchema))]
427#[serde(rename_all = "kebab-case")]
428pub enum NoteMarkerOrder {
429    /// Place the note marker before the closest movable punctuation mark.
430    Before,
431    /// Place the note marker after the closest movable punctuation mark.
432    After,
433}
434
435/// Page range formatting options.
436#[derive(Debug, Default, PartialEq, Clone, Serialize, Deserialize)]
437#[cfg_attr(feature = "schema", derive(JsonSchema))]
438#[serde(rename_all = "kebab-case")]
439#[non_exhaustive]
440pub enum PageRangeFormat {
441    /// Full expansion: 321-328 → 321–328
442    #[default]
443    Expanded,
444    /// Minimal digits: 321-328 → 321–8
445    Minimal,
446    /// Minimal two digits: 321-328 → 321–28
447    MinimalTwo,
448    /// Chicago Manual of Style 15th ed rules
449    Chicago,
450    /// Chicago Manual of Style 16th/17th ed rules
451    Chicago16,
452}
453
454pub mod titles;
455
456pub use titles::{TextCase, TitleRendering, TitlesConfig, TitlesConfigEntry};
457
458/// Structured link options.
459#[derive(Debug, Default, PartialEq, Clone, Serialize, Deserialize)]
460#[cfg_attr(feature = "schema", derive(JsonSchema))]
461#[serde(rename_all = "kebab-case")]
462pub struct LinksConfig {
463    /// Link value to the item's DOI.
464    #[serde(skip_serializing_if = "Option::is_none")]
465    pub doi: Option<bool>,
466    /// Link value to the item's URL.
467    #[serde(skip_serializing_if = "Option::is_none")]
468    pub url: Option<bool>,
469    /// The target for the link (url, doi, etc.).
470    #[serde(skip_serializing_if = "Option::is_none")]
471    pub target: Option<LinkTarget>,
472    /// What text should be hyperlinked (title, url, etc.).
473    #[serde(skip_serializing_if = "Option::is_none")]
474    pub anchor: Option<LinkAnchor>,
475}
476
477/// Link target options.
478#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
479#[cfg_attr(feature = "schema", derive(JsonSchema))]
480#[serde(rename_all = "kebab-case")]
481pub enum LinkTarget {
482    Url,
483    Doi,
484    UrlOrDoi,
485    Pubmed,
486    Pmcid,
487}
488
489/// Link anchor options.
490#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
491#[cfg_attr(feature = "schema", derive(JsonSchema))]
492#[serde(rename_all = "kebab-case")]
493pub enum LinkAnchor {
494    /// Link the title component.
495    Title,
496    /// Link the URL component itself.
497    Url,
498    /// Link the DOI component itself.
499    Doi,
500    /// Link the specific component this config is attached to.
501    Component,
502    /// Link the entire bibliography entry.
503    Entry,
504}
505
506impl Config {
507    /// Merge another config into this one, with `other` taking precedence.
508    ///
509    /// Used for combining global options with context-specific (citation/bibliography) options.
510    /// Only non-None fields from `other` override fields in `self`.
511    pub fn merge(&mut self, other: &Config) {
512        crate::merge_options!(
513            self,
514            other,
515            processing,
516            locale_override,
517            localize,
518            multilingual,
519            dates,
520            titles,
521            locators,
522            page_range_format,
523            links,
524            volume_pages_delimiter,
525            locale_override,
526            strip_periods,
527            notes,
528            integral_name_memory,
529            org_abbreviation_memory,
530            custom,
531        );
532
533        if let Some(other_substitute) = &other.substitute {
534            if let Some(this_substitute) = &self.substitute {
535                self.substitute = Some(SubstituteConfig::merged(this_substitute, other_substitute));
536            } else {
537                self.substitute = Some(other_substitute.clone());
538            }
539        }
540
541        if let Some(other_contributors) = &other.contributors {
542            if let Some(this_contributors) = &mut self.contributors {
543                this_contributors.merge(other_contributors);
544            } else {
545                self.contributors = Some(other_contributors.clone());
546            }
547        }
548
549        if other.punctuation_in_quote {
550            self.punctuation_in_quote = true;
551        }
552    }
553
554    /// Create a merged config from base and override, returning a new Config.
555    ///
556    /// Convenience method that clones base, then merges override into it.
557    pub fn merged(base: &Config, override_config: &Config) -> Config {
558        let mut result = base.clone();
559        result.merge(override_config);
560        result
561    }
562}
563
564impl CitationOptions {
565    /// Convert citation-local overrides into the runtime config shape.
566    #[must_use]
567    pub fn to_config(&self) -> Config {
568        Config {
569            substitute: self.substitute.clone(),
570            processing: self.processing.clone(),
571            locale_override: None,
572            localize: self.localize.clone(),
573            multilingual: self.multilingual.clone(),
574            contributors: self.contributors.clone(),
575            dates: self.dates.clone(),
576            titles: self.titles.clone(),
577            locators: self.locators.clone(),
578            page_range_format: self.page_range_format.clone(),
579            links: self.links.clone(),
580            punctuation_in_quote: self.punctuation_in_quote,
581            volume_pages_delimiter: self.volume_pages_delimiter.clone(),
582            strip_periods: self.strip_periods,
583            notes: self.notes.clone(),
584            integral_name_memory: self.integral_name_memory.clone(),
585            org_abbreviation_memory: self.org_abbreviation_memory.clone(),
586            custom: self.custom.clone(),
587            unknown_fields: std::collections::BTreeMap::new(),
588        }
589    }
590
591    /// Merge citation-local overrides over a base config.
592    #[must_use]
593    pub fn merged_with(&self, base: &Config) -> Config {
594        Config::merged(base, &self.to_config())
595    }
596
597    /// Merge `other` into `self`, with `other` taking precedence for each field.
598    pub fn merge(&mut self, other: &CitationOptions) {
599        crate::merge_options!(
600            self,
601            other,
602            processing,
603            localize,
604            multilingual,
605            dates,
606            titles,
607            locators,
608            page_range_format,
609            links,
610            volume_pages_delimiter,
611            strip_periods,
612            notes,
613            integral_name_memory,
614            org_abbreviation_memory,
615            label_wrap,
616            group_delimiter,
617            custom,
618        );
619
620        if let Some(other_substitute) = &other.substitute {
621            if let Some(this_substitute) = &self.substitute {
622                self.substitute = Some(SubstituteConfig::merged(this_substitute, other_substitute));
623            } else {
624                self.substitute = Some(other_substitute.clone());
625            }
626        }
627
628        if let Some(other_contributors) = &other.contributors {
629            if let Some(this_contributors) = &mut self.contributors {
630                this_contributors.merge(other_contributors);
631            } else {
632                self.contributors = Some(other_contributors.clone());
633            }
634        }
635
636        if other.punctuation_in_quote {
637            self.punctuation_in_quote = true;
638        }
639    }
640}
641
642impl BibliographyOptions {
643    /// Convert bibliography-entry overrides into bibliography-only runtime config.
644    #[must_use]
645    pub fn to_bibliography_config(&self) -> BibliographyConfig {
646        BibliographyConfig {
647            article_journal: self.article_journal.clone(),
648            subsequent_author_substitute: self.subsequent_author_substitute.clone(),
649            subsequent_author_substitute_rule: self.subsequent_author_substitute_rule.clone(),
650            hanging_indent: self.hanging_indent,
651            entry_suffix: self.entry_suffix.clone(),
652            separator: self.separator.clone(),
653            suppress_period_after_url: self.suppress_period_after_url,
654            custom: None,
655            compound_numeric: self.compound_numeric.clone(),
656            sort_partitioning: self.sort_partitioning.clone(),
657            unknown_fields: std::collections::BTreeMap::new(),
658        }
659    }
660
661    /// Convert bibliography-local overrides into the runtime config shape.
662    #[must_use]
663    pub fn to_config(&self) -> Config {
664        Config {
665            substitute: self.substitute.clone(),
666            processing: self.processing.clone(),
667            locale_override: None,
668            localize: self.localize.clone(),
669            multilingual: self.multilingual.clone(),
670            contributors: self.contributors.clone(),
671            dates: self.dates.clone(),
672            titles: self.titles.clone(),
673            locators: None,
674            page_range_format: self.page_range_format.clone(),
675            links: self.links.clone(),
676            punctuation_in_quote: self.punctuation_in_quote,
677            volume_pages_delimiter: self.volume_pages_delimiter.clone(),
678            strip_periods: self.strip_periods,
679            notes: None,
680            integral_name_memory: None,
681            org_abbreviation_memory: None,
682            custom: self.custom.clone(),
683            unknown_fields: std::collections::BTreeMap::new(),
684        }
685    }
686
687    /// Merge bibliography-local overrides over a base config.
688    #[must_use]
689    pub fn merged_with(&self, base: &Config) -> Config {
690        Config::merged(base, &self.to_config())
691    }
692
693    /// Merge `other` into `self`, with `other` taking precedence for each field.
694    pub fn merge(&mut self, other: &BibliographyOptions) {
695        crate::merge_options!(
696            self,
697            other,
698            processing,
699            localize,
700            multilingual,
701            dates,
702            titles,
703            page_range_format,
704            links,
705            volume_pages_delimiter,
706            strip_periods,
707            article_journal,
708            subsequent_author_substitute,
709            subsequent_author_substitute_rule,
710            hanging_indent,
711            entry_suffix,
712            separator,
713            compound_numeric,
714            sort_partitioning,
715            label_mode,
716            label_wrap,
717            date_position,
718            title_terminator,
719            repeated_author_rendering,
720            custom,
721        );
722
723        self.merge_shared_fields(other);
724    }
725
726    fn merge_shared_fields(&mut self, other: &BibliographyOptions) {
727        if let Some(other_substitute) = &other.substitute {
728            if let Some(this_substitute) = &self.substitute {
729                self.substitute = Some(SubstituteConfig::merged(this_substitute, other_substitute));
730            } else {
731                self.substitute = Some(other_substitute.clone());
732            }
733        }
734
735        if let Some(other_contributors) = &other.contributors {
736            if let Some(this_contributors) = &mut self.contributors {
737                this_contributors.merge(other_contributors);
738            } else {
739                self.contributors = Some(other_contributors.clone());
740            }
741        }
742
743        if other.punctuation_in_quote {
744            self.punctuation_in_quote = true;
745        }
746        if other.suppress_period_after_url {
747            self.suppress_period_after_url = true;
748        }
749
750        for (key, value) in &other.unknown_fields {
751            self.unknown_fields.insert(key.clone(), value.clone());
752        }
753    }
754}
755
756/// Deserialize contributor config from either a preset name or explicit config.
757fn deserialize_contributor_config<'de, D>(
758    deserializer: D,
759) -> Result<Option<ContributorConfig>, D::Error>
760where
761    D: serde::Deserializer<'de>,
762{
763    let value: Option<ContributorConfigEntry> = Option::deserialize(deserializer)?;
764    Ok(value.map(|entry| entry.resolve()))
765}
766
767/// Deserialize date config from either a preset name or explicit config.
768fn deserialize_date_config<'de, D>(deserializer: D) -> Result<Option<DateConfig>, D::Error>
769where
770    D: serde::Deserializer<'de>,
771{
772    let value: Option<DateConfigEntry> = Option::deserialize(deserializer)?;
773    Ok(value.map(|entry| entry.resolve()))
774}
775
776/// Deserialize titles config from either a preset name or explicit config.
777fn deserialize_titles_config<'de, D>(
778    deserializer: D,
779) -> Result<Option<crate::options::titles::TitlesConfig>, D::Error>
780where
781    D: serde::Deserializer<'de>,
782{
783    let value: Option<crate::options::titles::TitlesConfigEntry> =
784        Option::deserialize(deserializer)?;
785    Ok(value.map(|entry| entry.resolve()))
786}
787
788/// Deserialize locator config from either a preset name or explicit config.
789fn deserialize_locator_config<'de, D>(deserializer: D) -> Result<Option<LocatorConfig>, D::Error>
790where
791    D: serde::Deserializer<'de>,
792{
793    let value: Option<LocatorConfigEntry> = Option::deserialize(deserializer)?;
794    Ok(value.map(|entry| entry.resolve()))
795}
796
797impl<'de> Deserialize<'de> for Config {
798    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
799    where
800        D: serde::Deserializer<'de>,
801    {
802        #[derive(Deserialize)]
803        #[serde(rename_all = "kebab-case")]
804        struct ConfigWire {
805            #[serde(skip_serializing_if = "Option::is_none")]
806            substitute: Option<SubstituteConfig>,
807            #[serde(skip_serializing_if = "Option::is_none")]
808            processing: Option<Processing>,
809            #[serde(skip_serializing_if = "Option::is_none")]
810            locale_override: Option<String>,
811            #[serde(skip_serializing_if = "Option::is_none")]
812            localize: Option<Localize>,
813            #[serde(skip_serializing_if = "Option::is_none")]
814            multilingual: Option<MultilingualConfig>,
815            #[serde(
816                skip_serializing_if = "Option::is_none",
817                deserialize_with = "deserialize_contributor_config",
818                default
819            )]
820            contributors: Option<ContributorConfig>,
821            #[serde(
822                skip_serializing_if = "Option::is_none",
823                deserialize_with = "deserialize_date_config",
824                default
825            )]
826            dates: Option<DateConfig>,
827            #[serde(
828                skip_serializing_if = "Option::is_none",
829                deserialize_with = "deserialize_titles_config",
830                default
831            )]
832            titles: Option<crate::options::titles::TitlesConfig>,
833            #[serde(
834                skip_serializing_if = "Option::is_none",
835                deserialize_with = "deserialize_locator_config",
836                default
837            )]
838            locators: Option<LocatorConfig>,
839            #[serde(skip_serializing_if = "Option::is_none")]
840            page_range_format: Option<PageRangeFormat>,
841            #[serde(skip_serializing_if = "Option::is_none")]
842            links: Option<LinksConfig>,
843            #[serde(default, skip_serializing_if = "std::ops::Not::not")]
844            punctuation_in_quote: bool,
845            #[serde(skip_serializing_if = "Option::is_none")]
846            volume_pages_delimiter: Option<DelimiterPunctuation>,
847            #[serde(skip_serializing_if = "Option::is_none", rename = "strip-periods")]
848            strip_periods: Option<bool>,
849            #[serde(skip_serializing_if = "Option::is_none")]
850            notes: Option<NoteConfig>,
851            #[serde(skip_serializing_if = "Option::is_none")]
852            integral_name_memory: Option<IntegralNameMemoryConfig>,
853            #[serde(skip_serializing_if = "Option::is_none")]
854            org_abbreviation_memory: Option<OrgAbbreviationMemoryConfig>,
855            #[serde(default)]
856            profile: Option<serde_yaml::Value>,
857            #[serde(skip_serializing_if = "Option::is_none")]
858            custom: Option<HashMap<String, serde_json::Value>>,
859            #[serde(flatten)]
860            unknown_fields: std::collections::BTreeMap<String, serde_yaml::Value>,
861        }
862
863        let wire = ConfigWire::deserialize(deserializer)?;
864        if wire.profile.is_some() {
865            return Err(serde::de::Error::custom(
866                "`options.profile` was removed; use `options.contributors`, `citation.options.label-wrap`, `citation.options.group-delimiter`, `bibliography.options.label-mode`, `bibliography.options.label-wrap`, `bibliography.options.date-position`, `bibliography.options.title-terminator`, `bibliography.options.repeated-author-rendering`, or `bibliography.options.volume-pages-delimiter`",
867            ));
868        }
869
870        Ok(Self {
871            substitute: wire.substitute,
872            processing: wire.processing,
873            locale_override: wire.locale_override,
874            localize: wire.localize,
875            multilingual: wire.multilingual,
876            contributors: wire.contributors,
877            dates: wire.dates,
878            titles: wire.titles,
879            locators: wire.locators,
880            page_range_format: wire.page_range_format,
881            links: wire.links,
882            punctuation_in_quote: wire.punctuation_in_quote,
883            volume_pages_delimiter: wire.volume_pages_delimiter,
884            strip_periods: wire.strip_periods,
885            notes: wire.notes,
886            integral_name_memory: wire.integral_name_memory,
887            org_abbreviation_memory: wire.org_abbreviation_memory,
888            custom: wire.custom,
889            unknown_fields: wire.unknown_fields,
890        })
891    }
892}
893
894#[cfg(test)]
895#[allow(
896    clippy::unwrap_used,
897    clippy::expect_used,
898    clippy::panic,
899    clippy::indexing_slicing,
900    clippy::todo,
901    clippy::unimplemented,
902    clippy::unreachable,
903    clippy::get_unwrap,
904    reason = "Panicking is acceptable and often desired in tests."
905)]
906mod tests {
907    use super::*;
908
909    #[test]
910    fn test_config_default() {
911        let config = Config::default();
912        assert!(config.substitute.is_none());
913        assert!(config.processing.is_none());
914    }
915
916    #[test]
917    fn test_author_date_processing() {
918        let processing = Processing::AuthorDate;
919        let config = processing.config();
920        assert!(config.disambiguate.unwrap().year_suffix);
921        assert_eq!(
922            processing.default_bibliography_sort(),
923            Some(crate::presets::SortPreset::AuthorDateTitle)
924        );
925        assert_eq!(
926            config.sort,
927            Some(SortEntry::Preset(
928                crate::presets::SortPreset::AuthorDateTitle
929            ))
930        );
931    }
932
933    #[test]
934    fn test_processing_default_bibliography_sorts() {
935        assert_eq!(Processing::Numeric.default_bibliography_sort(), None);
936        assert_eq!(
937            Processing::Note.default_bibliography_sort(),
938            Some(crate::presets::SortPreset::AuthorTitleDate)
939        );
940        assert_eq!(
941            Processing::Label(LabelConfig::default()).default_bibliography_sort(),
942            Some(crate::presets::SortPreset::AuthorDateTitle)
943        );
944    }
945
946    #[test]
947    fn test_processing_default_citation_sort_policy_is_explicit_only() {
948        assert_eq!(
949            Processing::AuthorDate.default_citation_sort_policy(),
950            CitationSortPolicy::ExplicitOnly
951        );
952        assert_eq!(
953            Processing::Note.default_citation_sort_policy(),
954            CitationSortPolicy::ExplicitOnly
955        );
956    }
957
958    #[test]
959    fn test_substitute_default() {
960        let sub = Substitute::default();
961        assert_eq!(sub.template.len(), 3);
962    }
963
964    #[test]
965    fn test_config_yaml_roundtrip() {
966        let yaml = r#"
967substitute:
968  contributor-role-form: short
969  template:
970    - editor
971    - title
972processing: author-date
973contributors:
974  display-as-sort: first
975  and: symbol
976"#;
977        let config: Config = serde_yaml::from_str(yaml).unwrap();
978        assert!(config.substitute.is_some());
979        assert_eq!(config.processing, Some(Processing::AuthorDate));
980        assert_eq!(
981            config.contributors.as_ref().unwrap().and,
982            Some(AndOptions::Symbol)
983        );
984    }
985
986    #[test]
987    fn test_contributor_config_preset() {
988        // Test that a preset name parses and resolves correctly for contributors
989        let yaml = r#"contributors: apa"#;
990        let config: Config = serde_yaml::from_str(yaml).unwrap();
991        let contributors = config.contributors.unwrap();
992        assert_eq!(contributors.and, Some(AndOptions::Symbol));
993        assert_eq!(contributors.display_as_sort, Some(DisplayAsSort::First));
994    }
995
996    #[test]
997    fn test_role_label_presets_parse_and_resolve_precedence() {
998        let yaml = r#"
999contributors:
1000  role:
1001    preset: short-suffix
1002    roles:
1003      editor:
1004        preset: long-suffix
1005"#;
1006        let config: Config = serde_yaml::from_str(yaml).unwrap();
1007        let contributors = config.contributors.unwrap();
1008
1009        assert_eq!(
1010            contributors.effective_role_label_preset(&crate::template::ContributorRole::Editor),
1011            Some(RoleLabelPreset::LongSuffix)
1012        );
1013        assert_eq!(
1014            contributors.effective_role_label_preset(&crate::template::ContributorRole::Translator),
1015            Some(RoleLabelPreset::ShortSuffix)
1016        );
1017
1018        // Scalar shorthand form — must parse identically for preset-only case
1019        let yaml_scalar = r#"
1020contributors:
1021  role: short-suffix
1022"#;
1023        let config2: Config = serde_yaml::from_str(yaml_scalar).unwrap();
1024        let contributors2 = config2.contributors.unwrap();
1025
1026        assert_eq!(
1027            contributors2
1028                .effective_role_label_preset(&crate::template::ContributorRole::Translator),
1029            Some(RoleLabelPreset::ShortSuffix)
1030        );
1031    }
1032
1033    #[test]
1034    fn test_role_specific_name_order_override_is_available() {
1035        let yaml = r#"
1036contributors:
1037  role:
1038    roles:
1039      translator:
1040        name-order: given-first
1041"#;
1042        let config: Config = serde_yaml::from_str(yaml).unwrap();
1043        let contributors = config.contributors.unwrap();
1044
1045        assert_eq!(
1046            contributors.effective_role_name_order(&crate::template::ContributorRole::Translator),
1047            Some(&crate::template::NameOrder::GivenFirst)
1048        );
1049    }
1050
1051    #[test]
1052    fn test_date_config_preset() {
1053        // Test that a preset name parses and resolves correctly for dates
1054        let yaml = r#"dates: long"#;
1055        let config: Config = serde_yaml::from_str(yaml).unwrap();
1056        let dates = config.dates.unwrap();
1057        assert_eq!(dates.month, MonthFormat::Long);
1058    }
1059
1060    #[test]
1061    fn test_titles_config_preset() {
1062        // Test that a preset name parses and resolves correctly for titles
1063        let yaml = r#"titles: chicago"#;
1064        let config: Config = serde_yaml::from_str(yaml).unwrap();
1065        let titles = config.titles.unwrap();
1066        assert_eq!(titles.component.unwrap().quote, Some(true));
1067        assert_eq!(titles.monograph.unwrap().emph, Some(true));
1068    }
1069
1070    #[test]
1071    fn test_substitute_config_preset() {
1072        // Test that a preset name parses correctly
1073        let yaml = r#"substitute: standard"#;
1074        let config: Config = serde_yaml::from_str(yaml).unwrap();
1075        assert!(config.substitute.is_some());
1076        let resolved = config.substitute.unwrap().resolve();
1077        assert_eq!(resolved.template.len(), 3);
1078        assert_eq!(resolved.template[0], SubstituteKey::Editor);
1079    }
1080
1081    #[test]
1082    fn test_substitute_config_explicit() {
1083        // Test that explicit config still works
1084        let yaml = r#"
1085substitute:
1086  template:
1087    - title
1088    - editor
1089"#;
1090        let config: Config = serde_yaml::from_str(yaml).unwrap();
1091        let resolved = config.substitute.unwrap().resolve();
1092        assert_eq!(resolved.template[0], SubstituteKey::Title);
1093        assert_eq!(resolved.template[1], SubstituteKey::Editor);
1094    }
1095
1096    #[test]
1097    fn test_config_merge_precedence() {
1098        // Base config with global options
1099        let base_yaml = r#"
1100processing: author-date
1101locale-override: en-US-base
1102contributors:
1103  display-as-sort: first
1104  and: symbol
1105"#;
1106        let mut base: Config = serde_yaml::from_str(base_yaml).unwrap();
1107
1108        // Override config (e.g., citation-specific options)
1109        let override_yaml = r#"
1110contributors:
1111  and: text
1112locale-override: en-US-chicago
1113"#;
1114        let override_config: Config = serde_yaml::from_str(override_yaml).unwrap();
1115
1116        // Merge: override takes precedence
1117        base.merge(&override_config);
1118
1119        // Processing should remain from base (not overridden)
1120        assert_eq!(base.processing, Some(Processing::AuthorDate));
1121        assert_eq!(base.locale_override.as_deref(), Some("en-US-chicago"));
1122
1123        // Contributors should be merged with override values taking precedence
1124        assert_eq!(
1125            base.contributors.as_ref().unwrap().and,
1126            Some(AndOptions::Text)
1127        );
1128    }
1129
1130    #[test]
1131    fn test_config_deserializes_locale_override() {
1132        let config: Config = serde_yaml::from_str("locale-override: en-US-chicago").unwrap();
1133        assert_eq!(config.locale_override.as_deref(), Some("en-US-chicago"));
1134    }
1135
1136    #[test]
1137    fn test_config_merged_convenience() {
1138        let base = Config {
1139            processing: Some(Processing::AuthorDate),
1140            ..Default::default()
1141        };
1142        let override_config = Config {
1143            punctuation_in_quote: true,
1144            ..Default::default()
1145        };
1146
1147        let merged = Config::merged(&base, &override_config);
1148
1149        // Both fields preserved
1150        assert_eq!(merged.processing, Some(Processing::AuthorDate));
1151        assert!(merged.punctuation_in_quote);
1152    }
1153
1154    #[test]
1155    fn test_citation_options_merge_overrides_citation_fields_only() {
1156        let base = Config {
1157            processing: Some(Processing::AuthorDate),
1158            ..Default::default()
1159        };
1160
1161        let overrides = CitationOptions {
1162            strip_periods: Some(true),
1163            locators: Some(LocatorConfig::default()),
1164            ..Default::default()
1165        };
1166
1167        let merged = overrides.merged_with(&base);
1168        assert_eq!(merged.processing, Some(Processing::AuthorDate));
1169        assert!(merged.strip_periods.unwrap_or(false));
1170        assert!(merged.locators.is_some());
1171    }
1172
1173    #[test]
1174    fn test_bibliography_options_merge_projects_shared_fields_only() {
1175        let base = Config {
1176            processing: Some(Processing::AuthorDate),
1177            ..Default::default()
1178        };
1179
1180        let overrides = BibliographyOptions {
1181            entry_suffix: Some(".".to_string()),
1182            separator: Some(", ".to_string()),
1183            suppress_period_after_url: true,
1184            ..Default::default()
1185        };
1186
1187        let merged = overrides.merged_with(&base);
1188        assert_eq!(merged.processing, Some(Processing::AuthorDate));
1189        assert!(merged.locators.is_none());
1190        assert!(merged.notes.is_none());
1191        let bibliography = overrides.to_bibliography_config();
1192        assert_eq!(bibliography.entry_suffix.as_deref(), Some("."));
1193        assert_eq!(bibliography.separator.as_deref(), Some(", "));
1194        assert!(bibliography.suppress_period_after_url);
1195    }
1196
1197    #[test]
1198    fn test_bibliography_options_merge_leaves_shared_base_when_only_shared_overrides_exist() {
1199        let base = Config {
1200            processing: Some(Processing::AuthorDate),
1201            ..Default::default()
1202        };
1203
1204        let overrides = BibliographyOptions {
1205            contributors: Some(ContributorConfig::default()),
1206            ..Default::default()
1207        };
1208
1209        let merged = overrides.merged_with(&base);
1210        assert_eq!(merged.processing, Some(Processing::AuthorDate));
1211        assert!(merged.contributors.is_some());
1212    }
1213
1214    #[test]
1215    fn citation_options_captures_unknown_fields_for_forward_compat() {
1216        let yaml = "future-key: true\n";
1217        let opts: CitationOptions = serde_yaml::from_str(yaml).unwrap();
1218        assert!(opts.unknown_fields.contains_key("future-key"));
1219    }
1220
1221    #[test]
1222    fn bibliography_options_captures_unknown_fields_for_forward_compat() {
1223        let yaml = "future-key: true\n";
1224        let opts: BibliographyOptions = serde_yaml::from_str(yaml).unwrap();
1225        assert!(opts.unknown_fields.contains_key("future-key"));
1226    }
1227
1228    #[test]
1229    fn note_config_captures_unknown_fields_for_forward_compat() {
1230        let yaml = "punctuation: inside\nfuture-key: true\n";
1231        let cfg: NoteConfig = serde_yaml::from_str(yaml).unwrap();
1232        assert!(cfg.unknown_fields.contains_key("future-key"));
1233        assert_eq!(cfg.punctuation, Some(NoteQuotePlacement::Inside));
1234    }
1235}