Skip to content

Glk Stylesheets: All the Details

erkyrath edited this page Jan 31, 2012 · 5 revisions

Glk Stylesheets: All the Details

The style model

A text stream (the contents of a story or buffer window) is a sequence of paragraphs. A paragraph has a single (paragraph-level) style.

The text in a paragraph can be marked up with style spans. Each span has a single style. Spans can be nested (whereas paragraphs cannot).

Text in a paragraph can contain line breaks. So one logical paragraph can appear to the player as several lines, or even as several apparent paragraphs. This gives us a fallback path: paragraph breaks are optional. A game that doesn't want to take advantage of the paragraph model can just generate old-fashioned double line breaks instead. The result will be a single HTML paragraph div, but the player won't care. (Obviously, any older game running on a new interpreter will wind up doing this.)

A text grid (status window) has style spans, but no paragraphs. The grid is just a list of lines of text.

The current style

In the classic Glk system, a text stream had exactly one "current style" in play at a time. In the new system, styles can be nested, so there's really a "current style stack": zero or more styles. (Plain-old-normal text is no longer a distinct style; it's the "zero styles" case.)

However, we need to be able to apply styles to entire paragraphs, not just spans of text. This is because not all CSS properties are meaningful for spans. (If you want to block-indent some text, you need to apply that style to a paragraph div, not a span.)

QUERY: What is the best way to do this, consistent with I7?

What is a style?

A style, from the library's point of view, is a pair of values: a numeric constant and a string. The number is one of the classic eleven constants: Normal, Emphasized, Note, Alert, Preformatted, BlockQuote, Header, Subheader, Input, User1, User2. The string can be anything, or empty. (I'll write these with a slash: Normal/"Quotation", Header/"Chapter", etc.)

This dual value gives us another fallback path. If the interpreter doesn't understand the style string -- either because it's not in the player's stylesheet, or because the interpreter doesn't handle CSS at all -- it can fall back to its knowledge of the eleven classic style constants. The game might use Header/"Title" in one place and Header/"Chapter" in another; it would define them both in its stylesheet, but an older interpreter could render them both as Header.

When possible, authors should pick appropriate classic constants for their styles. However, this is never mandatory. If you have a style that doesn't fit the old categories, use Normal/"Whatever". Contrariwise, if you want to only use the classic styles, you can use Whatever/"". (In other words, both parts of the dual value are optional.)

Two new styles are defined by default: Emphasized/"Italic" and Emphasized/"Bold". (The classic Emphasized constant had no way to distinguish these, although it was italic by de facto agreement.)

The classic constants User1 and User2 are deprecated. They remain in place for backwards compatibility, but there's no reason to use them in the new system.

The core style API

void glk_set_style_name(glui32 num, char *name)
void glk_set_style(glui32 num)

Clear all current styles from the stack, and set Num/"Name" as the sole current style. If you set Normal/"", the stack is simply cleared.

Passing null for the string, or calling the one-argument version, is equivalent to passing "".

Note that glk_set_style() is the classic Glk call; it exists even in old interpreters, and old games will call it exclusively. A game that relies on this call will effectively be sticking to the old one-style-at-a-time model, and the results should be backwards-compatible on newer interpreters. (At least, I hope I've written this spec to ensure that.)

void glk_begin_style_name(glui32 num, char *name)
void glk_begin_style(glui32 num)

Add Num/"Name" to the current style stack.

glui32 glk_end_style_name(glui32 num, char *name)
glui32 glk_end_style(glui32 num)

Remove Num/"Name" from the current style stack, and return 1. If the stack is empty, or if the top style does not match (both values), this does nothing and returns 0.

The return value here is intended for debugging. A game in development can track the return value and display a runtime warning ("mismatched end style") if it is zero. A release game can skip the check.

void glk_end_style_any()

Remove the top style from the current style stack, whatever it is. If the stack is empty, this does nothing.

(I am not sure whether games will want their style nesting to be strictly checked or not. The library can ensure correct nesting either way, so I'm willing to offer both options.)

void glk_set_paragraph_style_name(glui32 num, char *name)
void glk_set_paragraph_style(glui32 num)

Set the current paragraph style to Num/"Name".

glui32 glk_paragraph_break()

End the current paragraph and begin a new one. This does not affect the current style stack. This returns a number that uniquely identifies the paragraph. (That's for the benefit of Javascript calls that might want to do something to it. The number winds up in the id attribute of the paragraph div.)

In a text grid (status window), this function just generates two line breaks. (The equivalent of glk_put_string("\n\n").) The function then returns zero. The same is true in a native-style interpreter which has no notion of HTML structure.

A new paragraph's style is set when text is printed to it, so you can call glk_set_paragraph_style_name() before or after glk_paragraph_break().

QUERY: Is there such a thing as an empty paragraph? (That is, if you call glk_paragraph_break() twice in a row, do you get a big blank space?) In HTML the answer is "no", and it would be easier to stick to that. It does introduce a small consistency problem with the non-HTML interpreters, though.

The I7 API

This is ultimately up to Graham, but what we've discussed looks like this:

Quotation is a text style. [ Maps to Normal/"Quotation" ]
Gods talking is a text style. [ Maps to Normal/"GodsTalking" ]
Chapter is a text style based on Header. [ Maps to Header/"Chapter" ]

(The "based on" clause would have to be one of the eleven classic names, or perhaps nine if you eliminate User1 and User2. The string part of a style name is limited to alphanumeric characters, I think. I7 would convert the symbol to title case and then remove spaces to create the CSS selector string.)

The standard rules would define

Italic is a text style based on Emphasized.
Bold is a text style based on Emphasized.

The presumption is that every style you define in your game this way would have visual attributes defined in your game-supplied stylesheet. Italic and Bold will be defined in every interpreter's default stylesheet.

You can then write code like

say "[Chapter]The Arrival Of Monkeys[/Chapter][paragraph break]"
say "They say, [quotation]'Look over [italic] there.[/italic]'[/quotation]"

(The exact details are subject to change. It might be [style quotation] or [quotation style] or something like that. Also, I don't know whether Graham wants to map end-style phrases to the strict glk_end_style_name() or the looser glk_end_style_any().)

The I7-generated style phrases would include the debugging checks on the return value of glk_end_style_name(). (It's possible that I7 would keep track of the style stack itself, and detect mismatches without needing this return value. I'm not sure how Graham wants to handle this.)

If you want to move beyond predefined styles, you can of course invoke the I6 glk_begin_style_name() call directly, and pass any string. This is probably only useful in conjunction with Javascript calls: you set an arbitrary style on some text, and then call a Javascript function that locates that span and does something to it.

QUERY: Will I7 be able to use this paragraph model? This spec's notion of a "pure" paragraph-oriented game is that it never prints newlines -- only text and glk_paragraph_break() calls. (That's why the fallback for glk_paragraph_break() is two line breaks.) But the current I7 compiler generates a lot of hardcoded line breaks. That would have to change.

The HTML translation

(These HTML formats are based on the current behavior of GlkOte, but are not exactly the same.)

Text buffer (story) windows

A text buffer window will have this structure:

<div id="..." class="WindowFrame BufferWindow WindowRock_201">
	<div id="para_1" class="BufferPara">
		First paragraph, unstyled.
	</div>
	<div id="para_2" class="BufferPara Para_BlockQuote">
		The paragraph style of this paragraph is BlockQuote.
	</div>
	<div id="para_3" class="BufferPara Para_Epigraph">
		This paragraph is Normal/"Epigraph".
	</div>
	<div id="para_4" class="BufferPara Para_Header Para_Chapter">
		This paragraph is Header/"Chapter".
	</div>
	<div id="para_5" class="BufferPara">
		This has no paragraph style, but
		<span class="Span_Preformatted">this</span>
		word is Preformatted.
		<span class="Span_Emphasized Span_Bold">This</span>
		word is Emphasized/"Bold".
	</div>
</div>

The div for the text window has three classes: WindowFrame (used for all text windows), BufferWindow (used for all text buffer windows), and WindowRock_201 (a rock number supplied when the game opens the window). CSS selectors can therefore target classes of windows or specific ones.

A paragraph div has an id attribute of para_N, where N is the integer returned by glk_paragraph_break(). These numbers are unique across the entire game (not just the window); they begin at 1 and increase monotonically within each window.

A paragraph div always has BufferPara in its class. If it also has a paragraph style (other than Normal/""), there will be one or two additional class terms. The default values (Normal and "") do not generate a class term; non-default values do.

Spans within a paragraph have, again, one or two class terms.

(The classic GlkOte behavior was to always add a class term, even for Normal style spans. However, the GlkOte stylesheet doesn't put any selectors on Span_Normal, so I feel justified in scrapping it.)

Line breaks and indentation have been inserted here for clarity. The base stylesheet must define white-space: pre-wrap; for text windows, so whitespace and line breaks in the game text will be significant. (This is not the normal behavior of HTML, but the white-space property changes this.)

(I think all modern HTML renderers support white-space. If yours does not, it would be acceptable to convert line breaks to <br> tags, and strings of multiple spaces to &nbsp; characters. This is what GlkOte currently does, but I intend to switch it over to white-space: pre-wrap;.)

TODO: Fill in examples for inline images and inline HTML frames.

Text grid (status) windows

This is similar, except there are no paragraphs or paragraph styles:

<div id="..." class="WindowFrame GridWindow WindowRock_202">
	<div class="GridLine">
		This is a plain line of text.
	</div>
	<div class="GridLine">
		In this line,
		<span class="Span_Preformatted">this</span>
		word is Preformatted.
		<span class="Span_Emphasized Span_Bold">This</span>
		word is Emphasized/"Bold".
	</div>
</div>

Grid windows also have white-space: pre-wrap;, so spaces are significant, but they will never contain line breaks within a line. Each GridLine div is exactly one line of text.

The default stylesheet

The default stylesheet for any interpreter should include declarations much like these:

.WindowFrame {
	white-space: pre-wrap;
}
.GridWindow {
	font-family: monospace; /* the terp's default fixed-width font */
}
.BufferWindow {
	font-family: serif; /* the terp's default text font */
}
.BufferPara + .BufferPara {
	margin-top: 1em; /* blank lines between paragraphs */
}
.Span_Emphasized { /* used for [italic type] */
	font-style: italic;
}
.Span_Bold { /* used for [bold type] */
	/* goes with Emphasized, so it has to turn off italics */
	font-style: normal;
	font-weight: bold;
}
.Span_Header { /* Used for the game title */
	font-size: 1.4em;
	font-weight: bold;
}
.Span_Subheader { /* Used for room names */
	font-weight: bold;
}
.Span_Note {
	font-style: italic;
}
.Span_Alert {
	font-weight: bold;
}
.Span_Preformatted { /* used for [fixed letter spacing] */
	font-family: monospace; /* the terp's default fixed-width font */
}

These defaults provide both a set of solid conventions for the standard styles, and reliable fallbacks for players who opt out of game stylesheets.

(Note that the current I7 uses Subheader for [bold type], because there is no Bold style in the classic Glk interface.)

Any stylesheet which is intended to support many games should have similar declarations -- or at least look reasonable when applied to a game that expects them.

More API features

HTML windows

void glk_window_set_web_document(winid_t win, glui32 docnum)

Display an HTML file in an HTML window. The file (and any resources it loads in) are taken from the Blorb package.

(Really, the file can be any format displayable by the HTML widget. So this could be an image file, SVG, or even video or some other plugin type. It is of course the author's responsibility to consider what document types are commonly available in browsers.)

Inline HTML frames

void glk_web_draw(winid_t win, glui32 docnum, glui32 alignment, glui32 width, glui32 height);

Display an HTML file in a text buffer window. The alignment argument has the same meaning as for glk_image_draw() -- MarginLeft, MarginRight, InlineUp, InlineDown, or InlineCenter.

Javascript interaction

void glk_script_send(winid_t win, glui32 framecount, char *buf, glui32 len)
void glk_script_send_uni(winid_t win, glui32 framecount, glui32 *buf, glui32 len)

Invoke a Javascript call in the given window. The destination's Javascript environment must have a GlkEntry object defined, with a GlkEntry.receive(string) method.

If win is a HTML window, its displayed document is the destination.

If win is a buffer window, the destination is one of the HTML frames displayed inline in it. The framecount argument indicates which: 0 is the most recent, 1 is the next most recent, etc.

(Interpreters generally trim buffer streams after some amount of output, so you cannot assume that inline frames stay around forever.)

If win is null, the destination is the interpreter itself. (This is of course only possible in a browser interpreter.)

QUERY: What about widget-style interpreters that use one HTML widget for the story window, another for the status window, etc? Is it worth having a way to invoke Javascript on those?

The return value of the Javascript call is not available. To pass a message back to the game, use the glk_request_script_event() call.

void glk_request_script_event(winid_t win, char *buf, glui32 len)
void glk_request_script_event_uni(winid_t win, glui32 *buf, glui32 len)

Begin waiting for a Javascript event. This inserts a GlkEvent object into the destination's Javascript context. (The author shouldn't define one -- let the interpreter create it.) Javascript in the given window can trigger the event by calling GlkEvent.send(string). The text of the string will be copied to buf (truncated to len characters if necessary) and the event will appear in the next glk_select() call.

The game can only receive one event per glk_select() call, so repeated invocations of GlkEvent.send() will queue them up.

The event will contain:

  • type: evtype_Script
  • win: the Glk window that sent the event (or null if it came from the interpreter context)
  • val1: the original length of the message, in characters (not truncated)
  • val2: if win is a textbuffer, which inline frame sent the event (0 being the most recent)

QUERY: An HTML frame (inline or window) can include any Javascript library it wants. However, if you want to run code in the context of a browser interpreter, you are at the mercy of whatever that interpreter has loaded up. Is it worth standardizing? Probably. I expect that I will rewrite GlkOte/Quixe to use jQuery (rather than Prototype), and then authors can assume that jQuery is available. (They will still have to be careful about jQuery version, I suppose.)

Deprecations

void glk_stylehint_set(glui32 wintype, glui32 styl, glui32 hint, glsi32 val)
void glk_stylehint_clear(glui32 wintype, glui32 styl, glui32 hint)
glui32 glk_style_distinguish(winid_t win, glui32 styl1, glui32 styl2)
glui32 glk_style_measure(winid_t win, glui32 styl, glui32 hint, glui32 *result)

These are how the classic Glk API tried to handle style customization. They weren't very good.

If a game includes a stylesheet, it is indicating its adherence to the new style system, so it should not use these style hint calls. (And the interpreter should ignore them if it does.)

If the game does not include a stylesheet, the interpreter may try to support these calls, but it does not have to. (In particular, combining style hints with a player-supplied CSS stylesheet is probably confusing. I wouldn't try to support that. Go for it if you really want to.)

Blorb resource support

The rough idea here is that the Blorb file can contain HTML files (to be loaded into HTML frames), and also everything which needs to be loaded into the HTML file (Javascript, images, more CSS, what have you).

HTML refers to everything by name, but Blorb chunks are nameless. Therefore, we will require an additional Blorb chunk that maps names to chunk IDs.

A native interpreter will then have everything it needs to load up HTML frames. (The easiest way to do this is simply to unpack the Blorb chunks into a temporary directory.)

For browser interpreters (Quixe), the problem is somewhat more complicated. We can unpack data from a Blorb file and insert it into the browser window with a data: URL. However, last time I tried this, I couldn't get Javascript calls into or out of the subdocument -- I think it counts a "cross-domain" call (from http: to data:, right?). I have not gone back to investigate this in detail.

Fortunately we have a back door for the most common case -- an Inform game that uses the "Release along with an interpreter" feature. In that case, I7 can simply set up all the necessary files in the Materials folder. Unpacking Blorb is only necessary for a web app that plays many games (iplayif.com, etc).

(Possibly those can be handled by a proxy web service, similar to zcode.appspot.com.)

Backwards and forwards compatibility

Let me go over some cases and try to document that they won't completely break.

Old game, new interpreter

The game will only print text and call glk_set_style(). It will never call glk_paragraph_break(). Text output will be structured with newlines, which will work, since the display HTML uses white-space: pre-wrap;.

Since there will be just one top-level paragraph, paragraph-level style attributes (indentation, centering, etc) will not work. (Note the standard stylesheet does not use such attributes. Also, they have never worked in Quixe at all.)

Note that glk_set_style() does not support nested styles; it ends any current style and begins a new top-level style. Old games rely on this behavior.

An old game might use the classic style hint calls (glk_stylehint_set(), etc). The interpreter is free to support these when there is no game-supplied stylesheet (i.e., for old games). (Again, Quixe has never supported these calls.)

New game, old interpreter

All games should check a gestalt selector before calling the new API calls (glk_set_paragraph_style(), glk_set_style_name(), glk_begin_style(), glk_paragraph_break(), etc). If the new calls are not available, the game must fall back to the basic glk_set_style() and split up paragraphs with line breaks.

The I7 library has enough information to handle this falling-back, so game authors will generally not have to worry about it.

New game, minimally-updated interpreter

It's worth considering the case of an interpreter which supports the new calls, but in the simplest possible way. (I expect that most interpreters will go through this stage.)

Such an interpreter will not be able to parse CSS. It will support glk_set_style_name(), but will ignore the string part of the style, and just use the classic constant. glk_paragraph_break() will be handled as glk_put_string("\n\n").

The game output will be reasonable, as long as the game author uses reasonable fallback style constants.