Skip to main content

rushdown_fenced_div/
lib.rs

1#![doc = include_str!("../README.md")]
2#![cfg_attr(not(feature = "std"), no_std)]
3
4extern crate alloc;
5
6use alloc::boxed::Box;
7
8use core::any::TypeId;
9use core::fmt;
10use core::fmt::Write;
11use core::str;
12
13use rushdown::{
14    ast::{
15        pp_indent, Arena, Attributes, KindData, NodeKind, NodeRef, NodeType, PrettyPrint,
16        WalkStatus,
17    },
18    context::{ContextKey, ContextKeyRegistry, UsizeValue},
19    parser::{
20        self, parse_attributes, AnyBlockParser, BlockParser, NoParserOptions, Parser,
21        ParserExtension, ParserExtensionFn, PRIORITY_LIST,
22    },
23    renderer::{
24        self,
25        html::{self, Renderer, RendererExtension, RendererExtensionFn},
26        BoxRenderNode, NodeRenderer, NodeRendererRegistry, RenderNode, RendererOptions, TextWrite,
27    },
28    text::{self, BlockReader, Reader as _, EOS},
29    util::{is_punct, is_space},
30    Result,
31};
32
33// AST {{{
34
35const OPEN_DIV_DEPTH: &str = "rushdown-fenced-div-depth";
36
37/// AST node for a fenced div container.
38#[derive(Debug)]
39pub struct FencedDiv {
40    depth: usize,
41}
42
43impl FencedDiv {
44    fn new(depth: usize) -> Self {
45        Self { depth }
46    }
47}
48
49impl NodeKind for FencedDiv {
50    fn typ(&self) -> NodeType {
51        NodeType::ContainerBlock
52    }
53
54    fn kind_name(&self) -> &'static str {
55        "FencedDiv"
56    }
57}
58
59impl PrettyPrint for FencedDiv {
60    fn pretty_print(&self, w: &mut dyn Write, _source: &str, level: usize) -> fmt::Result {
61        writeln!(w, "{}FencedDiv", pp_indent(level))
62    }
63}
64
65impl From<FencedDiv> for KindData {
66    fn from(e: FencedDiv) -> Self {
67        KindData::Extension(Box::new(e))
68    }
69}
70
71// }}} AST
72
73// Parser {{{
74
75#[derive(Debug)]
76struct FencedDivBlockParser {
77    open_div_depth: ContextKey<UsizeValue>,
78}
79
80impl FencedDivBlockParser {
81    fn new(reg: alloc::rc::Rc<core::cell::RefCell<ContextKeyRegistry>>) -> Self {
82        let open_div_depth = reg.borrow_mut().get_or_create::<UsizeValue>(OPEN_DIV_DEPTH);
83        Self { open_div_depth }
84    }
85}
86
87impl BlockParser for FencedDivBlockParser {
88    fn trigger(&self) -> &[u8] {
89        b":"
90    }
91
92    fn open(
93        &self,
94        arena: &mut Arena,
95        _parent_ref: NodeRef,
96        reader: &mut text::BasicReader,
97        ctx: &mut parser::Context,
98    ) -> Option<(NodeRef, parser::State)> {
99        let segment = reader.peek_line_segment()?;
100        let blk = [segment];
101        let mut blk_reader = BlockReader::new(reader.source(), &blk);
102        let fence_length = blk_reader.skip_while(|b| b == b':');
103        if fence_length < 3 {
104            return None;
105        }
106        let depth = ctx.get(self.open_div_depth).copied().unwrap_or(0) + 1;
107        let node_ref = parse_opening_fence(arena, &mut blk_reader, depth)?;
108        ctx.insert(self.open_div_depth, depth);
109        reader.advance_to_eol();
110        Some((node_ref, parser::State::HAS_CHILDREN))
111    }
112
113    fn cont(
114        &self,
115        arena: &mut Arena,
116        node_ref: NodeRef,
117        reader: &mut text::BasicReader,
118        ctx: &mut parser::Context,
119    ) -> Option<parser::State> {
120        if let Some(last_opened_block) = ctx.last_opened_block() {
121            // CodeBlock is a special case
122            if last_opened_block != node_ref
123                && matches!(arena[last_opened_block].kind_data(), KindData::CodeBlock(_))
124            {
125                return Some(parser::State::HAS_CHILDREN);
126            }
127        }
128        let (line, _) = reader.peek_line_bytes()?;
129        let fence_length = line.iter().take_while(|&&b| b == b':').count();
130        if fence_length < 3 {
131            return Some(parser::State::HAS_CHILDREN);
132        }
133        let rest = &line[fence_length..];
134        if rest
135            .iter()
136            .take_while(|&&b| b.is_ascii_whitespace())
137            .count()
138            < rest.len()
139        {
140            return Some(parser::State::HAS_CHILDREN);
141        }
142        let fenced_div = rushdown::as_extension_data!(arena, node_ref, FencedDiv);
143        let open_depth = ctx.get(self.open_div_depth).copied().unwrap_or(0);
144        // apparently, rushdown calls blocks from outermost to innermost, so
145        // we have to check the open_depth
146        if fenced_div.depth == open_depth {
147            reader.advance_to_eol();
148            return None;
149        }
150        Some(parser::State::HAS_CHILDREN)
151    }
152
153    fn close(
154        &self,
155        _arena: &mut Arena,
156        _node_ref: NodeRef,
157        _reader: &mut text::BasicReader,
158        ctx: &mut parser::Context,
159    ) {
160        if let Some(depth) = ctx.get_mut(self.open_div_depth) {
161            *depth = depth.saturating_sub(1);
162        }
163    }
164
165    fn can_interrupt_paragraph(&self) -> bool {
166        true
167    }
168}
169
170fn parse_opening_fence(
171    arena: &mut Arena,
172    reader: &mut text::BlockReader,
173    depth: usize,
174) -> Option<NodeRef> {
175    reader.skip_spaces();
176    let b = reader.peek_byte();
177    if b == EOS {
178        return None;
179    }
180    let attributes = if b == b'{' {
181        parse_attributes(reader)?
182    } else {
183        let (line, seg) = reader.peek_line_bytes()?;
184        let i = line
185            .iter()
186            .take_while(|&&b| {
187                !is_space(b) && (!is_punct(b) || b == b'_' || b == b'-' || b == b':' || b == b'.')
188            })
189            .count();
190        if i == 0 {
191            return None;
192        }
193        let mut attributes = Attributes::new();
194        attributes.insert("class", seg.with_stop(seg.start() + i).into());
195        reader.advance(i);
196        attributes
197    };
198    reader.skip_spaces();
199    reader.skip_while(|b| b == b':');
200    reader.skip_spaces();
201    if reader.peek_byte() != EOS {
202        return None;
203    }
204    let node_ref = arena.new_node(FencedDiv::new(depth));
205    arena[node_ref].attributes_mut().extend(attributes);
206    Some(node_ref)
207}
208
209impl From<FencedDivBlockParser> for AnyBlockParser {
210    fn from(p: FencedDivBlockParser) -> Self {
211        AnyBlockParser::Extension(Box::new(p))
212    }
213}
214
215/// Returns a parser extension that parses fenced div blocks.
216pub fn fenced_div_parser_extension() -> impl ParserExtension {
217    ParserExtensionFn::new(|p: &mut Parser| {
218        p.add_block_parser(
219            FencedDivBlockParser::new,
220            NoParserOptions,
221            PRIORITY_LIST + 100,
222        );
223    })
224}
225
226// }}} Parser
227
228// Renderer {{{
229
230#[derive(Debug, Clone, Default)]
231pub struct FencedDivHtmlRendererOptions;
232
233impl RendererOptions for FencedDivHtmlRendererOptions {}
234
235struct FencedDivHtmlRenderer<W: TextWrite> {
236    _phantom: core::marker::PhantomData<W>,
237    writer: html::Writer,
238}
239
240impl<W: TextWrite> FencedDivHtmlRenderer<W> {
241    fn with_options(html_opts: html::Options, _options: FencedDivHtmlRendererOptions) -> Self {
242        Self {
243            _phantom: core::marker::PhantomData,
244            writer: html::Writer::with_options(html_opts),
245        }
246    }
247}
248
249impl<W: TextWrite> RenderNode<W> for FencedDivHtmlRenderer<W> {
250    fn render_node<'a>(
251        &self,
252        w: &mut W,
253        source: &'a str,
254        arena: &'a Arena,
255        node_ref: NodeRef,
256        entering: bool,
257        _ctx: &mut renderer::Context,
258    ) -> Result<WalkStatus> {
259        if entering {
260            self.writer.write_safe_str(w, "<div")?;
261            html::render_attributes(w, source, arena[node_ref].attributes(), None)?;
262            self.writer.write_safe_str(w, ">")?;
263        } else {
264            self.writer.write_safe_str(w, "</div>")?;
265        }
266        Ok(WalkStatus::Continue)
267    }
268}
269
270impl<'cb, W> NodeRenderer<'cb, W> for FencedDivHtmlRenderer<W>
271where
272    W: TextWrite + 'cb,
273{
274    fn register_node_renderer_fn(self, nrr: &mut impl NodeRendererRegistry<'cb, W>) {
275        nrr.register_node_renderer_fn(TypeId::of::<FencedDiv>(), BoxRenderNode::new(self));
276    }
277}
278
279/// Returns a renderer extension that renders fenced div blocks in HTML.
280pub fn fenced_div_html_renderer_extension<'cb, W>(
281    options: impl Into<FencedDivHtmlRendererOptions>,
282) -> impl RendererExtension<'cb, W>
283where
284    W: TextWrite + 'cb,
285{
286    RendererExtensionFn::new(move |r: &mut Renderer<'cb, W>| {
287        r.add_node_renderer(FencedDivHtmlRenderer::with_options, options.into());
288    })
289}
290
291// }}} Renderer