Skip to main content

base_markdown/
code_fence.rs

1// Released under the MIT License.
2// Copyright, 2026, by Samuel Williams.
3
4/// Opening fence metadata, matching Markly's `Node::Fence` structure.
5///
6/// This describes the source opening fence. The closing fence can be longer or absent for ordinary code blocks. Markdown serialization may use a different fence according to its formatting options and content safety.
7///
8/// Code-block events retain this metadata independently of source offsets:
9///
10/// ```
11/// use base_markdown::{stream::EventKind, Document, ParseOptions};
12///
13/// let document = Document::parse("  ~~~~rust\n  let x = 1;\n  ~~~~", &ParseOptions::default()).unwrap();
14/// let code = document.children(document.root()).next().unwrap();
15/// let EventKind::CodeBlock { fence: Some(fence), .. } = &document.node(code).unwrap().kind else { panic!() };
16/// assert_eq!((fence.character, fence.length, fence.indent), ('~', 4, 2));
17/// ```
18#[derive(Clone, Copy, Debug, Eq, PartialEq)]
19#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
20pub struct CodeFence {
21    /// Fence marker: a backtick or tilde for code, or a dash/plus for frontmatter.
22    pub character: char,
23    /// Number of markers in the opening fence.
24    pub length: usize,
25    /// Opening indentation in columns, relative to the containing block.
26    /// Container markers and their required whitespace are excluded.
27    pub indent: usize,
28}
29
30impl CodeFence {
31    pub(crate) fn frontmatter_open(bytes: &[u8]) -> Option<Self> {
32        let marker = *bytes.first()?;
33        if !matches!(marker, b'-' | b'+' | b'`' | b'~') {
34            return None;
35        }
36        let length = bytes.iter().take_while(|&&byte| byte == marker).count();
37        if length < 3 || (matches!(marker, b'-' | b'+') && length != 3) {
38            return None;
39        }
40        Some(Self {
41            character: char::from(marker),
42            length,
43            indent: 0,
44        })
45    }
46
47    pub(crate) fn valid_frontmatter_info(self, info: &[u8]) -> bool {
48        !info.iter().any(|byte| matches!(byte, b'\r' | b'\n'))
49            && (!matches!(self.character, '`' | '~')
50                || !info.iter().all(|byte| matches!(byte, b' ' | b'\t')))
51            && (self.character != '`' || !info.contains(&b'`'))
52    }
53
54    // The caller strips only the indentation permitted in its source context.
55    pub(crate) fn frontmatter_close(self, line: &[u8]) -> Option<usize> {
56        let count = line
57            .iter()
58            .take_while(|&&byte| char::from(byte) == self.character)
59            .count();
60        (count >= self.length
61            && (!matches!(self.character, '-' | '+') || count == self.length)
62            && line[count..]
63                .iter()
64                .all(|byte| matches!(byte, b' ' | b'\t')))
65        .then_some(count)
66    }
67}