Skip to content

Commit 067e076

Browse files
committed
Adding XSL to ebooks for section numbering in headings
1 parent 65f811c commit 067e076

File tree

3 files changed

+640
-5
lines changed

3 files changed

+640
-5
lines changed

theme/epub/epub.xsl

Lines changed: 317 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,317 @@
1+
<xsl:stylesheet version="1.0"
2+
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
3+
xmlns:exsl="http://exslt.org/common"
4+
xmlns:func="http://exslt.org/functions"
5+
xmlns:l="http://docbook.sourceforge.net/xmlns/l10n/1.0"
6+
xmlns:h="http://www.w3.org/1999/xhtml"
7+
xmlns="http://www.w3.org/1999/xhtml"
8+
extension-element-prefixes="exsl func"
9+
xmlns:htmlbook="https://github.com/oreillymedia/HTMLBook"
10+
exclude-result-prefixes="exsl func h">
11+
<xsl:output method="xml"
12+
encoding="UTF-8"/>
13+
<xsl:preserve-space elements="*"/>
14+
15+
<!-- Add title heading elements for different admonition types that do not already have headings in markup -->
16+
<xsl:param name="add.title.heading.for.admonitions" select="1"/>
17+
18+
<!-- ***************** COOKBOOK PARAMS ***************** -->
19+
<!-- *************** Overrides param.xsl *************** -->
20+
21+
<!-- Recipe format should be "X.1 Title, no second period" -->
22+
<xsl:param name="recipe.number.and.title.separator" select="' '"/>
23+
24+
<!-- This book should show sect2s in TOC -->
25+
<xsl:param name="toc.section.depth" select="2"/>
26+
27+
<!-- ***************** LABEL HANDLING ***************** -->
28+
<!-- ************* Overrides common.xsl *************** -->
29+
30+
<!-- Logic for processing sect1 headings with labels (including section numbers) -->
31+
<xsl:template match="h:section[@data-type='chapter' and not(contains(@class, 'orm:non-recipe'))]/h:section[@data-type='sect1' and not(contains(@class, 'orm:non-recipe'))]/h:h1" mode="process-heading">
32+
<xsl:param name="autogenerate.labels" select="$autogenerate.labels"/>
33+
<!-- Labeled element is typically the parent element of the heading (e.g., <section> or <figure>) -->
34+
<xsl:param name="labeled-element" select="(parent::h:header/parent::*|parent::*[not(self::h:header)])[1]"/>
35+
<!-- Labeled element semantic name is typically the parent element of the heading's @data-type -->
36+
<xsl:param name="labeled-element-semantic-name" select="(parent::h:header/parent::*|parent::*[not(self::h:header)])[1]/@data-type"/>
37+
<!-- Name for output heading element; same as current node name by default -->
38+
<xsl:param name="output-element-name" select="local-name(.)"/>
39+
<xsl:element name="{$output-element-name}" namespace="http://www.w3.org/1999/xhtml">
40+
<xsl:apply-templates select="@*"/>
41+
<!-- BEGIN COOKBOOK OVERRIDE -->
42+
<!-- Recipes should have labels in format #.# -->
43+
<xsl:apply-templates select="$labeled-element" mode="label.markup"/>
44+
<xsl:value-of select="$recipe.number.and.title.separator"/>
45+
<xsl:apply-templates/>
46+
<!-- END COOKBOOK OVERRIDE -->
47+
</xsl:element>
48+
</xsl:template>
49+
50+
<!-- Logic for processing sect2 headings with labels (including section numbers) -->
51+
<xsl:template match="h:section[@data-type='chapter' and not(contains(@class, 'orm:non-recipe'))]/h:section[@data-type='sect1' and not(contains(@class, 'orm:non-recipe'))]/h:section[@data-type='sect2' and not(contains(@class, 'orm:non-recipe'))]/h:h2" mode="process-heading">
52+
<xsl:param name="autogenerate.labels" select="$autogenerate.labels"/>
53+
<!-- Labeled element is typically the parent element of the heading (e.g., <section> or <figure>) -->
54+
<xsl:param name="labeled-element" select="(parent::h:header/parent::*|parent::*[not(self::h:header)])[1]"/>
55+
<!-- Labeled element semantic name is typically the parent element of the heading's @data-type -->
56+
<xsl:param name="labeled-element-semantic-name" select="(parent::h:header/parent::*|parent::*[not(self::h:header)])[1]/@data-type"/>
57+
<!-- Name for output heading element; same as current node name by default -->
58+
<xsl:param name="output-element-name" select="local-name(.)"/>
59+
<xsl:element name="{$output-element-name}" namespace="http://www.w3.org/1999/xhtml">
60+
<xsl:apply-templates select="@*"/>
61+
<!-- BEGIN COOKBOOK OVERRIDE -->
62+
<!-- Recipes should have labels in format #.# -->
63+
<xsl:apply-templates select="$labeled-element" mode="label.markup"/>
64+
<xsl:value-of select="$recipe.number.and.title.separator"/>
65+
<xsl:apply-templates/>
66+
<!-- END COOKBOOK OVERRIDE -->
67+
</xsl:element>
68+
</xsl:template>
69+
70+
<!-- Creating the sect1 labels (read: creating the X.X section numbering) -->
71+
<xsl:template match="h:section[@data-type='chapter' and not(contains(@class, 'orm:non-recipe'))]/h:section[@data-type='sect1' and not(contains(@class, 'orm:non-recipe'))]" mode="label.markup">
72+
<xsl:variable name="current-node" select="."/>
73+
<!-- BEGIN COOKBOOK OVERRIDE -->
74+
<!-- Recipes should always be labeled with ancestor chapter -->
75+
<xsl:for-each select="ancestor::h:section[@data-type='chapter']">
76+
<xsl:call-template name="get-label-from-data-type">
77+
<xsl:with-param name="data-type" select="@data-type"/>
78+
</xsl:call-template>
79+
<xsl:apply-templates select="$current-node" mode="intralabel.punctuation"/>
80+
</xsl:for-each>
81+
82+
<!-- Custom Recipe numbering logic:
83+
* Don't number Recipes with class=orm:non-recipe
84+
* Introduction sections at the beginning of chapters have labeling start at #.0
85+
-->
86+
<xsl:variable name="is.numbered">
87+
<xsl:choose>
88+
<xsl:when test="@class='orm:non-recipe'">1</xsl:when>
89+
<xsl:otherwise>0</xsl:otherwise>
90+
</xsl:choose>
91+
</xsl:variable>
92+
93+
<xsl:variable name="chap.has.intro">
94+
<xsl:choose>
95+
<xsl:when test="$is.numbered = 0">
96+
<xsl:call-template name="check.chap.for.intro">
97+
<xsl:with-param name="chapter" select="parent::*"/>
98+
</xsl:call-template>
99+
</xsl:when>
100+
<xsl:otherwise>1</xsl:otherwise>
101+
</xsl:choose>
102+
</xsl:variable>
103+
104+
<xsl:variable name="recipe.level">
105+
<xsl:value-of select="count(preceding-sibling::h:section[@data-type='sect1'][not(@class='orm:non-recipe')]) + (1 - $chap.has.intro - $is.numbered)"/>
106+
</xsl:variable>
107+
<xsl:number format="1" value="$recipe.level"/>
108+
109+
<!-- END COOKBOOK OVERRIDE -->
110+
</xsl:template>
111+
112+
<!-- Creating the sect2 labels (read: creating the X.X.X section numbering) -->
113+
<xsl:template match="h:section[@data-type='chapter' and not(contains(@class, 'orm:non-recipe'))]/h:section[@data-type='sect1' and not(contains(@class, 'orm:non-recipe'))]/h:section[@data-type='sect2' and not(contains(@class, 'orm:non-recipe'))]" mode="label.markup">
114+
<!-- END OVERRIDE -->
115+
<xsl:variable name="current-node" select="."/>
116+
<!-- BEGIN COOKBOOK OVERRIDE -->
117+
<!-- Recipes should always be labeled with ancestor chapter -->
118+
<xsl:for-each select="ancestor::h:section[@data-type='chapter']">
119+
<xsl:call-template name="get-label-from-data-type">
120+
<xsl:with-param name="data-type" select="@data-type"/>
121+
</xsl:call-template>
122+
<xsl:apply-templates select="$current-node" mode="intralabel.punctuation"/>
123+
</xsl:for-each>
124+
125+
<!-- Custom Recipe numbering logic:
126+
* Don't number Recipes with class=orm:non-recipe
127+
* Introduction sections at the beginning of chapters have labeling start at #.0
128+
-->
129+
<xsl:variable name="is.numbered">
130+
<xsl:choose>
131+
<xsl:when test="parent::h:section/@class='orm:non-recipe'">1</xsl:when>
132+
<xsl:otherwise>0</xsl:otherwise>
133+
</xsl:choose>
134+
</xsl:variable>
135+
136+
<xsl:variable name="chap.has.intro">
137+
<xsl:choose>
138+
<xsl:when test="$is.numbered = 0">
139+
<xsl:call-template name="check.chap.for.intro">
140+
<xsl:with-param name="chapter" select="ancestor::h:section[data-type='chapter']"/>
141+
</xsl:call-template>
142+
</xsl:when>
143+
<xsl:otherwise>1</xsl:otherwise>
144+
</xsl:choose>
145+
</xsl:variable>
146+
147+
<xsl:variable name="sect1.recipe.level">
148+
<xsl:value-of select="count(../preceding-sibling::h:section[@data-type='sect1'][not(@class='orm:non-recipe')]) + (1 - $chap.has.intro - $is.numbered)"/>
149+
</xsl:variable>
150+
<xsl:number format="1" value="$sect1.recipe.level"/>
151+
<xsl:text>.</xsl:text>
152+
153+
<xsl:variable name="is.sect1.numbered">
154+
<xsl:choose>
155+
<xsl:when test="@class='orm:non-recipe'">1</xsl:when>
156+
<xsl:otherwise>0</xsl:otherwise>
157+
</xsl:choose>
158+
</xsl:variable>
159+
160+
<xsl:variable name="sect1.has.intro">
161+
<xsl:choose>
162+
<xsl:when test="$is.sect1.numbered = 0">
163+
<xsl:call-template name="check.sect1.for.intro">
164+
<xsl:with-param name="sect1" select="parent::*"/>
165+
</xsl:call-template>
166+
</xsl:when>
167+
<xsl:otherwise>1</xsl:otherwise>
168+
</xsl:choose>
169+
</xsl:variable>
170+
171+
<xsl:variable name="recipe.level">
172+
<xsl:value-of select="count(preceding-sibling::h:section[@data-type='sect2'][not(@class='orm:non-recipe')]) + (1 - $sect1.has.intro - $is.sect1.numbered)"/>
173+
</xsl:variable>
174+
<xsl:number format="1" value="$recipe.level"/>
175+
176+
<!-- END COOKBOOK OVERRIDE -->
177+
</xsl:template>
178+
<!-- Utility template -->
179+
<xsl:template name="check.chap.for.intro">
180+
<xsl:param name="chapter" select="."/>
181+
<xsl:choose>
182+
<xsl:when test="$chapter/h:section[@data-type='sect1'][1]/h:h1 = 'Introduction'">1</xsl:when>
183+
<xsl:otherwise>0</xsl:otherwise>
184+
</xsl:choose>
185+
</xsl:template>
186+
187+
<xsl:template name="check.sect1.for.intro">
188+
<xsl:param name="sect1" select="."/>
189+
<xsl:choose>
190+
<xsl:when test="$sect1/h:section[@data-type='sect2'][1]/h:h2 = 'Introduction'">1</xsl:when>
191+
<xsl:otherwise>0</xsl:otherwise>
192+
</xsl:choose>
193+
</xsl:template>
194+
195+
<!-- ***************** XREF HANDLING ***************** -->
196+
<!-- ************* Overrides xrefgen.xsl ************* -->
197+
198+
<xsl:template match="h:a[@data-type='xref']" mode="class.value">
199+
<xsl:param name="class" select="@class"/>
200+
<xsl:param name="xref.elements.pagenum.in.class" select="$xref.elements.pagenum.in.class"/>
201+
<xsl:param name="xref.target"/>
202+
<xsl:choose>
203+
<!-- BEGIN COOKBOOK OVERRIDE -->
204+
<!-- If there's an xref target, process that to determine whether a pagenum value should be added to the class -->
205+
<!-- No pagenum class for Recipe targets -->
206+
<xsl:when test="$xref.target[not(self::h:section[@data-type='sect1' and not(contains(@class, 'orm:non-recipe'))] and ancestor::h:section[@data-type='chapter' and not(contains(@class, 'orm:non-recipe'))])]">
207+
<!-- END COOKBOOK OVERRIDE -->
208+
<xsl:variable name="xref.target.semantic.name">
209+
<xsl:call-template name="semantic-name">
210+
<xsl:with-param name="node" select="$xref.target"/>
211+
</xsl:call-template>
212+
</xsl:variable>
213+
<xsl:if test="$class != ''">
214+
<xsl:value-of select="$class"/>
215+
</xsl:if>
216+
<!-- Check if target semantic name is in list of XREF elements containing pagenum -->
217+
<!-- ToDo: Consider modularizing logic into separate function if needed for reuse elsewhere -->
218+
<xsl:variable name="space-delimited-pagenum-elements" select="concat(' ', normalize-space($xref.elements.pagenum.in.class), ' ')"/>
219+
<xsl:variable name="substring-before-target-name" select="substring-before($space-delimited-pagenum-elements, $xref.target.semantic.name)"/>
220+
<xsl:variable name="substring-after-target-name" select="substring-after($space-delimited-pagenum-elements, $xref.target.semantic.name)"/>
221+
<!-- Make sure a match is both preceded and followed by a space -->
222+
<xsl:if test="substring($substring-after-target-name, 1, 1) and
223+
substring($substring-before-target-name, string-length($substring-before-target-name), 1)">
224+
<xsl:if test="$class != ''"><xsl:text> </xsl:text></xsl:if>
225+
<xsl:text>pagenum</xsl:text>
226+
</xsl:if>
227+
</xsl:when>
228+
<xsl:otherwise>
229+
<xsl:if test="$class != ''">
230+
<xsl:value-of select="$class"/>
231+
</xsl:if>
232+
</xsl:otherwise>
233+
</xsl:choose>
234+
</xsl:template>
235+
236+
<!-- Custom XREF style for XREFs to recipes -->
237+
<xsl:template match="h:section[@data-type='sect1' and not(contains(@class, 'orm:non-recipe')) and ancestor::h:section[@data-type='chapter' and not(contains(@class, 'orm:non-recipe'))]]" mode="xref-to">
238+
<xsl:param name="referrer"/>
239+
<xsl:param name="xrefstyle"/>
240+
<xsl:param name="verbose" select="1"/>
241+
242+
<xsl:apply-templates select="." mode="object.xref.markup">
243+
<xsl:with-param name="purpose" select="'xref'"/>
244+
<!-- BEGIN COOKBOOK OVERRIDE -->
245+
<xsl:with-param name="xrefstyle" select="$xrefstyle"/>
246+
<!-- END COOKBOOK OVERRIDE -->
247+
<xsl:with-param name="referrer" select="$referrer"/>
248+
<xsl:with-param name="verbose" select="$verbose"/>
249+
</xsl:apply-templates>
250+
</xsl:template>
251+
252+
<!-- ***************** TOC HANDLING ***************** -->
253+
<!-- ************* Overrides tocgen.xsl ************* -->
254+
255+
<xsl:template match="h:section[not(@data-type = 'dedication' or @data-type = 'titlepage' or @data-type = 'toc' or @data-type = 'colophon' or @data-type = 'copyright-page' or @data-type = 'halftitlepage')]|h:div[@data-type='part']" mode="tocgen">
256+
<xsl:param name="toc.section.depth" select="$toc.section.depth"/>
257+
<xsl:choose>
258+
<!-- Don't output entry for section elements at a level that is greater than specified $toc.section.depth -->
259+
<xsl:when test="self::h:section[contains(@data-type, 'sect') and htmlbook:section-depth(.) != '' and htmlbook:section-depth(.) &gt; $toc.section.depth]"/>
260+
<!-- Otherwise, go ahead -->
261+
<xsl:otherwise>
262+
<xsl:element name="li">
263+
<xsl:attribute name="data-type">
264+
<xsl:value-of select="@data-type"/>
265+
</xsl:attribute>
266+
<a>
267+
<xsl:attribute name="href">
268+
<xsl:call-template name="href.target">
269+
<xsl:with-param name="object" select="."/>
270+
</xsl:call-template>
271+
</xsl:attribute>
272+
<!-- BEGIN COOKBOOK OVERRIDE -->
273+
<xsl:if test="(self::h:section[@data-type='sect1'] and ancestor::h:section[@data-type='chapter'] or self::h:section[@data-type='sect2'] and ancestor::h:section[@data-type='sect1']) and not(ancestor-or-self::h:section[contains(@class, 'orm:non-recipe')])">
274+
<xsl:variable name="toc-entry-label">
275+
<xsl:apply-templates select="." mode="label.markup"/>
276+
</xsl:variable>
277+
<xsl:value-of select="normalize-space($toc-entry-label)"/>
278+
<xsl:value-of select="$recipe.number.and.title.separator"/>
279+
</xsl:if>
280+
<!-- END COOKBOOK OVERRIDE -->
281+
<xsl:apply-templates select="." mode="title.markup"/>
282+
</a>
283+
<!-- Make sure there are descendants that conform to $toc.section.depth restrictions before generating nested TOC <ol> -->
284+
<xsl:if test="descendant::h:section[not(contains(@data-type, 'sect')) or htmlbook:section-depth(.) &lt;= $toc.section.depth]|descendant::h:div[@data-type='part']">
285+
<ol>
286+
<xsl:apply-templates mode="tocgen">
287+
<xsl:with-param name="toc.section.depth" select="$toc.section.depth"/>
288+
</xsl:apply-templates>
289+
</ol>
290+
</xsl:if>
291+
</xsl:element>
292+
</xsl:otherwise>
293+
</xsl:choose>
294+
</xsl:template>
295+
296+
<xsl:template name="string-replace-all">
297+
<xsl:param name="text"/>
298+
<xsl:param name="replace"/>
299+
<xsl:param name="by"/>
300+
<xsl:choose>
301+
<xsl:when test="contains($text, $replace)">
302+
<xsl:value-of select="substring-before($text,$replace)"/>
303+
<xsl:value-of select="$by"/>
304+
<xsl:call-template name="string-replace-all">
305+
<xsl:with-param name="text" select="substring-after($text,$replace)"/>
306+
<xsl:with-param name="replace" select="$replace"/>
307+
<xsl:with-param name="by" select="$by"/>
308+
</xsl:call-template>
309+
</xsl:when>
310+
<xsl:otherwise>
311+
<xsl:value-of select="$text"/>
312+
</xsl:otherwise>
313+
</xsl:choose>
314+
</xsl:template>
315+
</xsl:stylesheet>
316+
317+

0 commit comments

Comments
 (0)