1use 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#[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 §ioned.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(§ion.heading),
174 id: section.id.as_ref().map(ToString::to_string),
175 span: span_from_multi(§ion.loc),
176 }];
177 blocks.extend(self.blocks_from_blocks(§ion.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}