I took a break from my world domination plans for a new game in order to hack together a little toy. Ramus is a lightweight system for creating self-contained hypertext documents. In less pompous words, you can have a whole mini-website in a single HTML file, and a small one at that. Ramus runs on less than 15K of Javascript (of which only about 40 lines of code are absolutely essential), and you get to write your story in plain old HTML.
Now, while these qualities are relatively unique, the concept is not. So why make yet another such tool?
To make it perfectly clear, if you want to write simple stateless hyperfiction, plain HTML is more than adequate. A wiki engine is even better, but it makes taking your work with you a pain. Unless you use TiddlyWiki, which also presents a unique advantage: the ability to navigate your story even as you're writing it. Unfortunately, TiddlyWiki comes with a 350K overhead — equivalent to the text of a novel.
But my primary source of inspiration wasn't TiddlyWiki. It was Undum, a system for hypertext interactive fiction with an emphasis on aesthetics and many features that seek to emulate the gamebooks of old. The bad news? You're expected to write your story in clumsy, non-idiomatic Javascript. That, and it trails a whole gallery of dependencies after it. So much for easily taking your work with you.
So one of my primary goals for Ramus was to let authors write in good old HTML, at least until they need something fancy such as conditional text. Which prompted a couple of people to ask me, why HTML? Why not go all the way and let them write in a wiki-like markup? This is a legitimate question, and indeed tools such as textallion (thanks, @farvardin) or the older Twee do just that. But it's not my favorite solution, for several reasons:
- In my experience, people who are baffled by HTML are equally baffled by wiki markup, BBCode and the like.
- Those who do get wiki markup strongly disagree on what makes a good syntax.
- Wiki syntax is inherently limited; sooner or later you'll want more anyway, especially if you're trying to use Ramus for something I didn't think of.
- Both the textallion and Twee rely on a compiler to generate the finished work, which is a dependency itself. With Ramus, like with Undum, you simply copy an existing work and replace the story text with your own.
So, how does Ramus work?
It all starts with a <div> that contains the raw story text. It has style="Display: none;" — we don't want it dumped on the reader all at once! It also has an id, though I ended up not having to refer to it in code:
<div id="story" style="Display: none;">
<div id="start">
<p>Ramus is a system for authoring (and reading) self-contained
non-linear documents, a.k.a. hypertext. As you read, you will see
<a rel="links">links</a> that, when clicked, will allow you to
follow one of several branches through the text.</p>
</div>
<div id="links">
<p>Links in Ramus work a bit differently from the normal
<a rel="html">HTML</a> kind...</p>
</div>
</div>
Note how the story nodes (I call them "fragments", according to HTML terminology) are contained each in their own <div>. The choice of container element is largely arbitrary; the important thing is to give each an id, so we can point links at them. Speaking of links, note how they use the rel attribute instead of href. That's so we can tell them apart from external links once we start putting text in front of the reader. A hack, certainly, but not much of a stretch.
<div id="transcript"></div>
<script type="text/javascript">
var transcript = null;
window.onload = function () {
transcript = document.getElementById("transcript");
transcript.innerHTML = document.getElementById("start").innerHTML;
var links = transcript.getElementsByTagName("a");
setup_links(links);
};
function setup_links(links) {
for (var i = 0; i < links.length; i++) {
var a = links[i];
if (!a.href) a.href = "#"; else a.className = "external";
if (a.rel) a.onclick = function () {
this.parentNode.replaceChild(this.firstChild, this);
var turn = clone_content(get_elements(this.rel));
setup_links(turn.getElementsByTagName("a"));
transcript.appendChild(turn);
transcript.lastChild.scrollIntoView();
return false;
};
}
}
</script>
Of course, links without a href attribute are not clickable, so we give them a bogus one. But the real magic happens in the onclick handler:
- First, remove the link to help guide the reader along.
- Then, copy the content from the referenced fragments. Yes, one link can point to several at a time — see below.
- Apply the
setup_links()
function recursively to keep the spell going and last but not least...
- Display the new content at the end, so the story reads smoothly.
But wait! There are two functions I didn't show you.
function get_elements(ids) {
ids = ids.split(" ");
var elements = [];
for (var i = 0; i < ids.length; i++) {
var elt = document.getElementById(ids[i]);
if (elt) elements.push(elt);
}
return elements;
}
function clone_content(elements) {
var turn = document.createElement("div");
turn.className = "turn";
for (var i = 0; i < elements.length; i++) {
turn.innerHTML += elements[i].innerHTML;
}
return turn;
}
That's something I often missed when authoring ordinary Web pages — the ability to point a link at several other pages simultaneously. I can't imagine any use for it in Ramus yet, but for only 10 lines of code it was too easy to pass on.
Believe it or not, that's all you need for a basic version of the system. Of course, then there's all kinds of frills you can add: CSS animation, to emphasize new text appearing, a template library to allow for flags, stats, dynamic text, things like that; and (something I'm still missing) a smooth scrolling solution.
You can see them all working together in the latest version. Also, as I was writing the last part of this article, @farvardin published a version that works with textallion, so if you do prefer to work with a lightweight markup language, now you can. Happy authoring!