the speeches

This document is about how willshake presents the speeches of Shakespeare’s plays. This is part of a larger subsystem for presenting the text of the plays.

1 speech prefixes

Before each speech is a mark telling you who’s about to speak. In fact, Shakespeare did not rigorously include speech prefixes, and it’s been up to editors over the years to infer who’s speaking when a speech prefix is missing.

In willshake, all speeches have a speech prefix (abbreviated “s.p.”). In a proper text, these “guesses” would be marked as not coming directly from the copy text. When willshake uses a proper text, that’s just what I’ll do.

Ultimately, the plan is to have a location dedicated to each role (at least, each identifiable one). The speech prefixes used to point to these locations, but at the moment, there is nothing at those paths.

href="/plays/{$play-key}/roles/{@role}"
<!-- mode: speech-mark -->
<xsl:template mode="speech-mark" match="node()|@*" />
<xsl:template mode="speech-mark" match="speech">

  <xsl:variable name="anchor" select="line[1]/@a"/>

  <xsl:for-each select="speaker">
    <!-- Can't use anchor link alone because section page also has subpaths -->
    <a href="/plays/{$play-key}/{$section-key}#{$anchor}"
       class="speech-mark"
       data-role="{@role}" />

  </xsl:for-each>

</xsl:template>


<!-- mode: speakers -->
<xsl:template mode="speakers" match="speech" />
<xsl:template mode="speakers" match="speech[speaker]">

  <!-- Special speech prefix, overrides tags in "speaker" elements -->
  <xsl:variable name="sp" select="sp"/>

  <i class="sps">

    <xsl:if test="$sp">
      <span class="sp"><xsl:value-of select="$sp"/></span>
    </xsl:if>

    <xsl:for-each select="speaker[not($sp)]">
      <xsl:if test="position() != 1">
	<xsl:if test="last() &gt; 2">, </xsl:if>
	<xsl:if test="position() = last()"> &amp; </xsl:if>
      </xsl:if>

      <xsl:choose>
	<xsl:when test="@role">
	  <a
	     data-role="{@role}"
	     class="sp">
	    <xsl:value-of select="."/>
	  </a>
	</xsl:when>
	<xsl:otherwise>
	  <span class="sp">
	    <xsl:value-of select="."/>
	  </span>
	</xsl:otherwise>
      </xsl:choose>

    </xsl:for-each>

  </i>

  <xsl:apply-templates select="." mode="speech-mark" />

</xsl:template>

2 lines

Speeches are all made of lines, of course.

The goal here is to render speeches in a way that lets us depict the structure of the verse and the dialog, while still using the most “semantic” markup.

<xsl:template mode="scene" match="speech">

  <!-- We can't use "p" to wrap speeches because they may contain blocks for
       marginal content. -->
  <div data-lines="{count(.//line)}">
    <xsl:attribute name="class">
      <xsl:text>speech</xsl:text>
      <xsl:if test="prose"> prose</xsl:if>
      <xsl:if test="sd"> has-sd</xsl:if>
      <xsl:if test="sd[not(following-sibling::*)]"> has-end-sd</xsl:if>

      <!-- Used to pad speech when there are multiple sp's.  But we don't emit
	   for single-speaker since it's so common. -->
      <xsl:if test="speaker[2]">
	<xsl:value-of select="concat(' speakers-', count(speaker))"/>
      </xsl:if>
    </xsl:attribute>

    <xsl:apply-templates select="." mode="speakers" />
    <xsl:call-template name="render-line-groups" />

    <br class="tty" />
  </div>

</xsl:template>

2.1 normal verse lines

“Normal” verse lines are rendered here. Lines may contain stage directions, but they are assumed to be before the text.

Note that we do not render a line-number anchor. We have never made any attempt to compute or coördinate line numbers with any other edition.

<xsl:template mode="scene" match="line">
  <xsl:variable name="anchor" select="@a" />

  <!-- Inline stage directions are assumed to be at the front of the line. -->
  <xsl:apply-templates mode="scene" select="sd"/>

  <xsl:if test="0 != string-length(text())">

    <a class="a-source" href="/plays/{$play-key}/{ancestor::section/@key}#{$anchor}" id="{$anchor}--n"
			 data-n="{1 + count(preceding::line[not(catch)])}"/>

    <<the line element>>
    <br/>

    <!-- Separate lines in case we're re-wrapping prose. -->
    <xsl:if test="following-sibling::line">
      <xsl:text> </xsl:text>
    </xsl:if>

  </xsl:if>
</xsl:template>

So the line itself is rendered as a link.

<a class="a line" href="#{$anchor}" id="{$anchor}">
  <xsl:apply-templates select="text()" />
</a>

Why? Well, it’s a compromise. It’s essential to support linking to the lines. And it’s preferable to use “semantic markup.”

And this is definitely not “semantic.” The lines are text, and the links to them are really a separate matter. The “correct” way to do this would be to have a link and a line. The link would be positioned (using stylesheets) so that it occupied the same space as the text of the line. And without stylesheets, the text would appear just as it is. The link would also have a marker (hidden by stylesheets), so that it were still usable in non-stylesheet browsers.

That’s almost possible with CSS. It works as long as lines aren’t wrapped. But once there’s a break in the middle of a line, a positioned element inside of the text won’t extend onto the next line. In some cases, this would mean lines had no clickable portion at all, and in any case, speeches—especially in prose—would be full of “dead zones,” unusable gaps. This would be confusing and frustrating.

Instead, the lines themselves are rendered as links. This makes the above matter trivial. It also means fewer DOM elements, which is good. And maybe it helps search engines understand that this site is really, really about Shakespeare’s works. Otherwise, it’s not really noticeable, since zero people use the web without stylesheets. I just like to acknowledge those zero people sometimes.

2.2 line groups

Splits lines in a speech on s.d.’s and renders each set in a separate box.

<xsl:template name="render-line-groups" match="speech">
  <xsl:variable name="speech" select="."/>

  <xsl:for-each select="line[not(preceding-sibling::*[self::line or self::sd])] | sd">
    <xsl:variable name="pos" select="count(self::sd | preceding-sibling::sd)"/>

    <xsl:variable name="lines"
		  select="self::line
			  | following-sibling::line[count(preceding-sibling::sd) = $pos]"/>

    <xsl:variable name="full-lines" select="$lines
					    [not(catch)]
					    [not((following::line)[1][catch])]" />

    <xsl:variable name="broken-start-line" select="$lines[1][catch]"/>

    <!-- Done in speech, but may revisit for mixed speeches (e.g. songs) -->
    <!-- <xsl:variable name="is-prose" select="$speech/prose or not($lines[not(prose)])"/> -->
    <!-- <xsl:variable name="more-class"> -->
    <!--    <xsl:if test="$is-prose"> prose</xsl:if> -->
    <!-- </xsl:variable> -->


    <xsl:apply-templates mode="scene" select="self::sd" />

    <xsl:if test="$lines">
      <div class="lines">
	<xsl:apply-templates select="$broken-start-line" mode="render-broken-line" />

	<xsl:if test="$full-lines">
	  <div class="text">
	    <xsl:apply-templates select="$full-lines" mode="scene" />
	  </div>                  
	</xsl:if>

	<!-- Broken end line -->
	<xsl:apply-templates select="$lines
				     [generate-id() != generate-id($broken-start-line)]
				     [ catch or (following::line)[1][catch] ]"
			     mode="render-broken-line" />

      </div>
    </xsl:if>

  </xsl:for-each>
</xsl:template>

<!-- mode: render-broken-line -->
<xsl:template mode="render-broken-line" match="node()|@*" />

<!-- Kind of a hack: sd's get selected along with broken lines if they occur
     in a break. -->
<xsl:template mode="render-broken-line" match="sd">
  <xsl:apply-templates select="." mode="scene" />
</xsl:template>

<xsl:template mode="render-broken-line" match="line">
  <xsl:variable name="next-catch" select="(following::line)[1][catch]"/>

  <xsl:variable name="spaces" select="'                                                                                                    '"/>
  <xsl:variable name="nbsps" select="translate($spaces, ' ', ' ')"/>

  <div>
    <xsl:attribute name="class">
      <xsl:text>broken-line</xsl:text>
      <xsl:if test="following-sibling::line"> not-last</xsl:if>
      <xsl:if test="following-sibling::*[1][self::line[(following::line)[1][catch]]]"> ff</xsl:if>
    </xsl:attribute>

    <xsl:if test="catch">
      <xsl:variable name="spacer-text">
	<xsl:apply-templates select="." mode="catch-spacer-text" />
      </xsl:variable>

      <xsl:variable name="spacer-width">
	<xsl:call-template name="spacer-width">
	  <xsl:with-param name="text" select="$spacer-text"/>
	</xsl:call-template>
      </xsl:variable>

      <span class="spacer" style="width:{format-number($spacer-width div 1000, '0.##')}em">
	<xsl:value-of select="substring($nbsps, 1, string-length(normalize-space($spacer-text)))"/>
      </span>
    </xsl:if>

    <span>
      <xsl:attribute name="class">
	<xsl:text>text</xsl:text>
	<xsl:if test="catch"> catch</xsl:if>
	<!-- <xsl:if test="$next-catch"> caught</xsl:if> -->
      </xsl:attribute>

      <xsl:apply-templates mode="scene" select="." />
    </span>

    <xsl:if test="$next-catch">
      <span class="end-spacer" />
    </xsl:if>
  </div>

</xsl:template>

3 speech bubbles

Speeches are displayed as “bubbles,” as if in a messaging app (except not back-and-forth).

$bubbleRadius = 1em
$bubblePadding = .25em

// DUPLICATED in "typesetting"
$interSpeechMargin = 1em

.scene
	<<scene typesetting rules>>

Speeches break down into blocks of lines, which is where the real action is.

.lines                           // a group of lines, one per speech unless broken by, e.g., s.d
	clear both
	margin-bottom $interSpeechMargin
	@import fonts
	book-font()
	display inline-block // so that the box is sized to the length of the lines

	.text
		background #FFFFFF
		// Intriguing, but expensive.  Revisit when speeches are affordable.
		// Also, causes overlap issues between broken lines and full blocks.
		// box-shadow .5em .5em 1em -.66em #333

	> :first-child.text
	> :first-child .text
		padding-top: .25em;
		border-top-right-radius $bubbleRadius

	> :last-child.text
	> :last-child .text
		padding-bottom: .25em;
		border-bottom-left-radius $bubbleRadius
		border-bottom-right-radius $bubbleRadius

The main benefit of marking prose as prose is that you can re-wrap it as needed.

.prose
	br
		display none

	.lines
		max-width 25em

	.line
		// Override hanging indent on verse lines
		display inline
		padding-left 0
		text-indent 0

For short speeches, run block inline with s.p. has-sd is a hack because making s.d.’s flex children will cause them to run inline as well.

.speech[data-lines="1"]:not(.has-sd)
.speech[data-lines="2"]:not(.has-sd)
.speech[data-lines="3"]:not(.has-sd)
	display flex

	.sps
		white-space nowrap
		flex-shrink 0

	.lines
		flex-shrink 1
.text
	padding 0 1.25em
	line-height 1.33                // override browser choice from font metrics

	&.catch
		flex-grow 1             // extend to edge of box when

	// shouldn't apply to s.d.'s.  maybe there's a better way to express that.
	/.lines .text:not(:last-child)
		padding-bottom $bubblePadding
		border-bottom-right-radius $bubbleRadius

4 broken lines

In the templates above, there was some special processing of broken lines. A “broken line” is a verse line that is interrupted and resumed—sometimes by another speaker, or sometimes by the same speaker. Since broken lines represent a kind of structure, they are visually represented in the shape of the speech bubbles.

The determination of which lines belong together is somewhat editorial. In many cases, it’s obvious. Where it’s not obvious, I’ve tended to stay away. They’re marked in the source with a <catch /> element, which means that this line resumes the previous one.

Cymbline 2.4 has a number of goood test cases for broken lines (some of which are broken).

The handling of broken lines can’t quite be isolated from the handling of “normal” lines.

<!-- mode: catch-spacer-text -->
<xsl:template mode="catch-spacer-text" match="line">
  <xsl:variable name="previous-line-in-speech" select="(preceding-sibling::line)[last()]"/>
  <xsl:variable name="last-line-of-previous-speech"
		select="(../preceding-sibling::speech)[last()]
			/line[last()]"/>

  <xsl:variable name="previous-line" select="$previous-line-in-speech
					     | $last-line-of-previous-speech[not($previous-line-in-speech)]"/>

  <xsl:copy-of select="$previous-line/text()"/>

  <xsl:if test="$previous-line/catch">
    <xsl:apply-templates select="$previous-line" mode="catch-spacer-text" />            
  </xsl:if>

</xsl:template>

Note that the “previous line” is not determined in the obvious way,

<xsl:variable name="previous-line" select="(preceding::line)[last()]"/>

in order to avoid the preceding-axis, which is costly here.

One of the effects of using a background color around speeches is that you can make “broken” lines very easy to see.

.broken-line + .text
.text + .broken-line
	margin-top (- $bubblePadding) // offset the other's padding

.broken-line
	display flex

	.catch                                  // also works for .line
		white-space nowrap

	&.not-last > .text
		border-top-left-radius $bubbleRadius

	+ .text
		padding-top $bubblePadding

.broken-line > .spacer
	flex-grow 0

.end-spacer
	flex-grow 1

Bubble refinements for successive broken lines. The first line hangs to the right, and the second line hangs to the left. This gives the parts a “thicker” connection so that they appear more as a unit, without altering line spacing. It also extends a line between them in the rare cases where they would not otherwise be connected at all (as when the first part is just one word).

.broken-line.ff
	border-bottom-right-radius $bubbleRadius
	border-bottom $bubbleRadius solid white
	margin-bottom 0 - $bubbleRadius // offset the border being added

.broken-line.ff + .broken-line
	border-top $bubbleRadius solid white
	border-top-right-radius $bubbleRadius
	margin-top 0 - $bubbleRadius - .25em // offset the border being added

Wrap long verse lines. This is disabled below for prose.

.line
	display inline-block
	$hangingIndent = 2em
	text-indent 0 - $hangingIndent
	padding-left $hangingIndent

4.1 spacer width

To line up continued lines when proportional fonts are being used, we need a way to calculate the width of a given piece of text.

char-widths is a generated file—or at least, I generated it by a process that traces to a font metadata file that was available online. I intend to re-create that process in this system somewhere, but at the moment, it’s included as a static asset.

<xsl:variable name="widths" select="document('/static/doc/char-widths.xml')/cs" />
<xsl:key name="char-width" match="c" use="@c" />

<xsl:template name="spacer-width">
  <xsl:param name="text" />

  <xsl:variable name="c" select="substring($text, 1, 1)"/>
  <xsl:variable name="remaining-text" select="translate($text, $c, '')"/>
  <xsl:variable name="times" select="string-length($text) - string-length($remaining-text)"/>

  <xsl:variable name="width">
    <xsl:for-each select="$widths">
      <xsl:variable name="record" select="key('char-width', $c)"/>

      <xsl:choose>
	<xsl:when test="$record">
	  <xsl:value-of select="$record/@w"/>
	</xsl:when>
	<xsl:otherwise>500</xsl:otherwise>
      </xsl:choose>
    </xsl:for-each>
  </xsl:variable>

  <xsl:variable name="remaining-width">
    <xsl:text>0</xsl:text>
    <xsl:if test="$remaining-text != ''">
      <xsl:call-template name="spacer-width">
	<xsl:with-param name="text" select="$remaining-text"/>
      </xsl:call-template>
    </xsl:if>           
  </xsl:variable>

  <xsl:value-of select="$width * $times + $remaining-width"/>

</xsl:template>

Speaking of char-widths, I need to ship that file.

: foreach $(DATABASE)/char-widths.xml \
  | $(PROGRAM)/<minify_xml> \
|> ^o minify and ship %b ^ \
   %<minify_xml> "%f" > "%o" \
|> $(SITE_DOCS)/%b

But see embed char-widths into typesetting transform.

5 linking the lines

Links are the power gaze of the web. Without links pointing to it, a site might as well not exist.

And for willshake, links to the text of the plays are the most valuable kind of currency. Willshake “succeeds” if it becomes the go-to place for linking to Shakespeare.

Fortunately for willshake, people love doing two things on the Internet: quoting Shakespeare and making hyperlinks. And fortunately for people, linking to Shakespeare in willshake is both beautiful and pleasant. And dead simple.

As described in linking to the plays, every part of willshake already has an address. But how do you know that the address exists? Someone has to create a link to it in the first place. Usually, that’s done by a site—or even a document—linking to itself. Then you can use the link and copy it.

Every line in willshake includes a link to itself (actually, it is a link to itself; see above). The actual presentation of lines is not quite simple. But the link’s job is simply to occupy all the space occupied by the text of the line, so that touching it anywhere means that you’ve clicked on the link.

@import pointing
@import colors
.a.line
	+user_pointing_at()
		background $highlightColor

Again, since—at the moment—the text is also a link, you actually don’t need to do anything at all for that to be the case. The above rule just highlights it when hovered.

6 roadmap

6.1 use the speech mark

“Speech mark” isn’t used in the scene text now. It could be useful to show it while scrolling, to help correlate with the scene timeline, especially since the metrics don’t exactly line up (although this would just help you notice that).

7 issues

7.1 BUG embed char-widths into typesetting transform

Chrome does not support the document() function in client-side XSLT, which is used to load the char-widths data. Instead, it gets NaN when calculating spacer widths. This is not that bad of a fallback, but it could be fixed by embedding the char-widths data, which is only used here, into the transform (which you’d have to construct).

7.2 BUG character width map doesn’t include em dash

See e.g. https://willshake.net/plays/Ant/3.3#Most_gracious or https://willshake.net/plays/Ant/3.3#Madam

The dash is evidently 100% width (by definition, right?), but it must be getting the default of 50%.

Incidentally, is there a more appropriate default? Though it should never be used.

about willshake

Project “willshake” is an ongoing effort to bring the beauty and pleasure of Shakespeare to new media.

Please report problems on the issue tracker. For anything else, public@gavinpc.com

Willshake is an experiment in literate programming—not because it’s about literature, but because the program is written for a human audience.

Following is a visualization of the system. Each circle represents a document that is responsible for some part of the system. You can open the documents by touching the circles.

Starting with the project philosophy as a foundation, the layers are built up (or down, as it were): the programming system, the platform, the framework, the features, and so on. Everything that you see in the site is put there by these documents—even this message.

Again, this is an experiment. The documents contain a lot of “thinking out loud” and a lot of old thinking. The goal is not to make it perfect, but to maintain a reflective process that supports its own evolution.

graph of the program

about

Shakespeare

An edition of the plays and poems of Shakespeare.

the works