| 1 | //! Code fence |
| 2 | //! |
| 3 | //! ` ```lang ` or `~~~lang` |
| 4 | //! |
| 5 | //! <https://spec.commonmark.org/0.30/#code-fence> |
| 6 | use crate::common::utils::unescape_all; |
| 7 | use crate::parser::block::{BlockRule, BlockState}; |
| 8 | use crate::parser::extset::MarkdownItExt; |
| 9 | use crate::{MarkdownIt, Node, NodeValue, Renderer}; |
| 10 | |
| 11 | #[derive(Debug)] |
| 12 | pub 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 | |
| 20 | impl 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)] |
| 44 | struct FenceSettings(&'static str); |
| 45 | impl MarkdownItExt for FenceSettings {} |
| 46 | |
| 47 | impl Default for FenceSettings { |
| 48 | fn default() -> Self { |
| 49 | Self("language-") |
| 50 | } |
| 51 | } |
| 52 | |
| 53 | pub fn add(md: &mut MarkdownIt) { |
| 54 | md.block.add_rule::<FenceScanner>(); |
| 55 | } |
| 56 | |
| 57 | pub fn set_lang_prefix(md: &mut MarkdownIt, lang_prefix: &'static str) { |
| 58 | md.ext.insert(FenceSettings(lang_prefix)); |
| 59 | } |
| 60 | |
| 61 | #[doc(hidden)] |
| 62 | pub struct FenceScanner; |
| 63 | |
| 64 | impl 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 | |
| 89 | impl 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 | } |