Whitespace, line breaks, and screenreaders -- DEEP DIVE

Over the last few days, I’ve been experimenting with how ChoiceScript displays different kinds of vertical whitespace. Having searched the forums, I don’t think anyone’s investigated this topic in great depth yet and I thought it would be a good idea to post my results here.

If you’re new to ChoiceScript and need help navigating the (sometimes confusing) mix of options for creating paragraph breaks, I hope this can serve as a useful tutorial! And if you’re a veteran of ChoiceScript and want to gain a much deeper grasp of the tools at your disposal, then you might find my experimental results intriguing. There’s a lot going on under ChoiceScript’s hood, and it’s not immediately obvious what that stuff is.

Last but not least, I’m trying to investigate whether *line_break commands can cause problems for screenreaders. If you use screenreaders yourself or know people who do, I’d be very interested in hearing about your experiences. I intend to modify this post as I learn more about how various screenreaders cope with different kinds of line breaks.


  1. The Basics

ChoiceScript has three ways it generates vertical whitespace: blank lines of code, the *line_break command, and the [n/] character. Under the right circumstances, each of them will behave differently from the others.

Blank lines of code are what you get when you press the enter/return key twice on your keyboard. In the following code, a blank line is used to induce a paragraph break between the two lines of text:

A line of text.

A second line of text.

Blank line example Blank line example highlighted

Note that only pressing the enter/return key once will not induce a paragraph break. A blank line of code is necessary.

Any line of code that solely contains spaces/tabs is treated like a blank line of code. Furthermore, if an indented block ends with a blank line of code, ChoiceScript treats that blank line as if it lay outside the indented block, regardless of whether the blank line contains indenting:

A line of text.
*if (false)
    This text won't be printed, but the blank line below it will still induce a paragraph break.
    
A second line of text.

Blank if statement example Blank if statement example highlighted
(Try highlighting the above block of code to see the invisible indenting, then notice how it doesn’t cause the blank line to be treated as if it were inside the *if statement.)

The *line_break command generates a <br> tag. In isolation, these can “move your cursor” to a new line:

A line of text.
*line_break
A second line of text.

Linebreak command example Linebreak command example highlighted

… but that’s just in isolation. In conjunction with other commands, <br> tags can behave very strangely. We’ll discuss that soon.

As you might guess, two *line_breaks in a row create a blank line of text:

A line of text.
*line_break
*line_break
A second line of text.

Double linebreak command example Double linebreak command example highlighted

This result is visually distinct from the paragraph break produced by a blank line of code. However, for reasons we’ll also discuss soon, you should probably avoid using *line_breaks this way.

The [n/] character is an inline version of *line_break, similar to the \n character of many programming languages. Typing [n/] in the text of your story will produce a <br> tag at its location:

A line of text.[n/]
A second line of text.

Linebreak character example Linebreak character example highlighted

You can put the [n/] character anywhere there’s story text–even as a stand-alone line of code:

A line of text.
[n/]
A second line of text.

… although you shouldn’t, in your own stories, put it on its own line this way. While several examples in the rest of this post will use [n/]s on their own line, this is purely to explore how [n/] characters behave. In your own writing, it’s good practice to use [n/]s on the same line as other text.

At the time of this post, the [n/] character doesn’t possess nearly as much formal documentation as other features of ChoiceScript. It isn’t, for example, mentioned in the ChoiceScript tutorial on CoG’s website. Nevertheless, the [n/] character is a useful tool to have in your pocket.

One last thing: notice that the whitespace produced by both *line_break and [n/] can be highlighted, while the paragraph breaks induced by blank lines cannot. The latter is a result of the CSS margin properties of paragraph tags, while the former is an explicit whitespace character. In ChoiceScript, the convention is to use blank lines for everyday paragraph breaks, not *line_break or [n/]. You should not try to substitute *line_breaks or [n/]s in place of a blank line for creating a paragraph break, even if it may seem tempting. Using blank lines for some paragraph breaks and *line_breaks for others is a bad idea, and produces visually inconsistent results.

However, while you shouldn’t use *line_break or [n/] to create paragraph breaks, they do have some important use cases. They are best employed in text that utilizes stanzas (e.g. poetry), or when printing the multiple lines of a mailing address.


  1. Testing and Results

Unfortunately, whitespace in ChoiceScript is a lot more complicated than it appears. The descriptions I gave above don’t do the underlying complexity justice.

In order to understand how ChoiceScript really induces vertical whitespace, I set up a few enormous testing suites in which I combined blank lines of code, *line_break commands, and [n/] characters to see how they interacted. The results completely changed how I view whitespace in ChoiceScript.

Testing suite #1 tests 7 different ways of combining blank lines of code and *line_break commands, and doesn’t utilize any [n/] characters.

Testing suite #1
Let's investigate various kinds of line breaks.

^^ This is an example of pressing the enter-key character to create a blank line of code. This is the normal way paragraph breaks are generated. (1)
*line_break
^^ This is an example of a single *line_break. (2)
*line_break
*line_break
^^ This is an example of a double *line_break, with each *line_break on a separate line. (3)

*line_break
^^ This is an example of a single *line_break after an empty line. (4)
*line_break

^^ This is an example of an empty line after a single *line_break. (5)

*line_break

^^ This is an example of a *line_break sandwiched between two blank lines. This is the most "generous" a naive coder can be in making sure a line break shows up properly. (6)
*line_break

*line_break

^^ This is an example of a *line_break, followed by a blank line, followed by another *line_break, followed by another blank line. (7)

Testing suite #2 performs the exact same 7 tests, but uses a [n/] character in every place that testing suite #1 used a *line_break command.

Testing suite #2
Now, let's do the same thing with inline linebreaks!

^^ This is an example of pressing the enter-key character to create a blank line of code. This is the normal way paragraph breaks are generated. (1)
[n/]
^^ This is an example of a single inline linebreak. (2)
[n/]
[n/]
^^ This is an example of a double inline linebreak, with each inline linebreak on a separate line. (3)

[n/]
^^ This is an example of a single inline linebreak after an empty line. (4)
[n/]

^^ This is an example of an empty line after a single inline linebreak. (5)

[n/]

^^ This is an example of an inline linebreak sandwiched between two blank lines. This is the most "generous" a naive coder can be in making sure an inline linebreak shows up properly. (6)
[n/]

[n/]

^^ This is an example of an inline linebreak, followed by a blank line, followed by another inline linebreak, followed by another blank line. (7)

The first thing to note is that the results of these two testing suites aren’t the same. Specifically, test (4) produces a different outcome in suite #1 and suite #2. Why?

The second thing to note is that having more than one blank line of code in a test only seems to produce visual changes when the new blank line is placed at the start. Is this always true? And if so, why?

The third thing to note is that within a testing suite, some tests that we’d expect to come out looking different instead look the same. In testing suite #1, for example, tests (4) and (7) are visually identical, yet the code for (4) is a blank line followed by a *line_break:


*line_break

… while the code for (7) is this:

*line_break

*line_break

Once again: why?


  1. Interpreting the HTML and feeling confused

Let’s examine the HTML from testing suite #1:

… and the HTML from testing suite #2:

For those who aren’t familiar with HTML, a <p> opens a new paragraph and a </p> closes the current paragraph.

It looks like every single time we use a *line_break or a [n/] in our ChoiceScript code, a <br> tag shows up. But they’re located in some weird places! Sometimes our <br> tags end up inside of paragraph tags, and other times they end up outside of them. To add to the confusion, whether a <br> tag is inside a paragraph only sometimes impacts how it visually appears. For example, test (6) produced visually identical results in both testing suite 1 and 2, but only in testing suite 2 is the <br> tag enclosed in its own unique paragraph. Meanwhile, test (4) produces visually distinct results across testing suites, and the <br> tag is located in a paragraph only in testing suite #2.

Fortunately, these results do make sense. There’s a few simple rules which govern how ChoiceScript handles whitespace, and they explain all the messiness of our tests.


  1. What’s happening at a deeper level?

I reverse-engineered ChoiceScript’s behavior over the course of several other tests (of which there were too many to post here). I’ve come up with a formalization that you can use to predict how ChoiceScript will interpret your whitespace.

For the sake of this formalization, let’s divide lines of ChoiceScript into three categories: commands, text, and blank lines. Commands are anything that starts with an asterisk (e.g. *comments, *gotos, *sets, and of course *line_breaks). Text is any line in your story that ChoiceScript will print (e.g. Today I went to the ${storeType}, and *broke into the line* [n/] to see the cashier. Behold my wordplay.). Blank lines are any lines that solely contain spaces/tabs.

As ChoiceScript converts your code into HTML, it keeps track of a state variable for whether it’s currently inside of a paragraph block. Then, based on whether it’s currently parsing a line with a command, a line of text, or a blank line, it behaves as follows:

*if (command)
    Perform the command. If the command is *line_break, that means printing a <br> tag.
*elseif (text)
    First, if we're not currently in a paragraph, open a new one with a <p> tag.
    Second, print the text. Any [n/] characters in the text are converted into <br> tags.
*elseif (blankline)
    If we're currently in a paragraph, close it with a </p> tag.

This reveals a lot of (potentially counter-intuitive) implications for how ChoiceScript processes and produces whitespace:

First, since [n/] is “text” and *line_break is a “command,” [n/] and *line_break are not perfect substitutes. Yes, [n/] may be an inline version of *line_break, but there are circumstances (see test (4)) where using [n/] on a line all to itself will produce a different result than a *line_break in the same position. For example, this code produces a <br> tag between paragraphs 1 and 2:

This is paragraph 1, now prepare for a line break!

*line_break
This is a paragraph 2, we just printed a line break!

… while replacing the *line_break with a [n/] character instead produces a <br> tag at the start of the inside of paragraph 2:

This is paragraph 1, now prepare for a line break (character)!

[n/]
This is a paragraph 2, we just printed a line break (character)!

This produces visually distinct outcomes:
Line break character vs command

Second, blank lines don’t just mean “end our current paragraph and start a new one.” They more specifically mean “end the current paragraph (unless there is no ‘current paragraph’).” ChoiceScript only starts a new paragraph when it encounters the next line of text. As a result, multiple consecutive blank lines will have the same behavior as a single blank line. More broadly, a single blank line followed by any number of blank lines intermixed with some number N *line_break commands, regardless of the ordering of these blank lines and *line_breaks, will have the same behavior as a single blank line followed by N *line_breaks.

Third, *line_breaks and [n/]s don’t always produce a visible line break. For example, following a line break character with a blank line like so

First line of text.
[n/]

Second line of text.

will look like a normal paragraph break, as if the [n/] wasn’t even there. Meanwhile, a blank line followed by a line break character

First line of text.

[n/]
Second line of text.

will stitch vertical whitespace to the top of the second paragraph. This is because, to a first approximation, a <br> tag causes the current line to take up vertical space and moves the cursor to a new line, but doesn’t cause that new line to take up vertical space until it contains either text or another <br> tag.

Test of the above principle
First paragraph here, followed by a normal paragraph break.
[n/]

Second paragraph here.

---

First paragraph here.

[n/]
Second paragraph here, with vertical whitespace stitched to the top.

Whitespace stitching 1 Whitespace stitching 1 highlighted

However, if we add a second [n/] when attempting to stitch vertical whitespace to the bottom of the first paragraph, it produces symmetric visual outcomes.

First paragraph here, with vertical whitespace stitched to the bottom.
[n/]
[n/]

Second paragraph here.

---

[n/]
Second paragraph here, with vertical whitespace stitched to the top.

Whitespace stitching 2 Whitespace stitching 2 highlighted

“Stitching” vertical whitespace like this to the bottom of paragraph 1 is also possible with two repeated *line_break commands. However, stitching vertical whitespace to the top of paragraph 2 requires the use of a [n/] character. This is due to the fact, discussed earlier, that [n/] characters count as “text” while *line_breaks count as “commands.”

One last thing to note: Thanks to the specific configuration of ChoiceScript’s CSS, repeated <br> tags outside of any paragraph are displayed identically to repeated <br> tags in their own little paragraph. This is why test (6) looks the same in both suites, despite each producing different HTML.


  1. Screenreaders

Now you know how to make all kinds of vertical whitespace using ChoiceScript! But now we have to ask: should you?

The ChoiceScript wiki claims that

I don’t have any visual disabilities, and don’t have a lot of firsthand experience with screenreaders. But for the purposes of this post, I opened Microsoft Narrator (a built-in screenreader for Windows devices) and experimented with it.

Here’s how it works: there’s a keyboard combination that prompts Microsoft Narrator to read the next chunk of text aloud to the user. After pressing it, you listen for a brief period. Then, once Narrator finishes reading the current chunk of text, you press the keyboard combination again to proceed to the next chunk of text.

Using multiple *line_breaks in a row didn’t “break” Narrator. It did, however, pose an inconvenience. Regardless of whether my <br> tags were located in or outside of paragraph blocks, regardless of whether they were used in immediate succession, regardless of anything, they would always be treated as their own chunk of text to read aloud.

Microsoft Narrator reads the <br> tags as if they were the phrase “space, separator.” That means the code

This is a paragraph.
*line_break
This is also a paragraph.

produces the following experience for the user:

  1. Narrator says “This is a paragraph.”
  2. User prompts Narrator for more.
  3. Narrator says “space, separator.”
  4. User prompts Narrator for more.
  5. Narrator says “This is also a paragraph.”

… And adding more *line_breaks makes this even more annoying. The code

This is a paragraph.
*line_break
*line_break
*line_break
This is also a paragraph.

produces the following horrible experience:

  1. Narrator says “This is a paragraph.”
  2. User prompts Narrator for more.
  3. Narrator says “space, separator.”
  4. User prompts Narrator for more.
  5. Narrator says “space, separator.”
  6. User prompts Narrator for more.
  7. Narrator says “space, separator.”
  8. User prompts Narrator for more.
  9. Narrator says “This is also a paragraph.”

To make this worse, Microsoft Narrator says the phrase “space, separator” very slowly. You can skip to the next chunk of text early, of course–but I imagine that dealing with these <br> tags when you’re trying to read a story is kind of an immersion killer.

By contrast, a normal paragraph break created via the code

This is a paragraph.

This is also a paragraph.

is a vastly less agonizing experience:

  1. Narrator says “This is a paragraph.”
  2. User prompts Narrator for more.
  3. Narrator says “This is also a paragraph.”

For this reason alone, I’d recommend keeping your *line_breaks and [n/] characters to an absolute minimum. Ideally, in order to avoid them breaking the flow of your story, they should only ever be used in places where the reader isn’t consuming narrative-related text, like the stat screen.

(As mentioned earlier, if you must use them in a narrative-related part of your text, they are best restricted to printing poetry or mailing addresses with multiple lines.)

I don’t know whether screenreaders other than Microsoft Narrator operate differently. And I especially don’t know why the ChoiceScript wiki says that multiple *line_breaks in particular will “break screenreaders,” since (1) there’s more than one screenreader on the market and multiple *line_breaks might only break some of them, and (2) my experience was that multiple consecutive *line_breaks didn’t “break” Microsoft Narrator so much as “make it very frustrating to use.”

I’ve heard some talk around the forum that this might have been an old issue that has since been patched, but I don’t know for sure. Therefore, I want check in with the visually-impaired members of the CoG community to find out if avoiding multiple *line_breaks is still a guideline worth following, and edit the ChoiceScript wiki accordingly!

  1. If you use a screenreader, what screenreader do you use?
  2. Do multiple consecutive *line_break commands break your screenreader? If so, how exactly does it break?
  3. Do *line_break commands affect your reading experience in other ways, positive or negative?

Hope this post can generate some positive discussion about these issues!

36 Likes

This is super cool!!! :grin:

1 Like

I’ll put my own Final Chapter: Conclusion to this little thesis of yours. Consider it as an… input from the team.

  1. Use [n/] exclusively as an inline character. Treat it as you would a text and not normal command. Do not put it on its own line. Consider:
    *set var "this text is broken [n/] into two lines"
    ${var}
    Comment: The examples use a lot of [n/] tag on its own line, but this is done for the purpose of documentation. [n/] is still intended to be an inline character.
  2. To enter a new paragraph, always stick to the double carriage enter/return method. Do not attempt any other way, unless you're specifically looking for a measured vertical whitespace size.
  3. The *line_break command (or alternatively, the [n/] tag), is best used in text utilizing stanzas such as poems.
6 Likes

Btw, I use NVDA as my testing program. Not sure how’s that’ll differ from MS Narrator.

P.s. Oh, right, you might want to see how whitespace interacts with ICF in an if/else/elseif block. Something I notice is that CS treats blank lines differently depending on the place they’re nested in, inside or outside.

3 Likes

Thank you @SilasLock for sharing this :slightly_smiling_face:

IMHO paragraph breaks and blank lines help a lot to set the “pace” and the “rhythm” of the narration. That is why I tend to use them a lot.

After reading this, I’ll try to keep it at a minimum. I hope the text readers will improve so to let us use the “br” more often :slight_smile: without any consequence for anyone.

Thanks!

Do you mind if I update my post to include the information you’ve written here? I think I could emphasize better that [n/] isn’t intended to be used on its own line and that I’m using it in an unorthodox way to showcase its unusual properties. And it would be good to mention how line breaks are best used for poetry (or mailing addresses, for that matter).

Although for point #2, I do say

here:

Should I word this more strongly?

I’m not sure I understand what you mean here. Are you saying that the amount of indenting used in a blank line within if/elseif/else blocks impacts how/whether it induces a paragraph break? I use ICF, and my experience is that this isn’t true.

Furthermore, from additional testing I performed, the rules I came up with for how ChoiceScript parses code here:

seem to describe how ChoiceScript behaves when indented and inside of an if/elseif/else block, just as much as it does outside of an if/elseif/else block.

No problem, I’m glad you liked it!

Wait, when you say “paragraph breaks”, do you mean the *line_break command? Because normal paragraph breaks are totally fine in Microsoft Narrator, there’s no reason to keep those to a minimum. :thinking:

3 Likes

Thanks so much for this. I always use return/enter for paragraph breaks but it’s great to know more about why that’s good!

3 Likes

Ok, now everything is crystal clear. :blush: I was afraid of making mistakes so I thought “better safe than sorry” :joy::joy:

1 Like

This is so useful! Thank you! I had already been planning to do a screen reader safe version of Retribution since I am using emojis, now I know to also include the *line_break sections.

4 Likes

Hi, and thanks for diving so deeply into such an obscure topic. This is super interesting.

I primarily use Nvda, linked above, as well. I’m curious whether the html displays differently depending on the browser its rendered in, since I’ve heard chrome and its derivatives sometimes do odd things with the order of html elements.

Just an fyi, most modern screen readers also parse emojis just fine. I wouldn’t recommend long strings of just emoji, because that can get tedious to hear, but a few to ornament text can be kind of neat. I’ve thought about using them myself to convey a personality archetype or stat change, much the same way Bioware does its color/symbol based dialogue wheel. My biggest hesitation in doing so is less a screen reader concern, and more a concern about how to do it inclusively of skintones without having to enter a bunch of emoji under *if’s every time. If you have thoughts about that, probably better in a different topic, I’d love to know them. Also always been slightly curious whether those look at all realistic or if they’re cartoonish.

Honestly, I’d love to test different screen reader and browser combinations with the whitespace options outlined here and will report back.

3 Likes

I’m a sucker for detailed stuff like this. I read it all. Now I feel very naive saying you can just slap an [n/] wherever you want!

1 Like

Wow, that is really a thorough analysis. Thanks for sharing.

1 Like

First of all, kudos for your curiosity and for sharing.

Some of this is already documented in the source-code. For example:


It’s important to keep in mind that some of these differences are a result of how the browser renders HTML pages and others are a result of how the CS Engine generates the HTML page.

So, for example, most browsers will ignore a break tag <br> which precedes a paragraph-closing tag </p>. But it may not ignore the break tag after a paragraph-opening tag <p>. This has more to do with the browser and not with CS itself. However, the screenreader will read the page’s source-code, ironically, it sees what we don’t.

Also, as far as I can tell, the CS Engine resolves the CS Code in two stages: the Interpreter stage and the Formatter stage. Commands (like *line_break) and empty lines are resolved in the Interpreter stage. But format tags like [i][/i] and [b][/b], multireplace @{} and especial characters like &quot; and the line feed (LF) character [n/] are resolved in a later Formatter stage. This happens very fast and for us puny humans it seems instantaneous.

That’s why the *line_break command and the LF character will resolve differently in unconventional circumstances, like your fourth example, because they are resolved at different times.


Just to reinforce what @Szaal has said, the LF character [n/] should be used inline always, if only for good practice.

2 Likes

Let me know what you find out!

Also, sorry for my late replies! Things have been hectic lately. :sweat_smile:

I actually wanted to make a whole section of my post dedicated to browser rendering of <br> tags, but I realized it was getting a little lengthy and I just roped the important stuff into the sections about the ChoiceScript Engine. It’s definitely important to be clear which is which, though–I probably smudged the line a little too much in my write-up.

I also want to mention that the big exception to when browsers ignore a <br> tag right before a </p> is when that paragraph block contains solely a <br> tag. Then they’ll just produce a line of vertical whitespace.

Which is consistent with the WHATWG recommendations:

If a paragraph consists of nothing but a single br element, it represents a placeholder blank line


This is really cool, I had no idea it worked this way! :smiley:

1 Like

BTW, I just came up with these names. There’s no documentation for an “Interpreter” and a “Fotmatter”, but that’s what’s going on if you take a look at the source code.

1 Like