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#[derive(Debug)]
50pub struct Emoji {
51 emoji: &'static emojis::Emoji,
52}
53
54impl Emoji {
55 pub fn new(emoji: &'static emojis::Emoji) -> Self {
57 Self { emoji }
58 }
59
60 #[inline(always)]
62 pub fn name(&self) -> &'static str {
63 self.emoji.name()
64 }
65
66 #[inline(always)]
68 pub fn shortcode(&self) -> Option<&str> {
69 self.emoji.shortcode()
70 }
71
72 #[inline(always)]
74 pub fn shortcodes(&self) -> impl Iterator<Item = &str> + Clone {
75 self.emoji.shortcodes()
76 }
77
78 #[inline(always)]
80 pub fn as_str(&self) -> &str {
81 self.emoji.as_str()
82 }
83
84 #[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#[derive(Debug, Clone, Default)]
125pub struct EmojiParserOptions {
126 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#[derive(Debug, Clone, Default)]
198pub struct EmojiHtmlRendererOptions {
199 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}
262pub 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
273pub 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
285mod 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