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() > 2">, </xsl:if>
<xsl:if test="position() = last()"> & </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.