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/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/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("");
for &child in branches {
- branch_to_html(s, treehouse, config, config_derived_data, file_id, child);
+ branch_to_html(
+ s,
+ treehouse,
+ config,
+ config_derived_data,
+ paths,
+ file_id,
+ child,
+ );
}
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/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..2aaaf61
--- /dev/null
+++ b/static/css/components/chat.css
@@ -0,0 +1,85 @@
+: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: block;
+
+ border: 1px solid var(--border-1);
+ padding: 12px 16px;
+ margin: 8px 0;
+ border-radius: 8px;
+
+ max-width: 60%;
+}
+
+th-chat-asked {
+ display: flex;
+ flex-direction: row-reverse;
+
+ &>button {
+ /* Reset