Skip to main content

asciidoc_parser/blocks/
toc.rs

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/// A TOC block represents the `toc::[]` block macro, which marks the position
12/// where a table of contents should be rendered when `toc-placement` is set to
13/// `macro`.
14///
15/// The macro carries no target; its optional attribute list can override the
16/// per-macro settings Asciidoctor honors on the generated TOC (`id`, `levels`,
17/// and `role`), while a block title above the macro overrides `toc-title`.
18#[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    /// Returns a document-order iterator over this block's direct child blocks.
31    ///
32    /// A TOC block never has child blocks, so this iterator is always empty.
33    /// See [`FindBlocks`](crate::blocks::FindBlocks) to search from a
34    /// [`Block`](crate::blocks::Block) or [`Document`](crate::Document).
35    pub fn child_blocks(&'src self) -> ChildBlocks<'src> {
36        ChildBlocks::empty()
37    }
38
39    /// Returns the block's title as a mutable [`Content`], if the block has
40    /// one.
41    ///
42    /// This narrow seam exists for the document-order title resolution pass
43    /// (see `document::title_refs`), which installs the re-rendered title
44    /// after resolving any cross-references embedded in it. All other access
45    /// goes through the read-only [`IsBlock::title`] accessor.
46    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        // Line must end with `]`; otherwise, it's not a block macro.
57        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        // The `toc` block macro takes no target: the double colon must be
86        // followed immediately by the attribute list. Anything else (e.g.
87        // `toc::foo[]`) is not a TOC block macro, so quietly decline and let it
88        // fall through to a paragraph.
89        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        // Note that we already checked that this line ends with a close brace.
99
100        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    /// Return the macro's attribute list.
124    ///
125    /// **IMPORTANT:** This is the list of attributes _within_ the macro block
126    /// definition itself (e.g. `toc::[levels=2]`), which Asciidoctor uses to
127    /// override the per-macro TOC settings (`id`, `levels`, and `role`).
128    ///
129    /// See also [`attrlist()`] for attributes that can be defined before the
130    /// macro invocation.
131    ///
132    /// [`attrlist()`]: Self::attrlist()
133    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        // In addition to a block anchor (`[[id]]`/`[#id]`) or the block
157        // attribute list above the macro, a TOC block may carry its ID as a
158        // named `id=` attribute _inside_ the macro attribute list (e.g.
159        // `toc::[id=contents]`), which the trait default does not consider.
160        // Fall back to that last, so the block-level forms win.
161        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        // Silly test to mark the #[derive(...)] line as covered.
200        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        // A different macro name is not a TOC block.
224        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        // A macro name must begin with a word character; a leading `#` is
235        // rejected before any name is captured (see `take_block_macro_name`).
236        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        // A name that merely starts with `toc` (e.g. `tocx`) is a different
247        // macro and is not recognized as a TOC block.
248        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        // A single colon is an inline macro form, not a block macro.
269        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        // The `toc` block macro takes no target.
280        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        // The macro attribute list carries the per-macro TOC overrides.
348        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        // The macro `id=` is surfaced through the block's `id()`.
369        assert_eq!(mi.item.id().unwrap(), "contents");
370    }
371}