acdc_parser/model/inlines/
mod.rs

1use std::collections::HashSet;
2
3use serde::{
4    Deserialize, Serialize,
5    de::{self, Deserializer},
6    ser::{Error as _, SerializeMap, Serializer},
7};
8
9pub(crate) mod converter;
10mod macros;
11mod text;
12
13pub use converter::inlines_to_string;
14pub use macros::*;
15pub use text::*;
16
17use crate::{Anchor, BlockMetadata, ElementAttributes, Image, Source, StemNotation, Title};
18
19/// An `InlineNode` represents an inline node in a document.
20///
21/// An inline node is a structural element in a document that can contain other inline
22/// nodes and are only valid within a paragraph (a leaf).
23#[non_exhaustive]
24#[derive(Clone, Debug, PartialEq)]
25pub enum InlineNode {
26    // This is just "normal" text
27    PlainText(Plain),
28    // This is raw text only found in Delimited Pass blocks
29    RawText(Raw),
30    // This is verbatim text found in Delimited Literal and Listing blocks
31    VerbatimText(Verbatim),
32    BoldText(Bold),
33    ItalicText(Italic),
34    MonospaceText(Monospace),
35    HighlightText(Highlight),
36    SubscriptText(Subscript),
37    SuperscriptText(Superscript),
38    CurvedQuotationText(CurvedQuotation),
39    CurvedApostropheText(CurvedApostrophe),
40    StandaloneCurvedApostrophe(StandaloneCurvedApostrophe),
41    LineBreak(LineBreak),
42    InlineAnchor(Anchor),
43    Macro(InlineMacro),
44    /// Callout reference marker in verbatim content: `<1>`, `<.>`, etc.
45    CalloutRef(CalloutRef),
46}
47
48/// An inline macro - a functional element that produces inline content.
49///
50/// Unlike a struct with `name`/`target`/`attributes` fields, `InlineMacro` is an **enum**
51/// where each variant represents a specific macro type with its own specialized fields.
52///
53/// # Variants Overview
54///
55/// | Variant | `AsciiDoc` Syntax | Description |
56/// |---------|-----------------|-------------|
57/// | `Link` | `link:url[text]` | Explicit link with optional text |
58/// | `Url` | `\https://...` or `link:` | URL reference |
59/// | `Mailto` | `mailto:addr[text]` | Email link |
60/// | `Autolink` | `<\https://...>` | Auto-detected URL |
61/// | `CrossReference` | `<<id>>` or `xref:id[]` | Internal document reference |
62/// | `Image` | `image:file.png[alt]` | Inline image |
63/// | `Icon` | `icon:name[]` | Icon reference (font or image) |
64/// | `Footnote` | `footnote:[text]` | Footnote reference |
65/// | `Keyboard` | `kbd:[Ctrl+C]` | Keyboard shortcut |
66/// | `Button` | `btn:[OK]` | UI button label |
67/// | `Menu` | `menu:File[Save]` | Menu navigation path |
68/// | `Pass` | `pass:[content]` | Passthrough (no processing) |
69/// | `Stem` | `stem:[formula]` | Math notation |
70/// | `IndexTerm` | `((term))` or `(((term)))` | Index term (visible or hidden) |
71///
72/// # Example
73///
74/// ```
75/// # use acdc_parser::{InlineMacro, InlineNode};
76/// fn extract_link_target(node: &InlineNode) -> Option<String> {
77///     match node {
78///         InlineNode::Macro(InlineMacro::Link(link)) => Some(link.target.to_string()),
79///         InlineNode::Macro(InlineMacro::Url(url)) => Some(url.target.to_string()),
80///         InlineNode::Macro(InlineMacro::CrossReference(xref)) => Some(xref.target.clone()),
81///         _ => None,
82///     }
83/// }
84/// ```
85#[non_exhaustive]
86#[derive(Clone, Debug, PartialEq, Serialize)]
87pub enum InlineMacro {
88    /// Footnote reference: `footnote:[content]` or `footnote:id[content]`
89    Footnote(Footnote),
90    /// Icon macro: `icon:name[attributes]`
91    Icon(Icon),
92    /// Inline image: `image:path[alt,width,height]`
93    Image(Box<Image>),
94    /// Keyboard shortcut: `kbd:[Ctrl+C]`
95    Keyboard(Keyboard),
96    /// UI button: `btn:[Label]`
97    Button(Button),
98    /// Menu path: `menu:TopLevel[Item > Subitem]`
99    Menu(Menu),
100    /// URL with optional text: parsed from `link:` macro or bare URLs
101    Url(Url),
102    /// Explicit link macro: `link:target[text]`
103    Link(Link),
104    /// Email link: `mailto:address[text]`
105    Mailto(Mailto),
106    /// Auto-detected URL: `<\https://example.com>`
107    Autolink(Autolink),
108    /// Cross-reference: `<<id,text>>` or `xref:id[text]`
109    CrossReference(CrossReference),
110    /// Inline passthrough: `pass:[content]` - not serialized to ASG
111    Pass(Pass),
112    /// Inline math: `stem:[formula]` or `latexmath:[...]` / `asciimath:[...]`
113    Stem(Stem),
114    /// Index term: `((term))` (visible) or `(((term)))` (hidden)
115    IndexTerm(IndexTerm),
116}
117
118/// Macro to serialize inline format types (Bold, Italic, Monospace, etc.)
119/// All these types share identical structure and serialization logic.
120macro_rules! serialize_inline_format {
121    ($map:expr, $value:expr, $variant:literal) => {{
122        $map.serialize_entry("name", "span")?;
123        $map.serialize_entry("type", "inline")?;
124        $map.serialize_entry("variant", $variant)?;
125        $map.serialize_entry("form", &$value.form)?;
126        if let Some(role) = &$value.role {
127            $map.serialize_entry("role", role)?;
128        }
129        if let Some(id) = &$value.id {
130            $map.serialize_entry("id", id)?;
131        }
132        $map.serialize_entry("inlines", &$value.content)?;
133        $map.serialize_entry("location", &$value.location)?;
134    }};
135}
136
137impl Serialize for InlineNode {
138    #[allow(clippy::too_many_lines)]
139    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
140    where
141        S: Serializer,
142    {
143        let mut map = serializer.serialize_map(None)?;
144
145        match self {
146            InlineNode::PlainText(plain) => {
147                map.serialize_entry("name", "text")?;
148                map.serialize_entry("type", "string")?;
149                map.serialize_entry("value", &plain.content)?;
150                map.serialize_entry("location", &plain.location)?;
151            }
152            InlineNode::RawText(raw) => {
153                map.serialize_entry("name", "raw")?;
154                map.serialize_entry("type", "string")?;
155                map.serialize_entry("value", &raw.content)?;
156                map.serialize_entry("location", &raw.location)?;
157            }
158            InlineNode::VerbatimText(verbatim) => {
159                // We use "text" here to make sure the TCK passes, even though this is raw
160                // text.
161                map.serialize_entry("name", "text")?;
162                map.serialize_entry("type", "string")?;
163                map.serialize_entry("value", &verbatim.content)?;
164                map.serialize_entry("location", &verbatim.location)?;
165            }
166            InlineNode::HighlightText(highlight) => {
167                serialize_inline_format!(map, highlight, "mark");
168            }
169            InlineNode::ItalicText(italic) => {
170                serialize_inline_format!(map, italic, "emphasis");
171            }
172            InlineNode::BoldText(bold) => {
173                serialize_inline_format!(map, bold, "strong");
174            }
175            InlineNode::MonospaceText(monospace) => {
176                serialize_inline_format!(map, monospace, "code");
177            }
178            InlineNode::SubscriptText(subscript) => {
179                serialize_inline_format!(map, subscript, "subscript");
180            }
181            InlineNode::SuperscriptText(superscript) => {
182                serialize_inline_format!(map, superscript, "superscript");
183            }
184            InlineNode::CurvedQuotationText(curved_quotation) => {
185                serialize_inline_format!(map, curved_quotation, "curved_quotation");
186            }
187            InlineNode::CurvedApostropheText(curved_apostrophe) => {
188                serialize_inline_format!(map, curved_apostrophe, "curved_apostrophe");
189            }
190            InlineNode::StandaloneCurvedApostrophe(standalone) => {
191                map.serialize_entry("name", "curved_apostrophe")?;
192                map.serialize_entry("type", "string")?;
193                map.serialize_entry("location", &standalone.location)?;
194            }
195            InlineNode::LineBreak(line_break) => {
196                map.serialize_entry("name", "break")?;
197                map.serialize_entry("type", "inline")?;
198                map.serialize_entry("location", &line_break.location)?;
199            }
200            InlineNode::InlineAnchor(anchor) => {
201                map.serialize_entry("name", "anchor")?;
202                map.serialize_entry("type", "inline")?;
203                map.serialize_entry("id", &anchor.id)?;
204                if let Some(xreflabel) = &anchor.xreflabel {
205                    map.serialize_entry("xreflabel", xreflabel)?;
206                }
207                map.serialize_entry("location", &anchor.location)?;
208            }
209            InlineNode::Macro(macro_node) => {
210                serialize_inline_macro::<S>(macro_node, &mut map)?;
211            }
212            InlineNode::CalloutRef(callout_ref) => {
213                map.serialize_entry("name", "callout_reference")?;
214                map.serialize_entry("type", "inline")?;
215                map.serialize_entry("variant", &callout_ref.kind)?;
216                map.serialize_entry("number", &callout_ref.number)?;
217                map.serialize_entry("location", &callout_ref.location)?;
218            }
219        }
220        map.end()
221    }
222}
223
224fn serialize_inline_macro<S>(
225    macro_node: &InlineMacro,
226    map: &mut S::SerializeMap,
227) -> Result<(), S::Error>
228where
229    S: Serializer,
230{
231    match macro_node {
232        InlineMacro::Footnote(f) => serialize_footnote::<S>(f, map),
233        InlineMacro::Icon(i) => serialize_icon::<S>(i, map),
234        InlineMacro::Image(i) => serialize_image::<S>(i, map),
235        InlineMacro::Keyboard(k) => serialize_keyboard::<S>(k, map),
236        InlineMacro::Button(b) => serialize_button::<S>(b, map),
237        InlineMacro::Menu(m) => serialize_menu::<S>(m, map),
238        InlineMacro::Url(u) => serialize_url::<S>(u, map),
239        InlineMacro::Mailto(m) => serialize_mailto::<S>(m, map),
240        InlineMacro::Link(l) => serialize_link::<S>(l, map),
241        InlineMacro::Autolink(a) => serialize_autolink::<S>(a, map),
242        InlineMacro::CrossReference(x) => serialize_xref::<S>(x, map),
243        InlineMacro::Stem(s) => serialize_stem::<S>(s, map),
244        InlineMacro::IndexTerm(i) => serialize_indexterm::<S>(i, map),
245        InlineMacro::Pass(_) => Err(S::Error::custom(
246            "inline passthrough macros are not part of the ASG specification and cannot be serialized",
247        )),
248    }
249}
250
251fn serialize_footnote<S>(f: &Footnote, map: &mut S::SerializeMap) -> Result<(), S::Error>
252where
253    S: Serializer,
254{
255    map.serialize_entry("name", "footnote")?;
256    map.serialize_entry("type", "inline")?;
257    map.serialize_entry("id", &f.id)?;
258    map.serialize_entry("inlines", &f.content)?;
259    map.serialize_entry("location", &f.location)
260}
261
262fn serialize_icon<S>(i: &Icon, map: &mut S::SerializeMap) -> Result<(), S::Error>
263where
264    S: Serializer,
265{
266    map.serialize_entry("name", "icon")?;
267    map.serialize_entry("type", "inline")?;
268    map.serialize_entry("target", &i.target)?;
269    if !i.attributes.is_empty() {
270        map.serialize_entry("attributes", &i.attributes)?;
271    }
272    map.serialize_entry("location", &i.location)
273}
274
275fn serialize_image<S>(i: &Image, map: &mut S::SerializeMap) -> Result<(), S::Error>
276where
277    S: Serializer,
278{
279    map.serialize_entry("name", "image")?;
280    map.serialize_entry("type", "inline")?;
281    map.serialize_entry("title", &i.title)?;
282    map.serialize_entry("target", &i.source)?;
283    map.serialize_entry("location", &i.location)
284}
285
286fn serialize_keyboard<S>(k: &Keyboard, map: &mut S::SerializeMap) -> Result<(), S::Error>
287where
288    S: Serializer,
289{
290    map.serialize_entry("name", "keyboard")?;
291    map.serialize_entry("type", "inline")?;
292    map.serialize_entry("keys", &k.keys)?;
293    map.serialize_entry("location", &k.location)
294}
295
296fn serialize_button<S>(b: &Button, map: &mut S::SerializeMap) -> Result<(), S::Error>
297where
298    S: Serializer,
299{
300    map.serialize_entry("name", "button")?;
301    map.serialize_entry("type", "inline")?;
302    map.serialize_entry("label", &b.label)?;
303    map.serialize_entry("location", &b.location)
304}
305
306fn serialize_menu<S>(m: &Menu, map: &mut S::SerializeMap) -> Result<(), S::Error>
307where
308    S: Serializer,
309{
310    map.serialize_entry("name", "menu")?;
311    map.serialize_entry("type", "inline")?;
312    map.serialize_entry("target", &m.target)?;
313    if !m.items.is_empty() {
314        map.serialize_entry("items", &m.items)?;
315    }
316    map.serialize_entry("location", &m.location)
317}
318
319fn serialize_url<S>(u: &Url, map: &mut S::SerializeMap) -> Result<(), S::Error>
320where
321    S: Serializer,
322{
323    map.serialize_entry("name", "ref")?;
324    map.serialize_entry("type", "inline")?;
325    map.serialize_entry("variant", "link")?;
326    map.serialize_entry("target", &u.target)?;
327    map.serialize_entry("location", &u.location)?;
328    map.serialize_entry("attributes", &u.attributes)
329}
330
331fn serialize_mailto<S>(m: &Mailto, map: &mut S::SerializeMap) -> Result<(), S::Error>
332where
333    S: Serializer,
334{
335    map.serialize_entry("name", "ref")?;
336    map.serialize_entry("type", "inline")?;
337    map.serialize_entry("variant", "mailto")?;
338    map.serialize_entry("target", &m.target)?;
339    map.serialize_entry("location", &m.location)?;
340    map.serialize_entry("attributes", &m.attributes)
341}
342
343fn serialize_link<S>(l: &Link, map: &mut S::SerializeMap) -> Result<(), S::Error>
344where
345    S: Serializer,
346{
347    map.serialize_entry("name", "ref")?;
348    map.serialize_entry("type", "inline")?;
349    map.serialize_entry("variant", "link")?;
350    map.serialize_entry("target", &l.target)?;
351    map.serialize_entry("location", &l.location)?;
352    map.serialize_entry("attributes", &l.attributes)
353}
354
355fn serialize_autolink<S>(a: &Autolink, map: &mut S::SerializeMap) -> Result<(), S::Error>
356where
357    S: Serializer,
358{
359    map.serialize_entry("name", "ref")?;
360    map.serialize_entry("type", "inline")?;
361    map.serialize_entry("variant", "autolink")?;
362    map.serialize_entry("target", &a.url)?;
363    map.serialize_entry("location", &a.location)
364}
365
366fn serialize_xref<S>(x: &CrossReference, map: &mut S::SerializeMap) -> Result<(), S::Error>
367where
368    S: Serializer,
369{
370    map.serialize_entry("name", "xref")?;
371    map.serialize_entry("type", "inline")?;
372    map.serialize_entry("target", &x.target)?;
373    if let Some(text) = &x.text {
374        map.serialize_entry("text", text)?;
375    }
376    map.serialize_entry("location", &x.location)
377}
378
379fn serialize_stem<S>(s: &Stem, map: &mut S::SerializeMap) -> Result<(), S::Error>
380where
381    S: Serializer,
382{
383    map.serialize_entry("name", "stem")?;
384    map.serialize_entry("type", "inline")?;
385    map.serialize_entry("content", &s.content)?;
386    map.serialize_entry("notation", &s.notation)?;
387    map.serialize_entry("location", &s.location)
388}
389
390fn serialize_indexterm<S>(i: &IndexTerm, map: &mut S::SerializeMap) -> Result<(), S::Error>
391where
392    S: Serializer,
393{
394    map.serialize_entry("name", "indexterm")?;
395    map.serialize_entry("type", "inline")?;
396    map.serialize_entry("term", i.term())?;
397    if let Some(secondary) = i.secondary() {
398        map.serialize_entry("secondary", secondary)?;
399    }
400    if let Some(tertiary) = i.tertiary() {
401        map.serialize_entry("tertiary", tertiary)?;
402    }
403    map.serialize_entry("visible", &i.is_visible())?;
404    map.serialize_entry("location", &i.location)
405}
406
407// =============================================================================
408// InlineNode Deserialization Infrastructure
409// =============================================================================
410
411/// Raw field collector for `InlineNode` deserialization.
412#[derive(Default, Deserialize)]
413#[serde(default)]
414struct RawInlineFields {
415    name: Option<String>,
416    r#type: Option<String>,
417    value: Option<String>,
418    variant: Option<String>,
419    form: Option<Form>,
420    location: Option<crate::Location>,
421    inlines: Option<Vec<InlineNode>>,
422    title: Option<Vec<InlineNode>>,
423    target: Option<serde_json::Value>,
424    attributes: Option<ElementAttributes>,
425    role: Option<String>,
426    id: Option<String>,
427    text: Option<String>,
428    items: Option<Vec<String>>,
429    keys: Option<Vec<String>>,
430    label: Option<String>,
431    content: Option<String>,
432    notation: Option<StemNotation>,
433    substitutions: Option<HashSet<crate::Substitution>>,
434    xreflabel: Option<String>,
435    bracketed: Option<bool>,
436    number: Option<usize>,
437    // Index term fields
438    term: Option<String>,
439    secondary: Option<String>,
440    tertiary: Option<String>,
441    visible: Option<bool>,
442}
443
444// -----------------------------------------------------------------------------
445// Per-variant InlineNode constructors
446// -----------------------------------------------------------------------------
447
448fn construct_plain_text<E: de::Error>(raw: RawInlineFields) -> Result<InlineNode, E> {
449    Ok(InlineNode::PlainText(Plain {
450        content: raw.value.ok_or_else(|| E::missing_field("value"))?,
451        location: raw.location.ok_or_else(|| E::missing_field("location"))?,
452    }))
453}
454
455fn construct_raw_text<E: de::Error>(raw: RawInlineFields) -> Result<InlineNode, E> {
456    Ok(InlineNode::RawText(Raw {
457        content: raw.value.ok_or_else(|| E::missing_field("value"))?,
458        location: raw.location.ok_or_else(|| E::missing_field("location"))?,
459    }))
460}
461
462fn construct_verbatim_text<E: de::Error>(raw: RawInlineFields) -> Result<InlineNode, E> {
463    Ok(InlineNode::VerbatimText(Verbatim {
464        content: raw.value.ok_or_else(|| E::missing_field("value"))?,
465        location: raw.location.ok_or_else(|| E::missing_field("location"))?,
466    }))
467}
468
469fn construct_standalone_curved_apostrophe<E: de::Error>(
470    raw: RawInlineFields,
471) -> Result<InlineNode, E> {
472    Ok(InlineNode::StandaloneCurvedApostrophe(
473        StandaloneCurvedApostrophe {
474            location: raw.location.ok_or_else(|| E::missing_field("location"))?,
475        },
476    ))
477}
478
479fn construct_line_break<E: de::Error>(raw: RawInlineFields) -> Result<InlineNode, E> {
480    Ok(InlineNode::LineBreak(LineBreak {
481        location: raw.location.ok_or_else(|| E::missing_field("location"))?,
482    }))
483}
484
485fn construct_anchor<E: de::Error>(raw: RawInlineFields) -> Result<InlineNode, E> {
486    Ok(InlineNode::InlineAnchor(Anchor {
487        id: raw.id.ok_or_else(|| E::missing_field("id"))?,
488        xreflabel: raw.xreflabel,
489        location: raw.location.ok_or_else(|| E::missing_field("location"))?,
490    }))
491}
492
493fn construct_icon<E: de::Error>(raw: RawInlineFields) -> Result<InlineNode, E> {
494    let target_val = raw.target.ok_or_else(|| E::missing_field("target"))?;
495    let target: Source = serde_json::from_value(target_val).map_err(E::custom)?;
496    Ok(InlineNode::Macro(InlineMacro::Icon(Icon {
497        attributes: raw.attributes.unwrap_or_default(),
498        target,
499        location: raw.location.ok_or_else(|| E::missing_field("location"))?,
500    })))
501}
502
503fn construct_image<E: de::Error>(raw: RawInlineFields) -> Result<InlineNode, E> {
504    let title = Title::new(raw.title.ok_or_else(|| E::missing_field("title"))?);
505    let target_val = raw.target.ok_or_else(|| E::missing_field("target"))?;
506    let source: Source = serde_json::from_value(target_val).map_err(E::custom)?;
507    Ok(InlineNode::Macro(InlineMacro::Image(Box::new(Image {
508        title,
509        source,
510        metadata: BlockMetadata::default(),
511        location: raw.location.ok_or_else(|| E::missing_field("location"))?,
512    }))))
513}
514
515fn construct_footnote<E: de::Error>(raw: RawInlineFields) -> Result<InlineNode, E> {
516    let inlines = raw.inlines.ok_or_else(|| E::missing_field("inlines"))?;
517    Ok(InlineNode::Macro(InlineMacro::Footnote(Footnote {
518        id: raw.id,
519        content: inlines,
520        number: 0,
521        location: raw.location.ok_or_else(|| E::missing_field("location"))?,
522    })))
523}
524
525fn construct_keyboard<E: de::Error>(raw: RawInlineFields) -> Result<InlineNode, E> {
526    Ok(InlineNode::Macro(InlineMacro::Keyboard(Keyboard {
527        keys: raw.keys.ok_or_else(|| E::missing_field("keys"))?,
528        location: raw.location.ok_or_else(|| E::missing_field("location"))?,
529    })))
530}
531
532fn construct_button<E: de::Error>(raw: RawInlineFields) -> Result<InlineNode, E> {
533    Ok(InlineNode::Macro(InlineMacro::Button(Button {
534        label: raw.label.ok_or_else(|| E::missing_field("label"))?,
535        location: raw.location.ok_or_else(|| E::missing_field("location"))?,
536    })))
537}
538
539fn construct_menu<E: de::Error>(raw: RawInlineFields) -> Result<InlineNode, E> {
540    let target_val = raw.target.ok_or_else(|| E::missing_field("target"))?;
541    let target: String = serde_json::from_value(target_val).map_err(E::custom)?;
542    Ok(InlineNode::Macro(InlineMacro::Menu(Menu {
543        target,
544        items: raw.items.unwrap_or_default(),
545        location: raw.location.ok_or_else(|| E::missing_field("location"))?,
546    })))
547}
548
549fn construct_stem<E: de::Error>(raw: RawInlineFields) -> Result<InlineNode, E> {
550    Ok(InlineNode::Macro(InlineMacro::Stem(Stem {
551        content: raw.content.ok_or_else(|| E::missing_field("content"))?,
552        notation: raw.notation.ok_or_else(|| E::missing_field("notation"))?,
553        location: raw.location.ok_or_else(|| E::missing_field("location"))?,
554    })))
555}
556
557fn construct_indexterm<E: de::Error>(raw: RawInlineFields) -> Result<InlineNode, E> {
558    let term = raw.term.ok_or_else(|| E::missing_field("term"))?;
559    let visible = raw.visible.ok_or_else(|| E::missing_field("visible"))?;
560    let location = raw.location.ok_or_else(|| E::missing_field("location"))?;
561
562    let kind = if visible {
563        IndexTermKind::Flow(term)
564    } else {
565        IndexTermKind::Concealed {
566            term,
567            secondary: raw.secondary,
568            tertiary: raw.tertiary,
569        }
570    };
571
572    Ok(InlineNode::Macro(InlineMacro::IndexTerm(IndexTerm {
573        kind,
574        location,
575    })))
576}
577
578fn construct_xref<E: de::Error>(raw: RawInlineFields) -> Result<InlineNode, E> {
579    let target_val = raw.target.ok_or_else(|| E::missing_field("target"))?;
580    let target: String = serde_json::from_value(target_val).map_err(E::custom)?;
581    Ok(InlineNode::Macro(InlineMacro::CrossReference(
582        crate::model::CrossReference {
583            target,
584            text: raw.text,
585            location: raw.location.ok_or_else(|| E::missing_field("location"))?,
586        },
587    )))
588}
589
590fn construct_ref<E: de::Error>(raw: RawInlineFields) -> Result<InlineNode, E> {
591    let variant = raw.variant.ok_or_else(|| E::missing_field("variant"))?;
592    let target_val = raw.target.ok_or_else(|| E::missing_field("target"))?;
593    let target: Source = serde_json::from_value(target_val).map_err(E::custom)?;
594    let location = raw.location.ok_or_else(|| E::missing_field("location"))?;
595
596    match variant.as_str() {
597        "url" => Ok(InlineNode::Macro(InlineMacro::Url(Url {
598            text: vec![],
599            attributes: raw.attributes.unwrap_or_default(),
600            target,
601            location,
602        }))),
603        "link" => Ok(InlineNode::Macro(InlineMacro::Link(Link {
604            text: None,
605            attributes: raw.attributes.unwrap_or_default(),
606            target,
607            location,
608        }))),
609        "mailto" => Ok(InlineNode::Macro(InlineMacro::Mailto(Mailto {
610            text: vec![],
611            attributes: raw.attributes.unwrap_or_default(),
612            target,
613            location,
614        }))),
615        "autolink" => Ok(InlineNode::Macro(InlineMacro::Autolink(Autolink {
616            url: target,
617            bracketed: raw.bracketed.unwrap_or(false),
618            location,
619        }))),
620        "pass" => Ok(InlineNode::Macro(InlineMacro::Pass(Pass {
621            text: raw.text,
622            substitutions: raw.substitutions.unwrap_or_default(),
623            location,
624            kind: PassthroughKind::default(),
625        }))),
626        _ => {
627            tracing::error!(variant = %variant, "invalid inline macro variant");
628            Err(E::custom("invalid inline macro variant"))
629        }
630    }
631}
632
633fn construct_span<E: de::Error>(raw: RawInlineFields) -> Result<InlineNode, E> {
634    let variant = raw.variant.ok_or_else(|| E::missing_field("variant"))?;
635    let inlines = raw.inlines.ok_or_else(|| E::missing_field("inlines"))?;
636    let location = raw.location.ok_or_else(|| E::missing_field("location"))?;
637    let role = raw.role;
638    let id = raw.id;
639
640    match variant.as_str() {
641        "strong" => Ok(InlineNode::BoldText(Bold {
642            role,
643            id,
644            form: raw.form.unwrap_or(Form::Constrained),
645            content: inlines,
646            location,
647        })),
648        "emphasis" => Ok(InlineNode::ItalicText(Italic {
649            role,
650            id,
651            form: raw.form.unwrap_or(Form::Constrained),
652            content: inlines,
653            location,
654        })),
655        "code" => Ok(InlineNode::MonospaceText(Monospace {
656            role,
657            id,
658            form: raw.form.unwrap_or(Form::Constrained),
659            content: inlines,
660            location,
661        })),
662        "mark" => Ok(InlineNode::HighlightText(Highlight {
663            role,
664            id,
665            form: raw.form.unwrap_or(Form::Constrained),
666            content: inlines,
667            location,
668        })),
669        "subscript" => Ok(InlineNode::SubscriptText(Subscript {
670            role,
671            id,
672            form: raw.form.unwrap_or(Form::Unconstrained),
673            content: inlines,
674            location,
675        })),
676        "superscript" => Ok(InlineNode::SuperscriptText(Superscript {
677            role,
678            id,
679            form: raw.form.unwrap_or(Form::Unconstrained),
680            content: inlines,
681            location,
682        })),
683        "curved_quotation" => Ok(InlineNode::CurvedQuotationText(CurvedQuotation {
684            role,
685            id,
686            form: raw.form.unwrap_or(Form::Unconstrained),
687            content: inlines,
688            location,
689        })),
690        "curved_apostrophe" => Ok(InlineNode::CurvedApostropheText(CurvedApostrophe {
691            role,
692            id,
693            form: raw.form.unwrap_or(Form::Unconstrained),
694            content: inlines,
695            location,
696        })),
697        _ => {
698            tracing::error!(variant = %variant, "invalid inline node variant");
699            Err(E::custom("invalid inline node variant"))
700        }
701    }
702}
703
704fn construct_callout_ref<E: de::Error>(raw: RawInlineFields) -> Result<InlineNode, E> {
705    let variant = raw.variant.ok_or_else(|| E::missing_field("variant"))?;
706    let number = raw.number.ok_or_else(|| E::missing_field("number"))?;
707    let location = raw.location.ok_or_else(|| E::missing_field("location"))?;
708
709    let kind = match variant.as_str() {
710        "explicit" => CalloutRefKind::Explicit,
711        "auto" => CalloutRefKind::Auto,
712        _ => {
713            tracing::error!(variant = %variant, "invalid callout ref variant");
714            return Err(E::custom("invalid callout ref variant"));
715        }
716    };
717
718    Ok(InlineNode::CalloutRef(CalloutRef {
719        kind,
720        number,
721        location,
722    }))
723}
724
725/// Dispatch to the appropriate `InlineNode` constructor based on name/type
726fn dispatch_inline<E: de::Error>(raw: RawInlineFields) -> Result<InlineNode, E> {
727    let name = raw.name.clone().ok_or_else(|| E::missing_field("name"))?;
728    let ty = raw.r#type.clone().ok_or_else(|| E::missing_field("type"))?;
729
730    match (name.as_str(), ty.as_str()) {
731        ("text", "string") => construct_plain_text(raw),
732        ("raw", "string") => construct_raw_text(raw),
733        ("verbatim", "string") => construct_verbatim_text(raw),
734        ("curved_apostrophe", "string") => construct_standalone_curved_apostrophe(raw),
735        ("break", "inline") => construct_line_break(raw),
736        ("anchor", "inline") => construct_anchor(raw),
737        ("icon", "inline") => construct_icon(raw),
738        ("image", "inline") => construct_image(raw),
739        ("footnote", "inline") => construct_footnote(raw),
740        ("keyboard", "inline") => construct_keyboard(raw),
741        ("btn" | "button", "inline") => construct_button(raw),
742        ("menu", "inline") => construct_menu(raw),
743        ("stem", "inline") => construct_stem(raw),
744        ("indexterm", "inline") => construct_indexterm(raw),
745        ("xref", "inline") => construct_xref(raw),
746        ("ref", "inline") => construct_ref(raw),
747        ("span", "inline") => construct_span(raw),
748        ("callout_reference", "inline") => construct_callout_ref(raw),
749        _ => {
750            tracing::error!(name = %name, r#type = %ty, "invalid inline node");
751            Err(E::custom("invalid inline node"))
752        }
753    }
754}
755
756impl<'de> Deserialize<'de> for InlineNode {
757    fn deserialize<D>(deserializer: D) -> Result<InlineNode, D::Error>
758    where
759        D: Deserializer<'de>,
760    {
761        let raw: RawInlineFields = RawInlineFields::deserialize(deserializer)?;
762        dispatch_inline(raw)
763    }
764}