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
33const OPEN_DIV_DEPTH: &str = "rushdown-fenced-div-depth";
36
37#[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#[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 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 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
215pub 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#[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
279pub 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