diff --git a/.gitignore b/.gitignore
index 735fe92..1ccf600 100644
--- a/.gitignore
+++ b/.gitignore
@@ -25,3 +25,5 @@ bin
*.wasm
.wrangler
node_modules
+
+web/example02
\ No newline at end of file
diff --git a/internal/jabl/lexer.go b/internal/jabl/lexer.go
index 8fa1dad..2f5a93d 100644
--- a/internal/jabl/lexer.go
+++ b/internal/jabl/lexer.go
@@ -6,6 +6,7 @@ import (
"fmt"
"io"
"strconv"
+ "strings"
"text/scanner"
"unicode"
)
@@ -169,7 +170,15 @@ func (l *lexer) Lex(lval *yySymType) int {
default:
if text[0] == '"' && text[len(text)-1] == '"' {
// trim the start and end quotes and add a newline
- lval.String = text[1 : len(text)-1]
+ inner := text[1 : len(text)-1]
+
+ // replace escaped characters
+ inner = strings.ReplaceAll(inner, "\\n", "\n")
+ inner = strings.ReplaceAll(inner, "\\t", "\t")
+ inner = strings.ReplaceAll(inner, "\\\"", "\"")
+
+ lval.String = inner
+
return STRING
} else if text == "true" {
lval.Boolean = true
diff --git a/internal/jabl/testdata/examples/dynamic_goto/code.jabl b/internal/jabl/testdata/examples/dynamic_goto/code.jabl
new file mode 100644
index 0000000..5ddfc90
--- /dev/null
+++ b/internal/jabl/testdata/examples/dynamic_goto/code.jabl
@@ -0,0 +1,7 @@
+{
+ goto(1d6 + ".jabl")
+ set("a", 1d6)
+ choice("a", {
+ goto(get("a") + 2d6 + ".jabl")
+ })
+}
\ No newline at end of file
diff --git a/internal/jabl/testdata/examples/dynamic_goto/result.json b/internal/jabl/testdata/examples/dynamic_goto/result.json
new file mode 100644
index 0000000..44fb496
--- /dev/null
+++ b/internal/jabl/testdata/examples/dynamic_goto/result.json
@@ -0,0 +1,10 @@
+{
+ "output": "",
+ "choices": [
+ {
+ "text": "a",
+ "code": "{\n\tgoto(get(\"a\") + 2d6 + \".jabl\")\n}"
+ }
+ ],
+ "transition": "6.jabl"
+}
diff --git a/internal/jabl/testdata/examples/dynamic_goto/state_after.json b/internal/jabl/testdata/examples/dynamic_goto/state_after.json
new file mode 100644
index 0000000..2728c9d
--- /dev/null
+++ b/internal/jabl/testdata/examples/dynamic_goto/state_after.json
@@ -0,0 +1,3 @@
+{
+ "a": 6
+}
diff --git a/internal/jabl/testdata/examples/dynamic_goto/state_before.json b/internal/jabl/testdata/examples/dynamic_goto/state_before.json
new file mode 100644
index 0000000..9e26dfe
--- /dev/null
+++ b/internal/jabl/testdata/examples/dynamic_goto/state_before.json
@@ -0,0 +1 @@
+{}
\ No newline at end of file
diff --git a/internal/jabl/testdata/examples/print_string/code.jabl b/internal/jabl/testdata/examples/print_string/code.jabl
index 5d1fd1a..e6c176b 100644
--- a/internal/jabl/testdata/examples/print_string/code.jabl
+++ b/internal/jabl/testdata/examples/print_string/code.jabl
@@ -2,4 +2,6 @@
print("hello world")
print(("hello world"))
print("hello" + " " + "world")
+ print("hello \"world\"")
+ print("hello\nworld")
}
\ No newline at end of file
diff --git a/internal/jabl/testdata/examples/print_string/result.json b/internal/jabl/testdata/examples/print_string/result.json
index 39e9e29..53bf54b 100644
--- a/internal/jabl/testdata/examples/print_string/result.json
+++ b/internal/jabl/testdata/examples/print_string/result.json
@@ -1,5 +1,5 @@
{
- "output": "hello world\nhello world\nhello world\n",
+ "output": "hello world\nhello world\nhello world\nhello \"world\"\nhello\nworld\n",
"choices": null,
"transition": ""
}
diff --git a/web/app.css b/web/app.css
index 64e7a67..5ad3f41 100644
--- a/web/app.css
+++ b/web/app.css
@@ -145,4 +145,8 @@ body {
.option:hover {
background-color: #023021;
+}
+
+code {
+ background-color: #033827;
}
\ No newline at end of file
diff --git a/web/app.js b/web/app.js
index 140bf9d..c56cf45 100644
--- a/web/app.js
+++ b/web/app.js
@@ -11,11 +11,52 @@ const renderText = (text) => {
intervalId = setInterval(() => {
if (text && textIndex < text.length) {
- if (text.charAt(textIndex) === "\n") {
- consoleText.innerHTML += "
";
- } else {
- consoleText.innerHTML += text.charAt(textIndex);
+ switch (text.charAt(textIndex)) {
+ case "\n":
+ consoleText.innerHTML += "
";
+ break;
+ case "*":
+ let boldText = "";
+ textIndex++;
+ while (text.charAt(textIndex) !== "*") {
+ boldText += text.charAt(textIndex);
+ textIndex++;
+ }
+ consoleText.innerHTML += `${boldText}`;
+ break;
+ case "_":
+ let underlineText = "";
+ textIndex++;
+ while (text.charAt(textIndex) !== "_") {
+ underlineText += text.charAt(textIndex);
+ textIndex++;
+ }
+ consoleText.innerHTML += `${underlineText}`;
+ break;
+ case "`":
+ let codeText = "";
+ textIndex++;
+ while (text.charAt(textIndex) !== "`") {
+ codeText += text.charAt(textIndex);
+ textIndex++;
+ }
+ consoleText.innerHTML += `${codeText}
`;
+ break;
+ case "/":
+ let italicText = "";
+ textIndex++;
+ while (text.charAt(textIndex) !== "/") {
+ italicText += text.charAt(textIndex);
+ textIndex++;
+ }
+ consoleText.innerHTML += `${italicText}`;
+ break;
+ default:
+ console.log(text.charAt(textIndex));
+ consoleText.innerHTML += text.charAt(textIndex);
+ break;
}
+
// scroll consoleText to the bottom of what it is displaying
consoleText.parentElement.scrollTop =
consoleText.parentElement.scrollHeight;
@@ -24,6 +65,23 @@ const renderText = (text) => {
clearInterval(intervalId);
}
}, 10);
+
+ // can tap to skip the text animation
+ consoleText.onclick = () => {
+ if (text && textIndex < text.length) {
+ let html = text.replace(/\/([^/]+?)\//g, "$1");
+
+ html = html.replace(/\n/g, "
");
+ html = html.replace(/\*([^\*]*?)\*/g, "$1");
+ html = html.replace(/_([^_]+?)_/g, "$1");
+ html = html.replace(/`([^`]+)`/g, "$1
");
+
+ consoleText.innerHTML = html;
+
+ clearInterval(intervalId);
+ textIndex = text.length;
+ }
+ };
};
const renderChoices = (choices) => {
@@ -94,6 +152,25 @@ const run = async () => {
}
};
+const sources = [
+ {
+ id: 1,
+ name: "Example 1",
+ url: "https://raw.githubusercontent.com/jasoncabot/fabled-story-book/main/assets/example01/",
+ entrypoint: "entrypoint.jabl",
+ },
+ {
+ id: 2,
+ name: "Example 2",
+ url: "http://localhost:8788/example02/",
+ entrypoint: "0-choose-character.jabl",
+ },
+];
+const availableSources = sources.reduce((acc, source) => {
+ acc[source.id] = source.url;
+ return acc;
+}, {});
+
const registerGlobals = () => {
window.bookStorage = {
getItem: (key) => {
@@ -120,7 +197,10 @@ const registerGlobals = () => {
localStorage.removeItem(key);
}
}
- localStorage.setItem("system:section", "entrypoint.jabl");
+ const entrypoint = sources.find(
+ (source) => source.id == sourceId
+ )?.entrypoint;
+ localStorage.setItem("system:section", entrypoint);
// And restart the game
startSelection();
@@ -136,9 +216,7 @@ const registerGlobals = () => {
};
window.loadSection = (identifier, callback) => {
const sourceId = localStorage.getItem("system:source");
- const sourceURL = {
- 1: "https://raw.githubusercontent.com/jasoncabot/fabled-story-book/main/assets/example01/",
- }[sourceId];
+ const sourceURL = availableSources[sourceId];
if (!sourceURL) {
throw new Error("Invalid source id");
}
@@ -165,13 +243,15 @@ const startSelection = () => {
};
const showSelectionChoices = () => {
+ const choices = sources.map(
+ (source) =>
+ `choice("${source.name}", { set("system:source", ${source.id}) goto("${source.entrypoint}")})`
+ );
+
runJABL(`{
print("Welcome to the game!")
print("Which book would you like to play?")
- choice("Example 1", {
- set("system:source", 1)
- goto("entrypoint.jabl")
- })
+ ${choices.join("\n")}
}`);
};