asciidoc_parser/blocks/
compound_delimited.rs1use std::slice::Iter;
2
3use crate::{
4 HasSpan, Parser, Span,
5 attributes::Attrlist,
6 blocks::{
7 Block, ContentModel, IsBlock, metadata::BlockMetadata, parse_utils::parse_blocks_until,
8 },
9 span::MatchedItem,
10 strings::CowStr,
11 warnings::{MatchAndWarnings, Warning, WarningType},
12};
13
14#[derive(Clone, Debug, Eq, PartialEq)]
25pub struct CompoundDelimitedBlock<'src> {
26 blocks: Vec<Block<'src>>,
27 context: CowStr<'src>,
28 source: Span<'src>,
29 title_source: Option<Span<'src>>,
30 title: Option<String>,
31 anchor: Option<Span<'src>>,
32 attrlist: Option<Attrlist<'src>>,
33}
34
35impl<'src> CompoundDelimitedBlock<'src> {
36 pub(crate) fn is_valid_delimiter(line: &Span<'src>) -> bool {
37 let data = line.data();
38
39 if data == "--" {
40 return true;
41 }
42
43 if data.len() >= 4 {
48 if data.starts_with("====") {
49 data.split_at(4).1.chars().all(|c| c == '=')
50 } else if data.starts_with("****") {
51 data.split_at(4).1.chars().all(|c| c == '*')
52 } else if data.starts_with("____") {
53 data.split_at(4).1.chars().all(|c| c == '_')
54 } else {
55 false
56 }
57 } else {
58 false
59 }
60 }
61
62 pub(crate) fn parse(
63 metadata: &BlockMetadata<'src>,
64 parser: &mut Parser,
65 ) -> Option<MatchAndWarnings<'src, Option<MatchedItem<'src, Self>>>> {
66 let delimiter = metadata.block_start.take_normalized_line();
67 let maybe_delimiter_text = delimiter.item.data();
68
69 let context = match maybe_delimiter_text
73 .split_at(maybe_delimiter_text.len().min(4))
74 .0
75 {
76 "====" => "example",
77 "--" => "open",
78 "****" => "sidebar",
79 "____" => "quote",
80 _ => return None,
81 };
82
83 if !Self::is_valid_delimiter(&delimiter.item) {
84 return None;
85 }
86
87 let mut next = delimiter.after;
88 let (closing_delimiter, after) = loop {
89 if next.is_empty() {
90 break (next, next);
91 }
92
93 let line = next.take_normalized_line();
94 if line.item.data() == delimiter.item.data() {
95 break (line.item, line.after);
96 }
97 next = line.after;
98 };
99
100 let inside_delimiters = delimiter.after.trim_remainder(closing_delimiter);
101
102 let maw_blocks = parse_blocks_until(inside_delimiters, |_| false, parser);
103
104 let blocks = maw_blocks.item;
105 let source = metadata
106 .source
107 .trim_remainder(closing_delimiter.discard_all());
108
109 Some(MatchAndWarnings {
110 item: Some(MatchedItem {
111 item: Self {
112 blocks: blocks.item,
113 context: context.into(),
114 source: source.trim_trailing_whitespace(),
115 title_source: metadata.title_source,
116 title: metadata.title.clone(),
117 anchor: metadata.anchor,
118 attrlist: metadata.attrlist.clone(),
119 },
120 after,
121 }),
122 warnings: if closing_delimiter.is_empty() {
123 let mut warnings = maw_blocks.warnings;
124 warnings.insert(
125 0,
126 Warning {
127 source: delimiter.item,
128 warning: WarningType::UnterminatedDelimitedBlock,
129 },
130 );
131 warnings
132 } else {
133 maw_blocks.warnings
134 },
135 })
136 }
137}
138
139impl<'src> IsBlock<'src> for CompoundDelimitedBlock<'src> {
140 fn content_model(&self) -> ContentModel {
141 ContentModel::Compound
142 }
143
144 fn raw_context(&self) -> CowStr<'src> {
145 self.context.clone()
146 }
147
148 fn nested_blocks(&'src self) -> Iter<'src, Block<'src>> {
149 self.blocks.iter()
150 }
151
152 fn title_source(&'src self) -> Option<Span<'src>> {
153 self.title_source
154 }
155
156 fn title(&self) -> Option<&str> {
157 self.title.as_deref()
158 }
159
160 fn anchor(&'src self) -> Option<Span<'src>> {
161 self.anchor
162 }
163
164 fn attrlist(&'src self) -> Option<&'src Attrlist<'src>> {
165 self.attrlist.as_ref()
166 }
167}
168
169impl<'src> HasSpan<'src> for CompoundDelimitedBlock<'src> {
170 fn span(&self) -> Span<'src> {
171 self.source
172 }
173}