markdown_it/plugins/cmark/block/
fence.rs

1//! Code fence
2//!
3//! ` ```lang ` or `~~~lang`
4//!
5//! <https://spec.commonmark.org/0.30/#code-fence>
6use crate::common::utils::unescape_all;
7use crate::parser::block::{BlockRule, BlockState};
8use crate::parser::extset::MarkdownItExt;
9use crate::{MarkdownIt, Node, NodeValue, Renderer};
10
11#[derive(Debug)]
12pub struct CodeFence {
13    pub info: String,
14    pub marker: char,
15    pub marker_len: usize,
16    pub content: String,
17    pub lang_prefix: &'static str,
18}
19
20impl NodeValue for CodeFence {
21    fn render(&self, node: &Node, fmt: &mut dyn Renderer) {
22        let info = unescape_all(&self.info);
23        let mut split = info.split_whitespace();
24        let lang_name = split.next().unwrap_or("");
25        let mut attrs = node.attrs.clone();
26        let class;
27
28        if !lang_name.is_empty() {
29            class = format!("{}{}", self.lang_prefix, lang_name);
30            attrs.push(("class", class));
31        }
32
33        fmt.cr();
34        fmt.open("pre", &[]);
35            fmt.open("code", &attrs);
36            fmt.text(&self.content);
37            fmt.close("code");
38        fmt.close("pre");
39        fmt.cr();
40    }
41}
42
43#[derive(Debug, Clone, Copy)]
44struct FenceSettings(&'static str);
45impl MarkdownItExt for FenceSettings {}
46
47impl Default for FenceSettings {
48    fn default() -> Self {
49        Self("language-")
50    }
51}
52
53pub fn add(md: &mut MarkdownIt) {
54    md.block.add_rule::<FenceScanner>();
55}
56
57pub fn set_lang_prefix(md: &mut MarkdownIt, lang_prefix: &'static str) {
58    md.ext.insert(FenceSettings(lang_prefix));
59}
60
61#[doc(hidden)]
62pub struct FenceScanner;
63
64impl FenceScanner {
65    fn get_header<'a>(state: &'a mut BlockState) -> Option<(char, usize, &'a str)> {
66
67        if state.line_indent(state.line) >= state.md.max_indent { return None; }
68
69        let line = state.get_line(state.line);
70        let mut chars = line.chars();
71
72        let marker = chars.next()?;
73        if marker != '~' && marker != '`' { return None; }
74
75        // scan marker length
76        let mut len = 1;
77        while Some(marker) == chars.next() { len += 1; }
78
79        if len < 3 { return None; }
80
81        let params = &line[len..];
82
83        if marker == '`' && params.contains(marker) { return None; }
84
85        Some((marker, len, params))
86    }
87}
88
89impl BlockRule for FenceScanner {
90    fn check(state: &mut BlockState) -> Option<()> {
91        Self::get_header(state).map(|_| ())
92    }
93
94    fn run(state: &mut BlockState) -> Option<(Node, usize)> {
95        let (marker, len, params) = Self::get_header(state)?;
96        let params = params.to_owned();
97
98        let mut next_line = state.line;
99        let mut have_end_marker = false;
100
101        // search end of block
102        'outer: loop {
103            next_line += 1;
104            if next_line >= state.line_max {
105                // unclosed block should be autoclosed by end of document.
106                // also block seems to be autoclosed by end of parent
107                break;
108            }
109
110            let line = state.get_line(next_line);
111
112            if !line.is_empty() && state.line_indent(next_line) < 0 {
113                // non-empty line with negative indent should stop the list:
114                // - ```
115                //  test
116                break;
117            }
118
119            let mut chars = line.chars().peekable();
120
121            if Some(marker) != chars.next() { continue; }
122
123            if state.line_indent(next_line) >= state.md.max_indent {
124                continue;
125            }
126
127            // scan marker length
128            let mut len_end = 1;
129            while Some(&marker) == chars.peek() {
130                chars.next();
131                len_end += 1;
132            }
133
134            // closing code fence must be at least as long as the opening one
135            if len_end < len { continue; }
136
137            // make sure tail has spaces only
138            loop {
139                match chars.next() {
140                    Some(' ' | '\t') => {},
141                    Some(_) => continue 'outer,
142                    None => {
143                        have_end_marker = true;
144                        break 'outer;
145                    }
146                }
147            }
148        }
149
150        // If a fence has heading spaces, they should be removed from its inner block
151        let indent = state.line_offsets[state.line].indent_nonspace;
152        let (content, _) = state.get_lines(state.line + 1, next_line, indent as usize, true);
153
154        let lang_prefix = state.md.ext.get::<FenceSettings>().copied().unwrap_or_default().0;
155        let node = Node::new(CodeFence {
156            info: params,
157            marker,
158            marker_len: len,
159            content,
160            lang_prefix,
161        });
162        Some((node, next_line - state.line + if have_end_marker { 1 } else { 0 }))
163    }
164}