Skip to main content

rushdown_emoji/
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 alloc::rc::Rc;
8use alloc::string::String;
9use alloc::vec::Vec;
10use core::any::TypeId;
11use core::fmt;
12use core::fmt::Write;
13use rushdown::as_extension_data;
14use rushdown::ast::pp_indent;
15use rushdown::ast::Arena;
16use rushdown::ast::KindData;
17use rushdown::ast::NodeKind;
18use rushdown::ast::NodeRef;
19use rushdown::ast::NodeType;
20use rushdown::ast::PrettyPrint;
21use rushdown::ast::WalkStatus;
22use rushdown::parser;
23use rushdown::parser::AnyInlineParser;
24use rushdown::parser::InlineParser;
25use rushdown::parser::Parser;
26use rushdown::parser::ParserExtension;
27use rushdown::parser::ParserExtensionFn;
28use rushdown::parser::ParserOptions;
29use rushdown::parser::PRIORITY_EMPHASIS;
30use rushdown::renderer;
31use rushdown::renderer::html;
32use rushdown::renderer::html::Renderer;
33use rushdown::renderer::html::RendererExtension;
34use rushdown::renderer::html::RendererExtensionFn;
35use rushdown::renderer::BoxRenderNode;
36use rushdown::renderer::NodeRenderer;
37use rushdown::renderer::NodeRendererRegistry;
38use rushdown::renderer::RenderNode;
39use rushdown::renderer::RendererOptions;
40use rushdown::renderer::TextWrite;
41use rushdown::text;
42use rushdown::text::Reader;
43use rushdown::util::AsciiWordSet;
44use rushdown::Result;
45
46// AST {{{
47
48/// Represents an emoji in the AST.
49#[derive(Debug)]
50pub struct Emoji {
51    emoji: &'static emojis::Emoji,
52}
53
54impl Emoji {
55    /// Creates a new `Emoji` node with the given emoji data.
56    pub fn new(emoji: &'static emojis::Emoji) -> Self {
57        Self { emoji }
58    }
59
60    /// Returns the name of the emoji.
61    #[inline(always)]
62    pub fn name(&self) -> &'static str {
63        self.emoji.name()
64    }
65
66    /// Returns the first GitHub shortcode for this emoji.
67    #[inline(always)]
68    pub fn shortcode(&self) -> Option<&str> {
69        self.emoji.shortcode()
70    }
71
72    /// Returns an iterator over the GitHub shortcodes for this emoji.
73    #[inline(always)]
74    pub fn shortcodes(&self) -> impl Iterator<Item = &str> + Clone {
75        self.emoji.shortcodes()
76    }
77
78    /// Returns the Unicode character(s) for this emoji.
79    #[inline(always)]
80    pub fn as_str(&self) -> &str {
81        self.emoji.as_str()
82    }
83
84    /// Returns the Unicode character(s) for this emoji as bytes.
85    #[inline(always)]
86    pub fn as_bytes(&self) -> &[u8] {
87        self.emoji.as_str().as_bytes()
88    }
89}
90
91impl NodeKind for Emoji {
92    fn typ(&self) -> NodeType {
93        NodeType::Inline
94    }
95
96    fn kind_name(&self) -> &'static str {
97        "Emoji"
98    }
99}
100
101impl PrettyPrint for Emoji {
102    fn pretty_print(&self, w: &mut dyn Write, _source: &str, level: usize) -> fmt::Result {
103        writeln!(w, "{}name: {:?}", pp_indent(level), self.emoji.name())?;
104        writeln!(
105            w,
106            "{}shortcodes: {:?}",
107            pp_indent(level),
108            self.emoji.shortcodes().collect::<Vec<_>>()
109        )
110    }
111}
112
113impl From<Emoji> for KindData {
114    fn from(e: Emoji) -> Self {
115        KindData::Extension(Box::new(e))
116    }
117}
118
119// }}} AST
120
121// Parser {{{
122
123/// Options for the emoji parser.
124#[derive(Debug, Clone, Default)]
125pub struct EmojiParserOptions {
126    /// An optional set of shortcodes to ignore when parsing emojis. If provided, any shortcode in
127    /// this set will not be parsed as an emoji.
128    pub blacklist: Option<Rc<AsciiWordSet>>,
129}
130
131impl ParserOptions for EmojiParserOptions {}
132
133#[derive(Debug, Default)]
134struct EmojiParser {
135    options: EmojiParserOptions,
136}
137
138impl EmojiParser {
139    fn with_options(options: EmojiParserOptions) -> Self {
140        Self { options }
141    }
142}
143
144impl InlineParser for EmojiParser {
145    fn trigger(&self) -> &[u8] {
146        b":"
147    }
148
149    fn parse(
150        &self,
151        arena: &mut Arena,
152        _parent_ref: NodeRef,
153        reader: &mut text::BlockReader,
154        _ctx: &mut parser::Context,
155    ) -> Option<NodeRef> {
156        let (line, _) = reader.peek_line_bytes()?;
157        if line.len() < 2 {
158            return None;
159        }
160        let mut i = 1;
161        while i < line.len() {
162            let c = line[i];
163            if c.is_ascii_alphanumeric() || c == b'_' || c == b'-' || c == b'+' {
164                i += 1;
165            } else {
166                break;
167            }
168        }
169        if i >= line.len() || line[i] != b':' {
170            return None;
171        }
172        reader.advance(i + 1);
173        let shortcode = unsafe { str::from_utf8_unchecked(&line[1..i]) };
174        if self
175            .options
176            .blacklist
177            .as_ref()
178            .is_some_and(|blacklist| blacklist.contains(shortcode))
179        {
180            return None;
181        }
182        emojis::get_by_shortcode(shortcode).map(|emoji| arena.new_node(Emoji::new(emoji)))
183    }
184}
185
186impl From<EmojiParser> for AnyInlineParser {
187    fn from(p: EmojiParser) -> Self {
188        AnyInlineParser::Extension(Box::new(p))
189    }
190}
191
192// }}}
193
194// Renderer {{{
195
196/// Options for the emoji HTML renderer.
197#[derive(Debug, Clone, Default)]
198pub struct EmojiHtmlRendererOptions {
199    /// An optional template string for rendering emojis. If provided, this template will be used
200    /// to render emojis instead of the default behavior. The template can include a `{shortcode}`,
201    /// `{emoji}`, or `{name}` placeholder.
202    pub template: Option<String>,
203}
204
205impl RendererOptions for EmojiHtmlRendererOptions {}
206
207struct EmojiHtmlRenderer<W: TextWrite> {
208    _phantom: core::marker::PhantomData<W>,
209    writer: html::Writer,
210    options: EmojiHtmlRendererOptions,
211}
212
213impl<W: TextWrite> EmojiHtmlRenderer<W> {
214    fn new(html_opts: html::Options, options: EmojiHtmlRendererOptions) -> Self {
215        Self {
216            _phantom: core::marker::PhantomData,
217            writer: html::Writer::with_options(html_opts),
218            options,
219        }
220    }
221}
222
223impl<W: TextWrite> RenderNode<W> for EmojiHtmlRenderer<W> {
224    fn render_node<'a>(
225        &self,
226        w: &mut W,
227        _source: &'a str,
228        arena: &'a Arena,
229        node_ref: NodeRef,
230        entering: bool,
231        _context: &mut renderer::Context,
232    ) -> Result<WalkStatus> {
233        if entering {
234            let emoji = as_extension_data!(arena, node_ref, Emoji);
235            match &self.options.template {
236                Some(template) => {
237                    let rendered = template::render(
238                        template,
239                        &[
240                            ("emoji", emoji.as_str()),
241                            ("shortcode", emoji.shortcode().unwrap_or("")),
242                            ("name", emoji.name()),
243                        ],
244                    );
245                    self.writer.write_html(w, &rendered)?
246                }
247                None => self.writer.write_html(w, emoji.as_str())?,
248            }
249        }
250        Ok(WalkStatus::Continue)
251    }
252}
253
254impl<'cb, W> NodeRenderer<'cb, W> for EmojiHtmlRenderer<W>
255where
256    W: TextWrite + 'cb,
257{
258    fn register_node_renderer_fn(self, nrr: &mut impl NodeRendererRegistry<'cb, W>) {
259        nrr.register_node_renderer_fn(TypeId::of::<Emoji>(), BoxRenderNode::new(self));
260    }
261}
262// }}} Renderer
263
264// Extension {{{
265
266/// Returns a parser extension that parses emojis.
267pub fn emoji_parser_extension(options: EmojiParserOptions) -> impl ParserExtension {
268    ParserExtensionFn::new(|p: &mut Parser| {
269        p.add_inline_parser(EmojiParser::with_options, options, PRIORITY_EMPHASIS - 100);
270    })
271}
272
273/// Returns a renderer extension that renders emojis as HTML.
274pub fn emoji_html_renderer_extension<'cb, W>(
275    options: EmojiHtmlRendererOptions,
276) -> impl RendererExtension<'cb, W>
277where
278    W: TextWrite + 'cb,
279{
280    RendererExtensionFn::new(move |r: &mut Renderer<'cb, W>| {
281        r.add_node_renderer(EmojiHtmlRenderer::new, options);
282    })
283}
284
285// }}}
286
287// template {{{
288mod template {
289    use alloc::string::String;
290
291    pub(crate) fn render(tpl: &str, vars: &[(&str, &str)]) -> String {
292        let mut out = String::with_capacity(tpl.len());
293
294        let mut i = 0;
295        while let Some(open_rel) = tpl[i..].find('{') {
296            let open = i + open_rel;
297            out.push_str(&tpl[i..open]);
298
299            let rest = &tpl[open + 1..];
300            if let Some(close_rel) = rest.find('}') {
301                let key = &rest[..close_rel];
302                if let Some((_, v)) = vars.iter().find(|(k, _)| *k == key) {
303                    out.push_str(v);
304                } else {
305                    out.push('{');
306                    out.push_str(key);
307                    out.push('}');
308                }
309                i = open + 1 + close_rel + 1;
310            } else {
311                out.push_str(&tpl[open..]);
312                return out;
313            }
314        }
315
316        out.push_str(&tpl[i..]);
317        out
318    }
319}
320// }}}