Skip to main content

rushdown_definition_list/
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;
7use core::any::TypeId;
8use core::fmt;
9use core::fmt::Write;
10use rushdown::as_extension_data;
11use rushdown::as_extension_data_mut;
12use rushdown::as_type_data;
13use rushdown::as_type_data_mut;
14use rushdown::ast;
15use rushdown::ast::pp_indent;
16use rushdown::ast::Arena;
17use rushdown::ast::KindData;
18use rushdown::ast::NodeKind;
19use rushdown::ast::NodeRef;
20use rushdown::ast::NodeType;
21use rushdown::ast::PrettyPrint;
22use rushdown::ast::WalkStatus;
23use rushdown::matches_extension_kind;
24use rushdown::matches_kind;
25use rushdown::parser;
26use rushdown::parser::parser_extension;
27use rushdown::parser::AnyBlockParser;
28use rushdown::parser::BlockParser;
29use rushdown::parser::NoParserOptions;
30use rushdown::parser::ParserExtension;
31use rushdown::renderer;
32use rushdown::renderer::html;
33use rushdown::renderer::html::renderer_extension;
34use rushdown::renderer::html::ParagraphRendererOptions;
35use rushdown::renderer::html::RendererExtension;
36use rushdown::renderer::BoxRenderNode;
37use rushdown::renderer::NoRendererOptions;
38use rushdown::renderer::NodeRenderer;
39use rushdown::renderer::NodeRendererRegistry;
40use rushdown::renderer::RenderNode;
41use rushdown::renderer::TextWrite;
42use rushdown::text;
43use rushdown::text::Reader;
44use rushdown::util::indent_position;
45use rushdown::util::indent_width;
46use rushdown::util::is_blank;
47use rushdown::Result;
48
49// AST {{{
50
51/// Represents a definition list in the AST.
52#[derive(Debug)]
53pub struct DefinitionList {
54    offset: u8,
55    temp_paragraph: Option<NodeRef>,
56    is_tight: bool,
57}
58
59impl Default for DefinitionList {
60    fn default() -> Self {
61        Self::new()
62    }
63}
64
65impl DefinitionList {
66    /// Creates a new `DefinitionList`.
67    pub fn new() -> Self {
68        Self {
69            offset: 0,
70            temp_paragraph: None,
71            is_tight: true,
72        }
73    }
74
75    fn with_offset_and_paragraph(offset: u8, temp_paragraph: NodeRef) -> Self {
76        Self {
77            offset,
78            temp_paragraph: Some(temp_paragraph),
79            is_tight: true,
80        }
81    }
82
83    /// Sets whether the definition list is tight or loose.
84    #[inline(always)]
85    pub fn set_tight(&mut self, tight: bool) {
86        self.is_tight = tight;
87    }
88
89    /// Returns whether the definition list is tight or loose.
90    #[inline(always)]
91    pub fn is_tight(&self) -> bool {
92        self.is_tight
93    }
94}
95
96impl NodeKind for DefinitionList {
97    fn typ(&self) -> NodeType {
98        NodeType::ContainerBlock
99    }
100
101    fn kind_name(&self) -> &'static str {
102        "DefinitionList"
103    }
104}
105
106impl PrettyPrint for DefinitionList {
107    fn pretty_print(&self, w: &mut dyn Write, _source: &str, level: usize) -> fmt::Result {
108        writeln!(w, "{}IsTight: {}", pp_indent(level), self.is_tight)?;
109        Ok(())
110    }
111}
112
113impl From<DefinitionList> for KindData {
114    fn from(e: DefinitionList) -> Self {
115        KindData::Extension(Box::new(e))
116    }
117}
118
119/// Represents a term in the AST.
120#[derive(Debug)]
121pub struct Term {}
122
123impl Default for Term {
124    fn default() -> Self {
125        Self::new()
126    }
127}
128
129impl Term {
130    /// Creates a new `Term`.
131    pub fn new() -> Self {
132        Self {}
133    }
134}
135
136impl NodeKind for Term {
137    fn typ(&self) -> NodeType {
138        NodeType::LeafBlock
139    }
140
141    fn kind_name(&self) -> &'static str {
142        "Term"
143    }
144}
145
146impl PrettyPrint for Term {
147    fn pretty_print(&self, _w: &mut dyn Write, _source: &str, _level: usize) -> fmt::Result {
148        Ok(())
149    }
150}
151
152impl From<Term> for KindData {
153    fn from(e: Term) -> Self {
154        KindData::Extension(Box::new(e))
155    }
156}
157
158/// Represents a term definition in the AST.
159#[derive(Debug)]
160pub struct TermDefinition {}
161
162impl Default for TermDefinition {
163    fn default() -> Self {
164        Self::new()
165    }
166}
167
168impl TermDefinition {
169    /// Creates a new `TermDefinition`.
170    pub fn new() -> Self {
171        Self {}
172    }
173}
174
175impl NodeKind for TermDefinition {
176    fn typ(&self) -> NodeType {
177        NodeType::ContainerBlock
178    }
179
180    fn kind_name(&self) -> &'static str {
181        "TermDefinition"
182    }
183}
184
185impl PrettyPrint for TermDefinition {
186    fn pretty_print(&self, _w: &mut dyn Write, _source: &str, _level: usize) -> fmt::Result {
187        Ok(())
188    }
189}
190
191impl From<TermDefinition> for KindData {
192    fn from(e: TermDefinition) -> Self {
193        KindData::Extension(Box::new(e))
194    }
195}
196
197// }}} AST
198
199// Parser {{{
200
201#[derive(Debug, Default)]
202struct DefinitionListParser {}
203
204impl DefinitionListParser {
205    fn new() -> Self {
206        Self {}
207    }
208}
209
210impl BlockParser for DefinitionListParser {
211    fn trigger(&self) -> &[u8] {
212        b":"
213    }
214
215    fn open(
216        &self,
217        arena: &mut Arena,
218        parent_ref: NodeRef,
219        reader: &mut text::BasicReader,
220        ctx: &mut parser::Context,
221    ) -> Option<(NodeRef, parser::State)> {
222        if matches_extension_kind!(arena, parent_ref, DefinitionList) {
223            return None;
224        }
225        let (line, _) = reader.peek_line_bytes()?;
226        let pos = ctx.block_offset()?;
227        let indent = ctx.block_indent().unwrap_or(1);
228        if line[pos] != b':' || indent != 0 {
229            return None;
230        }
231        let last_ref = arena[parent_ref].last_child()?;
232        let (mut w, _) = indent_width(&line[pos + 1..], pos + 1);
233        // need 1 or more spaces after ':'
234        if w < 1 {
235            return None;
236        }
237        if w > 8 {
238            // starts with indented code
239            w = 5;
240        }
241        w += pos + 1; // 1 = ':'
242
243        if matches_kind!(arena, last_ref, Paragraph) {
244            match arena[last_ref].previous_sibling() {
245                Some(prev_ref) if matches_extension_kind!(arena, prev_ref, DefinitionList) => {
246                    // not first item
247                    let kd = as_extension_data_mut!(arena, prev_ref, DefinitionList);
248                    kd.offset = w as u8;
249                    kd.temp_paragraph = Some(last_ref);
250                    prev_ref.remove(arena);
251                    Some((prev_ref, parser::State::HAS_CHILDREN))
252                }
253                _ => {
254                    // first item
255                    let list = DefinitionList::with_offset_and_paragraph(w as u8, last_ref);
256                    Some((
257                        arena.new_node(list),
258                        parser::State::HAS_CHILDREN | parser::State::REQUIRE_PARAGRAPH,
259                    ))
260                }
261            }
262        } else if matches_extension_kind!(arena, last_ref, DefinitionList) {
263            // multiple definition
264            let kd = as_extension_data_mut!(arena, last_ref, DefinitionList);
265            kd.offset = w as u8;
266            kd.temp_paragraph = None;
267            last_ref.remove(arena);
268            Some((last_ref, parser::State::HAS_CHILDREN))
269        } else {
270            None
271        }
272    }
273
274    fn cont(
275        &self,
276        arena: &mut Arena,
277        node_ref: NodeRef,
278        reader: &mut text::BasicReader,
279        _ctx: &mut parser::Context,
280    ) -> Option<parser::State> {
281        let (line, _) = reader.peek_line_bytes()?;
282        if is_blank(&line) {
283            return Some(parser::State::HAS_CHILDREN);
284        }
285        let kd = as_extension_data!(arena, node_ref, DefinitionList);
286        let w = indent_width(&line, reader.line_offset()).0;
287        if w < kd.offset as usize {
288            None
289        } else {
290            let (pos, padding) = indent_position(&line, reader.line_offset(), kd.offset as usize)?;
291            reader.advance_and_set_padding(pos, padding);
292            Some(parser::State::HAS_CHILDREN)
293        }
294    }
295
296    fn can_interrupt_paragraph(&self) -> bool {
297        true
298    }
299}
300
301impl From<DefinitionListParser> for AnyBlockParser {
302    fn from(p: DefinitionListParser) -> Self {
303        AnyBlockParser::Extension(Box::new(p))
304    }
305}
306
307#[derive(Debug, Default)]
308struct TermDefinitionParser {}
309
310impl TermDefinitionParser {
311    fn new() -> Self {
312        Self {}
313    }
314}
315
316impl BlockParser for TermDefinitionParser {
317    fn trigger(&self) -> &[u8] {
318        b":"
319    }
320
321    fn open(
322        &self,
323        arena: &mut Arena,
324        parent_ref: NodeRef,
325        reader: &mut text::BasicReader,
326        ctx: &mut parser::Context,
327    ) -> Option<(NodeRef, parser::State)> {
328        let (line, _) = reader.peek_line_bytes()?;
329        let pos = ctx.block_offset()?;
330        let indent = ctx.block_indent().unwrap_or(1);
331        if line[pos] != b':' || indent != 0 {
332            return None;
333        }
334        if !matches_extension_kind!(arena, parent_ref, DefinitionList) {
335            return None;
336        }
337        let para_opt = {
338            let list_kd = as_extension_data_mut!(arena, parent_ref, DefinitionList);
339            let para_opt = list_kd.temp_paragraph;
340            list_kd.temp_paragraph = None;
341            para_opt
342        };
343        if let Some(para_ref) = para_opt {
344            let para_td = as_type_data_mut!(arena, para_ref, Block);
345            let lines = para_td.take_source();
346            for line in &lines {
347                let term_ref = arena.new_node(Term::new());
348                as_type_data_mut!(arena, term_ref, Block)
349                    .append_source_line(line.trim_right_space(reader.source()));
350                parent_ref.append_child(arena, term_ref);
351            }
352            para_ref.remove(arena);
353        }
354        let list_kd = as_extension_data_mut!(arena, parent_ref, DefinitionList);
355        let (pos, padding) = indent_position(
356            &line[pos + 1..],
357            pos + 1,
358            (list_kd.offset as usize) - pos - 1,
359        )?;
360        reader.advance_and_set_padding(pos + 1, padding);
361        Some((
362            arena.new_node(TermDefinition::new()),
363            parser::State::HAS_CHILDREN,
364        ))
365    }
366
367    fn cont(
368        &self,
369        _arena: &mut Arena,
370        _node_ref: NodeRef,
371        _reader: &mut text::BasicReader,
372        _ctx: &mut parser::Context,
373    ) -> Option<parser::State> {
374        // definitionListParser detects end of the description.
375        // so this method will never be called.
376        Some(parser::State::HAS_CHILDREN)
377    }
378
379    fn close(
380        &self,
381        arena: &mut Arena,
382        node_ref: NodeRef,
383        _reader: &mut text::BasicReader,
384        _ctx: &mut parser::Context,
385    ) {
386        if as_type_data!(arena, node_ref, Block).has_blank_previous_line() {
387            let mut cur = node_ref;
388            while let Some(parent_ref) = arena[cur].parent() {
389                if matches_extension_kind!(arena, parent_ref, DefinitionList) {
390                    let kd = as_extension_data_mut!(arena, parent_ref, DefinitionList);
391                    kd.set_tight(false);
392                    break;
393                }
394                cur = parent_ref;
395            }
396        }
397    }
398
399    fn can_interrupt_paragraph(&self) -> bool {
400        true
401    }
402}
403
404impl From<TermDefinitionParser> for AnyBlockParser {
405    fn from(p: TermDefinitionParser) -> Self {
406        AnyBlockParser::Extension(Box::new(p))
407    }
408}
409
410// }}}
411
412// Renderer {{{
413
414struct DefinitionListHtmlRenderer<W: TextWrite> {
415    _phantom: core::marker::PhantomData<W>,
416    writer: html::Writer,
417}
418
419impl<W: TextWrite> DefinitionListHtmlRenderer<W> {
420    fn new(html_opts: html::Options) -> Self {
421        Self {
422            _phantom: core::marker::PhantomData,
423            writer: html::Writer::with_options(html_opts),
424        }
425    }
426}
427
428impl<W: TextWrite> RenderNode<W> for DefinitionListHtmlRenderer<W> {
429    fn render_node<'a>(
430        &self,
431        w: &mut W,
432        _source: &'a str,
433        _arena: &'a Arena,
434        _node_ref: NodeRef,
435        entering: bool,
436        _context: &mut renderer::Context,
437    ) -> Result<WalkStatus> {
438        if entering {
439            self.writer.write_safe_str(w, "<dl>\n")?
440        } else {
441            self.writer.write_safe_str(w, "</dl>\n")?
442        }
443        Ok(WalkStatus::Continue)
444    }
445}
446
447impl<'cb, W> NodeRenderer<'cb, W> for DefinitionListHtmlRenderer<W>
448where
449    W: TextWrite + 'cb,
450{
451    fn register_node_renderer_fn(self, nrr: &mut impl NodeRendererRegistry<'cb, W>) {
452        nrr.register_node_renderer_fn(TypeId::of::<DefinitionList>(), BoxRenderNode::new(self));
453    }
454}
455
456struct TermHtmlRenderer<W: TextWrite> {
457    _phantom: core::marker::PhantomData<W>,
458    writer: html::Writer,
459}
460
461impl<W: TextWrite> TermHtmlRenderer<W> {
462    fn new(html_opts: html::Options) -> Self {
463        Self {
464            _phantom: core::marker::PhantomData,
465            writer: html::Writer::with_options(html_opts),
466        }
467    }
468}
469
470impl<W: TextWrite> RenderNode<W> for TermHtmlRenderer<W> {
471    fn render_node<'a>(
472        &self,
473        w: &mut W,
474        _source: &'a str,
475        _arena: &'a Arena,
476        _node_ref: NodeRef,
477        entering: bool,
478        _context: &mut renderer::Context,
479    ) -> Result<WalkStatus> {
480        if entering {
481            self.writer.write_safe_str(w, "<dt>")?
482        } else {
483            self.writer.write_safe_str(w, "</dt>\n")?
484        }
485        Ok(WalkStatus::Continue)
486    }
487}
488
489impl<'cb, W> NodeRenderer<'cb, W> for TermHtmlRenderer<W>
490where
491    W: TextWrite + 'cb,
492{
493    fn register_node_renderer_fn(self, nrr: &mut impl NodeRendererRegistry<'cb, W>) {
494        nrr.register_node_renderer_fn(TypeId::of::<Term>(), BoxRenderNode::new(self));
495    }
496}
497
498struct TermDefinitionHtmlRenderer<W: TextWrite> {
499    _phantom: core::marker::PhantomData<W>,
500    writer: html::Writer,
501}
502
503impl<W: TextWrite> TermDefinitionHtmlRenderer<W> {
504    fn new(html_opts: html::Options) -> Self {
505        Self {
506            _phantom: core::marker::PhantomData,
507            writer: html::Writer::with_options(html_opts),
508        }
509    }
510}
511
512impl<W: TextWrite> RenderNode<W> for TermDefinitionHtmlRenderer<W> {
513    fn render_node<'a>(
514        &self,
515        w: &mut W,
516        _source: &'a str,
517        arena: &'a Arena,
518        node_ref: NodeRef,
519        entering: bool,
520        _context: &mut renderer::Context,
521    ) -> Result<WalkStatus> {
522        if entering {
523            self.writer.write_safe_str(w, "<dd>")?;
524            if let Some(p) = arena[node_ref].parent() {
525                if matches_extension_kind!(arena, p, DefinitionList) {
526                    let kd = as_extension_data!(arena, p, DefinitionList);
527                    if !kd.is_tight() {
528                        self.writer.write_safe_str(w, "\n")?;
529                    }
530                }
531            }
532        } else {
533            self.writer.write_safe_str(w, "</dd>\n")?
534        }
535        Ok(WalkStatus::Continue)
536    }
537}
538
539impl<'cb, W> NodeRenderer<'cb, W> for TermDefinitionHtmlRenderer<W>
540where
541    W: TextWrite + 'cb,
542{
543    fn register_node_renderer_fn(self, nrr: &mut impl NodeRendererRegistry<'cb, W>) {
544        nrr.register_node_renderer_fn(TypeId::of::<TermDefinition>(), BoxRenderNode::new(self));
545    }
546}
547
548/// Returns true if the given node is a child of a tight list, otherwise false.
549#[inline(always)]
550pub fn is_in_tight_list(arena: &ast::Arena, node_ref: ast::NodeRef) -> bool {
551    if let Some(p) = arena[node_ref].parent() {
552        if let Some(gp) = arena[p].parent() {
553            if matches_extension_kind!(arena, gp, DefinitionList) {
554                let kd = as_extension_data!(arena, gp, DefinitionList);
555                return kd.is_tight();
556            }
557        }
558    }
559    html::is_in_tight_list(arena, node_ref)
560}
561
562// }}} Renderer
563
564// Extension {{{
565
566/// Returns a parser extension that parses definition lists.
567pub fn definition_list_parser_extension() -> impl ParserExtension {
568    parser_extension(|p| {
569        p.add_block_parser(DefinitionListParser::new, NoParserOptions, 101);
570        p.add_block_parser(TermDefinitionParser::new, NoParserOptions, 102);
571    })
572}
573
574/// Returns a renderer extension that renders definition lists as HTML.
575pub fn definition_list_html_renderer_extension<'cb, W>() -> impl RendererExtension<'cb, W>
576where
577    W: TextWrite + 'cb,
578{
579    renderer_extension(move |r| {
580        r.add_node_renderer(DefinitionListHtmlRenderer::new, NoRendererOptions);
581        r.add_node_renderer(TermDefinitionHtmlRenderer::new, NoRendererOptions);
582        r.add_node_renderer(TermHtmlRenderer::new, NoRendererOptions);
583    })
584    .and(html::paragraph_renderer(ParagraphRendererOptions {
585        is_in_tight_block: Some(is_in_tight_list),
586        ..Default::default()
587    }))
588}
589
590// }}}