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#[non_exhaustive]
24#[derive(Clone, Debug, PartialEq)]
25pub enum InlineNode {
26 PlainText(Plain),
28 RawText(Raw),
30 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#[non_exhaustive]
83#[derive(Clone, Debug, PartialEq, Serialize)]
84pub enum InlineMacro {
85 Footnote(Footnote),
87 Icon(Icon),
89 Image(Box<Image>),
91 Keyboard(Keyboard),
93 Button(Button),
95 Menu(Menu),
97 Url(Url),
99 Link(Link),
101 Mailto(Mailto),
103 Autolink(Autolink),
105 CrossReference(CrossReference),
107 Pass(Pass),
109 Stem(Stem),
111}
112
113macro_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 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#[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
351fn 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
590fn 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}