From d794e88bdcc98963307cff8d86f6e3fda8ec6952 Mon Sep 17 00:00:00 2001 From: lqdev Date: Sun, 27 Aug 2023 18:25:21 +0200 Subject: [PATCH] add basic support for custom emoji --- content/about-treehouse.tree | 8 ++ content/about/emoji.tree | 8 ++ content/index.tree | 13 +-- crates/treehouse/src/config.rs | 6 ++ crates/treehouse/src/html/markdown.rs | 122 +++++++++++++++++++++++++- crates/treehouse/src/html/tree.rs | 2 +- static/css/main.css | 7 ++ static/emoji/hueh.png | Bin 0 -> 2637 bytes static/js/tree.js | 6 +- treehouse.toml | 3 + 10 files changed, 163 insertions(+), 12 deletions(-) create mode 100644 content/about/emoji.tree create mode 100755 static/emoji/hueh.png diff --git a/content/about-treehouse.tree b/content/about-treehouse.tree index 2644613..e060e50 100644 --- a/content/about-treehouse.tree +++ b/content/about-treehouse.tree @@ -19,6 +19,13 @@ % id = "01H89RFHCQDC73N0MD2ZF629MF" - wouldn't you make yourself at home? + % id = "01H8VWEFHZA94G0DNPD79YV535" + + … + + % content.link = "about/emoji" + id = "01H8VWEFHZ7Z71WJ347WFMC9YT" + + by the way did you know this website has custom emojis? and quite a lot of them, too + % id = "01H89RFHCQKAPHSGCDN832QRMD" + ### the treehouse is a statement of artistic expression @@ -125,6 +132,7 @@ % id = "01H89RFHCQ48R7BCZV8JWPVFCY" + have I invented something new here? + % id = "01H8VWEFJ1BGA21FBVHC4TFF3V" - the "Choose Your Own Poem" lol % id = "01H89RFHCQAXJ0ST31TP1A104V" diff --git a/content/about/emoji.tree b/content/about/emoji.tree new file mode 100644 index 0000000..9fbcd77 --- /dev/null +++ b/content/about/emoji.tree @@ -0,0 +1,8 @@ +% id = "01H8VWE6M29SZXJDDAX50K99V7" +- ### the emojipedia + + % id = "01H8VWE6M2EN8YBGPT1RTWE638" + - (no, not that one.) + + % id = "emoji/hueh" + - :hueh: - stolen from the Hat in Time Discord server diff --git a/content/index.tree b/content/index.tree index c0e61c7..c406604 100644 --- a/content/index.tree +++ b/content/index.tree @@ -4,31 +4,32 @@ % id = "01H8V556P1PND8DQ73XBTZZJH7" - welcome! make yourself at home + % id = "01H8VWEHX501SNYQTE61WX7YJC" - _"owo, what's this?"_ % id = "about" - content.link = "about" + content.link = "about" + ## about me % id = "about-treehouse" - content.link = "about-treehouse" + content.link = "about-treehouse" + ## about this % id = "01H8V556P1GRAA3717VH3QJFMV" - hobby corners % id = "programming" - content.link = "2023-08-20-under-construction" + content.link = "2023-08-20-under-construction" + ## programming % id = "music" - content.link = "2023-08-20-under-construction" + content.link = "2023-08-20-under-construction" + ## music % id = "games" - content.link = "2023-08-20-under-construction" + content.link = "2023-08-20-under-construction" + ## games % id = "3d-printing" - content.link = "2023-08-20-under-construction" + content.link = "2023-08-20-under-construction" + ## 3D printing diff --git a/crates/treehouse/src/config.rs b/crates/treehouse/src/config.rs index d92a401..01cf00d 100644 --- a/crates/treehouse/src/config.rs +++ b/crates/treehouse/src/config.rs @@ -15,6 +15,12 @@ pub struct Config { /// Links exported to Markdown for use with reference syntax `[text][def:key]`. pub defs: HashMap, + + /// Overrides for emoji filenames. Useful for setting up aliases. + /// + /// On top of this, emojis are autodiscovered by walking the `static/emoji` directory. + #[serde(default)] + pub emoji: HashMap, } impl Config { diff --git a/crates/treehouse/src/html/markdown.rs b/crates/treehouse/src/html/markdown.rs index 019566a..29700ae 100644 --- a/crates/treehouse/src/html/markdown.rs +++ b/crates/treehouse/src/html/markdown.rs @@ -30,12 +30,18 @@ use pulldown_cmark::escape::{escape_href, escape_html, StrWrite}; use pulldown_cmark::{Alignment, CodeBlockKind, Event, LinkType, Tag}; use pulldown_cmark::{CowStr, Event::*}; +use crate::config::Config; +use crate::state::Treehouse; + enum TableState { Head, Body, } struct HtmlWriter<'a, I, W> { + treehouse: &'a Treehouse, + config: &'a Config, + /// Iterator supplying events. iter: I, @@ -49,6 +55,8 @@ struct HtmlWriter<'a, I, W> { table_alignments: Vec, table_cell_index: usize, numbers: HashMap, usize>, + + in_code_block: bool, } impl<'a, I, W> HtmlWriter<'a, I, W> @@ -56,8 +64,10 @@ where I: Iterator>, W: StrWrite, { - fn new(iter: I, writer: W) -> Self { + fn new(treehouse: &'a Treehouse, config: &'a Config, iter: I, writer: W) -> Self { Self { + treehouse, + config, iter, writer, end_newline: true, @@ -65,6 +75,7 @@ where table_alignments: vec![], table_cell_index: 0, numbers: HashMap::new(), + in_code_block: false, } } @@ -95,7 +106,7 @@ where self.end_tag(tag)?; } Text(text) => { - escape_html(&mut self.writer, &text)?; + self.run_text(&text)?; self.end_newline = text.ends_with('\n'); } Code(text) => { @@ -211,6 +222,7 @@ where } } Tag::CodeBlock(info) => { + self.in_code_block = true; if !self.end_newline { self.write_newline()?; } @@ -342,6 +354,7 @@ where } Tag::CodeBlock(_) => { self.write("\n")?; + self.in_code_block = false; } Tag::List(Some(_)) => { self.write("\n")?; @@ -372,6 +385,107 @@ where Ok(()) } + fn run_text(&mut self, text: &str) -> io::Result<()> { + struct EmojiParser<'a> { + text: &'a str, + position: usize, + } + + enum Token<'a> { + Text(&'a str), + Emoji(&'a str), + } + + impl<'a> EmojiParser<'a> { + fn current(&self) -> Option { + self.text[self.position..].chars().next() + } + + fn next_token(&mut self) -> Option> { + match self.current() { + Some(':') => { + let text_start = self.position; + self.position += 1; + if self.current().is_some_and(|c| c.is_alphabetic()) { + let name_start = self.position; + while let Some(c) = self.current() { + if c.is_alphanumeric() || c == '_' { + self.position += c.len_utf8(); + } else { + break; + } + } + if self.current() == Some(':') { + let name_end = self.position; + self.position += 1; + Some(Token::Emoji(&self.text[name_start..name_end])) + } else { + Some(Token::Text(&self.text[text_start..self.position])) + } + } else { + Some(Token::Text(&self.text[text_start..self.position])) + } + } + Some(_) => { + let start = self.position; + while let Some(c) = self.current() { + if c == ':' { + break; + } else { + self.position += c.len_utf8(); + } + } + let end = self.position; + Some(Token::Text(&self.text[start..end])) + } + None => None, + } + } + } + + if self.in_code_block { + escape_html(&mut self.writer, text)?; + } else { + let mut parser = EmojiParser { text, position: 0 }; + while let Some(token) = parser.next_token() { + match token { + Token::Text(text) => escape_html(&mut self.writer, text)?, + Token::Emoji(name) => { + if let Some(filename) = self.config.emoji.get(name) { + let branch_id = self + .treehouse + .branches_by_named_id + .get(&format!("emoji/{name}")) + .copied(); + if let Some(branch) = branch_id.map(|id| self.treehouse.tree.branch(id)) + { + self.writer.write_str("")?; + } + self.writer.write_str("")?; + if branch_id.is_some() { + self.writer.write_str("")?; + } + } else { + self.writer.write_str(":")?; + escape_html(&mut self.writer, name)?; + self.writer.write_str(":")?; + } + } + } + } + } + + Ok(()) + } + // run raw text, consuming end tag fn raw_text(&mut self) -> io::Result<()> { let mut nest = 0; @@ -431,9 +545,9 @@ where /// /// "#); /// ``` -pub fn push_html<'a, I>(s: &mut String, iter: I) +pub fn push_html<'a, I>(s: &mut String, treehouse: &'a Treehouse, config: &'a Config, iter: I) where I: Iterator>, { - HtmlWriter::new(iter, s).run().unwrap(); + HtmlWriter::new(treehouse, config, iter, s).run().unwrap(); } diff --git a/crates/treehouse/src/html/tree.rs b/crates/treehouse/src/html/tree.rs index 44caa32..50f7abf 100644 --- a/crates/treehouse/src/html/tree.rs +++ b/crates/treehouse/src/html/tree.rs @@ -111,7 +111,7 @@ pub fn branch_to_html( }, Some(broken_link_callback), ); - markdown::push_html(s, markdown_parser); + markdown::push_html(s, treehouse, config, markdown_parser); if let Content::Link(link) = &branch.attributes.content { write!( diff --git a/static/css/main.css b/static/css/main.css index 19d4ec7..605dcb0 100644 --- a/static/css/main.css +++ b/static/css/main.css @@ -237,3 +237,10 @@ nav .logo { opacity: 100%; color: var(--text-color); } + +/* Style emojis to be readable */ + +img.emoji { + max-height: 1.375em; + vertical-align: bottom; +} diff --git a/static/emoji/hueh.png b/static/emoji/hueh.png new file mode 100755 index 0000000000000000000000000000000000000000..55677a805eed748f4dbcd87b03859b23dae53da2 GIT binary patch literal 2637 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