@@ -13,6 +13,7 @@ require_once (dirname(__FILE__) . "/../OPDS_renderer.php"); | |||
define ("OPDS_RELAX_NG", dirname(__FILE__) . "/opds-relax-ng/opds_catalog_1_1.rng"); | |||
define ("OPENSEARCHDESCRIPTION_RELAX_NG", dirname(__FILE__) . "/opds-relax-ng/opensearchdescription.rng"); | |||
define ("JING_JAR", dirname(__FILE__) . "/jing.jar"); | |||
define ("OPDSVALIDATOR_JAR", dirname(__FILE__) . "/OPDSValidator.jar"); | |||
define ("TEST_FEED", dirname(__FILE__) . "/text.atom"); | |||
class OpdsTest extends PHPUnit_Framework_TestCase | |||
@@ -25,7 +26,7 @@ class OpdsTest extends PHPUnit_Framework_TestCase | |||
unlink (TEST_FEED); | |||
} | |||
function opdsValidateSchema($feed, $relax = OPDS_RELAX_NG) { | |||
function jingValidateSchema($feed, $relax = OPDS_RELAX_NG) { | |||
$path = ""; | |||
if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') { | |||
// huge hack, not proud about it | |||
@@ -39,7 +40,26 @@ class OpdsTest extends PHPUnit_Framework_TestCase | |||
return true; | |||
} | |||
function opdsValidator($feed) { | |||
$oldcwd = getcwd(); // Save the old working directory | |||
chdir("test"); | |||
$path = ""; | |||
if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') { | |||
// huge hack, not proud about it | |||
$path = "c:\\Progra~1\\Java\\jre7\\bin\\"; | |||
} | |||
$res = system($path . 'java -jar ' . OPDSVALIDATOR_JAR . ' ' . $feed); | |||
chdir($oldcwd); | |||
if ($res != '') { | |||
echo 'OPDS validation error: '.$res; | |||
return false; | |||
} else | |||
return true; | |||
} | |||
function opdsCompleteValidation ($feed) { | |||
return $this->jingValidateSchema($feed) && $this->opdsValidator($feed); | |||
} | |||
public function testPageIndex () | |||
{ | |||
@@ -58,13 +78,38 @@ class OpdsTest extends PHPUnit_Framework_TestCase | |||
$OPDSRender = new OPDSRenderer (); | |||
file_put_contents (TEST_FEED, $OPDSRender->render ($currentPage)); | |||
$this->AssertTrue ($this->opdsValidateSchema (TEST_FEED)); | |||
file_put_contents (TEST_FEED, str_replace ("id>", "ido>", $OPDSRender->render ($currentPage))); | |||
$this->AssertFalse ($this->opdsValidateSchema (TEST_FEED)); | |||
$this->AssertTrue ($this->opdsCompleteValidation (TEST_FEED)); | |||
$_SERVER ["HTTP_USER_AGENT"] = "XXX"; | |||
$config['cops_generate_invalid_opds_stream'] = "1"; | |||
file_put_contents (TEST_FEED, $OPDSRender->render ($currentPage)); | |||
$this->AssertFalse ($this->jingValidateSchema (TEST_FEED)); | |||
$this->AssertFalse ($this->opdsValidator (TEST_FEED)); | |||
$_SERVER['QUERY_STRING'] = NULL; | |||
} | |||
public function testPageIndexMultipleDatabase () | |||
{ | |||
global $config; | |||
$config['calibre_directory'] = array ("Some books" => dirname(__FILE__) . "/BaseWithSomeBooks/", | |||
"One book" => dirname(__FILE__) . "/BaseWithOneBook/"); | |||
$page = Base::PAGE_INDEX; | |||
$query = NULL; | |||
$qid = "1"; | |||
$n = "1"; | |||
$_SERVER['QUERY_STRING'] = ""; | |||
$currentPage = Page::getPage ($page, $qid, $query, $n); | |||
$currentPage->InitializeContent (); | |||
$OPDSRender = new OPDSRenderer (); | |||
file_put_contents (TEST_FEED, $OPDSRender->render ($currentPage)); | |||
$this->AssertTrue ($this->opdsCompleteValidation (TEST_FEED)); | |||
} | |||
public function testOpenSearchDescription () | |||
{ | |||
$_SERVER['QUERY_STRING'] = ""; | |||
@@ -72,12 +117,12 @@ class OpdsTest extends PHPUnit_Framework_TestCase | |||
$OPDSRender = new OPDSRenderer (); | |||
file_put_contents (TEST_FEED, $OPDSRender->getOpenSearch ()); | |||
$this->AssertTrue ($this->opdsValidateSchema (TEST_FEED, OPENSEARCHDESCRIPTION_RELAX_NG)); | |||
$this->AssertTrue ($this->jingValidateSchema (TEST_FEED, OPENSEARCHDESCRIPTION_RELAX_NG)); | |||
$_SERVER['QUERY_STRING'] = NULL; | |||
} | |||
public function testPageIndexMultipleDatabase () | |||
public function testPageAuthorMultipleDatabase () | |||
{ | |||
global $config; | |||
$config['calibre_directory'] = array ("Some books" => dirname(__FILE__) . "/BaseWithSomeBooks/", | |||
@@ -95,7 +140,7 @@ class OpdsTest extends PHPUnit_Framework_TestCase | |||
$OPDSRender = new OPDSRenderer (); | |||
file_put_contents (TEST_FEED, $OPDSRender->render ($currentPage)); | |||
$this->AssertTrue ($this->opdsValidateSchema (TEST_FEED)); | |||
$this->AssertTrue ($this->opdsCompleteValidation (TEST_FEED)); | |||
} | |||
public function testPageAuthorsDetail () | |||
@@ -118,7 +163,7 @@ class OpdsTest extends PHPUnit_Framework_TestCase | |||
$OPDSRender = new OPDSRenderer (); | |||
file_put_contents (TEST_FEED, $OPDSRender->render ($currentPage)); | |||
$this->AssertTrue ($this->opdsValidateSchema (TEST_FEED)); | |||
$this->AssertTrue ($this->opdsCompleteValidation (TEST_FEED)); | |||
// Second page | |||
@@ -129,7 +174,7 @@ class OpdsTest extends PHPUnit_Framework_TestCase | |||
$OPDSRender = new OPDSRenderer (); | |||
file_put_contents (TEST_FEED, $OPDSRender->render ($currentPage)); | |||
$this->AssertTrue ($this->opdsValidateSchema (TEST_FEED)); | |||
$this->AssertTrue ($this->opdsCompleteValidation (TEST_FEED)); | |||
// No pagination | |||
$config['cops_max_item_per_page'] = -1; | |||
@@ -0,0 +1,338 @@ | |||
# -*- rnc -*- | |||
# RELAX NG Compact Syntax Grammar for the | |||
# Atom Format Specification Version 11 | |||
namespace atom = "http://www.w3.org/2005/Atom" | |||
namespace xhtml = "http://www.w3.org/1999/xhtml" | |||
namespace s = "http://www.ascc.net/xml/schematron" | |||
namespace local = "" | |||
start = atomFeed | atomEntry | |||
# Common attributes | |||
atomCommonAttributes = | |||
attribute xml:base { atomUri }?, | |||
attribute xml:lang { atomLanguageTag }?, | |||
undefinedAttribute* | |||
# Text Constructs | |||
atomPlainTextConstruct = | |||
atomCommonAttributes, | |||
attribute type { "text" | "html" }?, | |||
text | |||
atomXHTMLTextConstruct = | |||
atomCommonAttributes, | |||
attribute type { "xhtml" }, | |||
xhtmlDiv | |||
atomTextConstruct = atomPlainTextConstruct | atomXHTMLTextConstruct | |||
# Person Construct | |||
atomPersonConstruct = | |||
atomCommonAttributes, | |||
(element atom:name { text } | |||
& element atom:uri { atomUri }? | |||
& element atom:email { atomEmailAddress }? | |||
& extensionElement*) | |||
# Date Construct | |||
atomDateConstruct = | |||
atomCommonAttributes, | |||
xsd:dateTime | |||
# atom:feed | |||
atomFeed = | |||
[ | |||
s:rule [ | |||
context = "atom:feed" | |||
s:assert [ | |||
test = "atom:author or not(atom:entry[not(atom:author)])" | |||
"An atom:feed must have an atom:author unless all " | |||
~ "of its atom:entry children have an atom:author." | |||
] | |||
] | |||
] | |||
element atom:feed { | |||
atomCommonAttributes, | |||
(atomAuthor* | |||
& atomCategory* | |||
& atomContributor* | |||
& atomGenerator? | |||
& atomIcon? | |||
& atomId | |||
& atomLink* | |||
& atomLogo? | |||
& atomRights? | |||
& atomSubtitle? | |||
& atomTitle | |||
& atomUpdated | |||
& extensionElement*), | |||
atomEntry* | |||
} | |||
# atom:entry | |||
atomEntry = | |||
[ | |||
s:rule [ | |||
context = "atom:entry" | |||
s:assert [ | |||
test = "atom:link[@rel='alternate'] " | |||
~ "or atom:link[not(@rel)] " | |||
~ "or atom:content" | |||
"An atom:entry must have at least one atom:link element " | |||
~ "with a rel attribute of 'alternate' " | |||
~ "or an atom:content." | |||
] | |||
] | |||
s:rule [ | |||
context = "atom:entry" | |||
s:assert [ | |||
test = "atom:author or " | |||
~ "../atom:author or atom:source/atom:author" | |||
"An atom:entry must have an atom:author " | |||
~ "if its feed does not." | |||
] | |||
] | |||
] | |||
element atom:entry { | |||
atomCommonAttributes, | |||
(atomAuthor* | |||
& atomCategory* | |||
& atomContent? | |||
& atomContributor* | |||
& atomId | |||
& atomLink* | |||
& atomPublished? | |||
& atomRights? | |||
& atomSource? | |||
& atomSummary? | |||
& atomTitle | |||
& atomUpdated | |||
& extensionElement*) | |||
} | |||
# atom:content | |||
atomInlineTextContent = | |||
element atom:content { | |||
atomCommonAttributes, | |||
attribute type { "text" | "html" }?, | |||
(text)* | |||
} | |||
atomInlineXHTMLContent = | |||
element atom:content { | |||
atomCommonAttributes, | |||
attribute type { "xhtml" }, | |||
xhtmlDiv | |||
} | |||
atomInlineOtherContent = | |||
element atom:content { | |||
atomCommonAttributes, | |||
attribute type { atomMediaType }?, | |||
(text|anyElement)* | |||
} | |||
atomOutOfLineContent = | |||
element atom:content { | |||
atomCommonAttributes, | |||
attribute type { atomMediaType }?, | |||
attribute src { atomUri }, | |||
empty | |||
} | |||
atomContent = atomInlineTextContent | |||
| atomInlineXHTMLContent | |||
| atomInlineOtherContent | |||
| atomOutOfLineContent | |||
# atom:author | |||
atomAuthor = element atom:author { atomPersonConstruct } | |||
# atom:category | |||
atomCategory = | |||
element atom:category { | |||
atomCommonAttributes, | |||
attribute term { text }, | |||
attribute scheme { atomUri }?, | |||
attribute label { text }?, | |||
undefinedContent | |||
} | |||
# atom:contributor | |||
atomContributor = element atom:contributor { atomPersonConstruct } | |||
# atom:generator | |||
atomGenerator = element atom:generator { | |||
atomCommonAttributes, | |||
attribute uri { atomUri }?, | |||
attribute version { text }?, | |||
text | |||
} | |||
# atom:icon | |||
atomIcon = element atom:icon { | |||
atomCommonAttributes, | |||
(atomUri) | |||
} | |||
# atom:id | |||
atomId = element atom:id { | |||
atomCommonAttributes, | |||
(atomUri) | |||
} | |||
# atom:logo | |||
atomLogo = element atom:logo { | |||
atomCommonAttributes, | |||
(atomUri) | |||
} | |||
# atom:link | |||
atomLink = | |||
element atom:link { | |||
atomCommonAttributes, | |||
attribute href { atomUri }, | |||
attribute rel { atomNCName | atomUri }?, | |||
attribute type { atomMediaType }?, | |||
attribute hreflang { atomLanguageTag }?, | |||
attribute title { text }?, | |||
attribute length { text }?, | |||
undefinedContent | |||
} | |||
# atom:published | |||
atomPublished = element atom:published { atomDateConstruct } | |||
# atom:rights | |||
atomRights = element atom:rights { atomTextConstruct } | |||
# atom:source | |||
atomSource = | |||
element atom:source { | |||
atomCommonAttributes, | |||
(atomAuthor* | |||
& atomCategory* | |||
& atomContributor* | |||
& atomGenerator? | |||
& atomIcon? | |||
& atomId? | |||
& atomLink* | |||
& atomLogo? | |||
& atomRights? | |||
& atomSubtitle? | |||
& atomTitle? | |||
& atomUpdated? | |||
& extensionElement*) | |||
} | |||
# atom:subtitle | |||
atomSubtitle = element atom:subtitle { atomTextConstruct } | |||
# atom:summary | |||
atomSummary = element atom:summary { atomTextConstruct } | |||
# atom:title | |||
atomTitle = element atom:title { atomTextConstruct } | |||
# atom:updated | |||
atomUpdated = element atom:updated { atomDateConstruct } | |||
# Low-level simple types | |||
atomNCName = xsd:string { minLength = "1" pattern = "[^:]*" } | |||
# Whatever a media type is, it contains at least one slash | |||
atomMediaType = xsd:string { pattern = ".+/.+" } | |||
# As defined in RFC 3066 | |||
atomLanguageTag = xsd:string { | |||
pattern = "[A-Za-z]{1,8}(-[A-Za-z0-9]{1,8})*" | |||
} | |||
# Unconstrained; it's not entirely clear how IRI fit into | |||
# xsd:anyURI so let's not try to constrain it here | |||
atomUri = text | |||
# Whatever an email address is, it contains at least one @ | |||
atomEmailAddress = xsd:string { pattern = ".+@.+" } | |||
# Simple Extension | |||
simpleExtensionElement = | |||
element * - atom:* { | |||
text | |||
} | |||
# Structured Extension | |||
structuredExtensionElement = | |||
element * - atom:* { | |||
(attribute * { text }+, | |||
(text|anyElement)*) | |||
| (attribute * { text }*, | |||
(text?, anyElement+, (text|anyElement)*)) | |||
} | |||
# Other Extensibility | |||
extensionElement = | |||
simpleExtensionElement | structuredExtensionElement | |||
undefinedAttribute = | |||
attribute * - (xml:base | xml:lang | local:*) { text } | |||
undefinedContent = (text|anyForeignElement)* | |||
anyElement = | |||
element * { | |||
(attribute * { text } | |||
| text | |||
| anyElement)* | |||
} | |||
anyForeignElement = | |||
element * - atom:* { | |||
(attribute * { text } | |||
| text | |||
| anyElement)* | |||
} | |||
# XHTML | |||
anyXHTML = element xhtml:* { | |||
(attribute * { text } | |||
| text | |||
| anyXHTML)* | |||
} | |||
xhtmlDiv = element xhtml:div { | |||
(attribute * { text } | |||
| text | |||
| anyXHTML)* | |||
} | |||
# EOF |
@@ -0,0 +1,131 @@ | |||
# -*- rnc -*- | |||
# RELAX NG Compact Syntax Grammar for OPDS Catalog Feed & Entry Documents | |||
# Version 2010-08-18 | |||
namespace atom = "http://www.w3.org/2005/Atom" | |||
namespace opds = "http://opds-spec.org/2010/catalog" | |||
namespace local = "" | |||
# The OPDS Catalog spec extends Atom (RFC4287), and the additions require some | |||
# patterns not used in the Atom schema. The first is atomUriExceptOPDS, which | |||
# is used to describe an atomLink whose rel value is an atomNCName (no-colon | |||
# name) or any URI other than these from OPDS Catalogs. In these cases, no | |||
# opds:price element should appear. | |||
atomUriExceptOPDS = string - ( string "http://opds-spec.org/acquisition/buy" | |||
| string "http://opds-spec.org/acquisition/borrow" | |||
| string "http://opds-spec.org/acquisition/subscribe" | |||
| string "http://opds-spec.org/acquisition/sample" ) | |||
# Next is OPDSUrisExceptBuy, which is used to describe an atomLink whose | |||
# rel value is from OPDS Catalogs but is not ".../acquisition/buy". In such | |||
# cases, an opds:price element is optional. | |||
OPDSUrisExceptBuy = string "http://opds-spec.org/acquisition/borrow" | |||
| string "http://opds-spec.org/acquisition/subscribe" | |||
| string "http://opds-spec.org/acquisition/sample" | |||
# To simplify OPDS Catalog validation, we do not use Schematron to assert that | |||
# any atom:link with a rel value of ".../acquisition/buy" must be accompanied | |||
# by one or more opds:price elements. | |||
# Instead we rely on Relax NG to describe one of three situations: | |||
# - the rel value is ".../acquisition/buy" and at least one opds:price element | |||
# is required | |||
# - the rel value is ".../acquisition/borrow" or ".../acquisition/subscribe" or | |||
# ".../acquisition/sample", in case opds:price elements may be | |||
# included; or | |||
# - the value of the rel attribute is any other URI or an Atom-defined no-colon | |||
# name, and no opds:price element is permitted | |||
# Note that this OPDS Catalog schema includes atom.rnc, so that schema must be | |||
# present for validation. | |||
# | |||
# Note also that atom.rnc defines atomUri as text and not as xsd:anyURI, and so | |||
# wherever the Atom spec requires an IRI, the schema will not check the value | |||
# against any URI pattern or logic. The OPDS Catalog schema overrides atom.rnc | |||
# to provide a relatively accurate test. With the approval of XSD 1.1, the | |||
# schema definition should change to xsd:anyURI to match what the spec text | |||
# says. | |||
include "atom.rnc" { | |||
atomLink = | |||
element atom:link { | |||
atomCommonAttributes & | |||
attribute href { atomUri } & | |||
attribute type { atomMediaType }? & | |||
attribute hreflang { atomLanguageTag }? & | |||
attribute title { text }? & | |||
attribute length { text }? & | |||
((attribute rel { "http://opds-spec.org/acquisition/buy" }, opdsPrice+ ) | |||
| | |||
(attribute rel { OPDSUrisExceptBuy }, opdsPrice*) | |||
| | |||
(attribute rel { atomNCName | ( atomUriExceptOPDS ) } ))? & | |||
anyOPDSForeignElement* & | |||
text | |||
} | |||
# Here is where OPDS Catalogs use John Cowan's pragmatic evaluation of an | |||
# IRI. This modifies xsd:anyURI in XSD 1.0 to exclude ASCII characters not | |||
# valid in 1.1 or IRI's without being escaped. This matches the OPDS and Atom | |||
# specs, but not the non-normative atom.rnc. | |||
atomUri = xsd:anyURI - xsd:string {pattern = '.*[ <>{}|^`"\\\n\r\t].*'} | |||
# Here we override Atom to account for HTML abuse in the summary element, | |||
# restricting it in OPDS Catalog to text: | |||
atomSummary = | |||
element atom:summary { | |||
atomCommonAttributes, | |||
attribute type { "text" }?, | |||
text | |||
} | |||
} | |||
anyOPDSForeignElement = | |||
element * - ( atom:* | opds:* ) { | |||
( attribute * { text } | |||
| text | |||
| anyElement )* | |||
} | |||
# An opds:price element should not contain a currency symbol; it is | |||
# restricted to non-negative decimal numbers. | |||
opdsPrice = | |||
element opds:price { | |||
atomCommonAttributes, | |||
attribute currencycode { opdsPriceCurrencyCode }, | |||
xsd:decimal { minInclusive="0.0" } | |||
} | |||
# Instead of allowing every possible 3-letter or 3-digit combination as a | |||
# currency code, here the permissible codes (as identified in ISO4217 as of | |||
# 2010-08-25) are enumerated. In 2012 or so, that standard may add, remove or | |||
# change some currency codes, thus requiring this schema to be updated. Note | |||
# that codes for metals and funds are not included. | |||
opdsPriceCurrencyCode = ( | |||
"AED" | "AFN" | "ALL" | "AMD" | "ANG" | "AOA" | "ARS" | "AUD" | "AWG" | "AZN" | "BAM" | "BBD" | "BDT" | | |||
"BGN" | "BHD" | "BIF" | "BMD" | "BND" | "BOB" | "BOV" | "BRL" | "BSD" | "BTN" | "BWP" | "BYR" | "BZD" | | |||
"CAD" | "CDF" | "CHE" | "CHF" | "CHW" | "CLF" | "CLP" | "CNY" | "COP" | "COU" | "CRC" | "CUC" | "CUP" | | |||
"CVE" | "CZK" | "DJF" | "DKK" | "DOP" | "DZD" | "EEK" | "EGP" | "ERN" | "ETB" | "EUR" | "FJD" | "FKP" | | |||
"GBP" | "GEL" | "GHS" | "GIP" | "GMD" | "GNF" | "GTQ" | "GYD" | "HKD" | "HNL" | "HRK" | "HTG" | "HUF" | | |||
"IDR" | "ILS" | "INR" | "IQD" | "IRR" | "ISK" | "JMD" | "JOD" | "JPY" | "KES" | "KGS" | "KHR" | "KMF" | | |||
"KPW" | "KRW" | "KWD" | "KYD" | "KZT" | "LAK" | "LBP" | "LKR" | "LRD" | "LSL" | "LTL" | "LVL" | "LYD" | | |||
"MAD" | "MDL" | "MGA" | "MKD" | "MMK" | "MNT" | "MOP" | "MRO" | "MUR" | "MVR" | "MWK" | "MXN" | "MXV" | | |||
"MYR" | "MZN" | "NAD" | "NGN" | "NIO" | "NOK" | "NPR" | "NZD" | "OMR" | "PAB" | "PEN" | "PGK" | "PHP" | | |||
"PKR" | "PLN" | "PYG" | "QAR" | "RON" | "RSD" | "RUB" | "RWF" | "SAR" | "SBD" | "SCR" | "SDG" | "SEK" | | |||
"SGD" | "SHP" | "SLL" | "SOS" | "SRD" | "STD" | "SVC" | "SYP" | "SZL" | "THB" | "TJS" | "TMT" | "TND" | | |||
"TOP" | "TRY" | "TTD" | "TWD" | "TZS" | "UAH" | "UGX" | "USD" | "USN" | "USS" | "UYI" | "UYU" | "UZS" | | |||
"VEF" | "VND" | "VUV" | "WST" | "XAF" | "XAG" | "XAU" | "XBA" | "XBB" | "XBC" | "XBD" | "XCD" | "XDR" | | |||
"XFU" | "XOF" | "XPD" | "XPF" | "XPT" | "XTS" | "XXX" | "YER" | "ZAR" | "ZMK" | "ZWL" | "008" | "012" | | |||
"032" | "036" | "044" | "048" | "050" | "051" | "052" | "060" | "064" | "068" | "072" | "084" | "090" | | |||
"096" | "104" | "108" | "116" | "124" | "132" | "136" | "144" | "152" | "156" | "170" | "174" | "188" | | |||
"191" | "192" | "203" | "208" | "214" | "222" | "230" | "232" | "233" | "238" | "242" | "262" | "270" | | |||
"292" | "320" | "324" | "328" | "332" | "340" | "344" | "348" | "352" | "356" | "360" | "364" | "368" | | |||
"376" | "388" | "392" | "398" | "400" | "404" | "408" | "410" | "414" | "417" | "418" | "422" | "426" | | |||
"428" | "430" | "434" | "440" | "446" | "454" | "458" | "462" | "478" | "480" | "484" | "496" | "498" | | |||
"504" | "512" | "516" | "524" | "532" | "533" | "548" | "554" | "558" | "566" | "578" | "586" | "590" | | |||
"598" | "600" | "604" | "608" | "634" | "643" | "646" | "654" | "678" | "682" | "690" | "694" | "702" | | |||
"704" | "706" | "710" | "748" | "752" | "756" | "760" | "764" | "776" | "780" | "784" | "788" | "800" | | |||
"807" | "818" | "826" | "834" | "840" | "858" | "860" | "882" | "886" | "894" | "901" | "931" | "932" | | |||
"934" | "936" | "937" | "938" | "940" | "941" | "943" | "944" | "946" | "947" | "948" | "949" | "950" | | |||
"951" | "952" | "953" | "955" | "956" | "957" | "958" | "959" | "960" | "961" | "962" | "963" | "964" | | |||
"968" | "969" | "970" | "971" | "972" | "973" | "974" | "975" | "976" | "977" | "978" | "979" | "980" | | |||
"981" | "984" | "985" | "986" | "990" | "997" | "998" | "999" | |||
) |
@@ -0,0 +1,151 @@ | |||
# -*- rnc -*- | |||
# RELAX NG Compact Syntax Grammar for OPDS Catalog Feed & Entry Documents | |||
# Version 2010-08-18 | |||
namespace atom = "http://www.w3.org/2005/Atom" | |||
namespace opds = "http://opds-spec.org/2010/catalog" | |||
namespace local = "" | |||
# The OPDS Catalog spec extends Atom (RFC4287), and the additions require some | |||
# patterns not used in the Atom schema. The first is atomUriExceptOPDS, which | |||
# is used to describe an atomLink whose rel value is an atomNCName (no-colon | |||
# name) or any URI other than these from OPDS Catalogs. In these cases, no | |||
# opds:price element should appear. | |||
atomUriExceptOPDS = string - ( string "http://opds-spec.org/acquisition/buy" | |||
| string "http://opds-spec.org/acquisition/borrow" | |||
| string "http://opds-spec.org/acquisition/subscribe" | |||
| string "http://opds-spec.org/acquisition/sample" ) | |||
# Next is OPDSUrisExceptBuy, which is used to describe an atomLink whose | |||
# rel value is from OPDS Catalogs but is not ".../acquisition/buy". In such | |||
# cases, an opds:price element is optional. | |||
OPDSUrisExceptBuy = string "http://opds-spec.org/acquisition/borrow" | |||
| string "http://opds-spec.org/acquisition/subscribe" | |||
| string "http://opds-spec.org/acquisition/sample" | |||
# To simplify OPDS Catalog validation, we do not use Schematron to assert that | |||
# any atom:link with a rel value of ".../acquisition/buy" must be accompanied | |||
# by one or more opds:price elements. | |||
# Instead we rely on Relax NG to describe one of three situations: | |||
# - the rel value is ".../acquisition/buy" and at least one opds:price element | |||
# is required | |||
# - the rel value is ".../acquisition/borrow" or ".../acquisition/subscribe" or | |||
# ".../acquisition/sample", in case opds:price elements may be | |||
# included; or | |||
# - the value of the rel attribute is any other URI or an Atom-defined no-colon | |||
# name, and no opds:price element is permitted | |||
# Note that this OPDS Catalog schema includes atom.rnc, so that schema must be | |||
# present for validation. | |||
# | |||
# Note also that atom.rnc defines atomUri as text and not as xsd:anyURI, and so | |||
# wherever the Atom spec requires an IRI, the schema will not check the value | |||
# against any URI pattern or logic. The OPDS Catalog schema overrides atom.rnc | |||
# to provide a relatively accurate test. With the approval of XSD 1.1, the | |||
# schema definition should change to xsd:anyURI to match what the spec text | |||
# says. | |||
include "atom.rnc" { | |||
undefinedAttribute = | |||
attribute * - (xml:base | xml:lang | local:*| opds:* ) { text } | |||
atomLink = | |||
element atom:link { | |||
atomCommonAttributes , | |||
attribute href { atomUri }, | |||
attribute type { atomMediaType }? , | |||
attribute hreflang { atomLanguageTag }? , | |||
attribute title { text }? , | |||
attribute length { text }? , | |||
((attribute rel { "http://opds-spec.org/facet" }, (attribute opds:facetGroup { text }? & attribute opds:activeFacet { "true" }? )) | |||
| | |||
(attribute rel { "http://opds-spec.org/acquisition/buy" }, opdsPrice+ ) | |||
| | |||
(attribute rel { OPDSUrisExceptBuy }, opdsPrice*) | |||
| | |||
(attribute rel { atomNCName | ( atomUriExceptOPDS ) } ))? , | |||
(opdsIndirectAcquisition | | |||
anyOPDSForeignElement | | |||
text)* | |||
} | |||
# Here is where OPDS Catalogs use John Cowan's pragmatic evaluation of an | |||
# IRI. This modifies xsd:anyURI in XSD 1.0 to exclude ASCII characters not | |||
# valid in 1.1 or IRI's without being escaped. This matches the OPDS and Atom | |||
# specs, but not the non-normative atom.rnc. | |||
atomUri = xsd:anyURI - xsd:string {pattern = '.*[ <>{}|^`"\\\n\r\t].*'} | |||
# Here we override Atom to account for HTML abuse in the summary element, | |||
# restricting it in OPDS Catalog to text: | |||
atomSummary = | |||
element atom:summary { | |||
atomCommonAttributes, | |||
attribute type { "text" }?, | |||
text | |||
} | |||
} | |||
anyOPDSForeignElement = | |||
element * - ( atom:* | opds:* ) { | |||
( attribute * { text } | |||
| text | |||
| anyElement )* | |||
} | |||
# An opds:indirectAcquisition should use strictly MIME media type for | |||
#its type attribute | |||
opdsIndirectAcquisition = | |||
element opds:indirectAcquisition { | |||
atomCommonAttributes, | |||
attribute type { atomMediaType }, | |||
( anyOPDSForeignElement | | |||
opdsIndirectAcquisition) * | |||
} | |||
# An opds:price element should not contain a currency symbol; it is | |||
# restricted to non-negative decimal numbers. | |||
opdsPrice = | |||
element opds:price { | |||
atomCommonAttributes, | |||
attribute currencycode { opdsPriceCurrencyCode }, | |||
xsd:decimal { minInclusive="0.0" } | |||
} | |||
# Instead of allowing every possible 3-letter or 3-digit combination as a | |||
# currency code, here the permissible codes (as identified in ISO4217 as of | |||
# 2010-08-25) are enumerated. In 2012 or so, that standard may add, remove or | |||
# change some currency codes, thus requiring this schema to be updated. Note | |||
# that codes for metals and funds are not included. | |||
opdsPriceCurrencyCode = ( | |||
"AED" | "AFN" | "ALL" | "AMD" | "ANG" | "AOA" | "ARS" | "AUD" | "AWG" | "AZN" | "BAM" | "BBD" | "BDT" | | |||
"BGN" | "BHD" | "BIF" | "BMD" | "BND" | "BOB" | "BOV" | "BRL" | "BSD" | "BTN" | "BWP" | "BYR" | "BZD" | | |||
"CAD" | "CDF" | "CHE" | "CHF" | "CHW" | "CLF" | "CLP" | "CNY" | "COP" | "COU" | "CRC" | "CUC" | "CUP" | | |||
"CVE" | "CZK" | "DJF" | "DKK" | "DOP" | "DZD" | "EEK" | "EGP" | "ERN" | "ETB" | "EUR" | "FJD" | "FKP" | | |||
"GBP" | "GEL" | "GHS" | "GIP" | "GMD" | "GNF" | "GTQ" | "GYD" | "HKD" | "HNL" | "HRK" | "HTG" | "HUF" | | |||
"IDR" | "ILS" | "INR" | "IQD" | "IRR" | "ISK" | "JMD" | "JOD" | "JPY" | "KES" | "KGS" | "KHR" | "KMF" | | |||
"KPW" | "KRW" | "KWD" | "KYD" | "KZT" | "LAK" | "LBP" | "LKR" | "LRD" | "LSL" | "LTL" | "LVL" | "LYD" | | |||
"MAD" | "MDL" | "MGA" | "MKD" | "MMK" | "MNT" | "MOP" | "MRO" | "MUR" | "MVR" | "MWK" | "MXN" | "MXV" | | |||
"MYR" | "MZN" | "NAD" | "NGN" | "NIO" | "NOK" | "NPR" | "NZD" | "OMR" | "PAB" | "PEN" | "PGK" | "PHP" | | |||
"PKR" | "PLN" | "PYG" | "QAR" | "RON" | "RSD" | "RUB" | "RWF" | "SAR" | "SBD" | "SCR" | "SDG" | "SEK" | | |||
"SGD" | "SHP" | "SLL" | "SOS" | "SRD" | "STD" | "SVC" | "SYP" | "SZL" | "THB" | "TJS" | "TMT" | "TND" | | |||
"TOP" | "TRY" | "TTD" | "TWD" | "TZS" | "UAH" | "UGX" | "USD" | "USN" | "USS" | "UYI" | "UYU" | "UZS" | | |||
"VEF" | "VND" | "VUV" | "WST" | "XAF" | "XAG" | "XAU" | "XBA" | "XBB" | "XBC" | "XBD" | "XCD" | "XDR" | | |||
"XFU" | "XOF" | "XPD" | "XPF" | "XPT" | "XTS" | "XXX" | "YER" | "ZAR" | "ZMK" | "ZWL" | "008" | "012" | | |||
"032" | "036" | "044" | "048" | "050" | "051" | "052" | "060" | "064" | "068" | "072" | "084" | "090" | | |||
"096" | "104" | "108" | "116" | "124" | "132" | "136" | "144" | "152" | "156" | "170" | "174" | "188" | | |||
"191" | "192" | "203" | "208" | "214" | "222" | "230" | "232" | "233" | "238" | "242" | "262" | "270" | | |||
"292" | "320" | "324" | "328" | "332" | "340" | "344" | "348" | "352" | "356" | "360" | "364" | "368" | | |||
"376" | "388" | "392" | "398" | "400" | "404" | "408" | "410" | "414" | "417" | "418" | "422" | "426" | | |||
"428" | "430" | "434" | "440" | "446" | "454" | "458" | "462" | "478" | "480" | "484" | "496" | "498" | | |||
"504" | "512" | "516" | "524" | "532" | "533" | "548" | "554" | "558" | "566" | "578" | "586" | "590" | | |||
"598" | "600" | "604" | "608" | "634" | "643" | "646" | "654" | "678" | "682" | "690" | "694" | "702" | | |||
"704" | "706" | "710" | "748" | "752" | "756" | "760" | "764" | "776" | "780" | "784" | "788" | "800" | | |||
"807" | "818" | "826" | "834" | "840" | "858" | "860" | "882" | "886" | "894" | "901" | "931" | "932" | | |||
"934" | "936" | "937" | "938" | "940" | "941" | "943" | "944" | "946" | "947" | "948" | "949" | "950" | | |||
"951" | "952" | "953" | "955" | "956" | "957" | "958" | "959" | "960" | "961" | "962" | "963" | "964" | | |||
"968" | "969" | "970" | "971" | "972" | "973" | "974" | "975" | "976" | "977" | "978" | "979" | "980" | | |||
"981" | "984" | "985" | "986" | "990" | "997" | "998" | "999" | |||
) |