Add XSLT stylesheet for OPDS feeds

This commit is contained in:
Alex Cabal 2022-06-20 13:52:39 -05:00
parent 3b26e49509
commit 305a9d298b
10 changed files with 182 additions and 56 deletions

View file

@ -86,6 +86,10 @@ class OpdsFeed{
file_put_contents($parentFilepath, str_replace(" ns=", " xmlns=", $xmlString));
// If we include this stylsheet declaration in the OPDS template, `se clean` will remove it and also
// add a bunch of empty namespaces in the output. So, add it programatically here instead.
file_put_contents($tempFilename, str_replace("?>", "?>\n<?xml-stylesheet href=\"/opds/style\" type=\"text/xsl\"?>", file_get_contents($tempFilename)));
rename($tempFilename, $path);
}
else{

View file

@ -8,7 +8,7 @@ $collection = $collection ?? null;
$ebooks = $ebooks ?? [];
?>
<ol<? if($view == VIEW_LIST){ ?> class="list"<? } ?><? if($collection !== null){ ?> typeof="schema:BookSeries" about="<?= $collection->Url ?>"<? } ?>>
<ol class="ebooks-list<? if($view == VIEW_LIST){ ?> list<? }else{ ?> grid<? } ?>"<? if($collection !== null){ ?> typeof="schema:BookSeries" about="<?= $collection->Url ?>"<? } ?>>
<? if($collection !== null){ ?>
<meta property="schema:name" content="<?= Formatter::ToPlainText($collection->Name) ?>"/>
<? } ?>

View file

@ -4,8 +4,9 @@
<? foreach($ebook->Authors as $author){ ?>
<author>
<name><?= htmlspecialchars($author->Name, ENT_QUOTES|ENT_XML1, 'utf-8') ?></name>
<? if($author->WikipediaUrl !== null){ ?><uri><?= htmlspecialchars($author->WikipediaUrl, ENT_QUOTES|ENT_XML1, 'utf-8') ?></uri><? } ?>
<uri><?= SITE_URL . htmlspecialchars($ebook->AuthorsUrl, ENT_QUOTES|ENT_XML1, 'utf-8') ?></uri>
<? if($author->FullName !== null){ ?><schema:alternateName><?= htmlspecialchars($author->FullName, ENT_QUOTES|ENT_XML1, 'utf-8') ?></schema:alternateName><? } ?>
<? if($author->WikipediaUrl !== null){ ?><schema:sameAs><?= htmlspecialchars($author->WikipediaUrl, ENT_QUOTES|ENT_XML1, 'utf-8') ?></schema:sameAs><? } ?>
<? if($author->NacoafUrl !== null){ ?><schema:sameAs><?= htmlspecialchars($author->NacoafUrl, ENT_QUOTES|ENT_XML1, 'utf-8') ?></schema:sameAs><? } ?>
</author>
<? } ?>

View file

@ -10,15 +10,18 @@
$isCrawlable = $isCrawlable ?? false;
// Note that the XSL stylesheet gets stripped during `se clean` when we generate the OPDS feed.
// `se clean` will also start adding empty namespaces everywhere if we include the stylesheet declaration first.
// We have to add it programmatically when saving the OPDS file.
print("<?xml version=\"1.0\" encoding=\"utf-8\"?>\n");
?>
<feed xmlns="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:schema="http://schema.org/"<? if($isCrawlable){ ?> xmlns:fh="http://purl.org/syndication/history/1.0"<? } ?>>
<id><?= $id ?></id>
<link href="<?= $url ?>" rel="self" type="application/atom+xml;profile=opds-catalog;kind=acquisition"/>
<link href="/opds" rel="start" type="application/atom+xml;profile=opds-catalog;kind=navigation"/>
<link href="<?= $parentUrl ?>" rel="up" type="application/atom+xml;profile=opds-catalog;kind=navigation"/>
<link href="/opds/all" rel="http://opds-spec.org/crawlable" type="application/atom+xml;profile=opds-catalog;kind=acquisition"/>
<link href="/ebooks/opensearch" rel="search" type="application/opensearchdescription+xml"/>
<link href="<?= SITE_URL . $url ?>" rel="self" type="application/atom+xml;profile=opds-catalog;kind=acquisition"/>
<link href="<?= SITE_URL ?>/opds" rel="start" type="application/atom+xml;profile=opds-catalog;kind=navigation"/>
<link href="<?= SITE_URL ?><?= $parentUrl ?>" rel="up" type="application/atom+xml;profile=opds-catalog;kind=navigation"/>
<link href="<?= SITE_URL ?>/opds/all" rel="http://opds-spec.org/crawlable" type="application/atom+xml;profile=opds-catalog;kind=acquisition"/>
<link href="<?= SITE_URL ?>/ebooks/opensearch" rel="search" type="application/opensearchdescription+xml"/>
<title><?= htmlspecialchars($title, ENT_QUOTES|ENT_XML1, 'utf-8') ?></title>
<subtitle>Free and liberated ebooks, carefully produced for the true book lover.</subtitle>
<icon>/images/logo.png</icon>

View file

@ -7,15 +7,19 @@
- The <fh:complete/> element is required to note this as a "Complete Acquisition Feeds"; see https://specs.opds.io/opds-1.2#25-complete-acquisition-feeds
*/
// Note that the XSL stylesheet gets stripped during `se clean` when we generate the OPDS feed.
// `se clean` will also start adding empty namespaces everywhere if we include the stylesheet declaration first.
// We have to add it programmatically when saving the OPDS file.
print("<?xml version=\"1.0\" encoding=\"utf-8\"?>\n");
?>
<feed xmlns="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:schema="http://schema.org/">
<feed xmlns="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
<id><?= $id ?></id>
<link href="<?= $url ?>" rel="self" type="application/atom+xml;profile=opds-catalog;kind=acquisition"/>
<link href="/opds" rel="start" type="application/atom+xml;profile=opds-catalog;kind=navigation"/>
<link href="/opds/all" rel="http://opds-spec.org/crawlable" type="application/atom+xml;profile=opds-catalog;kind=acquisition"/>
<link href="/ebooks/opensearch" rel="search" type="application/opensearchdescription+xml"/>
<? if($parentUrl !== null){ ?><link href="<?= $parentUrl ?>" rel="up" type="application/atom+xml;profile=opds-catalog;kind=navigation"/><? } ?>
<link href="<?= SITE_URL . $url ?>" rel="self" type="application/atom+xml;profile=opds-catalog;kind=acquisition"/>
<link href="<?= SITE_URL ?>/opds" rel="start" type="application/atom+xml;profile=opds-catalog;kind=navigation"/>
<link href="<?= SITE_URL ?>/opds/all" rel="http://opds-spec.org/crawlable" type="application/atom+xml;profile=opds-catalog;kind=acquisition"/>
<link href="<?= SITE_URL ?>/ebooks/opensearch" rel="search" type="application/opensearchdescription+xml"/>
<? if($parentUrl !== null){ ?><link href="<?= SITE_URL ?><?= $parentUrl ?>" rel="up" type="application/atom+xml;profile=opds-catalog;kind=navigation"/><? } ?>
<title><?= htmlspecialchars($title, ENT_QUOTES|ENT_XML1, 'utf-8') ?></title>
<subtitle>Free and liberated ebooks, carefully produced for the true book lover.</subtitle>
<icon>/images/logo.png</icon>

View file

@ -1186,7 +1186,22 @@ main.ebooks > aside.alert + ol{
margin-top: 2rem;
}
main.ebooks > ol{
.opds ol.ebooks-list.list{
margin-top: 4rem;
}
.opds .download{
font-weight: bold;
margin-top: 1rem;
}
.opds .download + ul,
.opds .download + ul > li{
margin-top: 0;
}
ol.ebooks-list.grid{
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
grid-gap: 4rem;
@ -1195,7 +1210,7 @@ main.ebooks > ol{
list-style: none;
}
main.ebooks > ol.list{
ol.ebooks-list.list{
display: flex;
flex-direction: column;
gap: 0;
@ -1205,50 +1220,54 @@ main.ebooks > ol.list{
max-width: 30rem;
}
main.ebooks > ol > li{
ol.ebooks-list > li{
margin: 0;
text-align: center;
}
main.ebooks > ol.list > li{
ol.ebooks-list.list > li{
display: grid;
grid-template-columns: 6rem 1fr;
grid-column-gap: 1rem;
grid-template-rows: auto auto 1fr;
}
main.ebooks > ol.list > li + li {
ol.ebooks-list.list > li + li {
margin-top: 2rem;
}
main.ebooks > ol.list > li > a{
ol.ebooks-list.list > li > a{
grid-row: 1 / span 3;
}
main.ebooks > ol.list ul.tags{
.opds ol.ebooks-list.list > li > a{
grid-row: 1 / span 5;
}
ol.ebooks-list.list ul.tags{
display: flex;
justify-content: flex-start;
margin-left: -.25rem;
margin-top: .25rem;
}
main.ebooks > ol.list ul.tags li{
ol.ebooks-list.list ul.tags li{
margin-left: .25rem;
margin-top: .25rem;
}
main.ebooks > ol.list > li p{
ol.ebooks-list.list > li p{
text-align: left;
}
main.ebooks > ol a[tabindex]{
ol.ebooks-list a[tabindex]{
display: inline-block;
transition: all .2s ease;
position: relative;
font-size: 0;
}
main.ebooks > ol a[tabindex][data-ordinal]::before{
ol.ebooks-list a[tabindex][data-ordinal]::before{
display: block;
content: "№ "attr(data-ordinal);
position: absolute;
@ -1265,29 +1284,29 @@ main.ebooks > ol a[tabindex][data-ordinal]::before{
color: var(--dark-body-text);
}
main.ebooks > ol a[tabindex]:hover{
ol.ebooks-list a[tabindex]:hover{
transform: scale(1.05);
color: unset;
}
main.ebooks > ol a[tabindex]:hover img{
ol.ebooks-list a[tabindex]:hover img{
box-shadow: 3px 3px 1px rgba(0, 0, 0, .25);
}
main.ebooks > ol > li a[tabindex]:active{
ol.ebooks-list > li a[tabindex]:active{
transform: scale(1.025);
transition: none;
box-shadow: none;
}
main.ebooks > ol > li p{
ol.ebooks-list > li p{
margin: 0;
text-align: center;
-webkit-hyphens: none;
hyphens: none;
}
main.ebooks > ol > li img{
ol.ebooks-list > li img{
box-sizing: border-box;
max-width: 100%;
height: auto;
@ -1295,24 +1314,27 @@ main.ebooks > ol > li img{
border-radius: .25rem;
}
main.ebooks > ol > li > p:nth-of-type(1) > a{
ol.ebooks-list > li > p:nth-of-type(1) > a{
font-weight: bold;
text-decoration: none;
display: flex;
justify-content: center;
}
main.ebooks > ol.list > li p.author a,
main.ebooks > ol.list > li > p:nth-of-type(1) > a{
ol.ebooks-list.list > li p.author a,
ol.ebooks-list.list > li > p:nth-of-type(1) > a{
display: inline;
}
main.ebooks > ol > li > a:first-child{
ol.ebooks-list > li > a:first-child{
font-size: 0; /* for correct focus outline */
}
main.ebooks > ol > li p.author a{
ol.ebooks-list > li p.author{
font-style: italic;
}
ol.ebooks-list > li p.author a{
text-decoration: none;
display: flex;
justify-content: center;
@ -1376,11 +1398,6 @@ figure{
margin-top: 1rem;
}
.rss ul.tags + p{
font-style: italic;
margin-top: 0;
}
ul.tags{
list-style: none;
display: flex;
@ -1485,7 +1502,7 @@ p.no-results{
text-align: center;
}
.us-pd-warning{
.s-pd-warning{
font-style: italic;
margin-top: 1rem;
}
@ -2479,8 +2496,13 @@ ul.feed p{
margin: 0;
}
ul.feed p:last-child{
font-style: italic;
.url{
font-family: "Fira Mono", monospace;
font-size: .8rem;
}
.rss > li p:last-child{
margin-top: 0;
}
@media (hover: none) and (pointer: coarse){ /* target ipads and smartphones without a mouse */
@ -2655,7 +2677,7 @@ ul.feed p:last-child{
}
@media(max-width: 900px){
main.ebooks > ol{
ol.ebooks-list.grid{
grid-template-columns: repeat(3, minmax(0, 1fr));
}
@ -2751,7 +2773,7 @@ ul.feed p:last-child{
}
@media(max-width: 700px){
main.ebooks > ol{
ol.ebooks-list.grid{
grid-template-columns: repeat(2, minmax(0, 1fr));
}
@ -2995,7 +3017,7 @@ ul.feed p:last-child{
}
@media(max-width: 470px){
main.ebooks > ol{
ol.ebooks-list.grid{
grid-gap: 2rem;
}
@ -3047,7 +3069,7 @@ ul.feed p:last-child{
hyphens: auto;
}
main.ebooks > ol{
ol.ebooks-list.grid{
grid-template-columns: 1fr;
}
@ -3101,11 +3123,11 @@ ul.feed p:last-child{
padding: 1rem;
}
main.ebooks > ol.list > li{
ol.ebooks-list.list > li{
grid-template-columns: 1fr;
}
main.ebooks > ol.list > li img{
ol.ebooks-list.list > li img{
max-width: 50%;
margin-bottom: 1rem;
}
@ -3141,8 +3163,8 @@ ul.feed p:last-child{
article.ebook section#sources ul li a[class]::before,
article.ebook #more-ebooks img,
article.ebook #more-ebooks a:active img,
main.ebooks > ol a[tabindex],
main.ebooks > ol > li a[tabindex]:active,
ol.ebooks-list a[tabindex],
ol.ebooks-list > li a[tabindex]:active,
label.select > span + span::after,
label.search:focus-within::before,
label.search:hover::before,

View file

@ -64,7 +64,7 @@ input::placeholder{
color: rgba(255, 255, 255, .75);
}
main.ebooks > ol img:hover{
ol.ebooks-list img:hover{
box-shadow: 3px 3px 1px rgba(0, 0, 0, .5);
}

View file

@ -19,7 +19,7 @@ require_once('Core.php');
<ul class="feed">
<li>
<p><a href="/rss/new-releases">New releases</a> (RSS 2.0)</p>
<p><?= SITE_URL ?>/rss/new-releases</p>
<p class="url"><?= SITE_URL ?>/rss/new-releases</p>
<p>A list of the thirty latest Standard Ebooks ebook releases, most-recently-released first.</p>
</li>
</ul>
@ -29,9 +29,8 @@ require_once('Core.php');
<p><a href="https://en.wikipedia.org/wiki/Open_Publication_Distribution_System">OPDS feeds</a> are designed for use with ereading systems like <a href="http://koreader.rocks/">KOreader</a> or <a href="https://calibre-ebook.com">Calibre</a>, or with ereaders like <a href="https://johnfactotum.github.io/foliate/">Foliate</a>. They allow you to search, browse, and download from our catalog, directly in your ereader.</p>
<ul class="feed">
<li>
<p><a href="/opds">The Standard Ebooks OPDS feed</a> (OPDS 1.1)</p>
<p><?= SITE_URL ?>/opds</p>
<p>The main OPDS feed to use with ereading systems that support OPDS.</p>
<p><a href="/opds">The Standard Ebooks OPDS feed</a> (OPDS 1.2)</p>
<p class="url"><?= SITE_URL ?>/opds</p>
</li>
</ul>
<section>

93
www/opds/style.php Normal file
View file

@ -0,0 +1,93 @@
<?
require_once('Core.php');
// `text/xsl` is the only mime type recognized by Chrome for XSL stylesheets
header('Content-Type: text/xsl');
print("<?xml version=\"1.0\" encoding=\"utf-8\"?>\n")
?>
<xsl:stylesheet version="3.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:atom="http://www.w3.org/2005/Atom">
<xsl:output method="html" html-version="5.0" encoding="utf-8" indent="yes" doctype-system="about:legacy-compat"/> <? /* doctype-system outputs the HTML5 doctype */ ?>
<xsl:template match="/">
<?= Template::Header(['xmlDeclaration' => false]) ?>
<main class="opds">
<h1><xsl:value-of select="/atom:feed/atom:title"/></h1>
<p>This page is an OPDS feed. The URL in your browsers address bar (<a class="url"><xsl:attribute name="href"><xsl:value-of select="/atom:feed/atom:link[@rel='self']/@href"/></xsl:attribute><xsl:value-of select="/atom:feed/atom:link[@rel='self']/@href"/></a>) can be used in any OPDS client.</p>
<xsl:if test="/atom:feed/atom:entry[./atom:link[starts-with(@type, 'application/atom+xml;profile=opds-catalog;kind=')]]">
<ol class="rss">
<xsl:for-each select="/atom:feed/atom:entry[./atom:link[starts-with(@type, 'application/atom+xml;profile=opds-catalog;kind=')]]">
<li>
<a>
<xsl:attribute name="href">
<xsl:value-of select="atom:link/@href"/>
</xsl:attribute>
<xsl:value-of select="atom:title"/>
</a>
<p>
<xsl:value-of select="atom:content"/>
</p>
</li>
</xsl:for-each>
</ol>
</xsl:if>
<xsl:if test="/atom:feed/atom:entry[./atom:link[@rel='http://opds-spec.org/acquisition/open-access']]">
<ol class="ebooks-list list">
<xsl:for-each select="/atom:feed/atom:entry[./atom:link[@rel='http://opds-spec.org/acquisition/open-access']]">
<li>
<a tabindex="-1">
<xsl:attribute name="href">
<xsl:value-of select="atom:link[@rel='related']/@href"/>
</xsl:attribute>
<img alt="The cover for the Standard Ebooks edition of Winnie-the-Pooh, by A. A. Milne" width="224" height="335">
<xsl:attribute name="src">
<xsl:value-of select="atom:link[@rel='http://opds-spec.org/image/thumbnail']/@href"/>
</xsl:attribute>
</img>
</a>
<p>
<a>
<xsl:attribute name="href">
<xsl:value-of select="atom:link[@rel='related']/@href"/>
</xsl:attribute>
<xsl:value-of select="atom:title"/>
</a>
</p>
<div>
<xsl:for-each select="atom:author">
<p class="author">
<a>
<xsl:attribute name="href">
<xsl:value-of select="atom:uri"/>
</xsl:attribute>
<xsl:value-of select="atom:name"/>
</a>
</p>
</xsl:for-each>
</div>
<div class="details">
<p>
<xsl:value-of select="atom:summary"/>
</p>
</div>
<p class="download">Download</p>
<ul>
<xsl:for-each select="atom:link[@rel='http://opds-spec.org/acquisition/open-access']">
<li>
<p>
<a>
<xsl:attribute name="href">
<xsl:value-of select="@href"/>
</xsl:attribute>
<xsl:value-of select="@title"/>
</a>
</p>
</li>
</xsl:for-each>
</ul>
</li>
</xsl:for-each>
</ol>
</xsl:if>
</main>
<?= Template::Footer() ?>
</xsl:template>
</xsl:stylesheet>

View file

@ -12,7 +12,7 @@ print("<?xml version=\"1.0\" encoding=\"utf-8\"?>\n")
<main>
<h1><xsl:value-of select="substring-after(/rss/channel/title, 'Standard Ebooks - ')"/></h1>
<p><xsl:value-of select="/rss/channel/description"/></p>
<p>This page is an RSS feed. The URL in your address bar (<a ><xsl:attribute name="href"><xsl:value-of select="/rss/channel/atom:link/@href"/></xsl:attribute><xsl:value-of select="/rss/channel/atom:link/@href"/></a>) can be used in any RSS reader.</p>
<p>This page is an RSS feed. The URL in your browsers address bar (<a class="url"><xsl:attribute name="href"><xsl:value-of select="/rss/channel/atom:link/@href"/></xsl:attribute><xsl:value-of select="/rss/channel/atom:link/@href"/></a>) can be used in any RSS reader.</p>
<ol class="rss">
<xsl:for-each select="/rss/channel/item">
<li>