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}
45
46/// An inline macro - a functional element that produces inline content.
47///
48/// Unlike a struct with `name`/`target`/`attributes` fields, `InlineMacro` is an **enum**
49/// where each variant represents a specific macro type with its own specialized fields.
50///
51/// # Variants Overview
52///
53/// | Variant | `AsciiDoc` Syntax | Description |
54/// |---------|-----------------|-------------|
55/// | `Link` | `link:url[text]` | Explicit link with optional text |
56/// | `Url` | `\https://...` or `link:` | URL reference |
57/// | `Mailto` | `mailto:addr[text]` | Email link |
58/// | `Autolink` | `<\https://...>` | Auto-detected URL |
59/// | `CrossReference` | `<<id>>` or `xref:id[]` | Internal document reference |
60/// | `Image` | `image:file.png[alt]` | Inline image |
61/// | `Icon` | `icon:name[]` | Icon reference (font or image) |
62/// | `Footnote` | `footnote:[text]` | Footnote reference |
63/// | `Keyboard` | `kbd:[Ctrl+C]` | Keyboard shortcut |
64/// | `Button` | `btn:[OK]` | UI button label |
65/// | `Menu` | `menu:File[Save]` | Menu navigation path |
66/// | `Pass` | `pass:[content]` | Passthrough (no processing) |
67/// | `Stem` | `stem:[formula]` | Math notation |
68///
69/// # Example
70///
71/// ```
72/// # use acdc_parser::{InlineMacro, InlineNode};
73/// fn extract_link_target(node: &InlineNode) -> Option<String> {
74///     match node {
75///         InlineNode::Macro(InlineMacro::Link(link)) => Some(link.target.to_string()),
76///         InlineNode::Macro(InlineMacro::Url(url)) => Some(url.target.to_string()),
77///         InlineNode::Macro(InlineMacro::CrossReference(xref)) => Some(xref.target.clone()),
78///         _ => None,
79///     }
80/// }
81/// ```
82#[non_exhaustive]
83#[derive(Clone, Debug, PartialEq, Serialize)]
84pub enum InlineMacro {
85    /// Footnote reference: `footnote:[content]` or `footnote:id[content]`
86    Footnote(Footnote),
87    /// Icon macro: `icon:name[attributes]`
88    Icon(Icon),
89    /// Inline image: `image:path[alt,width,height]`
90    Image(Box<Image>),
91    /// Keyboard shortcut: `kbd:[Ctrl+C]`
92    Keyboard(Keyboard),
93    /// UI button: `btn:[Label]`
94    Button(Button),
95    /// Menu path: `menu:TopLevel[Item > Subitem]`
96    Menu(Menu),
97    /// URL with optional text: parsed from `link:` macro or bare URLs
98    Url(Url),
99    /// Explicit link macro: `link:target[text]`
100    Link(Link),
101    /// Email link: `mailto:address[text]`
102    Mailto(Mailto),
103    /// Auto-detected URL: `<\https://example.com>`
104    Autolink(Autolink),
105    /// Cross-reference: `<<id,text>>` or `xref:id[text]`
106    CrossReference(CrossReference),
107    /// Inline passthrough: `pass:[content]` - not serialized to ASG
108    Pass(Pass),
109    /// Inline math: `stem:[formula]` or `latexmath:[...]` / `asciimath:[...]`
110    Stem(Stem),
111}
112
113/// Macro to serialize inline format types (Bold, Italic, Monospace, etc.)
114/// All these types share identical structure and serialization logic.
115macro_rules! serialize_inline_format {
116    ($map:expr, $value:expr, $variant:literal) => {{
117        $map.serialize_entry("name", "span")?;
118        $map.serialize_entry("type", "inline")?;
119        $map.serialize_entry("variant", $variant)?;
120        $map.serialize_entry("form", &$value.form)?;
121        if let Some(role) = &$value.role {
122            $map.serialize_entry("role", role)?;
123        }
124        if let Some(id) = &$value.id {
125            $map.serialize_entry("id", id)?;
126        }
127        $map.serialize_entry("inlines", &$value.content)?;
128        $map.serialize_entry("location", &$value.location)?;
129    }};
130}
131
132impl Serialize for InlineNode {
133    #[allow(clippy::too_many_lines)]
134    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
135    where
136        S: Serializer,
137    {
138        let mut map = serializer.serialize_map(None)?;
139
140        match self {
141            InlineNode::PlainText(plain) => {
142                map.serialize_entry("name", "text")?;
143                map.serialize_entry("type", "string")?;
144                map.serialize_entry("value", &plain.content)?;
145                map.serialize_entry("location", &plain.location)?;
146            }
147            InlineNode::RawText(raw) => {
148                map.serialize_entry("name", "raw")?;
149                map.serialize_entry("type", "string")?;
150                map.serialize_entry("value", &raw.content)?;
151                map.serialize_entry("location", &raw.location)?;
152            }
153            InlineNode::VerbatimText(verbatim) => {
154                // We use "text" here to make sure the TCK passes, even though this is raw
155                // text.
156                map.serialize_entry("name", "text")?;
157                map.serialize_entry("type", "string")?;
158                map.serialize_entry("value", &verbatim.content)?;
159                map.serialize_entry("location", &verbatim.location)?;
160            }
161            InlineNode::HighlightText(highlight) => {
162                serialize_inline_format!(map, highlight, "mark");
163            }
164            InlineNode::ItalicText(italic) => {
165                serialize_inline_format!(map, italic, "emphasis");
166            }
167            InlineNode::BoldText(bold) => {
168                serialize_inline_format!(map, bold, "strong");
169            }
170            InlineNode::MonospaceText(monospace) => {
171                serialize_inline_format!(map, monospace, "code");
172            }
173            InlineNode::SubscriptText(subscript) => {
174                serialize_inline_format!(map, subscript, "subscript");
175            }
176            InlineNode::SuperscriptText(superscript) => {
177                serialize_inline_format!(map, superscript, "superscript");
178            }
179            InlineNode::CurvedQuotationText(curved_quotation) => {
180                serialize_inline_format!(map, curved_quotation, "curved_quotation");
181            }
182            InlineNode::CurvedApostropheText(curved_apostrophe) => {
183                serialize_inline_format!(map, curved_apostrophe, "curved_apostrophe");
184            }
185            InlineNode::StandaloneCurvedApostrophe(standalone) => {
186                map.serialize_entry("name", "curved_apostrophe")?;
187                map.serialize_entry("type", "string")?;
188                map.serialize_entry("location", &standalone.location)?;
189            }
190            InlineNode::LineBreak(line_break) => {
191                map.serialize_entry("name", "break")?;
192                map.serialize_entry("type", "inline")?;
193                map.serialize_entry("location", &line_break.location)?;
194            }
195            InlineNode::InlineAnchor(anchor) => {
196                map.serialize_entry("name", "anchor")?;
197                map.serialize_entry("type", "inline")?;
198                map.serialize_entry("id", &anchor.id)?;
199                if let Some(xreflabel) = &anchor.xreflabel {
200                    map.serialize_entry("xreflabel", xreflabel)?;
201                }
202                map.serialize_entry("location", &anchor.location)?;
203            }
204            InlineNode::Macro(macro_node) => {
205                serialize_inline_macro::<S>(macro_node, &mut map)?;
206            }
207        }
208        map.end()
209    }
210}
211
212fn serialize_inline_macro<S>(
213    macro_node: &InlineMacro,
214    map: &mut S::SerializeMap,
215) -> Result<(), S::Error>
216where
217    S: Serializer,
218{
219    match macro_node {
220        InlineMacro::Footnote(footnote) => {
221            map.serialize_entry("name", "footnote")?;
222            map.serialize_entry("type", "inline")?;
223            map.serialize_entry("id", &footnote.id)?;
224            map.serialize_entry("inlines", &footnote.content)?;
225            map.serialize_entry("location", &footnote.location)?;
226        }
227        InlineMacro::Icon(icon) => {
228            map.serialize_entry("name", "icon")?;
229            map.serialize_entry("type", "inline")?;
230            map.serialize_entry("target", &icon.target)?;
231            if !icon.attributes.is_empty() {
232                map.serialize_entry("attributes", &icon.attributes)?;
233            }
234            map.serialize_entry("location", &icon.location)?;
235        }
236        InlineMacro::Image(image) => {
237            map.serialize_entry("name", "image")?;
238            map.serialize_entry("type", "inline")?;
239            map.serialize_entry("title", &image.title)?;
240            map.serialize_entry("target", &image.source)?;
241            map.serialize_entry("location", &image.location)?;
242        }
243        InlineMacro::Keyboard(keyboard) => {
244            map.serialize_entry("name", "keyboard")?;
245            map.serialize_entry("type", "inline")?;
246            map.serialize_entry("keys", &keyboard.keys)?;
247            map.serialize_entry("location", &keyboard.location)?;
248        }
249        InlineMacro::Button(button) => {
250            map.serialize_entry("name", "button")?;
251            map.serialize_entry("type", "inline")?;
252            map.serialize_entry("label", &button.label)?;
253            map.serialize_entry("location", &button.location)?;
254        }
255        InlineMacro::Menu(menu) => {
256            map.serialize_entry("name", "menu")?;
257            map.serialize_entry("type", "inline")?;
258            map.serialize_entry("target", &menu.target)?;
259            if !menu.items.is_empty() {
260                map.serialize_entry("items", &menu.items)?;
261            }
262            map.serialize_entry("location", &menu.location)?;
263        }
264        InlineMacro::Url(url) => {
265            map.serialize_entry("name", "ref")?;
266            map.serialize_entry("type", "inline")?;
267            map.serialize_entry("variant", "link")?;
268            map.serialize_entry("target", &url.target)?;
269            map.serialize_entry("location", &url.location)?;
270            map.serialize_entry("attributes", &url.attributes)?;
271        }
272        InlineMacro::Mailto(mailto) => {
273            map.serialize_entry("name", "ref")?;
274            map.serialize_entry("type", "inline")?;
275            map.serialize_entry("variant", "mailto")?;
276            map.serialize_entry("target", &mailto.target)?;
277            map.serialize_entry("location", &mailto.location)?;
278            map.serialize_entry("attributes", &mailto.attributes)?;
279        }
280        InlineMacro::Link(link) => {
281            map.serialize_entry("name", "ref")?;
282            map.serialize_entry("type", "inline")?;
283            map.serialize_entry("variant", "link")?;
284            map.serialize_entry("target", &link.target)?;
285            map.serialize_entry("location", &link.location)?;
286            map.serialize_entry("attributes", &link.attributes)?;
287        }
288        InlineMacro::Autolink(autolink) => {
289            map.serialize_entry("name", "ref")?;
290            map.serialize_entry("type", "inline")?;
291            map.serialize_entry("variant", "autolink")?;
292            map.serialize_entry("target", &autolink.url)?;
293            map.serialize_entry("location", &autolink.location)?;
294        }
295        InlineMacro::CrossReference(xref) => {
296            map.serialize_entry("name", "xref")?;
297            map.serialize_entry("type", "inline")?;
298            map.serialize_entry("target", &xref.target)?;
299            if let Some(text) = &xref.text {
300                map.serialize_entry("text", text)?;
301            }
302            map.serialize_entry("location", &xref.location)?;
303        }
304        InlineMacro::Pass(_) => {
305            return Err(S::Error::custom(
306                "inline passthrough macros are not part of the ASG specification and cannot be serialized",
307            ));
308        }
309        InlineMacro::Stem(stem) => {
310            map.serialize_entry("name", "stem")?;
311            map.serialize_entry("type", "inline")?;
312            map.serialize_entry("content", &stem.content)?;
313            map.serialize_entry("notation", &stem.notation)?;
314            map.serialize_entry("location", &stem.location)?;
315        }
316    }
317    Ok(())
318}
319
320// =============================================================================
321// InlineNode Deserialization Infrastructure
322// =============================================================================
323
324/// Raw field collector for `InlineNode` deserialization.
325#[derive(Default, Deserialize)]
326#[serde(default)]
327struct RawInlineFields {
328    name: Option<String>,
329    r#type: Option<String>,
330    value: Option<String>,
331    variant: Option<String>,
332    form: Option<Form>,
333    location: Option<crate::Location>,
334    inlines: Option<Vec<InlineNode>>,
335    title: Option<Vec<InlineNode>>,
336    target: Option<serde_json::Value>,
337    attributes: Option<ElementAttributes>,
338    role: Option<String>,
339    id: Option<String>,
340    text: Option<String>,
341    items: Option<Vec<String>>,
342    keys: Option<Vec<String>>,
343    label: Option<String>,
344    content: Option<String>,
345    notation: Option<StemNotation>,
346    substitutions: Option<HashSet<crate::Substitution>>,
347    xreflabel: Option<String>,
348    bracketed: Option<bool>,
349}
350
351// -----------------------------------------------------------------------------
352// Per-variant InlineNode constructors
353// -----------------------------------------------------------------------------
354
355fn construct_plain_text<E: de::Error>(raw: RawInlineFields) -> Result<InlineNode, E> {
356    Ok(InlineNode::PlainText(Plain {
357        content: raw.value.ok_or_else(|| E::missing_field("value"))?,
358        location: raw.location.ok_or_else(|| E::missing_field("location"))?,
359    }))
360}
361
362fn construct_raw_text<E: de::Error>(raw: RawInlineFields) -> Result<InlineNode, E> {
363    Ok(InlineNode::RawText(Raw {
364        content: raw.value.ok_or_else(|| E::missing_field("value"))?,
365        location: raw.location.ok_or_else(|| E::missing_field("location"))?,
366    }))
367}
368
369fn construct_verbatim_text<E: de::Error>(raw: RawInlineFields) -> Result<InlineNode, E> {
370    Ok(InlineNode::VerbatimText(Verbatim {
371        content: raw.value.ok_or_else(|| E::missing_field("value"))?,
372        location: raw.location.ok_or_else(|| E::missing_field("location"))?,
373    }))
374}
375
376fn construct_standalone_curved_apostrophe<E: de::Error>(
377    raw: RawInlineFields,
378) -> Result<InlineNode, E> {
379    Ok(InlineNode::StandaloneCurvedApostrophe(
380        StandaloneCurvedApostrophe {
381            location: raw.location.ok_or_else(|| E::missing_field("location"))?,
382        },
383    ))
384}
385
386fn construct_line_break<E: de::Error>(raw: RawInlineFields) -> Result<InlineNode, E> {
387    Ok(InlineNode::LineBreak(LineBreak {
388        location: raw.location.ok_or_else(|| E::missing_field("location"))?,
389    }))
390}
391
392fn construct_anchor<E: de::Error>(raw: RawInlineFields) -> Result<InlineNode, E> {
393    Ok(InlineNode::InlineAnchor(Anchor {
394        id: raw.id.ok_or_else(|| E::missing_field("id"))?,
395        xreflabel: raw.xreflabel,
396        location: raw.location.ok_or_else(|| E::missing_field("location"))?,
397    }))
398}
399
400fn construct_icon<E: de::Error>(raw: RawInlineFields) -> Result<InlineNode, E> {
401    let target_val = raw.target.ok_or_else(|| E::missing_field("target"))?;
402    let target: Source = serde_json::from_value(target_val).map_err(E::custom)?;
403    Ok(InlineNode::Macro(InlineMacro::Icon(Icon {
404        attributes: raw.attributes.unwrap_or_default(),
405        target,
406        location: raw.location.ok_or_else(|| E::missing_field("location"))?,
407    })))
408}
409
410fn construct_image<E: de::Error>(raw: RawInlineFields) -> Result<InlineNode, E> {
411    let title = Title::new(raw.title.ok_or_else(|| E::missing_field("title"))?);
412    let target_val = raw.target.ok_or_else(|| E::missing_field("target"))?;
413    let source: Source = serde_json::from_value(target_val).map_err(E::custom)?;
414    Ok(InlineNode::Macro(InlineMacro::Image(Box::new(Image {
415        title,
416        source,
417        metadata: BlockMetadata::default(),
418        location: raw.location.ok_or_else(|| E::missing_field("location"))?,
419    }))))
420}
421
422fn construct_footnote<E: de::Error>(raw: RawInlineFields) -> Result<InlineNode, E> {
423    let inlines = raw.inlines.ok_or_else(|| E::missing_field("inlines"))?;
424    Ok(InlineNode::Macro(InlineMacro::Footnote(Footnote {
425        id: raw.id,
426        content: inlines,
427        number: 0,
428        location: raw.location.ok_or_else(|| E::missing_field("location"))?,
429    })))
430}
431
432fn construct_keyboard<E: de::Error>(raw: RawInlineFields) -> Result<InlineNode, E> {
433    Ok(InlineNode::Macro(InlineMacro::Keyboard(Keyboard {
434        keys: raw.keys.ok_or_else(|| E::missing_field("keys"))?,
435        location: raw.location.ok_or_else(|| E::missing_field("location"))?,
436    })))
437}
438
439fn construct_button<E: de::Error>(raw: RawInlineFields) -> Result<InlineNode, E> {
440    Ok(InlineNode::Macro(InlineMacro::Button(Button {
441        label: raw.label.ok_or_else(|| E::missing_field("label"))?,
442        location: raw.location.ok_or_else(|| E::missing_field("location"))?,
443    })))
444}
445
446fn construct_menu<E: de::Error>(raw: RawInlineFields) -> Result<InlineNode, E> {
447    let target_val = raw.target.ok_or_else(|| E::missing_field("target"))?;
448    let target: String = serde_json::from_value(target_val).map_err(E::custom)?;
449    Ok(InlineNode::Macro(InlineMacro::Menu(Menu {
450        target,
451        items: raw.items.unwrap_or_default(),
452        location: raw.location.ok_or_else(|| E::missing_field("location"))?,
453    })))
454}
455
456fn construct_stem<E: de::Error>(raw: RawInlineFields) -> Result<InlineNode, E> {
457    Ok(InlineNode::Macro(InlineMacro::Stem(Stem {
458        content: raw.content.ok_or_else(|| E::missing_field("content"))?,
459        notation: raw.notation.ok_or_else(|| E::missing_field("notation"))?,
460        location: raw.location.ok_or_else(|| E::missing_field("location"))?,
461    })))
462}
463
464fn construct_xref<E: de::Error>(raw: RawInlineFields) -> Result<InlineNode, E> {
465    let target_val = raw.target.ok_or_else(|| E::missing_field("target"))?;
466    let target: String = serde_json::from_value(target_val).map_err(E::custom)?;
467    Ok(InlineNode::Macro(InlineMacro::CrossReference(
468        crate::model::CrossReference {
469            target,
470            text: raw.text,
471            location: raw.location.ok_or_else(|| E::missing_field("location"))?,
472        },
473    )))
474}
475
476fn construct_ref<E: de::Error>(raw: RawInlineFields) -> Result<InlineNode, E> {
477    let variant = raw.variant.ok_or_else(|| E::missing_field("variant"))?;
478    let target_val = raw.target.ok_or_else(|| E::missing_field("target"))?;
479    let target: Source = serde_json::from_value(target_val).map_err(E::custom)?;
480    let location = raw.location.ok_or_else(|| E::missing_field("location"))?;
481
482    match variant.as_str() {
483        "url" => Ok(InlineNode::Macro(InlineMacro::Url(Url {
484            text: vec![],
485            attributes: raw.attributes.unwrap_or_default(),
486            target,
487            location,
488        }))),
489        "link" => Ok(InlineNode::Macro(InlineMacro::Link(Link {
490            text: None,
491            attributes: raw.attributes.unwrap_or_default(),
492            target,
493            location,
494        }))),
495        "mailto" => Ok(InlineNode::Macro(InlineMacro::Mailto(Mailto {
496            text: vec![],
497            attributes: raw.attributes.unwrap_or_default(),
498            target,
499            location,
500        }))),
501        "autolink" => Ok(InlineNode::Macro(InlineMacro::Autolink(Autolink {
502            url: target,
503            bracketed: raw.bracketed.unwrap_or(false),
504            location,
505        }))),
506        "pass" => Ok(InlineNode::Macro(InlineMacro::Pass(Pass {
507            text: raw.text,
508            substitutions: raw.substitutions.unwrap_or_default(),
509            location,
510            kind: PassthroughKind::default(),
511        }))),
512        _ => {
513            tracing::error!(variant = %variant, "invalid inline macro variant");
514            Err(E::custom("invalid inline macro variant"))
515        }
516    }
517}
518
519fn construct_span<E: de::Error>(raw: RawInlineFields) -> Result<InlineNode, E> {
520    let variant = raw.variant.ok_or_else(|| E::missing_field("variant"))?;
521    let inlines = raw.inlines.ok_or_else(|| E::missing_field("inlines"))?;
522    let location = raw.location.ok_or_else(|| E::missing_field("location"))?;
523    let role = raw.role;
524    let id = raw.id;
525
526    match variant.as_str() {
527        "strong" => Ok(InlineNode::BoldText(Bold {
528            role,
529            id,
530            form: raw.form.unwrap_or(Form::Constrained),
531            content: inlines,
532            location,
533        })),
534        "emphasis" => Ok(InlineNode::ItalicText(Italic {
535            role,
536            id,
537            form: raw.form.unwrap_or(Form::Constrained),
538            content: inlines,
539            location,
540        })),
541        "code" => Ok(InlineNode::MonospaceText(Monospace {
542            role,
543            id,
544            form: raw.form.unwrap_or(Form::Constrained),
545            content: inlines,
546            location,
547        })),
548        "mark" => Ok(InlineNode::HighlightText(Highlight {
549            role,
550            id,
551            form: raw.form.unwrap_or(Form::Constrained),
552            content: inlines,
553            location,
554        })),
555        "subscript" => Ok(InlineNode::SubscriptText(Subscript {
556            role,
557            id,
558            form: raw.form.unwrap_or(Form::Unconstrained),
559            content: inlines,
560            location,
561        })),
562        "superscript" => Ok(InlineNode::SuperscriptText(Superscript {
563            role,
564            id,
565            form: raw.form.unwrap_or(Form::Unconstrained),
566            content: inlines,
567            location,
568        })),
569        "curved_quotation" => Ok(InlineNode::CurvedQuotationText(CurvedQuotation {
570            role,
571            id,
572            form: raw.form.unwrap_or(Form::Unconstrained),
573            content: inlines,
574            location,
575        })),
576        "curved_apostrophe" => Ok(InlineNode::CurvedApostropheText(CurvedApostrophe {
577            role,
578            id,
579            form: raw.form.unwrap_or(Form::Unconstrained),
580            content: inlines,
581            location,
582        })),
583        _ => {
584            tracing::error!(variant = %variant, "invalid inline node variant");
585            Err(E::custom("invalid inline node variant"))
586        }
587    }
588}
589
590/// Dispatch to the appropriate `InlineNode` constructor based on name/type
591fn dispatch_inline<E: de::Error>(raw: RawInlineFields) -> Result<InlineNode, E> {
592    let name = raw.name.clone().ok_or_else(|| E::missing_field("name"))?;
593    let ty = raw.r#type.clone().ok_or_else(|| E::missing_field("type"))?;
594
595    match (name.as_str(), ty.as_str()) {
596        ("text", "string") => construct_plain_text(raw),
597        ("raw", "string") => construct_raw_text(raw),
598        ("verbatim", "string") => construct_verbatim_text(raw),
599        ("curved_apostrophe", "string") => construct_standalone_curved_apostrophe(raw),
600        ("break", "inline") => construct_line_break(raw),
601        ("anchor", "inline") => construct_anchor(raw),
602        ("icon", "inline") => construct_icon(raw),
603        ("image", "inline") => construct_image(raw),
604        ("footnote", "inline") => construct_footnote(raw),
605        ("keyboard", "inline") => construct_keyboard(raw),
606        ("btn" | "button", "inline") => construct_button(raw),
607        ("menu", "inline") => construct_menu(raw),
608        ("stem", "inline") => construct_stem(raw),
609        ("xref", "inline") => construct_xref(raw),
610        ("ref", "inline") => construct_ref(raw),
611        ("span", "inline") => construct_span(raw),
612        _ => {
613            tracing::error!(name = %name, r#type = %ty, "invalid inline node");
614            Err(E::custom("invalid inline node"))
615        }
616    }
617}
618
619impl<'de> Deserialize<'de> for InlineNode {
620    fn deserialize<D>(deserializer: D) -> Result<InlineNode, D::Error>
621    where
622        D: Deserializer<'de>,
623    {
624        let raw: RawInlineFields = RawInlineFields::deserialize(deserializer)?;
625        dispatch_inline(raw)
626    }
627}