No Time To Play

Hypertext: it doesn’t get much easier

by on Jun.11, 2011, under Miscellaneous

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");

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));
      return false;

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!

Creative Commons License
Hypertext: it doesn’t get much easier by Felix Pleșoianu is licensed under a Creative Commons Attribution-ShareAlike 3.0 Unported License.

:, , ,

6 Comments for this entry

  • farvardin


    It would be great to give to Ramus a scrolling effect like on Undum.

    For integrating Ramus to textallion, I just replaced the href= by rel=

    The output is less clean that what it was at first, because my regex was removing sometimes some relevant informations, so I had to adapt it after that.

    It’s also not supporting text anchors at the moment (only numbers) but I’m working on it!

  • Felix
    Felix Pleșoianu

    A scrolling effect is the next item on my to do list. As for the Ramus/textallion integration, let me know if you need any support.

  • John

    Hi I’ve been looking for something like Ramus for a long time. Easily distributable cyoa stories could be a lot of fun, and I hope that since all the script is visible in the page source, websites might be willing to host them.

    While I’m okay with html and css, I know zip about javascript – please would you add some examples of how to remember a choice, set a flag, turn off one particular link (so that a player can do only this, or that, not both) and display conditional text?

    Is it possible for a player to click on a link which shows a fragment, and then that fragment automatically displays another fragment? (For merging story branches.)

    Wow, lot of requests all at once. Blame the excitement. Okay, and the gin. 🙂

    (2nd attempt)
    I don’t care what calculators say: seven times nine IS forty two.

    • Felix
      Felix Pleșoianu

      Hello John, and thank you for the kind words. You have a practical example in the demo document. [?do distance = 0; ?] sets a flag; [?do distance++; ?] increments it and [?if distance < 10 ?] ... [?else?] ... [?if.?] displays different things depending on whether a threshold was reached. But you're right, I ought to write a proper manual someday. Can't promise anything, though. As for your second question, you can make a link point to two different fragments at once (or several). I didn't add an easy way to render a template inside another, but that's possible as well. Again, can't promise anything. Again, thanks for the kind words. You might just have given me motivation to work on Ramus again.

  • John

    No no no — please don’t think you have to write a comprehensive multi-volume manual to explain every single possibility that Ramus could cover. That’s far too much work — and much too inefficient, too.

    I was hoping you’d add commented examples to the Ramus page and let it become its own mini “how-to.” Then people like me who want to write stories can use them boiler-plate style without ever having to learn the byways of javascript. (The term is “picking your brains” I think 😉 )

    (Does Ramus use vanilla javascript, or is it some sort of dialect?)

    You said I can make a fragment point at two or more others, but exactly how, please? What’s the syntax?

    By the way, last night I thought of a natural use for the “hide text” function — for separating the actual story from instructions on how Ramus works and how readers can create their own stories.

    Anyway, thanks for replying, I shall now go and see if I can implement state in my test project!


    • Felix
      Felix Pleșoianu

      John, Ramus already is its own mini-howto, but you need to look at the source code and see how it’s done. Admittedly, I didn’t add examples for everything. To point at two fragments at once, you simply list all the IDs with spaces between them in the link’s rel attribute. As for the template language, it is provided by a library called Mold, which ultimately uses vanilla Javascript for expressions and such. E.g. in [?do distance = 0; ?], the [?do … ?] part is the template language, while distance = 0; is perfectly normal JS.

1 Trackback or Pingback for this entry

Posts by date

June 2011
« May   Jul »

Posts by month