diff --git a/content/index.tree b/content/index.tree index 6573d60..4b6850c 100644 --- a/content/index.tree +++ b/content/index.tree @@ -13,7 +13,7 @@ - welcome! make yourself at home :ralsei_wave: % id = "01H8VWEHX501SNYQTE61WX7YJC" - - _"owo, what's this?"_ + - _"owo, what's this?"_ % id = "about" content.link = "about" diff --git a/content/kuroneko.tree b/content/kuroneko.tree new file mode 100644 index 0000000..90c0f17 --- /dev/null +++ b/content/kuroneko.tree @@ -0,0 +1,8 @@ +%% title = "back porch" + scripts = ["components/chat.js"] + styles = ["components/chat.css"] + +% template = true + cast = "chat js" + id = "01HSS5TMDD4WSZSXW17ANQN7VK" +- {% include_static chat/kuroneko.json %} diff --git a/content/treehouse/dev/chats.tree b/content/treehouse/dev/chats.tree new file mode 100644 index 0000000..30507ab --- /dev/null +++ b/content/treehouse/dev/chats.tree @@ -0,0 +1,11 @@ +%% title = "testing ground for (chit)chats" + scripts = ["components/chat.js"] + styles = ["components/chat.css"] + +% id = "01HSR695VNDXRGPC3XCHS3A61V" +- this is a test page for the treehouse's chat system. + +% cast = "chat js" + id = "01HSR695VNDYS0GDB527B9NPJS" + template = true +- {% include_static chat/treehouse/dev/chats/test.json %} diff --git a/crates/treehouse/src/cli/fix.rs b/crates/treehouse/src/cli/fix.rs index 8992c0b..1e48753 100644 --- a/crates/treehouse/src/cli/fix.rs +++ b/crates/treehouse/src/cli/fix.rs @@ -1,4 +1,4 @@ -use std::ops::Range; +use std::{ffi::OsStr, ops::Range}; use anyhow::Context; use treehouse_format::ast::Branch; @@ -154,7 +154,7 @@ pub fn fix_file_cli(fix_args: FixArgs) -> anyhow::Result<()> { pub fn fix_all_cli(fix_all_args: FixAllArgs, paths: &Paths<'_>) -> anyhow::Result<()> { for entry in WalkDir::new(paths.content_dir) { let entry = entry?; - if entry.file_type().is_file() { + if entry.file_type().is_file() && entry.path().extension() == Some(OsStr::new("tree")) { let file = std::fs::read_to_string(entry.path()) .with_context(|| format!("cannot read file to fix: {:?}", entry.path()))?; let utf8_filename = entry.path().to_string_lossy(); diff --git a/crates/treehouse/src/cli/generate.rs b/crates/treehouse/src/cli/generate.rs index de3001e..17c3887 100644 --- a/crates/treehouse/src/cli/generate.rs +++ b/crates/treehouse/src/cli/generate.rs @@ -291,6 +291,7 @@ impl Generator { treehouse, config, &mut config_derived_data, + paths, parsed_tree.file_id, &roots.branches, ); diff --git a/crates/treehouse/src/cli/serve.rs b/crates/treehouse/src/cli/serve.rs index 94647a0..6ab4ef2 100644 --- a/crates/treehouse/src/cli/serve.rs +++ b/crates/treehouse/src/cli/serve.rs @@ -86,6 +86,7 @@ fn get_content_type(path: &str) -> Option<&'static str> { _ if path.ends_with(".html") => Some("text/html"), _ if path.ends_with(".js") => Some("text/javascript"), _ if path.ends_with(".woff2") => Some("font/woff2"), + _ if path.ends_with(".svg") => Some("image/svg+xml"), _ => None, } } diff --git a/crates/treehouse/src/html/tree.rs b/crates/treehouse/src/html/tree.rs index 988b642..7961037 100644 --- a/crates/treehouse/src/html/tree.rs +++ b/crates/treehouse/src/html/tree.rs @@ -1,9 +1,10 @@ -use std::fmt::Write; +use std::{borrow::Cow, fmt::Write}; use pulldown_cmark::{BrokenLink, LinkType}; use treehouse_format::pull::BranchKind; use crate::{ + cli::Paths, config::{Config, ConfigDerivedData}, html::EscapeAttribute, state::{FileId, Treehouse}, @@ -20,6 +21,7 @@ pub fn branch_to_html( treehouse: &mut Treehouse, config: &Config, config_derived_data: &mut ConfigDerivedData, + paths: &Paths<'_>, file_id: FileId, branch_id: SemaBranchId, ) { @@ -49,6 +51,11 @@ pub fn branch_to_html( } else { "b" }; + let component = if !branch.attributes.cast.is_empty() { + Cow::Owned(format!("{component} {}", branch.attributes.cast)) + } else { + Cow::Borrowed(component) + }; let linked_branch = if let Content::Link(link) = &branch.attributes.content { format!(" data-th-link=\"{}\"", EscapeHtml(link)) @@ -62,9 +69,19 @@ pub fn branch_to_html( "" }; + let mut data_attributes = String::new(); + for (key, value) in &branch.attributes.data { + write!( + data_attributes, + " data-{key}=\"{}\"", + EscapeAttribute(value) + ) + .unwrap(); + } + write!( s, - "
  • ", + "
  • ", EscapeAttribute(&branch.html_id) ) .unwrap(); @@ -136,7 +153,7 @@ pub fn branch_to_html( } }; if branch.attributes.template { - final_markdown = mini_template::render(config, treehouse, &final_markdown); + final_markdown = mini_template::render(config, treehouse, paths, &final_markdown); } let markdown_parser = pulldown_cmark::Parser::new_with_broken_link_callback( &final_markdown, @@ -204,7 +221,15 @@ pub fn branch_to_html( let num_children = branch.children.len(); for i in 0..num_children { let child_id = treehouse.tree.branch(branch_id).children[i]; - branch_to_html(s, treehouse, config, config_derived_data, file_id, child_id); + branch_to_html( + s, + treehouse, + config, + config_derived_data, + paths, + file_id, + child_id, + ); } s.push_str(""); } @@ -221,12 +246,21 @@ pub fn branches_to_html( treehouse: &mut Treehouse, config: &Config, config_derived_data: &mut ConfigDerivedData, + paths: &Paths<'_>, file_id: FileId, branches: &[SemaBranchId], ) { s.push_str(""); } diff --git a/crates/treehouse/src/tree/attributes.rs b/crates/treehouse/src/tree/attributes.rs index 6a317ca..025195c 100644 --- a/crates/treehouse/src/tree/attributes.rs +++ b/crates/treehouse/src/tree/attributes.rs @@ -1,3 +1,5 @@ +use std::collections::HashMap; + use serde::{Deserialize, Serialize}; /// Top-level `%%` root attributes. @@ -79,6 +81,14 @@ pub struct Attributes { /// debug mode. #[serde(default)] pub stage: Stage, + + /// List of extra spells to cast on the branch. + #[serde(default)] + pub cast: String, + + /// List of extra `data` attributes to add to the block. + #[serde(default)] + pub data: HashMap, } /// Controls for block content presentation. diff --git a/crates/treehouse/src/tree/mini_template.rs b/crates/treehouse/src/tree/mini_template.rs index 947613e..3a1ae86 100644 --- a/crates/treehouse/src/tree/mini_template.rs +++ b/crates/treehouse/src/tree/mini_template.rs @@ -8,7 +8,7 @@ use std::ops::Range; use pulldown_cmark::escape::escape_html; -use crate::{config::Config, state::Treehouse}; +use crate::{cli::Paths, config::Config, state::Treehouse}; struct Lexer<'a> { input: &'a str, @@ -149,7 +149,7 @@ impl<'a> Renderer<'a> { self.output.push_str(&self.lexer.input[token.range.clone()]); } - fn render(&mut self, config: &Config, treehouse: &Treehouse) { + fn render(&mut self, config: &Config, treehouse: &Treehouse, paths: &Paths<'_>) { let kind_of = |token: &Token| token.kind; while let Some(token) = self.lexer.next() { @@ -166,6 +166,7 @@ impl<'a> Renderer<'a> { match Self::render_template( config, treehouse, + paths, self.lexer.input[inside.as_ref().unwrap().range.clone()].trim(), ) { Ok(s) => match escaping { @@ -192,22 +193,24 @@ impl<'a> Renderer<'a> { fn render_template( config: &Config, _treehouse: &Treehouse, + paths: &Paths<'_>, template: &str, ) -> Result { let (function, arguments) = template.split_once(' ').unwrap_or((template, "")); match function { "pic" => Ok(config.pic_url(arguments)), - "c++" => Ok("".into()), + "include_static" => std::fs::read_to_string(paths.static_dir.join(arguments)) + .map_err(|_| InvalidTemplate), _ => Err(InvalidTemplate), } } } -pub fn render(config: &Config, treehouse: &Treehouse, input: &str) -> String { +pub fn render(config: &Config, treehouse: &Treehouse, paths: &Paths<'_>, input: &str) -> String { let mut renderer = Renderer { lexer: Lexer::new(input), output: String::new(), }; - renderer.render(config, treehouse); + renderer.render(config, treehouse, paths); renderer.output } diff --git a/static/character/coco/eyes_closed.svg b/static/character/coco/eyes_closed.svg new file mode 100644 index 0000000..67e67ab --- /dev/null +++ b/static/character/coco/eyes_closed.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/static/character/coco/neutral.svg b/static/character/coco/neutral.svg new file mode 100644 index 0000000..e8f1ee3 --- /dev/null +++ b/static/character/coco/neutral.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/static/chat/kuroneko.json b/static/chat/kuroneko.json new file mode 100644 index 0000000..b804f78 --- /dev/null +++ b/static/chat/kuroneko.json @@ -0,0 +1,322 @@ +{ + "nodes": { + "init": { + "kind": "say", + "character": "coco", + "expression": "eyes_closed", + "content": "…", + "then": "initQuestion" + }, + "initQuestion": { + "kind": "ask", + "questions": [{ "content": "Kitty?", "then": "kitty" }] + }, + "kitty": { + "kind": "say", + "character": "coco", + "expression": "eyes_closed", + "content": "I'm no ordinary cat.", + "then": "introductions" + }, + "introductions": { + "kind": "ask", + "questions": [ + { + "content": "…woah! You speak!", + "then": "introductions.youSpeak" + }, + { "content": "Certainly.", "then": "introductions.certainly" } + ] + }, + "introductions.youSpeak": { + "kind": "say", + "character": "coco", + "content": "Yeah. No clue what's so surprising about that. I mean, I've spoken for as long as I remember! But you're not the first person I've met that was surprised that a tiny thing like me could speak.", + "then": "introductions.youSpeak2" + }, + "introductions.youSpeak2": { + "kind": "ask", + "questions": [ + { + "content": "Who was the other?", + "then": "introductions.youSpeak.theOther" + }, + { + "content": "I mean, obviously.", + "then": "introductions.youSpeak.anyways" + } + ] + }, + "introductions.youSpeak.theOther": { + "kind": "say", + "character": "coco", + "content": "My owner. I've never seen someone this freaked out in my life!", + "then": "introductions.youSpeak.theOther2" + }, + "introductions.youSpeak.theOther2": { + "kind": "ask", + "questions": [ + { + "content": "I'm not surprised at all.", + "then": "introductions.youSpeak.anyways" + }, + { + "content": "What about me?", + "then": "introductions.youSpeak.theOther.whatAboutMe" + }, + { + "content": "Who's your owner?", + "then": "introductions.youSpeak.theOther.owner" + } + ] + }, + "introductions.youSpeak.theOther.whatAboutMe": { + "kind": "say", + "character": "coco", + "expression": "eyes_closed", + "content": "You're a brave soul. Us cats can feel that. Nothing about your reaction came off as being scared.", + "then": "introductions.youSpeak.theOther.whatAboutMe2" + }, + "introductions.youSpeak.theOther.whatAboutMe2": { + "kind": "ask", + "questions": [ + { + "content": "Glad to hear that.", + "then": "introductions.youSpeak.anyways" + } + ] + }, + "introductions.youSpeak.theOther.owner": { + "kind": "say", + "character": "coco", + "content": "I'd ask her to introduce herself, but she hasn't been around lately. I believe she's out on a trip or something. No clue what the trip's about or when she'll be back.", + "then": "introductions.youSpeak.theOther.owner2" + }, + "introductions.youSpeak.theOther.owner2": { + "kind": "ask", + "questions": [ + { + "content": "\"She?\" You mean, your owner isn't liquidex?", + "then": "introductions.youSpeak.theOther.owner3" + }, + { + "content": "I see.", + "then": "introductions.youSpeak.anyways" + } + ] + }, + "introductions.youSpeak.theOther.owner3": { + "kind": "say", + "character": "coco", + "expression": "eyes_closed", + "content": "No.", + "then": "introductions.youSpeak.theOther.owner4" + }, + "introductions.youSpeak.theOther.owner4": { + "kind": "ask", + "questions": [ + { + "content": "I see.", + "then": "introductions.youSpeak.anyways" + } + ] + }, + "introductions.youSpeak.anyways": { + "kind": "say", + "character": "coco", + "content": "Anyways. Is there anything in particular you're looking for?", + "then": "introductions.whatAreYouLookingFor" + }, + "introductions.certainly": { + "kind": "say", + "character": "coco", + "content": "Interesting to see you keeping your composure after meeting a speaking cat.", + "then": "introductions.certainly2" + }, + "introductions.certainly2": { + "kind": "ask", + "questions": [ + { + "content": "I see no reason to be upset. I've seen weirder things.", + "then": "introductions.certainly3" + } + ] + }, + "introductions.certainly3": { + "kind": "say", + "character": "coco", + "content": "Well anyways, while my owner is out, why don't we have a little chat?", + "then": "introductions.whatAreYouLookingFor" + }, + "introductions.whatAreYouLookingFor": { + "kind": "ask", + "questions": [ + { + "content": "Tell me more about yourself.", + "then": "aboutYourself" + }, + { + "content": "Tell me more about this place.", + "then": "aboutPlace" + } + ] + }, + "aboutYourself": { + "kind": "say", + "character": "coco", + "content": "I'm Coco, a black cat that lives in the treehouse. I like scritches and treats. One day I climbed into the treehouse and got lost. Fortunately my fine owner lady found me and took good care of me! Nowadays I live a happy life with the treefolk.", + "then": "aboutYourself2" + }, + "aboutYourself2": { + "kind": "ask", + "questions": [ + { + "content": "(scritch Coco)", + "then": "aboutYourself2.scritch" + }, + { + "content": "Are you friends with the treefolk?", + "then": "aboutYourself2.friends" + }, + { + "content": "You said earlier you're no ordinary cat. What did you mean?", + "then": "aboutYourself2.extraordinary" + }, + { + "content": "That's all I wanted to know about you, thanks.", + "then": "introductions.whatAreYouLookingFor" + } + ] + }, + "aboutYourself2.scritch": { + "kind": "say", + "character": "coco", + "expression": "eyes_closed", + "content": "*purrs*", + "then": "aboutYourself2" + }, + "aboutYourself2.friends": { + "kind": "say", + "character": "coco", + "content": "I'm just here for the warmth, food, and scritches. The people here could just as easily be someone else. This is just a nice place to live, so I'm not complaining.", + "then": "aboutYourself2" + }, + "aboutYourself2.extraordinary": { + "kind": "say", + "character": "coco", + "content": "I speak. Is there anything more to say?", + "then": "aboutYourself2.extraordinary2" + }, + "aboutYourself2.extraordinary2": { + "kind": "ask", + "questions": [ + { + "content": "Well, that's the obvious part. Is there anything else?", + "then": "aboutYourself2.extraordinary3" + } + ] + }, + "aboutYourself2.extraordinary3": { + "kind": "say", + "character": "coco", + "expression": "eyes_closed", + "content": "…Maybe.", + "then": "aboutYourself2" + }, + "aboutPlace": { + "kind": "say", + "character": "coco", + "content": "This is the back porch of the house. What do you think?", + "then": "aboutPlace.definition" + }, + "aboutPlace.definition": { + "kind": "ask", + "questions": [ + { + "content": "It's empty.", + "then": "aboutPlace.definition.empty" + }, + { + "content": "It's wild.", + "then": "aboutPlace.definition.wild" + }, + { + "content": "It's odd.", + "then": "aboutPlace.definition.odd" + }, + { + "content": "It's even.", + "then": "aboutPlace.definition.even" + }, + { + "content": "What even are these answers?", + "then": "aboutPlace.definition.what" + } + ] + }, + "aboutPlace.definition.empty": { + "kind": "set", + "fact": "kuroneko/empty", + "then": "aboutPlace.definition.thankYou" + }, + "aboutPlace.definition.wild": { + "kind": "set", + "fact": "kuroneko/wild", + "then": "aboutPlace.definition.thankYou" + }, + "aboutPlace.definition.odd": { + "kind": "set", + "fact": "kuroneko/odd", + "then": "aboutPlace.definition.thankYou" + }, + "aboutPlace.definition.even": { + "kind": "set", + "fact": "kuroneko/even", + "then": "aboutPlace.definition.thankYou" + }, + "aboutPlace.definition.what": { + "kind": "set", + "fact": "kuroneko/what", + "then": "aboutPlace.definition.thankYou2" + }, + "aboutPlace.definition.thankYou": { + "kind": "say", + "character": "coco", + "expression": "eyes_closed", + "content": "Yeah, I think I agree.", + "then": "aboutPlace.thatsIt" + }, + "aboutPlace.definition.thankYou2": { + "kind": "say", + "character": "coco", + "expression": "eyes_closed", + "content": "I just asked you what you think about how the back porch looks. That's all.", + "then": "aboutPlace.theWhat" + }, + "aboutPlace.thatsIt": { + "kind": "ask", + "questions": [ + { + "content": "That's it? You're not gonna tell me anything else?", + "then": "aboutPlace.end" + } + ] + }, + "aboutPlace.theWhat": { + "kind": "ask", + "questions": [ + { + "content": "Seriously though, where in the world did that come from.", + "then": "aboutPlace.end" + } + ] + }, + "aboutPlace.end": { + "kind": "say", + "character": "coco", + "expression": "eyes_closed", + "content": "…", + "then": "introductions.whatAreYouLookingFor" + } + } +} diff --git a/static/chat/treehouse/dev/chats/test.json b/static/chat/treehouse/dev/chats/test.json new file mode 100644 index 0000000..94bff5a --- /dev/null +++ b/static/chat/treehouse/dev/chats/test.json @@ -0,0 +1,42 @@ +{ + "nodes": { + "init": { + "kind": "say", + "content": "hello! nice to meet you.", + "then": "question1" + }, + "question1": { + "kind": "ask", + "questions": [ + { "content": "Who are you?", "then": "whoAreYou" }, + { "content": "What is this?", "then": "whatIsThis" } + ] + }, + "whoAreYou": { + "kind": "say", + "content": "I'm liquidex. y'know, the guy running this place.", + "then": "question1" + }, + "whatIsThis": { + "kind": "say", + "content": "this is a test of the treehouse dialogue system. basically, I made myself a little framework for writing conversations between characters, and this is a test page for it.", + "then": "whatIsThisQuestion" + }, + "whatIsThisQuestion": { + "kind": "ask", + "questions": [ + { "content": "Right.", "then": "question1" }, + { + "content": "\"Dialogues\", seriously? What, are you British?", + "then": "british" + } + ] + }, + "british": { + "kind": "say", + "content": "no, but I think British is pretty damn funny.", + "then": "question1" + }, + "end": { "kind": "end" } + } +} diff --git a/static/css/components/chat.css b/static/css/components/chat.css new file mode 100644 index 0000000..bd2f13e --- /dev/null +++ b/static/css/components/chat.css @@ -0,0 +1,96 @@ +:root { + --icon-choose: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHZpZXdCb3g9IjAgMCAxNiAxNiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTYgMTJMMTAgOEw2IDQiIHN0cm9rZT0iIzU1NDIzZSIgc3Ryb2tlLXdpZHRoPSIyIi8+Cjwvc3ZnPgo='); +} + +@media (prefers-color-scheme: dark) { + :root { + --icon-choose: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHZpZXdCb3g9IjAgMCAxNiAxNiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTYgMTJMMTAgOEw2IDQiIHN0cm9rZT0iI2Q3Y2RiZiIgc3Ryb2tlLXdpZHRoPSIyIi8+Cjwvc3ZnPgo='); + } +} + +th-chat { + display: flex; + flex-direction: column; +} + +th-chat-said { + display: flex; + flex-direction: row; + align-items: center; + + border: 1px solid var(--border-1); + padding: 12px 16px; + margin: 8px 0; + border-radius: 8px; + + max-width: 60%; + + &>.picture { + padding-right: 16px; + align-self: baseline; + } + + &>.text-container { + display: inline-block; + } +} + +th-chat-asked { + display: flex; + flex-direction: row-reverse; + + &>button { + /* Reset