diff --git a/test/OPDSTest.php b/test/OPDSTest.php index 55b40b0..336ea7d 100644 --- a/test/OPDSTest.php +++ b/test/OPDSTest.php @@ -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; diff --git a/test/OPDSValidator.jar b/test/OPDSValidator.jar new file mode 100644 index 0000000..3fe7b33 Binary files /dev/null and b/test/OPDSValidator.jar differ diff --git a/test/res/atom.rnc b/test/res/atom.rnc new file mode 100644 index 0000000..15ce284 --- /dev/null +++ b/test/res/atom.rnc @@ -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 diff --git a/test/res/opds_v1.0.rnc b/test/res/opds_v1.0.rnc new file mode 100644 index 0000000..7d90e5e --- /dev/null +++ b/test/res/opds_v1.0.rnc @@ -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" +) diff --git a/test/res/opds_v1.1.rnc b/test/res/opds_v1.1.rnc new file mode 100644 index 0000000..9a0778f --- /dev/null +++ b/test/res/opds_v1.1.rnc @@ -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" +) \ No newline at end of file