Skip to main content

sim_codec_doc/
asciidoc.rs

1//! AsciiDoc backend implementation over `asciidork-parser`.
2
3use std::collections::BTreeMap;
4
5use asciidork_ast as adoc;
6use asciidork_parser::prelude::{Bump, Parser, SourceFile};
7
8use crate::backend::{
9    MarkupBackend, MarkupDecodeOptions, MarkupEncodeOptions, MarkupError, MarkupFidelity,
10    MarkupLoss,
11};
12use crate::markup::{BackendId, Inline, MarkupBlock, MarkupDoc, MathSource, SourceDoc, Span};
13
14#[path = "asciidoc_support.rs"]
15mod asciidoc_support;
16#[path = "asciidoc_writer.rs"]
17mod asciidoc_writer;
18
19use asciidoc_support::{
20    RawDirective, asciidoc_id, block_plain_text, block_start, inline_plain_text, link_target,
21    mask_directives, raw_directives, span_from_loc, span_from_multi, symbol_text,
22};
23use asciidoc_writer::AsciiDocEncoder;
24
25/// Safe AsciiDoc markup backend.
26#[derive(Clone, Debug, Default)]
27pub struct AsciiDocBackend;
28
29impl MarkupBackend for AsciiDocBackend {
30    fn id(&self) -> BackendId {
31        asciidoc_id()
32    }
33
34    fn decode(
35        &self,
36        input: &str,
37        opts: &MarkupDecodeOptions,
38    ) -> Result<(MarkupDoc, MarkupFidelity), MarkupError> {
39        let directives = raw_directives(input);
40        let masked = mask_directives(input, &directives);
41        let parse_input = masked.as_ref();
42        let bump = Bump::new();
43        let parser = Parser::from_str(parse_input, SourceFile::Tmp, &bump);
44        let result = parser.parse().map_err(|diagnostics| {
45            MarkupError::Decode(
46                diagnostics
47                    .into_iter()
48                    .map(|diagnostic| diagnostic.message)
49                    .collect::<Vec<_>>()
50                    .join("; "),
51            )
52        })?;
53
54        let mut decoder = AsciiDocDecoder::new(input, opts);
55        decoder.fidelity.warnings.extend(
56            result
57                .warnings
58                .into_iter()
59                .map(|diagnostic| diagnostic.message),
60        );
61        decoder.record_directives(&directives);
62        let title = result
63            .document
64            .title()
65            .map(|title| inline_plain_text(&decoder.inlines(&title.main)));
66        let parsed_blocks = decoder.blocks_from_content(&result.document.content);
67        let blocks = decoder.merge_directives(parsed_blocks);
68        let source = opts.preserve_source.then(|| SourceDoc {
69            backend: asciidoc_id(),
70            text: input.to_owned(),
71        });
72
73        Ok((
74            MarkupDoc {
75                title,
76                blocks,
77                attrs: BTreeMap::new(),
78                source,
79            },
80            decoder.fidelity,
81        ))
82    }
83
84    fn encode(
85        &self,
86        doc: &MarkupDoc,
87        opts: &MarkupEncodeOptions,
88    ) -> Result<(String, MarkupFidelity), MarkupError> {
89        let mut encoder = AsciiDocEncoder::new(opts);
90        let source = encoder.write_doc(doc);
91        if opts.fail_on_loss && !encoder.fidelity().dropped.is_empty() {
92            return Err(MarkupError::Encode(format!(
93                "asciidoc encode dropped {} unsupported fragment(s)",
94                encoder.fidelity().dropped.len()
95            )));
96        }
97        Ok((source, encoder.into_fidelity()))
98    }
99}
100
101struct AsciiDocDecoder<'a> {
102    input: &'a str,
103    preserve_raw: bool,
104    fidelity: MarkupFidelity,
105    raw_blocks: Vec<MarkupBlock>,
106}
107
108impl<'a> AsciiDocDecoder<'a> {
109    fn new(input: &'a str, opts: &MarkupDecodeOptions) -> Self {
110        Self {
111            input,
112            preserve_raw: opts.preserve_raw,
113            fidelity: MarkupFidelity::exact(asciidoc_id()),
114            raw_blocks: Vec::new(),
115        }
116    }
117
118    fn blocks_from_content(&mut self, content: &adoc::DocContent<'_>) -> Vec<MarkupBlock> {
119        match content {
120            adoc::DocContent::Blocks(blocks) => self.blocks_from_blocks(blocks),
121            adoc::DocContent::Sections(sectioned) => {
122                let mut out = sectioned
123                    .preamble
124                    .as_ref()
125                    .map(|blocks| self.blocks_from_blocks(blocks))
126                    .unwrap_or_default();
127                for section in &sectioned.sections {
128                    out.extend(self.blocks_from_section(section));
129                }
130                out
131            }
132            adoc::DocContent::Parts(book) => {
133                let mut out = book
134                    .preamble
135                    .as_ref()
136                    .map(|blocks| self.blocks_from_blocks(blocks))
137                    .unwrap_or_default();
138                for section in &book.opening_special_sects {
139                    out.extend(self.blocks_from_section(section));
140                }
141                for part in &book.parts {
142                    out.push(MarkupBlock::Heading {
143                        level: 1,
144                        text: self.inlines(&part.title.text),
145                        id: part.title.id.as_ref().map(ToString::to_string),
146                        span: span_from_loc(part.title.meta.start_loc),
147                    });
148                    if let Some(intro) = &part.intro {
149                        out.extend(self.blocks_from_blocks(intro));
150                    }
151                    for section in &part.sections {
152                        out.extend(self.blocks_from_section(section));
153                    }
154                }
155                for section in &book.closing_special_sects {
156                    out.extend(self.blocks_from_section(section));
157                }
158                out
159            }
160        }
161    }
162
163    fn blocks_from_blocks(&mut self, blocks: &[adoc::Block<'_>]) -> Vec<MarkupBlock> {
164        blocks
165            .iter()
166            .flat_map(|block| self.blocks_from_block(block))
167            .collect()
168    }
169
170    fn blocks_from_section(&mut self, section: &adoc::Section<'_>) -> Vec<MarkupBlock> {
171        let mut blocks = vec![MarkupBlock::Heading {
172            level: section.level.saturating_add(1).clamp(1, 6),
173            text: self.inlines(&section.heading),
174            id: section.id.as_ref().map(ToString::to_string),
175            span: span_from_multi(&section.loc),
176        }];
177        blocks.extend(self.blocks_from_blocks(&section.blocks));
178        blocks
179    }
180
181    fn blocks_from_block(&mut self, block: &adoc::Block<'_>) -> Vec<MarkupBlock> {
182        let span = span_from_multi(&block.loc);
183        match &block.content {
184            adoc::BlockContent::Compound(blocks)
185                if block.context == adoc::BlockContext::BlockQuote =>
186            {
187                vec![MarkupBlock::Quote {
188                    blocks: self.blocks_from_blocks(blocks),
189                    span,
190                }]
191            }
192            adoc::BlockContent::Compound(blocks) => self.blocks_from_blocks(blocks),
193            adoc::BlockContent::Simple(inlines) => self.simple_block(block, inlines, span),
194            adoc::BlockContent::Empty(empty) => self.empty_block(block, empty, span),
195            adoc::BlockContent::Table(table) => {
196                vec![MarkupBlock::Table {
197                    header: table
198                        .header_row
199                        .as_ref()
200                        .map(|row| self.row_cells(row))
201                        .unwrap_or_default(),
202                    rows: table.rows.iter().map(|row| self.row_cells(row)).collect(),
203                    span,
204                }]
205            }
206            adoc::BlockContent::Section(section) => self.blocks_from_section(section),
207            adoc::BlockContent::DocumentAttribute(name, _) => self
208                .raw_from_span(
209                    span,
210                    name,
211                    "asciidoc document attribute is not semantic markup",
212                )
213                .into_iter()
214                .collect(),
215            adoc::BlockContent::QuotedParagraph { quote, .. } => vec![MarkupBlock::Quote {
216                blocks: vec![MarkupBlock::Paragraph {
217                    content: self.inlines(quote),
218                    span: None,
219                }],
220                span,
221            }],
222            adoc::BlockContent::List { variant, items, .. } => {
223                self.list_block(*variant, items, span)
224            }
225        }
226    }
227
228    fn simple_block(
229        &mut self,
230        block: &adoc::Block<'_>,
231        inlines: &adoc::InlineNodes<'_>,
232        span: Option<Span>,
233    ) -> Vec<MarkupBlock> {
234        match block.context {
235            adoc::BlockContext::Listing | adoc::BlockContext::Literal => {
236                vec![MarkupBlock::CodeBlock {
237                    lang: adoc::AttrData::source_language(&block.meta.attrs).map(str::to_owned),
238                    code: inline_plain_text(&self.inlines(inlines)),
239                    span,
240                }]
241            }
242            adoc::BlockContext::Passthrough
243                if adoc::AttrData::has_str_positional(&block.meta.attrs, "stem") =>
244            {
245                vec![MarkupBlock::MathBlock {
246                    source: MathSource {
247                        notation: "asciidoc".to_owned(),
248                        text: inline_plain_text(&self.inlines(inlines)),
249                    },
250                    span,
251                }]
252            }
253            adoc::BlockContext::Passthrough => self
254                .raw_from_span(
255                    span,
256                    "passthrough",
257                    "asciidoc passthrough block is preserved raw",
258                )
259                .into_iter()
260                .collect(),
261            _ => {
262                let content = self.inlines(inlines);
263                if inline_plain_text(&content).trim().is_empty() {
264                    Vec::new()
265                } else {
266                    vec![MarkupBlock::Paragraph { content, span }]
267                }
268            }
269        }
270    }
271
272    fn empty_block(
273        &mut self,
274        block: &adoc::Block<'_>,
275        empty: &adoc::EmptyMetadata<'_>,
276        span: Option<Span>,
277    ) -> Vec<MarkupBlock> {
278        match empty {
279            adoc::EmptyMetadata::Image { target, attrs, .. } => {
280                let caption = block
281                    .meta
282                    .title()
283                    .or_else(|| adoc::AttrData::positional_at(attrs, 0))
284                    .map(|nodes| self.inlines(nodes))
285                    .unwrap_or_default();
286                vec![MarkupBlock::Figure {
287                    src: target.src.to_string(),
288                    caption,
289                    span,
290                }]
291            }
292            adoc::EmptyMetadata::DiscreteHeading { level, content, id } => {
293                vec![MarkupBlock::Heading {
294                    level: level.saturating_add(1).clamp(1, 6),
295                    text: self.inlines(content),
296                    id: id.as_ref().map(ToString::to_string),
297                    span,
298                }]
299            }
300            adoc::EmptyMetadata::Comment(source) => self
301                .raw_text(
302                    source.src.to_string(),
303                    span,
304                    "comment",
305                    "asciidoc comment is preserved raw",
306                )
307                .into_iter()
308                .collect(),
309            adoc::EmptyMetadata::AudioVideo { .. } => self
310                .raw_from_span(span, "media", "asciidoc media block is not semantic markup")
311                .into_iter()
312                .collect(),
313            adoc::EmptyMetadata::None => Vec::new(),
314        }
315    }
316
317    fn list_block(
318        &mut self,
319        variant: adoc::ListVariant,
320        items: &[adoc::ListItem<'_>],
321        span: Option<Span>,
322    ) -> Vec<MarkupBlock> {
323        let ordered = match variant {
324            adoc::ListVariant::Ordered => true,
325            adoc::ListVariant::Unordered => false,
326            _ => {
327                return self
328                    .raw_from_span(span, "list", "asciidoc list variant is not supported")
329                    .into_iter()
330                    .collect();
331            }
332        };
333        let items = items
334            .iter()
335            .map(|item| {
336                let mut blocks = Vec::new();
337                let principle = self.inlines(&item.principle);
338                if !inline_plain_text(&principle).trim().is_empty() {
339                    blocks.push(MarkupBlock::Paragraph {
340                        content: principle,
341                        span: span_from_loc(item.loc()),
342                    });
343                }
344                blocks.extend(self.blocks_from_blocks(&item.blocks));
345                blocks
346            })
347            .collect();
348        vec![MarkupBlock::List {
349            ordered,
350            items,
351            span,
352        }]
353    }
354
355    fn row_cells(&mut self, row: &adoc::Row<'_>) -> Vec<Vec<Inline>> {
356        row.cells
357            .iter()
358            .map(|cell| self.cell_inlines(cell))
359            .collect()
360    }
361
362    fn cell_inlines(&mut self, cell: &adoc::Cell<'_>) -> Vec<Inline> {
363        match &cell.content {
364            adoc::CellContent::AsciiDoc(doc) => self.blocks_plain_inlines(&doc.content),
365            adoc::CellContent::Literal(nodes) => {
366                vec![Inline::Code(inline_plain_text(&self.inlines(nodes)))]
367            }
368            adoc::CellContent::Default(paras)
369            | adoc::CellContent::Header(paras)
370            | adoc::CellContent::Emphasis(paras)
371            | adoc::CellContent::Monospace(paras)
372            | adoc::CellContent::Strong(paras) => {
373                let mut out = Vec::new();
374                for (index, para) in paras.iter().enumerate() {
375                    if index > 0 {
376                        out.push(Inline::Text("\n".to_owned()));
377                    }
378                    out.extend(self.inlines(para));
379                }
380                out
381            }
382        }
383    }
384
385    fn blocks_plain_inlines(&mut self, content: &adoc::DocContent<'_>) -> Vec<Inline> {
386        vec![Inline::Text(
387            self.blocks_from_content(content)
388                .iter()
389                .map(block_plain_text)
390                .collect::<Vec<_>>()
391                .join("\n"),
392        )]
393    }
394
395    fn inlines(&mut self, nodes: &adoc::InlineNodes<'_>) -> Vec<Inline> {
396        let mut out = Vec::new();
397        for node in nodes.iter() {
398            if let Some(item) = self.inline(&node.content, node.loc) {
399                out.push(item);
400            }
401        }
402        out
403    }
404
405    fn inline(&mut self, node: &adoc::Inline<'_>, loc: adoc::SourceLocation) -> Option<Inline> {
406        match node {
407            adoc::Inline::Text(text) => Some(Inline::Text(text.to_string())),
408            adoc::Inline::MultiCharWhitespace(_) | adoc::Inline::Newline => {
409                Some(Inline::Text(" ".to_owned()))
410            }
411            adoc::Inline::LineBreak => Some(Inline::Text("\n".to_owned())),
412            adoc::Inline::Quote(kind, children) => {
413                let quote = match kind {
414                    adoc::QuoteKind::Double => "\"",
415                    adoc::QuoteKind::Single => "'",
416                };
417                Some(Inline::Text(format!(
418                    "{quote}{}{quote}",
419                    inline_plain_text(&self.inlines(children))
420                )))
421            }
422            adoc::Inline::Span(kind, _, children) => match kind {
423                adoc::SpanKind::Bold => Some(Inline::Strong(self.inlines(children))),
424                adoc::SpanKind::Italic => Some(Inline::Emph(self.inlines(children))),
425                adoc::SpanKind::LitMono | adoc::SpanKind::Mono => {
426                    Some(Inline::Code(inline_plain_text(&self.inlines(children))))
427                }
428                adoc::SpanKind::Text => {
429                    Some(Inline::Text(inline_plain_text(&self.inlines(children))))
430                }
431                _ => self.raw_inline_from_loc(loc, "span", "unsupported asciidoc span"),
432            },
433            adoc::Inline::InlinePassthru(children) => Some(Inline::Raw {
434                backend: asciidoc_id(),
435                text: inline_plain_text(&self.inlines(children)),
436            }),
437            adoc::Inline::Macro(macro_node) => self.macro_inline(macro_node, loc),
438            adoc::Inline::SpecialChar(kind) => Some(Inline::Text(
439                match kind {
440                    adoc::SpecialCharKind::Ampersand => "&",
441                    adoc::SpecialCharKind::LessThan => "<",
442                    adoc::SpecialCharKind::GreaterThan => ">",
443                }
444                .to_owned(),
445            )),
446            adoc::Inline::Symbol(kind) => Some(Inline::Text(symbol_text(*kind).to_owned())),
447            adoc::Inline::CurlyQuote(kind) => Some(Inline::Text(
448                match kind {
449                    adoc::CurlyKind::LeftDouble | adoc::CurlyKind::RightDouble => "\"",
450                    adoc::CurlyKind::LeftSingle
451                    | adoc::CurlyKind::RightSingle
452                    | adoc::CurlyKind::LegacyImplicitApostrophe => "'",
453                }
454                .to_owned(),
455            )),
456            adoc::Inline::LineComment(text) => self.raw_inline(
457                text.to_string(),
458                "comment",
459                "asciidoc line comment is preserved raw",
460            ),
461            adoc::Inline::Discarded
462            | adoc::Inline::CalloutNum(_)
463            | adoc::Inline::CalloutTuck(_)
464            | adoc::Inline::InlineAnchor(_)
465            | adoc::Inline::BiblioAnchor(_)
466            | adoc::Inline::IndexTerm(_)
467            | adoc::Inline::SpacedDashes(_, _) => None,
468        }
469    }
470
471    fn macro_inline(
472        &mut self,
473        macro_node: &adoc::MacroNode<'_>,
474        loc: adoc::SourceLocation,
475    ) -> Option<Inline> {
476        match macro_node {
477            adoc::MacroNode::Link {
478                scheme,
479                target,
480                attrs,
481                ..
482            } => {
483                let target = link_target(*scheme, target);
484                let label = attrs
485                    .as_ref()
486                    .and_then(|attrs| adoc::AttrData::positional_at(attrs, 0))
487                    .map(|nodes| self.inlines(nodes))
488                    .unwrap_or_else(|| vec![Inline::Text(target.clone())]);
489                Some(Inline::Link { label, target })
490            }
491            adoc::MacroNode::Mailto {
492                address, linktext, ..
493            } => {
494                let target = format!("mailto:{}", address.src);
495                let label = linktext
496                    .as_ref()
497                    .map(|nodes| self.inlines(nodes))
498                    .unwrap_or_else(|| vec![Inline::Text(address.src.to_string())]);
499                Some(Inline::Link { label, target })
500            }
501            adoc::MacroNode::Xref {
502                target, linktext, ..
503            } => {
504                let target = format!("#{}", target.src);
505                let label = linktext
506                    .as_ref()
507                    .map(|nodes| self.inlines(nodes))
508                    .unwrap_or_else(|| vec![Inline::Text(target.clone())]);
509                Some(Inline::Link { label, target })
510            }
511            adoc::MacroNode::Plugin(plugin)
512                if matches!(plugin.name.as_str(), "stem" | "latexmath" | "asciimath") =>
513            {
514                let text = plugin
515                    .target
516                    .as_ref()
517                    .map(|target| target.src.to_string())
518                    .unwrap_or_else(|| plugin.source.src.to_string());
519                Some(Inline::Math(MathSource {
520                    notation: "asciidoc".to_owned(),
521                    text,
522                }))
523            }
524            adoc::MacroNode::InlineImage { target, .. } => self.raw_inline(
525                target.src.to_string(),
526                "inline-image",
527                "asciidoc inline image is preserved raw",
528            ),
529            adoc::MacroNode::Plugin(plugin) => self.raw_inline(
530                plugin.source.src.to_string(),
531                plugin.name.as_str(),
532                "unsupported asciidoc macro is preserved raw",
533            ),
534            _ => self.raw_inline_from_loc(loc, "macro", "unsupported asciidoc macro"),
535        }
536    }
537
538    fn record_directives(&mut self, directives: &[RawDirective]) {
539        for directive in directives {
540            self.fidelity.warnings.push(format!(
541                "asciidoc {} directive is not resolved",
542                directive.kind
543            ));
544            if self.preserve_raw {
545                self.fidelity.preserved_raw.push(directive.text.clone());
546                self.raw_blocks.push(MarkupBlock::Raw {
547                    backend: asciidoc_id(),
548                    text: directive.text.clone(),
549                    span: Some(directive.span.clone()),
550                });
551            } else {
552                self.fidelity.dropped.push(MarkupLoss {
553                    path: directive.kind.clone(),
554                    reason: "asciidoc directive omitted by raw-fragment policy".to_owned(),
555                });
556            }
557        }
558    }
559
560    fn merge_directives(&mut self, blocks: Vec<MarkupBlock>) -> Vec<MarkupBlock> {
561        if self.raw_blocks.is_empty() {
562            return blocks;
563        }
564        let mut entries = blocks
565            .into_iter()
566            .enumerate()
567            .map(|(index, block)| (block_start(&block).unwrap_or(usize::MAX), index, block))
568            .collect::<Vec<_>>();
569        let offset = entries.len();
570        entries.extend(self.raw_blocks.drain(..).enumerate().map(|(index, block)| {
571            (
572                block_start(&block).unwrap_or(usize::MAX),
573                offset + index,
574                block,
575            )
576        }));
577        entries.sort_by_key(|(start, index, _)| (*start, *index));
578        entries.into_iter().map(|(_, _, block)| block).collect()
579    }
580
581    fn raw_from_span(
582        &mut self,
583        span: Option<Span>,
584        path: &str,
585        reason: &str,
586    ) -> Option<MarkupBlock> {
587        let text = span
588            .as_ref()
589            .and_then(|span| self.input.get(span.start..span.end))
590            .unwrap_or_default()
591            .to_owned();
592        self.raw_text(text, span, path, reason)
593    }
594
595    fn raw_text(
596        &mut self,
597        text: String,
598        span: Option<Span>,
599        path: &str,
600        reason: &str,
601    ) -> Option<MarkupBlock> {
602        if self.preserve_raw {
603            self.fidelity.preserved_raw.push(text.clone());
604            Some(MarkupBlock::Raw {
605                backend: asciidoc_id(),
606                text,
607                span,
608            })
609        } else {
610            self.fidelity.dropped.push(MarkupLoss {
611                path: path.to_owned(),
612                reason: reason.to_owned(),
613            });
614            None
615        }
616    }
617
618    fn raw_inline_from_loc(
619        &mut self,
620        loc: adoc::SourceLocation,
621        path: &str,
622        reason: &str,
623    ) -> Option<Inline> {
624        let text = self
625            .input
626            .get(usize::try_from(loc.start).ok()?..usize::try_from(loc.end).ok()?)
627            .unwrap_or_default()
628            .to_owned();
629        self.raw_inline(text, path, reason)
630    }
631
632    fn raw_inline(&mut self, text: String, path: &str, reason: &str) -> Option<Inline> {
633        if self.preserve_raw {
634            self.fidelity.preserved_raw.push(text.clone());
635            Some(Inline::Raw {
636                backend: asciidoc_id(),
637                text,
638            })
639        } else {
640            self.fidelity.dropped.push(MarkupLoss {
641                path: path.to_owned(),
642                reason: reason.to_owned(),
643            });
644            None
645        }
646    }
647}