Hypertext: it doesn't get much easier

2011-06-11

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:

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:

  1. First, remove the link to help guide the reader along.
  2. Then, copy the content from the referenced fragments. Yes, one link can point to several at a time -- see below.
  3. Apply the setup_links() function recursively to keep the spell going and last but not least...
  4. 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!