1use crate::{
2 HasSpan, Parser, Span,
3 attributes::{Attrlist, AttrlistContext},
4 blocks::{ChildBlocks, ContentModel, IsBlock, metadata::BlockMetadata},
5 content::Content,
6 span::MatchedItem,
7 strings::CowStr,
8 warnings::MatchAndWarnings,
9};
10
11#[derive(Clone, Debug, Eq, Hash, PartialEq)]
19pub struct TocBlock<'src> {
20 macro_attrlist: Attrlist<'src>,
21 source: Span<'src>,
22 title_source: Option<Span<'src>>,
23 title: Option<Content<'src>>,
24 anchor: Option<Span<'src>>,
25 anchor_reftext: Option<Span<'src>>,
26 attrlist: Option<Attrlist<'src>>,
27}
28
29impl<'src> TocBlock<'src> {
30 pub fn child_blocks(&'src self) -> ChildBlocks<'src> {
36 ChildBlocks::empty()
37 }
38
39 pub(crate) fn title_content_mut(&mut self) -> Option<&mut Content<'src>> {
47 self.title.as_mut()
48 }
49
50 pub(crate) fn parse(
51 metadata: &BlockMetadata<'src>,
52 parser: &mut Parser,
53 ) -> MatchAndWarnings<'src, Option<MatchedItem<'src, Self>>> {
54 let line = metadata.block_start.take_normalized_line();
55
56 if !line.item.ends_with(']') {
58 return MatchAndWarnings {
59 item: None,
60 warnings: vec![],
61 };
62 }
63
64 let Some(name) = line.item.take_block_macro_name() else {
65 return MatchAndWarnings {
66 item: None,
67 warnings: vec![],
68 };
69 };
70
71 if name.item.data() != "toc" {
72 return MatchAndWarnings {
73 item: None,
74 warnings: vec![],
75 };
76 }
77
78 let Some(colons) = name.after.take_prefix("::") else {
79 return MatchAndWarnings {
80 item: None,
81 warnings: vec![],
82 };
83 };
84
85 let Some(open_brace) = colons.after.take_prefix("[") else {
90 return MatchAndWarnings {
91 item: None,
92 warnings: vec![],
93 };
94 };
95
96 let attrlist = open_brace.after.slice(0..open_brace.after.len() - 1);
97
98 let macro_attrlist = Attrlist::parse(attrlist, parser, AttrlistContext::Inline);
101
102 let source: Span = metadata.source.trim_remainder(line.after);
103 let source = source.slice(0..source.trim().len());
104
105 MatchAndWarnings {
106 item: Some(MatchedItem {
107 item: Self {
108 macro_attrlist: macro_attrlist.item.item,
109 source,
110 title_source: metadata.title_source,
111 title: metadata.title.clone(),
112 anchor: metadata.anchor,
113 anchor_reftext: metadata.anchor_reftext,
114 attrlist: metadata.attrlist.clone(),
115 },
116
117 after: line.after.discard_empty_lines(),
118 }),
119 warnings: macro_attrlist.warnings,
120 }
121 }
122
123 pub fn macro_attrlist(&'src self) -> &'src Attrlist<'src> {
134 &self.macro_attrlist
135 }
136}
137
138impl<'src> IsBlock<'src> for TocBlock<'src> {
139 fn content_model(&self) -> ContentModel {
140 ContentModel::Empty
141 }
142
143 fn raw_context(&self) -> CowStr<'src> {
144 "toc".into()
145 }
146
147 fn title_source(&'src self) -> Option<Span<'src>> {
148 self.title_source
149 }
150
151 fn title(&self) -> Option<&str> {
152 self.title.as_ref().map(Content::rendered_str)
153 }
154
155 fn id(&'src self) -> Option<&'src str> {
156 self.anchor()
162 .map(|a| a.data())
163 .or_else(|| self.attrlist().and_then(|attrlist| attrlist.id()))
164 .or_else(|| self.macro_attrlist.id())
165 }
166
167 fn anchor(&'src self) -> Option<Span<'src>> {
168 self.anchor
169 }
170
171 fn anchor_reftext(&'src self) -> Option<Span<'src>> {
172 self.anchor_reftext
173 }
174
175 fn attrlist(&'src self) -> Option<&'src Attrlist<'src>> {
176 self.attrlist.as_ref()
177 }
178}
179
180impl<'src> HasSpan<'src> for TocBlock<'src> {
181 fn span(&self) -> Span<'src> {
182 self.source
183 }
184}
185
186#[cfg(test)]
187mod tests {
188 #![allow(clippy::unwrap_used)]
189
190 use std::ops::Deref;
191
192 use crate::{
193 blocks::{ContentModel, metadata::BlockMetadata},
194 tests::prelude::*,
195 };
196
197 #[test]
198 fn impl_clone() {
199 let mut parser = Parser::default();
201
202 let b1 = crate::blocks::TocBlock::parse(&BlockMetadata::new("toc::[]"), &mut parser)
203 .unwrap_if_no_warnings()
204 .unwrap()
205 .item;
206
207 let b2 = b1.clone();
208 assert_eq!(b1, b2);
209 }
210
211 #[test]
212 fn err_empty_source() {
213 let mut parser = Parser::default();
214 assert!(
215 crate::blocks::TocBlock::parse(&BlockMetadata::new(""), &mut parser)
216 .unwrap_if_no_warnings()
217 .is_none()
218 );
219 }
220
221 #[test]
222 fn err_not_toc_macro() {
223 let mut parser = Parser::default();
225 assert!(
226 crate::blocks::TocBlock::parse(&BlockMetadata::new("image::foo.png[]"), &mut parser)
227 .unwrap_if_no_warnings()
228 .is_none()
229 );
230 }
231
232 #[test]
233 fn err_macro_name_not_word_char() {
234 let mut parser = Parser::default();
237 assert!(
238 crate::blocks::TocBlock::parse(&BlockMetadata::new("#toc::[]"), &mut parser)
239 .unwrap_if_no_warnings()
240 .is_none()
241 );
242 }
243
244 #[test]
245 fn err_macro_name_not_exactly_toc() {
246 let mut parser = Parser::default();
249 assert!(
250 crate::blocks::TocBlock::parse(&BlockMetadata::new("tocx::[]"), &mut parser)
251 .unwrap_if_no_warnings()
252 .is_none()
253 );
254 }
255
256 #[test]
257 fn err_not_closed() {
258 let mut parser = Parser::default();
259 assert!(
260 crate::blocks::TocBlock::parse(&BlockMetadata::new("toc::["), &mut parser)
261 .unwrap_if_no_warnings()
262 .is_none()
263 );
264 }
265
266 #[test]
267 fn err_missing_double_colon() {
268 let mut parser = Parser::default();
270 assert!(
271 crate::blocks::TocBlock::parse(&BlockMetadata::new("toc:[]"), &mut parser)
272 .unwrap_if_no_warnings()
273 .is_none()
274 );
275 }
276
277 #[test]
278 fn err_has_target() {
279 let mut parser = Parser::default();
281 assert!(
282 crate::blocks::TocBlock::parse(&BlockMetadata::new("toc::foo[]"), &mut parser)
283 .unwrap_if_no_warnings()
284 .is_none()
285 );
286 }
287
288 #[test]
289 fn simplest_toc_macro() {
290 let mut parser = Parser::default();
291
292 let mi = crate::blocks::TocBlock::parse(&BlockMetadata::new("toc::[]"), &mut parser)
293 .unwrap_if_no_warnings()
294 .unwrap();
295
296 assert_eq!(
297 mi.item,
298 TocBlock {
299 macro_attrlist: Attrlist {
300 attributes: &[],
301 anchor: None,
302 source: Span {
303 data: "",
304 line: 1,
305 col: 7,
306 offset: 6,
307 }
308 },
309 source: Span {
310 data: "toc::[]",
311 line: 1,
312 col: 1,
313 offset: 0,
314 },
315 title_source: None,
316 title: None,
317 anchor: None,
318 anchor_reftext: None,
319 attrlist: None,
320 }
321 );
322
323 assert_eq!(
324 mi.after,
325 Span {
326 data: "",
327 line: 1,
328 col: 8,
329 offset: 7
330 }
331 );
332
333 assert_eq!(mi.item.content_model(), ContentModel::Empty);
334 assert_eq!(mi.item.raw_context().deref(), "toc");
335 assert_eq!(mi.item.resolved_context().deref(), "toc");
336 assert!(mi.item.child_blocks().next().is_none());
337 assert!(mi.item.title_source().is_none());
338 assert!(mi.item.title().is_none());
339 assert!(mi.item.anchor().is_none());
340 assert!(mi.item.anchor_reftext().is_none());
341 assert!(mi.item.attrlist().is_none());
342 assert_eq!(mi.item.substitution_group(), SubstitutionGroup::Normal);
343 }
344
345 #[test]
346 fn macro_attrlist_overrides() {
347 let mut parser = Parser::default();
349
350 let mi = crate::blocks::TocBlock::parse(
351 &BlockMetadata::new("toc::[id=contents,levels=2,role=toc2]"),
352 &mut parser,
353 )
354 .unwrap_if_no_warnings()
355 .unwrap();
356
357 let macro_attrlist = mi.item.macro_attrlist();
358 assert_eq!(macro_attrlist.id().unwrap(), "contents");
359 assert_eq!(
360 macro_attrlist.named_attribute("levels").unwrap().value(),
361 "2"
362 );
363 assert_eq!(
364 macro_attrlist.named_attribute("role").unwrap().value(),
365 "toc2"
366 );
367
368 assert_eq!(mi.item.id().unwrap(), "contents");
370 }
371}