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