wayver's git archive


an obsidian renderer
git clone https://git.wayver.dev/sable

sable-markdown/src/render/mod.rs@2b84405277e54ab809e328cf0237374d4b4dbd0c

raw
Date Commit Message Author Files + -
2026-02-23 01:55 initial mvp wayverd 139 17808 0
...

1mod block;
2mod highlighter;
3mod index;
4mod inline;
5mod tests;
6mod util;
7
8use std::collections::HashMap;
9
10use pretty::{Arena, DocBuilder};
11
12use crate::ast::{Document, Inline, LinkDefinition};
13
14pub trait UrlRewriter {
15    /// Rewrite a given URL to someplace new.
16    ///
17    /// `target` is only used for Wikilinks currently.
18    fn rewrite(&self, path: &str, target: Option<&str>) -> String;
19}
20
21pub struct Config<'a> {
22    pub(crate) width: usize,
23    pub(crate) link_rewriter: Option<Box<dyn UrlRewriter + 'a>>,
24    pub(crate) wikilink_rewriter: Option<Box<dyn UrlRewriter + 'a>>,
25}
26
27impl Default for Config<'_> {
28    fn default() -> Self {
29        Self {
30            width: 80,
31            link_rewriter: None,
32            wikilink_rewriter: None,
33        }
34    }
35}
36
37impl<'a> Config<'a> {
38    #[must_use]
39    pub fn with_width(self, width: usize) -> Self {
40        Self { width, ..self }
41    }
42
43    #[must_use]
44    pub fn with_link_rewriter<R: UrlRewriter + 'a>(self, url_rewriter: R) -> Self {
45        Self {
46            link_rewriter: Some(Box::new(url_rewriter)),
47            ..self
48        }
49    }
50
51    #[must_use]
52    pub fn with_wikilink_rewriter<R: UrlRewriter + 'a>(self, url_rewriter: R) -> Self {
53        Self {
54            wikilink_rewriter: Some(Box::new(url_rewriter)),
55            ..self
56        }
57    }
58
59    pub(crate) fn rewrite_link(&self, path: &str, target: Option<&str>) -> String {
60        if let Some(rewriter) = &self.link_rewriter {
61            return rewriter.rewrite(path, target);
62        }
63
64        path.to_owned()
65    }
66
67    pub(crate) fn rewrite_wikilink(&self, path: &str, target: Option<&str>) -> String {
68        if let Some(rewriter) = &self.wikilink_rewriter {
69            return rewriter.rewrite(path, target);
70        }
71
72        path.to_owned()
73    }
74}
75
76pub(crate) struct Context {
77    // Mapping of footnote labels to their indices in the footnote list.
78    footnote_index: HashMap<String, usize>,
79    // Mapping of link labels to their definitions.
80    link_definitions: HashMap<Vec<Inline>, LinkDefinition>,
81}
82
83impl Context {
84    pub(crate) fn new(ast: &Document) -> Self {
85        let (footnote_index, link_definitions) = index::get_indicies(ast);
86        Self {
87            footnote_index,
88            link_definitions,
89        }
90    }
91
92    pub(crate) fn get_footnote_index(&self, label: &str) -> Option<&usize> {
93        self.footnote_index.get(label)
94    }
95
96    pub(crate) fn get_link_definition(&self, label: &Vec<Inline>) -> Option<&LinkDefinition> {
97        self.link_definitions.get(label)
98    }
99}
100
101/// Render the given Markdown AST to HTML.
102#[must_use]
103pub fn render_html(ast: &Document, config: &Config<'_>) -> String {
104    let mut buf = Vec::new();
105
106    {
107        let width = config.width;
108
109        let context = Context::new(ast);
110        let arena = Arena::new();
111        ast.to_doc(config, &context, &arena)
112            .render(width, &mut buf)
113            .unwrap();
114    }
115
116    String::from_utf8(buf).unwrap()
117}
118
119trait ToDoc<'a> {
120    fn to_doc(
121        &self,
122        config: &'a Config<'a>,
123        context: &'a Context,
124        arena: &'a Arena<'a>,
125    ) -> DocBuilder<'a, Arena<'a>, ()>;
126}
127
128impl<'a> ToDoc<'a> for Document {
129    fn to_doc(
130        &self,
131        config: &'a Config<'a>,
132        context: &'a Context,
133        arena: &'a Arena<'a>,
134    ) -> DocBuilder<'a, Arena<'a>, ()> {
135        self.blocks.to_doc(config, context, arena)
136    }
137}
138