sim-codec-doc 0.1.3

Markup document codec backends for SIM.
Documentation
use asciidork_ast as adoc;

use crate::backend::MarkupLoss;
use crate::markup::{Inline, MathSource};

use super::super::asciidoc_support::{asciidoc_id, inline_plain_text, link_target, symbol_text};
use super::AsciiDocDecoder;

impl AsciiDocDecoder<'_> {
    pub(in crate::asciidoc) fn inlines(&mut self, nodes: &adoc::InlineNodes<'_>) -> Vec<Inline> {
        let mut out = Vec::new();
        for node in nodes.iter() {
            if let Some(item) = self.inline(&node.content, node.loc) {
                out.push(item);
            }
        }
        out
    }

    fn inline(&mut self, node: &adoc::Inline<'_>, loc: adoc::SourceLocation) -> Option<Inline> {
        match node {
            adoc::Inline::Text(text) => Some(Inline::Text(text.to_string())),
            adoc::Inline::MultiCharWhitespace(_) | adoc::Inline::Newline => {
                Some(Inline::Text(" ".to_owned()))
            }
            adoc::Inline::LineBreak => Some(Inline::Text("\n".to_owned())),
            adoc::Inline::Quote(kind, children) => {
                let quote = match kind {
                    adoc::QuoteKind::Double => "\"",
                    adoc::QuoteKind::Single => "'",
                };
                Some(Inline::Text(format!(
                    "{quote}{}{quote}",
                    inline_plain_text(&self.inlines(children))
                )))
            }
            adoc::Inline::Span(kind, _, children) => match kind {
                adoc::SpanKind::Bold => Some(Inline::Strong(self.inlines(children))),
                adoc::SpanKind::Italic => Some(Inline::Emph(self.inlines(children))),
                adoc::SpanKind::LitMono | adoc::SpanKind::Mono => {
                    Some(Inline::Code(inline_plain_text(&self.inlines(children))))
                }
                adoc::SpanKind::Text => {
                    Some(Inline::Text(inline_plain_text(&self.inlines(children))))
                }
                _ => self.raw_inline_from_loc(loc, "span", "unsupported asciidoc span"),
            },
            adoc::Inline::InlinePassthru(children) => Some(Inline::Raw {
                backend: asciidoc_id(),
                text: inline_plain_text(&self.inlines(children)),
            }),
            adoc::Inline::Macro(macro_node) => self.macro_inline(macro_node, loc),
            adoc::Inline::SpecialChar(kind) => Some(Inline::Text(
                match kind {
                    adoc::SpecialCharKind::Ampersand => "&",
                    adoc::SpecialCharKind::LessThan => "<",
                    adoc::SpecialCharKind::GreaterThan => ">",
                }
                .to_owned(),
            )),
            adoc::Inline::Symbol(kind) => Some(Inline::Text(symbol_text(*kind).to_owned())),
            adoc::Inline::CurlyQuote(kind) => Some(Inline::Text(
                match kind {
                    adoc::CurlyKind::LeftDouble | adoc::CurlyKind::RightDouble => "\"",
                    _ => "'",
                }
                .to_owned(),
            )),
            adoc::Inline::LineComment(text) => self.raw_inline(
                text.to_string(),
                "comment",
                "asciidoc line comment is preserved raw",
            ),
            adoc::Inline::Discarded
            | adoc::Inline::CalloutNum(_)
            | adoc::Inline::CalloutTuck(_)
            | adoc::Inline::InlineAnchor(_)
            | adoc::Inline::BiblioAnchor(_)
            | adoc::Inline::IndexTerm(_)
            | adoc::Inline::SpacedDashes(_, _) => None,
        }
    }

    fn macro_inline(
        &mut self,
        macro_node: &adoc::MacroNode<'_>,
        loc: adoc::SourceLocation,
    ) -> Option<Inline> {
        match macro_node {
            adoc::MacroNode::Link {
                scheme,
                target,
                attrs,
                ..
            } => {
                let target = link_target(*scheme, target);
                let label = attrs
                    .as_ref()
                    .and_then(|attrs| adoc::AttrData::positional_at(attrs, 0))
                    .map(|nodes| self.inlines(nodes))
                    .unwrap_or_else(|| vec![Inline::Text(target.clone())]);
                Some(Inline::Link { label, target })
            }
            adoc::MacroNode::Mailto {
                address, linktext, ..
            } => {
                let target = format!("mailto:{}", address.src);
                let label = linktext
                    .as_ref()
                    .map(|nodes| self.inlines(nodes))
                    .unwrap_or_else(|| vec![Inline::Text(address.src.to_string())]);
                Some(Inline::Link { label, target })
            }
            adoc::MacroNode::Xref {
                target, linktext, ..
            } => {
                let target = format!("#{}", target.src);
                let label = linktext
                    .as_ref()
                    .map(|nodes| self.inlines(nodes))
                    .unwrap_or_else(|| vec![Inline::Text(target.clone())]);
                Some(Inline::Link { label, target })
            }
            adoc::MacroNode::Plugin(plugin)
                if matches!(plugin.name.as_str(), "stem" | "latexmath" | "asciimath") =>
            {
                let text = plugin
                    .target
                    .as_ref()
                    .map(|target| target.src.to_string())
                    .unwrap_or_else(|| plugin.source.src.to_string());
                Some(Inline::Math(MathSource {
                    notation: "asciidoc".to_owned(),
                    text,
                }))
            }
            adoc::MacroNode::InlineImage { target, .. } => self.raw_inline(
                target.src.to_string(),
                "inline-image",
                "asciidoc inline image is preserved raw",
            ),
            adoc::MacroNode::Plugin(plugin) => self.raw_inline(
                plugin.source.src.to_string(),
                plugin.name.as_str(),
                "unsupported asciidoc macro is preserved raw",
            ),
            _ => self.raw_inline_from_loc(loc, "macro", "unsupported asciidoc macro"),
        }
    }

    fn raw_inline_from_loc(
        &mut self,
        loc: adoc::SourceLocation,
        path: &str,
        reason: &str,
    ) -> Option<Inline> {
        let text = self
            .input
            .get(usize::try_from(loc.start).ok()?..usize::try_from(loc.end).ok()?)
            .unwrap_or_default()
            .to_owned();
        self.raw_inline(text, path, reason)
    }

    fn raw_inline(&mut self, text: String, path: &str, reason: &str) -> Option<Inline> {
        if self.preserve_raw {
            self.fidelity.preserved_raw.push(text.clone());
            Some(Inline::Raw {
                backend: asciidoc_id(),
                text,
            })
        } else {
            self.fidelity.dropped.push(MarkupLoss {
                path: path.to_owned(),
                reason: reason.to_owned(),
            });
            None
        }
    }
}