Repository: w3c/epubcheck
Pull request #1569 (2024-07-15 11:11:14)

chore: bump GitHub Action versions
`actions/checkout@v2` and `actions/cache@v2` currently issue deprecation warnings during execution about deprecated Node.js version. 

> The following actions uses node12 which is deprecated and will be forced to run on node16: actions/checkout@v2, actions/cache@v2. For more info: https://github.blog/changelog/2023-06-13-github-actions-all-actions-will-run-on-node16-instead-of-node12-by-default/

> The following actions uses Node.js version which is deprecated and will be forced to run on node20: actions/checkout@v2, actions/cache@v2. For more info: https://github.blog/changelog/2024-03-07-github-actions-all-actions-will-run-on-node20-instead-of-node16-by-default/


Patch:
diff --git a/.github/workflows/maven-ci.yml b/.github/workflows/maven-ci.yml
index 1886df10..8fa685bb 100644
--- a/.github/workflows/maven-ci.yml
+++ b/.github/workflows/maven-ci.yml
@@ -4,7 +4,6 @@ on:
   push:
     branches: [ main ]
   pull_request:
-    branches: [ main ]
 
 jobs:
 
@@ -16,7 +15,7 @@ jobs:
     runs-on: ubuntu-latest
     steps:
     - name: "Checkout sources"
-      uses: actions/checkout@v2
+      uses: actions/checkout@v4
     - name: "Set up Java"
       uses: actions/setup-java@v4
       with:
@@ -24,7 +23,7 @@ jobs:
         distribution: "temurin"
         cache: 'maven'
     - name: "Get Maven dependencies from cache"
-      uses: actions/cache@v2
+      uses: actions/cache@v4
       with:
         path: ~/.m2/repository
         key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
@@ -39,7 +38,7 @@ jobs:
     runs-on: ubuntu-latest
     steps:
     - name: "Checkout sources"
-      uses: actions/checkout@v2
+      uses: actions/checkout@v4
     - name: "Setup Java"
       uses: actions/setup-java@v4
       with:
@@ -49,7 +48,7 @@ jobs:
         server-username: OSSRH_USERNAME
         server-password: OSSRH_PASSWORD
     - name: "Get Maven dependencies from cache"
-      uses: actions/cache@v2
+      uses: actions/cache@v4
       with:
         path: ~/.m2/repository
         key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
diff --git a/Dockerfile b/Dockerfile
index a432cbc1..a7a953f6 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,5 +1,5 @@
 # build the epubcheck.jar file
-FROM maven:slim as builder
+FROM maven:slim AS builder
 
 WORKDIR /app
 COPY . .
diff --git a/pom.xml b/pom.xml
index a506f21e..16d3671a 100644
--- a/pom.xml
+++ b/pom.xml
@@ -145,6 +145,11 @@
             <artifactId>jackson-mapper-asl</artifactId>
             <version>1.9.12</version>
         </dependency>
+        <dependency>
+            <groupId>xerces</groupId>
+            <artifactId>xercesImpl</artifactId>
+            <version>2.12.2</version>
+        </dependency>
         <!-- ================= -->
         <!-- Test Dependencies -->
         <!-- ================= -->
@@ -165,12 +170,6 @@
             <artifactId>json-path-assert</artifactId>
             <version>2.8.0</version>
         </dependency>
-        <dependency>
-            <groupId>xmlunit</groupId>
-            <artifactId>xmlunit</artifactId>
-            <version>1.3</version>
-            <scope>test</scope>
-        </dependency>
         <dependency>
     	        <groupId>io.cucumber</groupId>
     	        <artifactId>cucumber-java</artifactId>
diff --git a/src/build/update-validator-schemas.sh b/src/build/update-validator-schemas.sh
index 62ff2fe2..665f53e3 100755
--- a/src/build/update-validator-schemas.sh
+++ b/src/build/update-validator-schemas.sh
@@ -35,7 +35,7 @@ LAST_UPDATE_FILE=$EPUBCHECK_DIR/src/main/resources/com/adobe/epubcheck/schema/30
 if [ ! -f "$LAST_UPDATE_FILE" ]; then
   error_exit "could not find the last commit sha-1; make sure you are in the EPUBCheck git repository"
 fi
-LAST_SHA1=`cat $LAST_UPDATE_FILE`
+SHA_LAST_UPDATE=`cat $LAST_UPDATE_FILE`
 
 ## Move to validator.nu repo
 echo "Moving to the validator.nu repository"
@@ -55,22 +55,23 @@ fi
 echo "Fetching new commits from remote '$UPSTREAM'"
 git fetch $UPSTREAM
 
-## Create patches since last update (to the `latest` tag)
-echo "Creating paches of changes since the last update"
-git format-patch -o $EPUBCHECK_DIR/ $LAST_SHA1..latest schema
+## Get the SHA-1 of the SHA_VALIDATOR_LATEST revision on upstream
+SHA_VALIDATOR_LATEST=`git rev-list -n 1 $UPSTREAM/main`
+echo $SHA_VALIDATOR_LATEST
 
-## Get the SHA-1 of the `latest` tag
-LATEST=`git rev-list -n 1 latest`
-echo $LATEST
+## Create patches since last update (to the `SHA_VALIDATOR_LATEST` tag)
+echo "Creating paches of changes since the last update"
+git format-patch -o $EPUBCHECK_DIR/ $SHA_LAST_UPDATE..$SHA_VALIDATOR_LATEST schema
 
 ## Move to EPUBCheck repo
 echo "Moving back to the EPUBCheck repository"
 cd "$EPUBCHECK_DIR" # let's move to the validator repo
 
 ## Edit the LAST_UPDATE file
-echo $LATEST > $LAST_UPDATE_FILE
+echo $SHA_VALIDATOR_LATEST > $LAST_UPDATE_FILE
 git add $LAST_UPDATE_FILE
 git ci -m "feat: update to latest schemas from the Nu HTML Checker"
+SHA_BEFORE_PATCH=`git rev-list -n 1 HEAD`
 
 ## Update the paths in patches to the EPUBCheck location
 echo "Updating paths in patch files"
@@ -80,10 +81,30 @@ sed -i "" -E  "/^(diff|---|\+\+\+) / s:/schema/:/src/main/resources/com/adobe/ep
 echo "Applying the patches"
 git am *.patch
 
-## FROM HERE ON, THE INTERACTIVE APPLICATION OF PATCHES MAY EXIT THE SCRIPT
-
-# Remaing tasks:
-# - commit new last commit ID
-# - squash commits (add "Co-Authored by" credits)
-#   `git log --pretty=format:"---------------------%n%n%s%n%nhttps://github.com/validator/validator/commit/%h%n%nCo-authored-by: %aN <%aE>%n" $LAST-SHA1..latest schema``
-# - remove patch files
\ No newline at end of file
+###
+echo ""
+echo "WARNING! From here on, the interactive application of patches may exit the script."
+echo ""
+###
+
+SHA_AFTER_PATCH=`git rev-list -n 1 HEAD`
+
+## Build a commit message
+## - with proper credits to original committers
+## - replacing issue references to the validator repo
+echo "Creating the consolidated commit message"
+printf "feat: update to latest schemas from the Nu HTML Checker\n\n" > commit-message.txt
+git log \
+ --pretty=format:"---------------------%n%n%s%n%nhttps://github.com/validator/validator/commit/%h%n%nCo-authored-by: %aN <%aE>%n" \
+ $SHA_BEFORE_PATCH..$SHA_AFTER_PATCH \
+ | sed -E "s/#([0-9]+)/validator\/validator\/#\1/" >> commit-message.txt
+
+## Squash the patch commits
+echo "Consolidating the commits"
+git reset --soft $SHA_BEFORE_PATCH~1
+git ci -F commit-message.txt
+
+## Clean up the files
+echo "Cleaning the patch files"
+rm *.patch
+rm commit-message.txt
diff --git a/src/main/java/com/adobe/epubcheck/messages/DefaultSeverities.java b/src/main/java/com/adobe/epubcheck/messages/DefaultSeverities.java
index 70c47db7..f5b23b7f 100644
--- a/src/main/java/com/adobe/epubcheck/messages/DefaultSeverities.java
+++ b/src/main/java/com/adobe/epubcheck/messages/DefaultSeverities.java
@@ -204,6 +204,7 @@ class DefaultSeverities implements Severities
     severities.put(MessageId.OPF_007, Severity.WARNING);
     severities.put(MessageId.OPF_007a, Severity.ERROR);
     severities.put(MessageId.OPF_007b, Severity.ERROR);
+    severities.put(MessageId.OPF_007c, Severity.ERROR);
     severities.put(MessageId.OPF_008, Severity.SUPPRESSED);
     severities.put(MessageId.OPF_009, Severity.SUPPRESSED);
     severities.put(MessageId.OPF_010, Severity.ERROR);
diff --git a/src/main/java/com/adobe/epubcheck/messages/MessageId.java b/src/main/java/com/adobe/epubcheck/messages/MessageId.java
index 41befa2b..8c647198 100644
--- a/src/main/java/com/adobe/epubcheck/messages/MessageId.java
+++ b/src/main/java/com/adobe/epubcheck/messages/MessageId.java
@@ -198,6 +198,7 @@ public enum MessageId implements Comparable<MessageId>
   OPF_007("OPF-007"),
   OPF_007a("OPF-007a"),
   OPF_007b("OPF-007b"),
+  OPF_007c("OPF-007c"),
   OPF_008("OPF-008"),
   OPF_009("OPF-009"),
   OPF_010("OPF-010"),
diff --git a/src/main/java/com/adobe/epubcheck/ocf/OCFChecker.java b/src/main/java/com/adobe/epubcheck/ocf/OCFChecker.java
index 1461b3f8..cb2a3ada 100755
--- a/src/main/java/com/adobe/epubcheck/ocf/OCFChecker.java
+++ b/src/main/java/com/adobe/epubcheck/ocf/OCFChecker.java
@@ -250,16 +250,6 @@ public final class OCFChecker extends AbstractChecker
       }
       return false;
     }
-    // FIXME 2022 - report container info
-    // long l = container.getTimeEntry(OCFData.containerEntry);
-    // if (l > 0)
-    // {
-    // Date d = new Date(l);
-    // String formattedDate = new
-    // SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'").format(d);
-    // report.info(OCFData.containerEntry, FeatureEnum.CREATION_DATE,
-    // formattedDate);
-    // }
 
     ValidationContext containerFileContext = state.context()
         .url(OCFMetaFile.CONTAINER.asURL(container)).mimetype("application/xml").build();
@@ -559,9 +549,14 @@ public final class OCFChecker extends AbstractChecker
   {
     for (FeatureEnum feature : resource.getProperties().keySet())
     {
-      // report.info(context.path, feature,
-      // resource.getProperties().get(feature));
-      report.info(resource.getPath(), feature, resource.getProperties().get(feature));
+      if (feature == FeatureEnum.CREATION_DATE
+          && !OCFMetaFile.CONTAINER.asPath().equals(resource.getPath()))
+      {
+        // we only report the creation date once
+        continue;
+      }
+      report.info(resource.getPath(), feature,
+          resource.getProperties().get(feature));
     }
   }
 
diff --git a/src/main/java/com/adobe/epubcheck/ocf/OCFContainer.java b/src/main/java/com/adobe/epubcheck/ocf/OCFContainer.java
index ebaf4d08..554d3f2e 100644
--- a/src/main/java/com/adobe/epubcheck/ocf/OCFContainer.java
+++ b/src/main/java/com/adobe/epubcheck/ocf/OCFContainer.java
@@ -131,7 +131,7 @@ public final class OCFContainer implements GenericResourceProvider
   public boolean isRemote(URL url)
   {
     Preconditions.checkArgument(url != null, "URL is null");
-    if (contains(url))
+    if (!url.isHierarchical() || contains(url))
     {
       return false;
     }
diff --git a/src/main/java/com/adobe/epubcheck/ocf/OCFZipResources.java b/src/main/java/com/adobe/epubcheck/ocf/OCFZipResources.java
index 6ee1bf75..170c170f 100644
--- a/src/main/java/com/adobe/epubcheck/ocf/OCFZipResources.java
+++ b/src/main/java/com/adobe/epubcheck/ocf/OCFZipResources.java
@@ -5,6 +5,8 @@ import java.io.IOException;
 import java.io.InputStream;
 import java.nio.charset.StandardCharsets;
 import java.security.MessageDigest;
+import java.text.SimpleDateFormat;
+import java.util.Date;
 import java.util.Enumeration;
 import java.util.Iterator;
 import java.util.Map;
@@ -48,11 +50,14 @@ public class OCFZipResources implements Iterable<OCFResource>
         throws NoSuchElementException
       {
         final ZipEntry entry = entries.nextElement();
+
         final Map<FeatureEnum, String> properties = ImmutableMap.<FeatureEnum, String> builder()
             .put(FeatureEnum.SIZE, String.valueOf(entry.getSize()))
             .put(FeatureEnum.COMPRESSED_SIZE, String.valueOf(entry.getCompressedSize()))
             .put(FeatureEnum.COMPRESSION_METHOD, getCompressionMethod(entry))
             .put(FeatureEnum.SHA_256, getSHAHash(entry, zip))
+            .put(FeatureEnum.CREATION_DATE,
+                new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'").format(new Date(entry.getTime())))
             .build();
 
         return new OCFResource()
@@ -87,7 +92,7 @@ public class OCFZipResources implements Iterable<OCFResource>
           {
             return entry.getName();
           }
-          
+
           @Override
           public String toString()
           {
diff --git a/src/main/java/com/adobe/epubcheck/opf/OPFChecker.java b/src/main/java/com/adobe/epubcheck/opf/OPFChecker.java
index caef644b..7c16db8a 100755
--- a/src/main/java/com/adobe/epubcheck/opf/OPFChecker.java
+++ b/src/main/java/com/adobe/epubcheck/opf/OPFChecker.java
@@ -163,19 +163,18 @@ public class OPFChecker extends AbstractChecker
       Optional<OPFItem> item = opfHandler.getItemByURL(ref.getDocumentURL());
       if (!item.isPresent())
       {
-        // FIXME 2022 check how to report URL
         report.message(MessageId.OPF_031,
-            EPUBLocation.of(context).at(ref.getLineNumber(), ref.getColumnNumber()), ref.getURL());
+            EPUBLocation.of(context).at(ref.getLineNumber(), ref.getColumnNumber()),
+            context.relativize(ref.getURL()));
       }
       else
       {
         if (!isBlessedItemType(item.get().getMimeType(), version)
             && !isDeprecatedBlessedItemType(item.get().getMimeType()))
         {
-          // FIXME 2022 check how to report URL
           report.message(MessageId.OPF_032,
               EPUBLocation.of(context).at(ref.getLineNumber(), ref.getColumnNumber()),
-              ref.getURL());
+              context.relativize(ref.getURL()));
         }
       }
     }
diff --git a/src/main/java/com/adobe/epubcheck/opf/OPFChecker30.java b/src/main/java/com/adobe/epubcheck/opf/OPFChecker30.java
index 0338efa6..626b2bee 100644
--- a/src/main/java/com/adobe/epubcheck/opf/OPFChecker30.java
+++ b/src/main/java/com/adobe/epubcheck/opf/OPFChecker30.java
@@ -302,9 +302,8 @@ public class OPFChecker30 extends OPFChecker
         Optional<OPFItem> item = opfHandler.getItemByURL(resource.getDocumentURL());
         if (!item.isPresent())
         {
-          // FIXME 2022 check how to report the URL
           report.message(MessageId.OPF_081, EPUBLocation.of(context),
-              resource.getDocumentURL().path());
+              context.relativize(resource.getDocumentURL()));
         }
         else if ("application/vnd.epub.search-key-map+xml".equals(item.get().getMimeType()))
         {
@@ -317,9 +316,8 @@ public class OPFChecker30 extends OPFChecker
         }
         else if (!"application/xhtml+xml".equals(item.get().getMimeType()))
         {
-          // FIXME 2022 check how to report the URL
           report.message(MessageId.OPF_084, EPUBLocation.of(context),
-              resource.getDocumentURL().path());
+              context.relativize(resource.getDocumentURL()));
         }
       }
       if (!skmFound)
diff --git a/src/main/java/com/adobe/epubcheck/overlay/ClipTime.java b/src/main/java/com/adobe/epubcheck/overlay/ClipTime.java
index 78a94f8a..4bbd8fd2 100644
--- a/src/main/java/com/adobe/epubcheck/overlay/ClipTime.java
+++ b/src/main/java/com/adobe/epubcheck/overlay/ClipTime.java
@@ -16,7 +16,7 @@ public class ClipTime {
 	}
 	
 	public ClipTime(double timeInMs) {
-		this.timeInMs = new Double(timeInMs);
+		this.timeInMs = timeInMs;
 	}
 
 	public double getTimeInMs() {
diff --git a/src/main/java/com/adobe/epubcheck/tool/EpubChecker.java b/src/main/java/com/adobe/epubcheck/tool/EpubChecker.java
index bd38ac8d..f5e1d45c 100644
--- a/src/main/java/com/adobe/epubcheck/tool/EpubChecker.java
+++ b/src/main/java/com/adobe/epubcheck/tool/EpubChecker.java
@@ -92,7 +92,8 @@ public class EpubChecker
   File listChecksOut;
   File customMessageFile;
   boolean listChecks = false;
-  boolean displayHelpOrVersion = false;
+  boolean displayHelp = false;
+  boolean displayVersion = false;
   boolean useCustomMessageFile = false;
   boolean failOnWarnings = false;
   private Messages messages = Messages.getInstance();
@@ -132,6 +133,10 @@ public class EpubChecker
     {
       if (processArguments(args))
       {
+        if (displayHelp || (displayVersion && path == null))
+        {
+          return 0;
+        }
         report = createReport();
         report.initialize();
         if (listChecks)
@@ -139,10 +144,6 @@ public class EpubChecker
           dumpMessageDictionary(report);
           return 0;
         }
-        if (displayHelpOrVersion)
-        {
-          return 0;
-        }
         if (useCustomMessageFile)
         {
           report.setCustomMessageFile(customMessageFile.getAbsolutePath());
@@ -159,7 +160,9 @@ public class EpubChecker
       returnValue = 1;
     } finally
     {
-      printEpubCheckCompleted(report);
+      if (report != null) {
+        printEpubCheckCompleted(report);
+      }
     }
     return returnValue;  
   }
@@ -663,8 +666,7 @@ public class EpubChecker
               outWriter.setQuiet(true);
             break;
           case "failonwarnings":
-              String fw = args[i].substring("--failonwarnings".length());
-              failOnWarnings = (fw.compareTo("-") != 0);
+              failOnWarnings = true;
             break;
           case "r":
           case "redir":
@@ -743,11 +745,11 @@ public class EpubChecker
           case "?":
           case "help":
               displayHelp(); // display help message
-              displayHelpOrVersion = true;
-            break;
+              displayHelp = true;
+              break;
           case "version":
             displayVersion();
-            displayHelpOrVersion = true;
+            displayVersion = true;
             break;
           default:
               System.err.println(String.format(messages.get("unrecognized_argument"), args[i]));
@@ -796,7 +798,7 @@ public class EpubChecker
 
     if (path == null)
     {
-      if (listChecks || displayHelpOrVersion)
+      if (listChecks || displayHelp || displayVersion)
       {
         return true;
       }
diff --git a/src/main/java/com/adobe/epubcheck/vocab/AccessibilityVocab.java b/src/main/java/com/adobe/epubcheck/vocab/AccessibilityVocab.java
index fbcce543..1bdd29aa 100644
--- a/src/main/java/com/adobe/epubcheck/vocab/AccessibilityVocab.java
+++ b/src/main/java/com/adobe/epubcheck/vocab/AccessibilityVocab.java
@@ -15,6 +15,7 @@ public final class AccessibilityVocab
   {
     CERTIFIED_BY,
     CERTIFIER_CREDENTIAL,
+    EXEMPTION
   }
 
   public static enum LINKREL_PROPERTIES
diff --git a/src/main/java/com/adobe/epubcheck/vocab/VocabUtil.java b/src/main/java/com/adobe/epubcheck/vocab/VocabUtil.java
index c86e10da..6a2203c9 100644
--- a/src/main/java/com/adobe/epubcheck/vocab/VocabUtil.java
+++ b/src/main/java/com/adobe/epubcheck/vocab/VocabUtil.java
@@ -41,15 +41,15 @@ public final class VocabUtil
    * Parses a single property value and report validation errors on the fly.
    * 
    * @param value
-   *          the value to parse.
+   *        the value to parse.
    * @param vocabs
-   *          a map of prefix to vocabularies.
+   *        a map of prefix to vocabularies.
    * @param context
-   *          the validation context (report, locale, path, etc).
+   *        the validation context (report, locale, path, etc).
    * @param location
-   *          the location in the validated file.
+   *        the location in the validated file.
    * @return an {@link Optional} containing the property if it was parsed
-   *         successfully or nothing if there was a parsing error
+   *           successfully or nothing if there was a parsing error
    */
   public static Optional<Property> parseProperty(String value, Map<String, Vocab> vocabs,
       ValidationContext context, EPUBLocation location)
@@ -64,13 +64,13 @@ public final class VocabUtil
    * errors on the fly.
    * 
    * @param value
-   *          the value to parse.
+   *        the value to parse.
    * @param vocabs
-   *          a map of prefix to vocabularies.
+   *        a map of prefix to vocabularies.
    * @param context
-   *          the validation context (report, locale, path, etc).
+   *        the validation context (report, locale, path, etc).
    * @param location
-   *          the location in the validated file.
+   *        the location in the validated file.
    * @return
    */
   public static Set<Property> parsePropertyList(String value, Map<String, ? extends Vocab> vocabs,
@@ -158,17 +158,17 @@ public final class VocabUtil
    * vocabularies, and default vocabularies that cannot be re-declared.
    * 
    * @param value
-   *          the prefix declaration to parse.
+   *        the prefix declaration to parse.
    * @param predefined
-   *          a map of reserved prefixes to associated vocabularies.
+   *        a map of reserved prefixes to associated vocabularies.
    * @param known
-   *          a map of known URIs to known vocabularies.
+   *        a map of known URIs to known vocabularies.
    * @param forbidden
-   *          a set of URIs of default vocabularies that cannot be re-declared.
+   *        a set of URIs of default vocabularies that cannot be re-declared.
    * @param report
-   *          to report errors on the fly.
+   *        to report errors on the fly.
    * @param location
-   *          the location of the attribute in the source file.
+   *        the location of the attribute in the source file.
    * @return
    */
   public static Map<String, Vocab> parsePrefixDeclaration(String value,
@@ -192,6 +192,10 @@ public final class VocabUtil
         // must not declare a default vocab
         report.message(MessageId.OPF_007b, location, prefix);
       }
+      else if ("http://purl.org/dc/elements/1.1/".equals(uri))
+      {
+        report.message(MessageId.OPF_007c, location);
+      }
       else
       {
         if (predefined.containsKey(prefix)
diff --git a/src/main/java/com/adobe/epubcheck/xml/XMLEncodingSniffer.java b/src/main/java/com/adobe/epubcheck/xml/XMLEncodingSniffer.java
index bb4b584f..a7d242b7 100644
--- a/src/main/java/com/adobe/epubcheck/xml/XMLEncodingSniffer.java
+++ b/src/main/java/com/adobe/epubcheck/xml/XMLEncodingSniffer.java
@@ -108,8 +108,28 @@ public final class XMLEncodingSniffer
     return encoding.toUpperCase(Locale.ROOT);
   }
 
+  /**
+   * Checks if the parameter input stream has a UTF-8 byte order mark.
+   *
+   * @param in
+   *        an input stream
+   * @return <code>true</code> if and only if the input stream starts with a
+   *           UTF-8 BOM
+   * @throws IOException
+   */
+  public static boolean hasUTF8BOM(InputStream in)
+    throws IOException
+  {
+    byte[] buffer = new byte[3];
+    in.mark(buffer.length);
+    int len = in.read(buffer);
+    in.reset();
+    return (len == 3 && matchesMagic(UTF8_MAGIC, buffer));
+  }
+
   private XMLEncodingSniffer()
   {
     // Not instanciable.
   }
+
 }
diff --git a/src/main/java/com/adobe/epubcheck/xml/XMLParser.java b/src/main/java/com/adobe/epubcheck/xml/XMLParser.java
index 4b01d026..3ca5d8b4 100755
--- a/src/main/java/com/adobe/epubcheck/xml/XMLParser.java
+++ b/src/main/java/com/adobe/epubcheck/xml/XMLParser.java
@@ -25,6 +25,8 @@ package com.adobe.epubcheck.xml;
 import java.io.BufferedInputStream;
 import java.io.IOException;
 import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.nio.charset.StandardCharsets;
 
 import javax.xml.parsers.SAXParser;
 import javax.xml.parsers.SAXParserFactory;
@@ -55,8 +57,6 @@ import io.mola.galimatias.URL;
 public class XMLParser
 {
 
-  private static final String SAXPROP_LEXICAL_HANDLER = "http://xml.org/sax/properties/lexical-handler";
-  private static final String SAXPROP_DECL_HANDLER = "http://xml.org/sax/properties/declaration-handler";
   private final ValidationContext context;
   private final Report report;
   private final URL url;
@@ -76,9 +76,9 @@ public class XMLParser
     {
       factory.setNamespaceAware(true);
       factory.setValidating(false);
+      factory.setFeature("http://xml.org/sax/features/validation", false);
       factory.setFeature("http://xml.org/sax/features/external-general-entities", false);
       factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
-      factory.setFeature("http://xml.org/sax/features/validation", false);
       if (context.version == EPUBVersion.VERSION_3)
       {
         factory.setXIncludeAware(false);
@@ -93,9 +93,12 @@ public class XMLParser
       handler.setEntityResolver(new DefaultResolver(context.version));
 
       XMLReader reader = parser.getXMLReader();
-      DeclarationHandler docTypeHandler = new DeclarationHandler(context);
-      reader.setProperty(SAXPROP_LEXICAL_HANDLER, docTypeHandler);
-      reader.setProperty(SAXPROP_DECL_HANDLER, docTypeHandler);
+      DeclarationHandler docTypeHandler = new DeclarationHandler(context, parser);
+      reader.setProperty("http://xml.org/sax/properties/lexical-handler", docTypeHandler);
+      reader.setProperty("http://xml.org/sax/properties/declaration-handler", docTypeHandler);
+      // add the declaration handler as a content handler
+      // to check for skipped entities
+      handler.addContentHandler(docTypeHandler);
 
     } catch (Exception e)
     {
@@ -135,8 +138,8 @@ public class XMLParser
         return;
       }
 
-      // Check encoding
-      // If the result is null, the XML parser will must parse it as UTF-8
+      // Create the InputSource based on the encoding
+      final InputSource source;
       String encoding = XMLEncodingSniffer.sniffEncoding(buffered);
       if (encoding != null && !encoding.equals("UTF-8"))
       {
@@ -157,13 +160,27 @@ public class XMLParser
         {
           report.message(MessageId.RSC_028, EPUBLocation.of(context), encoding);
         }
+
+        // We do not set the source encoding name, but instead let the SAXParser
+        // apply its own encoding-sniffing logic, as it can report useful errors
+        // (for instance a mismatch between a BOM and the XML declaration)
+        source = new InputSource(buffered);
+
+      }
+      else
+      {
+        // Decode the UTF-8 stream with java.io instead of letting Xerces
+        // do it, to work around Xerces issue #1668
+        // (see https://issues.apache.org/jira/browse/XERCESJ-1668),
+        // skipping any UTF-8 BOM first (disallowed by that constructor)
+        if (XMLEncodingSniffer.hasUTF8BOM(buffered))
+        {
+          buffered.skip(3);
+        }
+        source = new InputSource(new InputStreamReader(buffered, StandardCharsets.UTF_8));
       }
 
-      // Build the input source
-      // We do not set the source encoding name, but instead let the SAXParser
-      // apply its own encoding-sniffing logic, as it can report useful errors
-      // (for instance a mismatch between a BOM and the XML declaration)
-      InputSource source = new InputSource(buffered);
+      // Set the source's system ID
       source.setSystemId(url.toString());
 
       // Set the error handler
diff --git a/src/main/java/com/adobe/epubcheck/xml/handlers/DeclarationHandler.java b/src/main/java/com/adobe/epubcheck/xml/handlers/DeclarationHandler.java
index 732cca9c..0ba7a300 100644
--- a/src/main/java/com/adobe/epubcheck/xml/handlers/DeclarationHandler.java
+++ b/src/main/java/com/adobe/epubcheck/xml/handlers/DeclarationHandler.java
@@ -3,6 +3,10 @@ package com.adobe.epubcheck.xml.handlers;
 import java.util.HashSet;
 import java.util.Set;
 
+import javax.xml.parsers.SAXParser;
+
+import org.apache.xerces.impl.XMLErrorReporter;
+import org.apache.xerces.impl.msg.XMLMessageFormatter;
 import org.xml.sax.SAXException;
 
 import com.adobe.epubcheck.api.Report;
@@ -18,13 +22,15 @@ public class DeclarationHandler extends LocationHandler
   private final EPUBVersion version;
   private boolean firstStartDTDInvocation = true;
   private final Set<String> entities = new HashSet<String>();
+  private final SAXParser parser;
 
-  public DeclarationHandler(ValidationContext context)
+  public DeclarationHandler(ValidationContext context, SAXParser parser)
   {
     super(context);
     this.report = context.report;
     this.mimeType = context.mimeType;
     this.version = context.version;
+    this.parser = parser;
 
     // XML predefined
     entities.add("gt");
@@ -191,4 +197,32 @@ public class DeclarationHandler extends LocationHandler
   {
     entities.add(name);
   }
+
+  @Override
+  public void skippedEntity(String name)
+    throws SAXException
+  {
+    if (!name.startsWith("%") && !name.equals("[dtd]"))
+    {
+      // A non-validating parser with an external subset with
+      // skip any unknown entity reference.
+      // This is the case in EPUB 2.0.1 content documents.
+      // We check if the entity was declared and report an
+      // error if not.
+      if (!entities.contains("name"))
+      {
+        // We use the Xerces XNI error reporter to make
+        // the reporting message consistent with what Xerces
+        // would report itself.
+        XMLErrorReporter xniReporter = (XMLErrorReporter) parser
+            .getProperty("http://apache.org/xml/properties/internal/error-reporter");
+        xniReporter.reportError(XMLMessageFormatter.XML_DOMAIN,
+            "EntityNotDeclared",
+            new Object[] { name },
+            XMLErrorReporter.SEVERITY_FATAL_ERROR);
+      }
+    }
+  }
+  
+  
 }
diff --git a/src/main/java/org/idpf/epubcheck/util/css/CssParser.java b/src/main/java/org/idpf/epubcheck/util/css/CssParser.java
index 1dc9bb7a..4d9a8af2 100644
--- a/src/main/java/org/idpf/epubcheck/util/css/CssParser.java
+++ b/src/main/java/org/idpf/epubcheck/util/css/CssParser.java
@@ -373,7 +373,7 @@ public final class CssParser
         CssToken value = iter.next();
         if (MATCH_SEMI_CLOSEBRACE.apply(value))
         {
-          if (declaration.components.size() < 1)
+          if (declaration.components.size() < 1 && !declaration.name.or("").startsWith("--"))
           {
             err.error(new CssGrammarException(GRAMMAR_EXPECTING_TOKEN, iter.last.location,
                 messages.getLocale(), value.getChar(), messages.get("a_property_value")));
diff --git a/src/main/java/org/w3c/epubcheck/core/references/ReferenceRegistry.java b/src/main/java/org/w3c/epubcheck/core/references/ReferenceRegistry.java
index 7c7662d0..1e36ff21 100644
--- a/src/main/java/org/w3c/epubcheck/core/references/ReferenceRegistry.java
+++ b/src/main/java/org/w3c/epubcheck/core/references/ReferenceRegistry.java
@@ -49,7 +49,10 @@ public final class ReferenceRegistry
     if (url == null) return;
 
     // Remove query component of local URLs
-    if (url.query() != null && !container.isRemote(url))
+    // Note: we only do this for hierarchical URLs, to work around a bug
+    // in Galimatias that would transform a non-hierarchical URL into a
+    // hierarchical one. Queries for data URLs can safely be ignored here.
+    if (url.isHierarchical() && url.query() != null && !container.isRemote(url))
     {
       try
       {
diff --git a/src/main/resources/com/adobe/epubcheck/messages/MessageBundle.properties b/src/main/resources/com/adobe/epubcheck/messages/MessageBundle.properties
index 4a168c1d..bbc50028 100644
--- a/src/main/resources/com/adobe/epubcheck/messages/MessageBundle.properties
+++ b/src/main/resources/com/adobe/epubcheck/messages/MessageBundle.properties
@@ -120,6 +120,7 @@ OPF_006=Invalid prefix declaration: URI "%1$s" is not a valid URI.
 OPF_007=Re-declaration of reserved prefix "%1$s".
 OPF_007a=Invalid prefix mapping: prefix "_" must not be declared.
 OPF_007b=Invalid prefix mapping: default vocabulary "%1$s" must not be re-declared.
+OPF_007c=Invalid prefix mapping: a prefix to the Dublin Core /elements/1.1/ namespace must not be declared
 OPF_010=Error resolving reference: "%1$s".
 OPF_011=itemref can’t have both page-spread-right and page-spread-left properties.
 OPF_012=Item property "%1$s" is not defined for media type "%2$s".
diff --git a/src/main/resources/com/adobe/epubcheck/schema/30/epub-svg-30.rnc b/src/main/resources/com/adobe/epubcheck/schema/30/epub-svg-30.rnc
index f38bac08..5332da76 100644
--- a/src/main/resources/com/adobe/epubcheck/schema/30/epub-svg-30.rnc
+++ b/src/main/resources/com/adobe/epubcheck/schema/30/epub-svg-30.rnc
@@ -12,3 +12,6 @@ include "./mod/epub-xhtml-svg-mathml.rnc" {
 
 # Allow `body` element as a child of `foreignObject` 
 svg.foreignObject.inner |= body.elem 
+
+# Allow `epub:prefix` attribute on `svg` root
+svg.attrs &= epub.prefix.attr.ns?
diff --git a/src/main/resources/com/adobe/epubcheck/schema/30/mod/epub-svg-forgiving-inc.rnc b/src/main/resources/com/adobe/epubcheck/schema/30/mod/epub-svg-forgiving-inc.rnc
index c08a94ac..bda63460 100644
--- a/src/main/resources/com/adobe/epubcheck/schema/30/mod/epub-svg-forgiving-inc.rnc
+++ b/src/main/resources/com/adobe/epubcheck/schema/30/mod/epub-svg-forgiving-inc.rnc
@@ -35,7 +35,7 @@
   svg.generic.attr =
     attribute *
       - ( id
-        | epub:type
+        | epub:*
         ) { text }
 
   # Attribute list of elements where `epub:type` is allowed
diff --git a/src/main/resources/com/adobe/epubcheck/schema/30/mod/html5/LAST_UPDATE b/src/main/resources/com/adobe/epubcheck/schema/30/mod/html5/LAST_UPDATE
index 126b31fe..62110fd7 100644
--- a/src/main/resources/com/adobe/epubcheck/schema/30/mod/html5/LAST_UPDATE
+++ b/src/main/resources/com/adobe/epubcheck/schema/30/mod/html5/LAST_UPDATE
@@ -1,1 +1,1 @@
-b118bf6031b0b06797f3ccdc25d0f74c1447139a
+9855955c12fa95b0b452ddf62ca731172b5c7cea
diff --git a/src/main/resources/com/adobe/epubcheck/schema/30/mod/html5/embed.rnc b/src/main/resources/com/adobe/epubcheck/schema/30/mod/html5/embed.rnc
index 6a271d4c..4ee91217 100644
--- a/src/main/resources/com/adobe/epubcheck/schema/30/mod/html5/embed.rnc
+++ b/src/main/resources/com/adobe/epubcheck/schema/30/mod/html5/embed.rnc
@@ -38,6 +38,7 @@ namespace local = ""
 			|	common.attrs.aria.role.checkbox
 			|	common.attrs.aria.role.img
 			|	common.attrs.aria.role.link
+			|	common.attrs.aria.role.math
 			|	common.attrs.aria.role.menuitem
 			|	common.attrs.aria.role.menuitemcheckbox
 			|	common.attrs.aria.role.menuitemradio
diff --git a/src/main/resources/com/adobe/epubcheck/schema/30/mod/html5/structural.rnc b/src/main/resources/com/adobe/epubcheck/schema/30/mod/html5/structural.rnc
index 75792b1b..9370b4a5 100644
--- a/src/main/resources/com/adobe/epubcheck/schema/30/mod/html5/structural.rnc
+++ b/src/main/resources/com/adobe/epubcheck/schema/30/mod/html5/structural.rnc
@@ -125,6 +125,7 @@
 			|	common.attrs.aria.role.doc-dedication
 			|	common.attrs.aria.role.doc-example
 			|	common.attrs.aria.role.doc-footnote
+			|	common.attrs.aria.role.doc-glossary
 			|	common.attrs.aria.role.doc-pullquote
 			|	common.attrs.aria.role.doc-tip
 			)?
diff --git a/src/main/resources/com/adobe/epubcheck/util/messages.properties b/src/main/resources/com/adobe/epubcheck/util/messages.properties
index 14e86762..735c5e92 100644
--- a/src/main/resources/com/adobe/epubcheck/util/messages.properties
+++ b/src/main/resources/com/adobe/epubcheck/util/messages.properties
@@ -87,7 +87,7 @@ help_text = \
           -o <file>                = same as --out\n\
           -x <file>                = same as --xmp\n\
           -j <file>                = same as --json\n\
-          --failonwarnings[+|-]    = By default, the tool returns a 1 if errors are found in the file or 0 if no errors\n\
+          --failonwarnings         = By default, the tool returns a 1 if errors are found in the file or 0 if no errors\n\
           \                           are found.  Using --failonwarnings will cause the process to exit with a status of\n\
           \                           1 if either warnings or errors are present and 0 only when there are no errors or warnings.\n\
           -q, --quiet              = no message on console, except errors, only in the output\n\
diff --git a/src/main/resources/com/adobe/epubcheck/util/messages_da.properties b/src/main/resources/com/adobe/epubcheck/util/messages_da.properties
index 367c6b36..b0a59846 100644
--- a/src/main/resources/com/adobe/epubcheck/util/messages_da.properties
+++ b/src/main/resources/com/adobe/epubcheck/util/messages_da.properties
@@ -80,7 +80,7 @@ help_text = EPUBCheck v%1$s\n\n\
           -o <file>                = synonym for --out\n\
           -x <file>                = synonym for --xmp\n\
           -j <file>                = synonym for --json\n\
-          --failonwarnings[+|-]    = som standard afsluttes EPUBCheck med ExitCode 1 hvis der blev fundet fejl i filen, og med ExitCode 0 hvis der ikke blev fundet fejl.\n\
+          --failonwarnings         = som standard afsluttes EPUBCheck med ExitCode 1 hvis der blev fundet fejl i filen, og med ExitCode 0 hvis der ikke blev fundet fejl.\n\
           \                           Ved brug af --failonwarnings vil EPUBCheck blive klar til afslutning\n\
           \                           med ExitCode 1 når der er advarsler, og ExitCode 0 ved ingen fejl eller mangler.\n\
           -q, --quiet              = kun fejl udsendes på konsollen\n\
diff --git a/src/main/resources/com/adobe/epubcheck/util/messages_de.properties b/src/main/resources/com/adobe/epubcheck/util/messages_de.properties
index 843cadbf..a4658e4b 100644
--- a/src/main/resources/com/adobe/epubcheck/util/messages_de.properties
+++ b/src/main/resources/com/adobe/epubcheck/util/messages_de.properties
@@ -84,7 +84,7 @@ help_text = EPUBCheck v%1$s\n\n\
           -o <datei>              = Synonym zu --out\n\
           -x <datei>              = Synonym zu --xmp\n\
           -j <datei>              = Synonym zu --json\n\
-          --failonwarnings[+|-]   = Standardmäßig wird EPUBCheck mit ExitCode 1 beendet falls Fehler im EPUB gefunden wurden\n\
+          --failonwarnings        = Standardmäßig wird EPUBCheck mit ExitCode 1 beendet falls Fehler im EPUB gefunden wurden\n\
           \                          und mit ExitCode 0, falls keine Fehler gefunden wurden. Bei Nutzung von --failonwarnings\n\
           \                          wird EPUBCheck auch bereits bei Auftreten von Warnungen mit ExitCode 1 beendet.\n\
           -q, --quiet             = Nur Fehler werden auf der Konsole ausgegeben\n\
diff --git a/src/main/resources/com/adobe/epubcheck/util/messages_es.properties b/src/main/resources/com/adobe/epubcheck/util/messages_es.properties
index 903dc1c3..06d3317b 100644
--- a/src/main/resources/com/adobe/epubcheck/util/messages_es.properties
+++ b/src/main/resources/com/adobe/epubcheck/util/messages_es.properties
@@ -84,7 +84,7 @@ help_text = EPUBCheck v%1$s\n\n\
           -o <file>                = igual a --out\n\
           -x <file>                = igual a --xmp\n\
           -j <file>                = igual a --json\n\
-          --failonwarnings[+|-]    = Por defecto, la herramienta devuelve 1 si se encuentran errores en el archivo o 0 si no hay errores\n\
+          --failonwarnings         = Por defecto, la herramienta devuelve 1 si se encuentran errores en el archivo o 0 si no hay errores\n\
           \                           Utilizar --failonwarnings provoca la detención del proceso con un resultado\n\
           \                           de 1 si hay errores o advertencias y 0 cuando no los hay.\n\
           -q, --quiet              = no hay mensajes en la consola; excepto errores, solo en la salida\n\
diff --git a/src/main/resources/com/adobe/epubcheck/util/messages_fr.properties b/src/main/resources/com/adobe/epubcheck/util/messages_fr.properties
index d4a4b2c5..5d3b69fe 100644
--- a/src/main/resources/com/adobe/epubcheck/util/messages_fr.properties
+++ b/src/main/resources/com/adobe/epubcheck/util/messages_fr.properties
@@ -84,7 +84,7 @@ help_text = EPUBCheck v%1$s\n\n\
           -o <file>               = identique à --out\n\
           -x <file>               = identique à --xmp\n\
           -j <file>               = identique à --json\n\
-          --failonwarnings[+|-]   = Par défaut, cet outil renvoie 1 si des erreurs sont trouvées ou 0 si le fichier\n\
+          --failonwarnings        = Par défaut, cet outil renvoie 1 si des erreurs sont trouvées ou 0 si le fichier\n\
           \                          ne contient aucune erreur. En utilisant --failonwarnings, 1 sera renvoyé également\n\
           \                          si des warnings ou des erreurs sont présents, et 0 ne sera renvoyés que si le fichier\n\
           \                          ne contient ni erreurs ni warnings.\n\
diff --git a/src/main/resources/com/adobe/epubcheck/util/messages_it.properties b/src/main/resources/com/adobe/epubcheck/util/messages_it.properties
index c44c56bd..67264dbd 100644
--- a/src/main/resources/com/adobe/epubcheck/util/messages_it.properties
+++ b/src/main/resources/com/adobe/epubcheck/util/messages_it.properties
@@ -82,7 +82,7 @@ help_text = EPUBCheck v%1$s\n\n\
           -o <file>               = sinonimo di --out\n\
           -x <file>               = sinonimo di --xmp\n\
           -j <file>               = sinonimo di --json\n\
-          --failonwarnings[+|-]   = di norma, ritorna 1 se sono rilevati errori,\n\
+          --failonwarnings        = di norma, ritorna 1 se sono rilevati errori,\n\
           \                          e 0 altrimenti.\n\
           \                          Specificando --failonwarnings, sarà ritornato\n\
           \                          1 in presenza di errori o potenziali errori\n\
diff --git a/src/main/resources/com/adobe/epubcheck/util/messages_ja.properties b/src/main/resources/com/adobe/epubcheck/util/messages_ja.properties
index 4c28d05e..bb7cf795 100644
--- a/src/main/resources/com/adobe/epubcheck/util/messages_ja.properties
+++ b/src/main/resources/com/adobe/epubcheck/util/messages_ja.properties
@@ -81,7 +81,7 @@ help_text = EPUBCheck v%1$s\n\n\
           -o <file>                = --out と同様\n\
           -x <file>                = --xmp と同様\n\
           -j <file>                = --json と同様\n\
-          --failonwarnings[+|-]    = デフォルトでは、終了ステータスコードとして、エラーが検出されれば1を、検出されなければ\n\
+          --failonwarnings         = デフォルトでは、終了ステータスコードとして、エラーが検出されれば1を、検出されなければ\n\
           \                           0を返します. --failonwarnings オプションを指定すると、警告かエラーがあれば1を、警告もエラーもどちらも\n\
           \                           ない場合のみ0を返すようになります.\n\
           -q, --quiet              = エラー以外のメッセージをコンソールに出力しないようにします\n\
diff --git a/src/main/resources/com/adobe/epubcheck/util/messages_ko_KR.properties b/src/main/resources/com/adobe/epubcheck/util/messages_ko_KR.properties
index 666bf3d2..b2f105c5 100644
--- a/src/main/resources/com/adobe/epubcheck/util/messages_ko_KR.properties
+++ b/src/main/resources/com/adobe/epubcheck/util/messages_ko_KR.properties
@@ -78,7 +78,7 @@ help_text = EPUBCheck v%1$s\n\n\
           -o <file>                = --out과 같음\n\
           -x <file>                = --xmp와 같음\n\
           -j <file>                = --json과 같음\n\
-          --failonwarnings[+|-]    = 유효성 검사를 완료한 후 오류가 검출되면 1을 오류가 없으면 0을 반환합니다.\n\
+          --failonwarnings         = 유효성 검사를 완료한 후 오류가 검출되면 1을 오류가 없으면 0을 반환합니다.\n\
           \                           --failonwarnings 옵션을 지정하면 경고 또는 오류가있는 경우 1을 모두 없을 경우에만 0을 반환합니다.\n\
           -q, --quiet              = 오류를 제외하고 화면에 메시지를 출력하지 않습니다.\n\
           -f, --fatal              = 심각한 오류 메시지만 출력 파일에 표시합니다.\n\
diff --git a/src/main/resources/com/adobe/epubcheck/util/messages_nl.properties b/src/main/resources/com/adobe/epubcheck/util/messages_nl.properties
index 888e1a30..c71a9dd3 100644
--- a/src/main/resources/com/adobe/epubcheck/util/messages_nl.properties
+++ b/src/main/resources/com/adobe/epubcheck/util/messages_nl.properties
@@ -84,7 +84,7 @@ help_text = EPUBCheck v%1$s\n\n\
           -o <file>               = gelijk aan --out\n\
           -x <file>               = gelijk aan --xmp\n\
           -j <file>               = gelijk aan --json\n\
-          --failonwarnings[+|-]   = standaard geeft het programma een waarde van 1 als er fouten worden gedetecteerd\n\
+          --failonwarnings        = standaard geeft het programma een waarde van 1 als er fouten worden gedetecteerd\n\
           \                          en een waarde van 0 als alles OK is.\n\
           \                          Gebruik --failonwarnings als u wilt dat er een waarde van 1 wordt teruggegeven als er fouten \n\
           \                          OF waarschuwingen worden ontdekt. \n\
diff --git a/src/main/resources/com/adobe/epubcheck/util/messages_pt_BR.properties b/src/main/resources/com/adobe/epubcheck/util/messages_pt_BR.properties
index 1fd1ae3c..e6415583 100644
--- a/src/main/resources/com/adobe/epubcheck/util/messages_pt_BR.properties
+++ b/src/main/resources/com/adobe/epubcheck/util/messages_pt_BR.properties
@@ -81,7 +81,7 @@ help_text = EPUBCheck v%1$s\n\n\
           -o <arquivo>            = o mesmo que --out\n\
           -x <arquivo>            = o mesmo que --xmp\n\
           -j <arquivo>            = o mesmo que --json\n\
-          --failonwarnings[+|-]   = Por padrão, a ferramenta retorna 1 se erros forem encontrados no arquivo ou 0 se não forem\n\
+          --failonwarnings        = Por padrão, a ferramenta retorna 1 se erros forem encontrados no arquivo ou 0 se não forem\n\
           \                          encontrados erros.  Usar --failonwarnings irá fazer com que o processo finalize com o status de\n\
           \                          1 se houver tanto advertências como falhas presentes e 0 apenas quando não houver erros nem advertências.\n\
           -q, --quiet             = nenhuma mensagem no console, exceto erros, apenas na saída\n\
diff --git a/src/main/resources/com/adobe/epubcheck/util/messages_zh_TW.properties b/src/main/resources/com/adobe/epubcheck/util/messages_zh_TW.properties
index f254e695..84ef79b4 100644
--- a/src/main/resources/com/adobe/epubcheck/util/messages_zh_TW.properties
+++ b/src/main/resources/com/adobe/epubcheck/util/messages_zh_TW.properties
@@ -78,7 +78,7 @@ help_text = EPUB檢查器 %1$s版\n\n\
           -o <file> = 和 --out 相同\n\
           -x <file> = 和 --xmp 相同\n\
           -j <file> = 和 --json 相同\n\
-          --failonwarnings[+|-] = 預設上，當發現錯誤時回報值為 1 或者沒有錯誤時回報值為 0。使用 --failonwarnings 將會使得處理程序終止，狀態為 1 並且有警告訊息或者錯誤需要顯示亦然，而只會在沒有錯誤與警告訊息時才會回傳 0。\n\
+          --failonwarnings      = 預設上，當發現錯誤時回報值為 1 或者沒有錯誤時回報值為 0。使用 --failonwarnings 將會使得處理程序終止，狀態為 1 並且有警告訊息或者錯誤需要顯示亦然，而只會在沒有錯誤與警告訊息時才會回傳 0。\n\
           -q, --quiet = 除了錯誤以外不會在終端機上顯示訊息，但僅限於結果\n\
           -f, --fatal = 結果僅包含致命錯誤\n\
           -e, --error = 結果僅包含錯誤以及致命錯誤\n\


Test patch:
diff --git a/src/test/java/com/adobe/epubcheck/tools/CommandLineTest.java b/src/test/java/com/adobe/epubcheck/tools/CommandLineTest.java
deleted file mode 100644
index 20a51ff3..00000000
--- a/src/test/java/com/adobe/epubcheck/tools/CommandLineTest.java
+++ /dev/null
@@ -1,749 +0,0 @@
-package com.adobe.epubcheck.tools;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertTrue;
-
-import java.io.ByteArrayOutputStream;
-import java.io.File;
-import java.io.FileReader;
-import java.io.OutputStream;
-import java.io.PrintStream;
-import java.net.URL;
-import java.util.Locale;
-import java.util.regex.Pattern;
-
-import javax.xml.parsers.DocumentBuilder;
-import javax.xml.parsers.DocumentBuilderFactory;
-
-import org.json.simple.JSONValue;
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-
-import com.adobe.epubcheck.api.EpubCheck;
-import com.adobe.epubcheck.tool.EpubChecker;
-import com.adobe.epubcheck.util.Messages;
-
-public class CommandLineTest
-{
-
-  private final Pattern usagePattern = Pattern.compile("^([\\s\\S]*\\n)?USAGE\\([\\s\\S]*$");
-  private final Pattern warningPattern = Pattern.compile("^([\\s\\S]*\\n)?WARNING\\([\\s\\S]*$");
-  private final Pattern errorPattern = Pattern.compile("^([\\s\\S]*\\n)?ERROR\\([\\s\\S]*$");
-
-  private final ByteArrayOutputStream outContent = new ByteArrayOutputStream();
-  private final ByteArrayOutputStream errContent = new ByteArrayOutputStream();
-
-  private PrintStream originalOut;
-  private PrintStream originalErr;
-  private final Messages messages = Messages.getInstance(Locale.ENGLISH);
-  private Locale defaultLocale;
-
-  @Before
-  public void setUp()
-  {
-    defaultLocale = Locale.getDefault();
-    Locale.setDefault(Locale.ENGLISH);
-    originalOut = System.out;
-    originalErr = System.err;
-    System.setOut(new PrintStream(outContent));
-    System.setErr(new PrintStream(errContent));
-  }
-
-  @After
-  public void tearDown()
-  {
-    Locale.setDefault(defaultLocale);
-    System.setOut(originalOut);
-    System.setErr(originalErr);
-  }
-
-  /**
-   * Checks if for any reason the input arguments are null the return code is
-   * set appropriately.
-   */
-  @Test
-  public void nullPointerExceptionTest()
-  {
-    runCommandLineTest(1, (String[]) null);
-  }
-
-  /**
-   * Testing the save function, using a directory with a correct epub structure
-   * it will produce a correct packaged epub.
-   */
-  @Test
-  public void archivingValidEPUBDirectoryTest()
-  {
-    URL inputUrl = CommandLineTest.class.getResource("30-valid-test");
-    File inputFile = new File(inputUrl.getPath());
-    File out = new File(inputFile.getParent() + File.separator + "30-valid-test.epub");
-    if (out.exists())
-    {
-      out.delete();
-    }
-
-    runCommandLineTest(0, "--mode", "exp", inputUrl.getPath(), "--save");
-
-    runCommandLineTest(0, out.getAbsolutePath());
-
-    if (out.exists())
-    {
-      out.delete();
-    }
-  }
-
-  /**
-   * Check if we can validate a single navigation file.
-   */
-  @Test
-  public void singleNavigationFileTest()
-  {
-    URL inputUrl = CommandLineTest.class.getResource("30-valid-test/OPS/nav.xhtml");
-    runCommandLineTest(0, "-mode", "nav", inputUrl.getPath());
-  }
-
-  /**
-   * Validate that testing a single navigation file without table of contents
-   * will yield an error code.
-   */
-  @Test
-  public void singleNavigationWithoutTableOfContentsTest()
-  {
-    URL inputUrl = CommandLineTest.class.getResource("nav-no-toc.xhtml");
-    runCommandLineTest(1, "-mode", "nav", inputUrl.getPath());
-  }
-
-  /**
-   * This test runs the program without any arguments, expected is a message
-   * about that arguments are required.
-   */
-  @Test
-  public void noArgumentsTest()
-  {
-    runCommandLineTest(1);
-    assertEquals("Command output not as expected", messages.get("argument_needed"),
-        errContent.toString().trim());
-  }
-
-  /**
-   * Verify that an epub with a strange casing will generate a warning.
-   */
-  @Test
-  public void extensionTest1()
-  {
-    URL inputUrl = CommandLineTest.class.getResource("wrong_extension.ePub");
-    runCommandLineTest(0, inputUrl.getPath());
-    assertTrue("Warning PKG-016 should be present when file has an incorrect extension",
-        errContent.toString().contains("WARNING(PKG-016)"));
-  }
-
-  /**
-   * Verify that a zipfile will inform the user that this is an uncommon
-   * extension.
-   */
-  @Test
-  public void extensionTest2()
-  {
-    URL inputUrl = CommandLineTest.class.getResource("wrong_extension.zip");
-    runCommandLineTest(0, inputUrl.getPath(), "--profile", "default");
-    assertTrue("Info PKG-024 should be present when file has an uncommon extension",
-        errContent.toString().contains("INFO(PKG-024)"));
-  }
-
-  /**
-   * Verify that an epub without extension works without any exceptions.
-   */
-  @Test
-  public void extensionTest3()
-  {
-    URL inputUrl = CommandLineTest.class.getResource("wrong_extension");
-    runCommandLineTest(0, inputUrl.getPath(), "--profile", "default");
-  }
-
-  /**
-   * Verify that an unreadable EPUB file raises an error
-   */
-  @Test
-  public void notfoundTest()
-  {
-    runCommandLineTest(1, "-mode", "nav", "-v", "3.0", "http://localhost:1234/notfound");
-    assertTrue(errContent.toString().contains("FATAL(PKG-008)"));
-  }
-
-  /**
-   * Validate that the -out parameter will generate a well formed xml output.
-   *
-   * @throws Exception
-   *         Any parsing errors will be thrown as an exception.
-   */
-  @Test
-  public void outputXMLReportTest()
-    throws Exception
-  {
-    File tmpFile = File.createTempFile("test", ".xml");
-
-    URL inputUrl = CommandLineTest.class.getResource("valid.epub");
-    runCommandLineTest(0, inputUrl.getPath(), "-out", tmpFile.getAbsolutePath());
-
-    DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
-    DocumentBuilder db = dbf.newDocumentBuilder();
-    db.parse(tmpFile);
-
-    if (tmpFile.exists())
-    {
-      tmpFile.delete();
-    }
-  }
-
-  /**
-   * Validate that the -out parameter will generate a well formed xml output for
-   * unpacked epubs.
-   *
-   * @throws Exception
-   *         Any parsing errors will be thrown as an exception.
-   */
-  @Test
-  public void outputXMLModeExpandedReportTest()
-    throws Exception
-  {
-    File tmpFile = File.createTempFile("test", ".xml");
-
-    URL inputUrl = CommandLineTest.class.getResource("30-valid-test");
-    runCommandLineTest(0, inputUrl.getPath(), "-mode", "exp", "-out", tmpFile.getAbsolutePath());
-
-    DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
-    DocumentBuilder db = dbf.newDocumentBuilder();
-    db.parse(tmpFile);
-
-    if (tmpFile.exists())
-    {
-      tmpFile.delete();
-    }
-  }
-
-  /**
-   * Verify that no extra output is present when quiet mode is enabled.
-   */
-  @Test
-  public void quietTest()
-  {
-    URL inputUrl = CommandLineTest.class.getResource("20-warning-tester");
-    runCommandLineTest(1, inputUrl.getPath(), "-mode", "exp", "--quiet", "--failonwarnings");
-
-    assertEquals("Output should not be present", 0, outContent.size());
-  }
-
-  /**
-   * Testing that no output is present when the flag quiet is set and that we
-   * get a correct xml output report with the output flag.
-   *
-   * @throws Exception
-   *         Any parsing errors will be thrown as an exception.
-   */
-  @Test
-  public void quietRunWithOutputTest()
-    throws Exception
-  {
-    File tmpFile = File.createTempFile("test", ".xml");
-
-    URL inputUrl = CommandLineTest.class.getResource("valid.epub");
-    runCommandLineTest(0, inputUrl.getPath(), "--quiet", "-out", tmpFile.getAbsolutePath());
-
-    assertEquals("Output should not be present", 0, outContent.size());
-
-    DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
-    DocumentBuilder db = dbf.newDocumentBuilder();
-    db.parse(tmpFile);
-
-    if (tmpFile.exists())
-    {
-      tmpFile.delete();
-    }
-  }
-
-  /**
-   * Testing that invalid options will return an invalid return code. We also
-   * need to inform the user which option was invalid.
-   */
-  @Test
-  public void invalidOptionTest()
-  {
-    URL inputUrl = CommandLineTest.class.getResource("valid.epub");
-    runCommandLineTest(1, inputUrl.getPath(), "--invalidoption");
-
-    assertTrue("Should contain the invalid option",
-        errContent.toString().contains("--invalidoption"));
-  }
-
-  /**
-   * When a translation is missing in that language it should fallback to the
-   * JVM default localization language.
-   */
-  @Test
-  public void missingTranslationShouldFallbackTest()
-  {
-    Locale temp = Locale.getDefault();
-    Locale.setDefault(Locale.FRANCE);
-
-    URL inputUrl = CommandLineTest.class.getResource("valid.epub");
-    runCommandLineTest(0, inputUrl.getPath(), "--locale", "ar-eg");
-
-    assertTrue("Valid Locale without translation should fallback to JVM default.",
-        outContent.toString().contains("faites en utilisant"));
-    Locale.setDefault(temp);
-  }
-
-  /**
-   * When a translation is missing in that language it should fallback to the
-   * JVM default localization language.
-   */
-  @Test
-  public void localeShouldTranslateTest()
-  {
-    URL inputUrl = CommandLineTest.class.getResource("valid.epub");
-    runCommandLineTest(0, inputUrl.getPath(), "--locale", "fr-FR");
-
-    assertTrue("Valid Locale without translation should fallback to JVM default.",
-        outContent.toString().contains("faites en utilisant"));
-  }
-
-  /**
-   * When the user has forgot to specify the value for the locale parameter it
-   * should be clear that it is missing from the command line.
-   */
-  @Test
-  public void skippedLocaleShouldFailTest()
-  {
-    URL inputUrl = CommandLineTest.class.getResource("valid.epub");
-    runCommandLineTest(1, inputUrl.getPath(), "--locale", "--bad");
-
-    assertTrue("User should be informed about skipped locale",
-        errContent.toString().contains("--bad"));
-  }
-
-  /**
-   * When the user uses an incorrect locale the translation should fallback on
-   * the JVM default language.
-   */
-  @Test
-  public void incorrectLocaleShouldFailTest()
-  {
-    Locale.setDefault(Locale.FRANCE);
-
-    URL inputUrl = CommandLineTest.class.getResource("valid.epub");
-
-    runCommandLineTest(0, inputUrl.getPath(), "--locale", "foobar");
-
-    assertTrue("Invalid Locale should use JVM default.",
-        outContent.toString().contains("faites en utilisant"));
-  }
-
-  /**
-   * When the locale parameter value is missing the missing_locale message
-   * should be present in error output.
-   */
-  @Test
-  public void missingLocaleShouldFailTest()
-  {
-    URL inputUrl = CommandLineTest.class.getResource("valid.epub");
-    runCommandLineTest(1, inputUrl.getPath(), "--locale");
-
-    assertEquals("Missing locale message should be present", messages.get("missing_locale"),
-        errContent.toString().trim());
-  }
-
-  /**
-   * Running with the question mark argument, expected that we create some
-   * output where output contains the version number.
-   */
-  @Test
-  public void helpMessageTest1()
-  {
-    runCommandLineTest(0, "-?");
-    assertEquals("", errContent.toString().trim());
-    String expected = String.format(messages.get("help_text").replaceAll("[\\s]+", " "),
-        EpubCheck.version());
-    String actual = outContent.toString();
-    actual = actual.replaceAll("[\\s]+", " ");
-    assertTrue("Help output isn't as expected", actual.contains(expected));
-  }
-
-  /**
-   * Running with the single dash help argument, expected that we create some
-   * output where output contains the version number.
-   */
-  @Test
-  public void helpMessageTest2()
-  {
-    runCommandLineTest(0, "-help");
-    assertEquals("", errContent.toString().trim());
-    String expected = String.format(messages.get("help_text").replaceAll("[\\s]+", " "),
-        EpubCheck.version());
-    String actual = outContent.toString();
-    actual = actual.replaceAll("[\\s]+", " ");
-    assertTrue("Help output isn't as expected", actual.contains(expected));
-  }
-
-  /**
-   * Running with the double dash help argument, expected that we create some
-   * output where output contains the version number.
-   */
-  @Test
-  public void helpMessageTest3()
-  {
-    runCommandLineTest(0, "--help");
-    assertEquals("", errContent.toString().trim());
-    String expected = String.format(messages.get("help_text").replaceAll("[\\s]+", " "),
-        EpubCheck.version());
-    String actual = outContent.toString();
-    actual = actual.replaceAll("[\\s]+", " ");
-    assertTrue("Help output isn't as expected", actual.contains(expected));
-  }
-
-  /**
-   * Testing that the version command could be opened using double dash syntax.
-   */
-  @Test
-  public void versionDisplayTest1()
-  {
-    runCommandLineTest(0, "--version");
-    assertEquals("", errContent.toString().trim());
-    String expected = String.format(
-        messages.get("epubcheck_version_text").replaceAll("[\\s]+", " "), EpubCheck.version());
-    String actual = outContent.toString();
-    actual = actual.replaceAll("[\\s]+", " ");
-    assertTrue("Help output isn't as expected", actual.contains(expected));
-  }
-
-  /**
-   * Testing that the version command could be opened using single dash syntax.
-   */
-  @Test
-  public void versionDisplayTest2()
-  {
-    runCommandLineTest(0, "-version");
-    assertEquals("", errContent.toString().trim());
-    String expected = String.format(
-        messages.get("epubcheck_version_text").replaceAll("[\\s]+", " "), EpubCheck.version());
-    String actual = outContent.toString();
-    actual = actual.replaceAll("[\\s]+", " ");
-    assertTrue("Help output isn't as expected", actual.contains(expected));
-  }
-
-  /**
-   * Tests that the version message is displayed on the first line
-   */
-  @Test
-  public void versionInfoComesFirst()
-  {
-    OutputStream out = new ByteArrayOutputStream();
-    System.setOut(new PrintStream(out));
-    System.setErr(new PrintStream(out));
-    URL inputUrl = CommandLineTest.class.getResource("30-mimetype-invalid");
-    runCommandLineTest(1, inputUrl.getPath(), "-mode", "exp");
-    int versionMessageIndex = out.toString().indexOf("Validating using");
-    int mimetypeErrorMessageIndex = out.toString().indexOf("mimetype");
-    assertTrue("Version message should be in the output.", versionMessageIndex >= 0);
-    assertTrue("PKG-007 should be in the output.", mimetypeErrorMessageIndex >= 0);
-    // FIXME 2022 ensure all messages are printed after the versioning info.
-    // Using a temporary report?
-    // assertTrue("Version message should be printed before PKG-007",
-    // versionMessageIndex < mimetypeErrorMessageIndex);
-  }
-
-  /**
-   * This test checks if multiple output formats are chosen, expected is an
-   * error message instructing user to choose either output formats but not
-   * both.
-   */
-  @Test
-  public void conflictingOutputTest()
-  {
-    runCommandLineTest(1, "-o", "foo.xml", "-j", "bar.json");
-    assertEquals("Command output not as expected", messages.get("output_type_conflict"),
-        errContent.toString().trim());
-  }
-
-  /**
-   * Checks if the usage messages are shown when the usage flag is set.
-   */
-  @Test
-  public void severitiesUsageTest()
-  {
-    URL inputUrl = CommandLineTest.class.getResource("20-severity-tester");
-    runCommandLineTest(1, "-u", "--mode", "exp", inputUrl.getPath());
-
-    assertTrue("Errors should be present", errorPattern.matcher(errContent.toString()).matches());
-    assertTrue("Warnings should be present",
-        warningPattern.matcher(errContent.toString()).matches());
-    assertTrue("Usage should be present", usagePattern.matcher(outContent.toString()).matches());
-  }
-
-  /**
-   * Checks if the warning messages are shown when the warning flag is set, but
-   * no usage messages.
-   */
-  @Test
-  public void severitiesWarningTest()
-  {
-    URL inputUrl = CommandLineTest.class.getResource("20-severity-tester");
-    runCommandLineTest(1, "-w", "--mode", "exp", inputUrl.getPath());
-
-    assertTrue("Errors should be present", errorPattern.matcher(errContent.toString()).matches());
-    assertTrue("Warnings should be present",
-        warningPattern.matcher(errContent.toString()).matches());
-    assertTrue("Usage should not be present",
-        !usagePattern.matcher(outContent.toString()).matches());
-  }
-
-  /**
-   * Checks if the error messages are shown when the error flag is set, but no
-   * usage or warning messages.
-   */
-  @Test
-  public void severitiesErrorTest()
-  {
-    URL inputUrl = CommandLineTest.class.getResource("20-severity-tester");
-    runCommandLineTest(1, "-e", "--mode", "exp", inputUrl.getPath());
-
-    assertTrue("Errors should be present", errorPattern.matcher(errContent.toString()).matches());
-    assertTrue("Warnings should not be present",
-        !warningPattern.matcher(errContent.toString()).matches());
-    assertTrue("Usage should not be present",
-        !usagePattern.matcher(outContent.toString()).matches());
-  }
-
-  /**
-   * Checks that no error, warning or usage messages are shown when fatal flag
-   * is set.
-   */
-  @Test
-  public void severitiesFatalTest()
-  {
-    URL inputUrl = CommandLineTest.class.getResource("20-severity-tester");
-    runCommandLineTest(0, "-f", "--mode", "exp", inputUrl.getPath());
-
-    assertTrue("Errors should not be present",
-        !errorPattern.matcher(errContent.toString()).matches());
-    assertTrue("Warnings should not be present",
-        !warningPattern.matcher(errContent.toString()).matches());
-    assertTrue("Usage should not be present",
-        !usagePattern.matcher(outContent.toString()).matches());
-  }
-
-  /**
-   * This test checks that we can override some error severity, messages and/or
-   * suppress them all together.
-   */
-  @Test
-  public void severitiesOverrideTest()
-  {
-    URL configUrl = CommandLineTest.class.getResource("severity_override.txt");
-    URL inputUrl = CommandLineTest.class.getResource("20-severity-tester");
-
-    runCommandLineTest(1, "-c", configUrl.getPath(), "-u", "--mode", "exp", inputUrl.getPath());
-
-    assertTrue("Errors should be present", errorPattern.matcher(errContent.toString()).matches());
-    assertTrue("Warnings should not be present",
-        !warningPattern.matcher(errContent.toString()).matches());
-    assertTrue("Usage should not be present",
-        !usagePattern.matcher(outContent.toString()).matches());
-
-    assertTrue("Overridden message should be present",
-        errContent.toString().contains("This is an overridden message"));
-  }
-
-  /**
-   * Ensures that the right error code is present when the override
-   * configuration file is missing.
-   */
-  @Test
-  public void severitiesOverrideMissingFileTest()
-  {
-    URL configUrl = CommandLineTest.class.getResource(".");
-    URL inputUrl = CommandLineTest.class.getResource("20-severity-tester");
-
-    runCommandLineTest(1, "-c", configUrl.getPath() + "/severity_override.missing_file", "-u",
-        "--mode", "exp", inputUrl.getPath());
-
-    assertTrue("Error CHK-001 should be present when file is missing",
-        errContent.toString().contains("ERROR(CHK-001)"));
-  }
-
-  /**
-   * Ensures that the right error code is present when the override
-   * configuration contains a severity id that is not valid.
-   */
-  @Test
-  public void severitiesOverrideBadIdTest()
-  {
-    URL configUrl = CommandLineTest.class.getResource("severity_override_bad_id.txt");
-    URL inputUrl = CommandLineTest.class.getResource("20-severity-tester");
-
-    runCommandLineTest(1, "-c", configUrl.getPath(), "-u", "--mode", "exp", inputUrl.getPath());
-
-    assertTrue("Error CHK-002 should be present when file contains a bad id",
-        errContent.toString().contains("ERROR(CHK-002)"));
-  }
-
-  /**
-   * Ensures that the right error code is present when the override
-   * configuration contains a severity value that is not valid.
-   */
-  @Test
-  public void severitiesOverrideBadSeverityTest()
-  {
-    URL configUrl = CommandLineTest.class.getResource("severity_override_bad_severity.txt");
-    URL inputUrl = CommandLineTest.class.getResource("20-severity-tester");
-
-    runCommandLineTest(1, "-c", configUrl.getPath(), "-u", "--mode", "exp", inputUrl.getPath());
-
-    assertTrue("Error CHK-003 should be present when file contains a bad severity",
-        errContent.toString().contains("ERROR(CHK-003)"));
-  }
-
-  /**
-   * Ensures that the right error code is present when the override
-   * configuration contains a severity message that is not valid. (Incorrect
-   * number of parameters)
-   */
-  @Test
-  public void severitiesOverrideBadMessageTest()
-  {
-    URL configUrl = CommandLineTest.class.getResource("severity_override_bad_message.txt");
-    URL inputUrl = CommandLineTest.class.getResource("20-severity-tester");
-
-    runCommandLineTest(1, "-c", configUrl.getPath(), "-u", "--mode", "exp", inputUrl.getPath());
-
-    assertTrue("Error CHK-004 should be present when file contains a bad message",
-        errContent.toString().contains("ERROR(CHK-004)"));
-  }
-
-  /**
-   * Ensures that the right error code is present when the override
-   * configuration contains a severity suggestion that is not valid. (Incorrect
-   * number of parameters)
-   */
-  @Test
-  public void severitiesOverrideBadSuggestionTest()
-  {
-    URL configUrl = CommandLineTest.class.getResource("severity_override_bad_suggestion.txt");
-    URL inputUrl = CommandLineTest.class.getResource("20-severity-tester");
-
-    runCommandLineTest(1, "-c", configUrl.getPath(), "-u", "--mode", "exp", inputUrl.getPath());
-
-    assertTrue("Error CHK-005 should be present when file contains a bad message",
-        errContent.toString().contains("ERROR(CHK-005)"));
-  }
-
-  /**
-   * This test contains warnings but should not fail. We expect the return code
-   * to be 0
-   */
-  @Test
-  public void passOnWarningsTest()
-  {
-    URL inputUrl = CommandLineTest.class.getResource("20-warning-tester");
-
-    runCommandLineTest(0, "-u", "--mode", "exp", inputUrl.getPath());
-  }
-
-  /**
-   * This test contains warnings and the flag --failonwarnings will force
-   * warnings to fail. We expect the return code to be 1.
-   */
-  @Test
-  public void failOnWarningsTest()
-  {
-    URL inputUrl = CommandLineTest.class.getResource("20-warning-tester");
-
-    runCommandLineTest(1, "-u", "--mode", "exp", "--failonwarnings", inputUrl.getPath());
-  }
-
-  /**
-   * Create an json file output and validate that it parses as a correct json
-   * document.
-   *
-   * @throws Exception
-   *         Throws an exception if the temp file can't be created.
-   */
-  @Test
-  public void jsonFileTest()
-    throws Exception
-  {
-    URL inputUrl = CommandLineTest.class.getResource("20-warning-tester");
-
-    File tmpFile = File.createTempFile("test", ".json");
-    runCommandLineTest(0, "--mode", "exp", inputUrl.getPath(), "-j", tmpFile.getAbsolutePath());
-
-    Object document = JSONValue.parse(new FileReader(tmpFile));
-    assertNotNull("Incorrect json", document);
-
-    if (tmpFile.exists())
-    {
-      tmpFile.delete();
-    }
-  }
-
-  /**
-   * Create xml file output and validate that it parses as a correct XML
-   * document.
-   *
-   * @throws Exception
-   *         Any parsing errors will be thrown as an exception.
-   */
-  @Test
-  public void xmlFileTest()
-    throws Exception
-  {
-    URL inputUrl = CommandLineTest.class.getResource("20-warning-tester");
-
-    File tmpFile = File.createTempFile("test", ".xml");
-    runCommandLineTest(0, "--mode", "exp", inputUrl.getPath(), "-o", tmpFile.getAbsolutePath());
-
-    DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
-    DocumentBuilder db = dbf.newDocumentBuilder();
-    db.parse(tmpFile);
-
-    if (tmpFile.exists())
-    {
-      tmpFile.delete();
-    }
-  }
-
-  /**
-   * Create xmp file output and validate that it parses as a correct XML
-   * document.
-   *
-   * @throws Exception
-   *         Any parsing errors will be thrown as an exception.
-   */
-  @Test
-  public void xmpFileTest()
-    throws Exception
-  {
-    URL inputUrl = CommandLineTest.class.getResource("20-warning-tester");
-
-    File tmpFile = File.createTempFile("test", ".xmp");
-    runCommandLineTest(0, "--mode", "exp", inputUrl.getPath(), "-x", tmpFile.getAbsolutePath());
-
-    DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
-    DocumentBuilder db = dbf.newDocumentBuilder();
-    db.parse(tmpFile);
-
-    if (tmpFile.exists())
-    {
-      tmpFile.delete();
-    }
-  }
-
-  public void runCommandLineTest(int expectedReturnCode, String... args)
-  {
-
-    int returnCode = new EpubChecker().run(args);
-    assertEquals("Return code", expectedReturnCode, returnCode);
-  }
-
-}
\ No newline at end of file
diff --git a/src/test/java/org/idpf/epubcheck/util/css/CssParserTest.java b/src/test/java/org/idpf/epubcheck/util/css/CssParserTest.java
index 2c9b5692..63d241b0 100644
--- a/src/test/java/org/idpf/epubcheck/util/css/CssParserTest.java
+++ b/src/test/java/org/idpf/epubcheck/util/css/CssParserTest.java
@@ -178,6 +178,13 @@ public class CssParserTest {
     HandlerImpl handler = checkBasics(exec(s));
     assertEquals(0, handler.errors.size());
   }
+
+  @Test
+  public void testParser016() throws Exception {
+    String s = "E { --my-property: ; } ";
+    HandlerImpl handler = checkBasics(exec(s));
+    assertEquals(0, handler.errors.size());
+  }
 			
 	@Test
 	public void testParserAtRule001() throws Exception {
diff --git a/src/test/java/org/w3c/epubcheck/container/FilenameCheckerSteps.java b/src/test/java/org/w3c/epubcheck/container/FilenameCheckerSteps.java
index 860f6fba..0e434560 100644
--- a/src/test/java/org/w3c/epubcheck/container/FilenameCheckerSteps.java
+++ b/src/test/java/org/w3c/epubcheck/container/FilenameCheckerSteps.java
@@ -1,6 +1,6 @@
 package org.w3c.epubcheck.container;
 
-import org.w3c.epubcheck.test.TestConfiguration;
+import org.w3c.epubcheck.test.EPUBCheckConfiguration;
 
 import com.adobe.epubcheck.ocf.OCFFilenameChecker;
 import com.adobe.epubcheck.opf.ValidationContext;
@@ -13,7 +13,7 @@ public class FilenameCheckerSteps
 
   private final ValidationContext context;
 
-  public FilenameCheckerSteps(TestConfiguration configuration)
+  public FilenameCheckerSteps(EPUBCheckConfiguration configuration)
   {
     this.context = configuration.getContextBuilder().build();
   }
diff --git a/src/test/java/org/w3c/epubcheck/test/AssertionSteps.java b/src/test/java/org/w3c/epubcheck/test/AssertionSteps.java
index 033119e5..e3d3662e 100644
--- a/src/test/java/org/w3c/epubcheck/test/AssertionSteps.java
+++ b/src/test/java/org/w3c/epubcheck/test/AssertionSteps.java
@@ -28,7 +28,7 @@ public class AssertionSteps
   private TestReport report;
   private MessageInfo lastAssertedMessage;
 
-  public AssertionSteps(TestConfiguration configuration)
+  public AssertionSteps(EPUBCheckConfiguration configuration)
   {
     this.report = configuration.getReport();
   }
diff --git a/src/test/java/org/w3c/epubcheck/test/CLISteps.java b/src/test/java/org/w3c/epubcheck/test/CLISteps.java
new file mode 100644
index 00000000..002db348
--- /dev/null
+++ b/src/test/java/org/w3c/epubcheck/test/CLISteps.java
@@ -0,0 +1,214 @@
+package org.w3c.epubcheck.test;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.emptyString;
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.not;
+import static org.hamcrest.Matchers.startsWith;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.PrintStream;
+import java.net.URL;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+
+import com.adobe.epubcheck.api.EpubCheck;
+import com.adobe.epubcheck.tool.EpubChecker;
+import com.google.common.base.Splitter;
+import com.google.common.collect.ImmutableMap;
+
+import io.cucumber.java.After;
+import io.cucumber.java.Before;
+import io.cucumber.java.ParameterType;
+import io.cucumber.java.Scenario;
+import io.cucumber.java.en.Given;
+import io.cucumber.java.en.Then;
+import io.cucumber.java.en.When;
+
+public class CLISteps
+{
+
+  private static final Map<String, String> REPLACEMENTS = ImmutableMap.<String, String> builder()
+      .put("{{VERSION}}", EpubCheck.version()).build();
+
+  private static Pattern TEST_FILE_PATTERN = Pattern.compile("^\\{\\{([\\S&&[^{}]]+)\\}\\}$");
+
+  private static String replace(String template)
+  {
+    String result = template;
+    for (Entry<String, String> replacement : REPLACEMENTS.entrySet())
+    {
+      result = result.replace(replacement.getKey(), replacement.getValue());
+    }
+    return result;
+  }
+
+  private final Locale defaultLocale = Locale.getDefault();
+  private final PrintStream systemOut = System.out;
+  private final PrintStream systemErr = System.err;
+  private final ByteArrayOutputStream stdout = new ByteArrayOutputStream();
+  private final ByteArrayOutputStream stderr = new ByteArrayOutputStream();
+  private final TestEnvironment environment;
+  private int exitCode;
+
+  public CLISteps(TestEnvironment environment)
+  {
+    this.environment = environment;
+  }
+
+  @ParameterType("stdout|stderr")
+  public String outputStream(String name)
+  {
+    switch (name)
+    {
+    case "stdout":
+      return stdout.toString();
+    case "stderr":
+      return stderr.toString();
+    default:
+      throw new IllegalArgumentException("Unknown output stream: " + name);
+    }
+  }
+
+  @Before
+  public void before()
+  {
+    Locale.setDefault(Locale.ENGLISH);
+    System.setOut(new PrintStream(stdout));
+    System.setErr(new PrintStream(stderr));
+  }
+
+  @After
+  public void after()
+  {
+    Locale.setDefault(defaultLocale);
+    System.setOut(systemOut);
+    System.setErr(systemErr);
+  }
+
+  @After
+  public void after(Scenario scenario)
+  {
+    scenario.getSourceTagNames();
+  }
+
+  @After("@debug")
+  public void afterDebug()
+  {
+    if (!stdout.toString().isEmpty())
+    {
+      System.out.println("\n==== Output Stream ===");
+      System.out.print(stdout);
+      System.out.println("======================");
+    }
+    if (!stderr.toString().isEmpty())
+    {
+      System.err.println("\n==== Error Stream ====");
+      System.err.print(stderr);
+      System.err.println("======================");
+    }
+  }
+  
+  @Given("stderr is redirected to stdout")
+  public void redirectErrorStream() {
+    System.setErr(new PrintStream(stdout));
+  }
+
+  @Then("the return code is {int}")
+  public void assertReturnCode(int code)
+  {
+    assertThat(exitCode, equalTo(code));
+  }
+
+  @Then("{outputStream} contains")
+  @Then("{outputStream} contains {string}")
+  public void assertStreamContains(String stream, String string)
+  {
+
+    assertThat(stream, containsString(string));
+  }
+
+  @Then("{outputStream} does not contain")
+  @Then("{outputStream} does not contain {string}")
+  public void assertStreamDoesNotContain(String stream, String string)
+  {
+
+    assertThat(stream, not(containsString(string)));
+  }
+
+  @Then("{outputStream} is empty")
+  public void assertStreamIs(String stream)
+  {
+    assertThat(stream, is(emptyString()));
+  }
+
+  @Then("{outputStream} is")
+  @Then("{outputStream} is {string}")
+  public void assertStreamIsEmpty(String stream, String expected)
+  {
+    assertThat(stream, equalTo(replace(expected)));
+  }
+
+  @Then("{outputStream} starts with")
+  @Then("{outputStream} starts with {string}")
+  public void assertStreamStartsWith(String stream, String string)
+  {
+    assertThat(stream, startsWith(string));
+  }
+
+  @Then("file {string} does not exist")
+  public void assertFileDoesNotExist(String path)
+  {
+    URL url = this.getClass().getResource(environment.getBasepath());
+    File file = new File(url.getPath() + path);
+    assertThat(file.exists(), is(false));
+  }
+
+  @Then("file {string} was created")
+  public void assertFileWasCreated(String path)
+  {
+    URL url = this.getClass().getResource(environment.getBasepath());
+    File file = new File(url.getPath() + path);
+    assertThat(file.exists(), is(true));
+    if (!file.delete())
+    {
+      throw new AssertionError("Could not delete file " + file);
+    }
+  }
+
+  @When("running `{}`")
+  public void run(String command)
+  {
+    // Set the configured locale
+    Locale.setDefault(environment.getDefaultLocale());
+
+    // Filter the command spec
+    List<String> args = Splitter.on(' ').omitEmptyStrings()
+        .splitToStream(command)
+        // Remove the (optional) `epubcheck` command name
+        .skip(1)
+        // Map the {{file.ext}} test files to actual file path
+        .map(arg -> {
+          String mapped = arg;
+          Matcher matcher = TEST_FILE_PATTERN.matcher(arg);
+          if (matcher.matches())
+          {
+            String path = matcher.toMatchResult().group(1);
+            URL url = this.getClass().getResource(environment.getBasepath());
+            mapped = url.getPath() + path;
+          }
+          return mapped;
+        }).collect(Collectors.toList());
+
+    exitCode = new EpubChecker().run(args.toArray(new String[0]));
+
+  }
+}
diff --git a/src/test/java/org/w3c/epubcheck/test/TestConfiguration.java b/src/test/java/org/w3c/epubcheck/test/EPUBCheckConfiguration.java
similarity index 84%
rename from src/test/java/org/w3c/epubcheck/test/TestConfiguration.java
rename to src/test/java/org/w3c/epubcheck/test/EPUBCheckConfiguration.java
index 9b13225d..ec17119f 100644
--- a/src/test/java/org/w3c/epubcheck/test/TestConfiguration.java
+++ b/src/test/java/org/w3c/epubcheck/test/EPUBCheckConfiguration.java
@@ -14,7 +14,7 @@ import io.cucumber.java.Before;
 import io.cucumber.java.en.And;
 import io.cucumber.java.en.Given;
 
-public class TestConfiguration
+public class EPUBCheckConfiguration
 {
 
 
@@ -51,11 +51,9 @@ public class TestConfiguration
   }
 
   private TestReport report = new TestReport();
-  private String basepath = "";
   private EPUBVersion version = EPUBVersion.VERSION_3;
   private CheckerMode mode = null;
   private EPUBProfile profile = EPUBProfile.DEFAULT;
-  private Locale defaultLocale = Locale.ENGLISH;
 
   public ValidationContextBuilder getContextBuilder()
   {
@@ -74,16 +72,6 @@ public class TestConfiguration
     report.setVerbose(false);
   }
 
-  public String getBasepath()
-  {
-    return basepath;
-  }
-
-  public Locale getDefaultLocale()
-  {
-    return defaultLocale;
-  }
-
   public EPUBVersion getEPUBVersion()
   {
     return version;
@@ -115,18 +103,6 @@ public class TestConfiguration
     // nothing to do
   }
 
-  @And("(EPUB )test files located at {string}")
-  public void setBasepath(String basepath)
-  {
-    this.basepath = basepath;
-  }
-
-  @And("(the )default locale (is )set to ('){locale}(')")
-  public void setDefaultLocale(Locale defaultLocale)
-  {
-    this.defaultLocale = defaultLocale;
-  }
-
   @And("EPUBCheck configured to check EPUB {version} rules")
   public void setEPUBVersion(EPUBVersion version)
   {
diff --git a/src/test/java/org/w3c/epubcheck/test/ExecutionSteps.java b/src/test/java/org/w3c/epubcheck/test/ExecutionSteps.java
index b047de69..6b5d3b8c 100644
--- a/src/test/java/org/w3c/epubcheck/test/ExecutionSteps.java
+++ b/src/test/java/org/w3c/epubcheck/test/ExecutionSteps.java
@@ -10,7 +10,7 @@ import java.net.URL;
 import java.util.Locale;
 
 import org.w3c.epubcheck.core.Checker;
-import org.w3c.epubcheck.test.TestConfiguration.CheckerMode;
+import org.w3c.epubcheck.test.EPUBCheckConfiguration.CheckerMode;
 import org.w3c.epubcheck.util.url.URLUtils;
 
 import com.adobe.epubcheck.api.EpubCheck;
@@ -30,10 +30,12 @@ import io.cucumber.java.en.When;
 public class ExecutionSteps
 {
 
-  private final TestConfiguration configuration;
+  private final TestEnvironment environment;
+  private final EPUBCheckConfiguration configuration;
 
-  public ExecutionSteps(TestConfiguration configuration)
+  public ExecutionSteps(TestEnvironment environment, EPUBCheckConfiguration configuration)
   {
+    this.environment = environment;
     this.configuration = configuration;
   }
 
@@ -43,22 +45,22 @@ public class ExecutionSteps
     Locale oldDefaultLocale = Locale.getDefault();
     try
     {
-      // Complete configuration and get the test file 
-      Locale.setDefault(configuration.getDefaultLocale());
+      // Complete configuration and get the test file
+      Locale.setDefault(environment.getDefaultLocale());
       if (configuration.getMode() == null)
       {
         configuration.setMode(CheckerMode.fromExtension(path));
       }
-      File testFile = getEPUBFile(configuration.getBasepath() + path);
-      
+      File testFile = getEPUBFile(environment.getBasepath() + path);
+
       // Initialize the report
       configuration.getReport().setEpubFileName(testFile.getAbsolutePath());
       configuration.getReport().initialize();
-      
+
       // Create the checker and run checks
       Checker checker = getChecker(testFile);
       checker.check();
-      
+
       // Finalize the report
       configuration.getReport().generate();
     } finally
diff --git a/src/test/java/org/w3c/epubcheck/test/JSONReportAssertionSteps.java b/src/test/java/org/w3c/epubcheck/test/JSONReportAssertionSteps.java
index b69fa4ff..e940331f 100644
--- a/src/test/java/org/w3c/epubcheck/test/JSONReportAssertionSteps.java
+++ b/src/test/java/org/w3c/epubcheck/test/JSONReportAssertionSteps.java
@@ -19,9 +19,9 @@ import io.cucumber.java.en.Then;
 public class JSONReportAssertionSteps
 {
 
-  private TestReport report;
+  final private TestReport report;
 
-  public JSONReportAssertionSteps(TestConfiguration configuration)
+  public JSONReportAssertionSteps(EPUBCheckConfiguration configuration)
   {
     this.report = configuration.getReport();
   }
diff --git a/src/test/java/org/w3c/epubcheck/test/ParameterTypes.java b/src/test/java/org/w3c/epubcheck/test/ParameterTypes.java
index fecbf9f5..81559276 100644
--- a/src/test/java/org/w3c/epubcheck/test/ParameterTypes.java
+++ b/src/test/java/org/w3c/epubcheck/test/ParameterTypes.java
@@ -9,7 +9,7 @@ import java.util.List;
 import java.util.Locale;
 
 import org.hamcrest.Matcher;
-import org.w3c.epubcheck.test.TestConfiguration.CheckerMode;
+import org.w3c.epubcheck.test.EPUBCheckConfiguration.CheckerMode;
 
 import com.adobe.epubcheck.api.EPUBProfile;
 import com.adobe.epubcheck.messages.MessageId;
diff --git a/src/test/java/org/w3c/epubcheck/test/TestEnvironment.java b/src/test/java/org/w3c/epubcheck/test/TestEnvironment.java
new file mode 100644
index 00000000..c5f9a7ea
--- /dev/null
+++ b/src/test/java/org/w3c/epubcheck/test/TestEnvironment.java
@@ -0,0 +1,36 @@
+package org.w3c.epubcheck.test;
+
+import java.util.Locale;
+
+import io.cucumber.java.en.And;
+
+public class TestEnvironment
+{
+
+  private String basepath = "";
+  private Locale defaultLocale = Locale.ENGLISH;
+
+
+  public String getBasepath()
+  {
+    return basepath;
+  }
+
+  public Locale getDefaultLocale()
+  {
+    return defaultLocale;
+  }
+
+  @And("(the )(EPUB )test files (are)located at {string}")
+  public void setBasepath(String basepath)
+  {
+    this.basepath = basepath;
+  }
+
+  @And("(the )default locale (is )(set to )('){locale}(')")
+  public void setDefaultLocale(Locale defaultLocale)
+  {
+    this.defaultLocale = defaultLocale;
+  }
+
+}
diff --git a/src/test/java/org/w3c/epubcheck/test/XMLReportAssertionSteps.java b/src/test/java/org/w3c/epubcheck/test/XMLReportAssertionSteps.java
new file mode 100644
index 00000000..fdd81b21
--- /dev/null
+++ b/src/test/java/org/w3c/epubcheck/test/XMLReportAssertionSteps.java
@@ -0,0 +1,115 @@
+package org.w3c.epubcheck.test;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.emptyIterable;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.not;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.PrintStream;
+import java.io.StringReader;
+import java.util.function.Supplier;
+
+import javax.xml.transform.stream.StreamSource;
+
+import io.cucumber.java.en.Given;
+import io.cucumber.java.en.Then;
+import net.sf.saxon.s9api.DocumentBuilder;
+import net.sf.saxon.s9api.Processor;
+import net.sf.saxon.s9api.SaxonApiException;
+import net.sf.saxon.s9api.XPathCompiler;
+import net.sf.saxon.s9api.XPathSelector;
+import net.sf.saxon.s9api.XdmNode;
+
+public class XMLReportAssertionSteps
+{
+
+  private final TestReport report;
+  private final Processor processor;
+  private final XPathCompiler xpathCompiler;
+  Supplier<XdmNode> xml = new Supplier<XdmNode>()
+  {
+    XdmNode xml = null;
+
+    @Override
+    public XdmNode get()
+    {
+      if (xml == null)
+      {
+
+        // Parse the XML report
+        DocumentBuilder docBuilder = processor.newDocumentBuilder();
+        try
+        {
+          xml = docBuilder.build(new StreamSource(new StringReader(report.getOutput())));
+        } catch (SaxonApiException e)
+        {
+          assertThat(e.getMessage(), false);
+        }
+      }
+      return xml;
+    }
+  };
+
+  public XMLReportAssertionSteps(EPUBCheckConfiguration configuration)
+  {
+    this.report = configuration.getReport();
+
+    // Configure Saxon
+    this.processor = new Processor(false);
+    processor.getUnderlyingConfiguration()
+        .setStandardErrorOutput(new PrintStream(new OutputStream()
+        {
+          // TODO when upgrading to Java 11, replace by
+          // OutputStream#nullOutputStream()
+          @Override
+          public void write(int b)
+            throws IOException
+          {
+          }
+        }));
+    xpathCompiler = processor.newXPathCompiler();
+  }
+
+  @Given("the default namespace is {string}")
+  public void setDefaultNamespace(String namespace)
+  {
+    xpathCompiler.declareNamespace("", namespace);
+  }
+
+  @Then("the XML report is well-formed")
+  public void xmlIsWellFormed()
+  {
+    // Parsing errors would be raised in the constructor already
+  }
+
+  @Then("(the )XPath (value of ){string} is true")
+  public void xpathIsTrue(String xpath)
+    throws SaxonApiException
+  {
+    assertThat(eval(xpath).effectiveBooleanValue(), is(true));
+  }
+
+  @Then("(the )XPath {string} exists")
+  public void xpathExists(String xpath)
+    throws SaxonApiException
+  {
+    assertThat(eval(xpath), is(not(emptyIterable())));
+
+  }
+
+  private XPathSelector eval(String xpath)
+  {
+    try
+    {
+      XPathSelector result = xpathCompiler.compile(xpath).load();
+      result.setContextItem(xml.get());
+      return result;
+    } catch (SaxonApiException e)
+    {
+      throw new IllegalArgumentException(e);
+    }
+  }
+
+}
diff --git a/src/test/resources/cli/cli.feature b/src/test/resources/cli/cli.feature
new file mode 100644
index 00000000..86b53ab2
--- /dev/null
+++ b/src/test/resources/cli/cli.feature
@@ -0,0 +1,355 @@
+Feature: EPUBCheck Command Line
+
+	Tests the `epubcheck` command line interface.
+
+
+
+	Background:
+		Given EPUB test files located at '/cli/files/'
+
+
+
+	Rule: Display help
+
+		Example: Option `-h` is used to display the usage message 
+			When running `epubcheck -h`
+			Then the return code is 0
+			And stdout is
+				```
+				EPUBCheck v{{VERSION}}
+				
+				When running this tool, the first argument should be the name (with the path)
+				of the file to check.
+				
+				To specify a validation profile (to run checks against a specific EPUB 3 profile
+				or extension specification), use the -profile option:
+				
+				Validation profiles supported:
+				--profile default        = the default validation profile
+				--profile dict           = validates against the EPUB Dictionaries and Glossaries specification
+				--profile edupub         = validates against the EDUPUB Profile
+				--profile idx            = validates against the EPUB Indexes specification
+				--profile preview        = validates against the EPUB Previews specification
+				
+				If checking a non-epub file, the epub version of the file must
+				be specified using -v and the type of the file using -mode.
+				The default version is: 3.0.
+				
+				Modes and versions supported:
+				--mode opf -v 2.0
+				--mode opf -v 3.0
+				--mode xhtml -v 2.0
+				--mode xhtml -v 3.0
+				--mode svg -v 2.0
+				--mode svg -v 3.0
+				--mode nav -v 3.0
+				--mode mo  -v 3.0        = for Media Overlays validation
+				--mode exp               = for expanded EPUB archives
+				
+				This tool also accepts the following options:
+				--save                   = saves the epub created from the expanded epub
+				--out <file>             = output an assessment XML document file (use - to output to console)
+				--xmp <file>             = output an assessment XMP document file (use - to output to console)
+				--json <file>            = output an assessment JSON document file (use - to output to console)
+				-m <file>                = same as --mode
+				-p <profile>             = same as --profile
+				-o <file>                = same as --out
+				-x <file>                = same as --xmp
+				-j <file>                = same as --json
+				--failonwarnings         = By default, the tool returns a 1 if errors are found in the file or 0 if no errors
+				                           are found.  Using --failonwarnings will cause the process to exit with a status of
+				                           1 if either warnings or errors are present and 0 only when there are no errors or warnings.
+				-q, --quiet              = no message on console, except errors, only in the output
+				-f, --fatal              = include only fatal errors in the output
+				-e, --error              = include only error and fatal severity messages in ouput
+				-w, --warn               = include fatal, error, and warn severity messages in output
+				-u, --usage              = include ePub feature usage information in output
+				                           (default is OFF); if enabled, usage information will
+				                           always be included in the output file
+				--locale <locale>        = output localized messages according to the provided IETF BCP 47 language tag string.
+				
+				-l, --listChecks [<file>]       = list message ids and severity levels to the custom message file named <file>
+				                                  or the console
+				-c, --customMessages [<file>]   = override message severity levels as defined in the custom message file named <file>
+				
+				--version                = displays the EPUBCheck version
+				
+				-h, -? or --help         = displays this help message
+
+
+				```
+
+		Example: Option `-?` is used to display the usage message 
+			When running `epubcheck -?`
+			Then the return code is 0
+			And stdout contains "displays this help message"
+
+		Example: Option `-help` is used to display the usage message 
+			When running `epubcheck -?`
+			Then the return code is 0
+			And stdout contains "displays this help message"
+
+		Example: Option `--help` is used to display the usage message 
+			When running `epubcheck -?`
+			Then the return code is 0
+			And stdout contains "displays this help message"
+
+
+
+	Rule: Display version information
+
+		Example: Option `-version` is used to display the version
+			When running `epubcheck -version`
+			Then the return code is 0
+			Then stdout is
+				```
+				EPUBCheck v{{VERSION}}
+
+				```
+
+		Example: Option `--version` is used to display the version
+			When running `epubcheck --version`
+			Then the return code is 0
+			Then stdout is
+				```
+				EPUBCheck v{{VERSION}}
+
+				```
+
+		Example: Option `--version` is used when checking file
+			When running `epubcheck --version {{valid.epub}}`
+			Then the return code is 0
+			Then stdout starts with "EPUBCheck v"
+			And stdout contains "No errors or warnings detected."
+
+
+
+	Rule: Reject invalid commands
+
+		Example: no command argument
+			When running `epubcheck`
+			Then the return code is 1
+			And stderr contains 'At least one argument expected'
+
+		Example: unknown option
+			When running `epubcheck --unknown {{valid.epub}}`
+			Then the return code is 1
+			And stderr contains 'Unrecognized argument: "--unknown"'
+
+
+
+	Rule: Check EPUB publication
+
+		Example: check a valid packaged EPUB
+			When running `epubcheck {{valid.epub}}`
+			Then the return code is 0
+			And stderr is empty
+
+		Example: check a packaged EPUB with warnings
+			When running `epubcheck -mode exp {{20-warning-tester}}`
+			Then the return code is 0
+			And stderr contains 'WARNING(PKG-010)'
+
+		Example: information about the EPUB version comes first
+			Given stderr is redirected to stdout
+			When running `epubcheck -mode exp {{30-mimetype-invalid}}`
+			# FIXME 2022 ensure all messages are printed after the versioning info.
+			# Then stdout starts with "Validating using EPUB version"
+
+		Example: check a packaged EPUB with poorly cased extension
+			# TODO this should be tested outside CLI tests
+			When running `epubcheck {{wrong_extension.ePub}}`
+			Then the return code is 0
+			And stderr contains 'WARNING(PKG-016)'
+
+		Example: check a packaged EPUB with non-epub extension
+			# TODO this should be tested outside CLI tests
+			When running `epubcheck --profile default {{wrong_extension.zip}}`
+			And stderr contains 'INFO(PKG-024)'
+			Then the return code is 0
+
+		Example: check a packaged EPUB with no extension
+			# TODO this should probably raise a usage
+			When running `epubcheck --profile default {{wrong_extension}}`
+			Then the return code is 0
+
+
+
+	Rule: Check single EPUB files
+
+		Example: check a single valid navigation document
+			When running `epubcheck -mode nav {{30-valid-test/OPS/nav.xhtml}}`
+			Then the return code is 0
+
+		Example: check a single invalid navigation document
+			When running `epubcheck -mode nav {{nav-no-toc.xhtml}}`
+			Then the return code is 1
+
+		Example: check an unreadable file
+			# TODO this should be tested outside CLI tests
+			When running `epubcheck --mode nav http://localhost/notfound`
+			Then the return code is 1
+
+
+
+	Rule: Save an EPUB archive
+
+		Example: save the resulting EPUB
+			Given file '30-valid-test.epub' does not exist
+			When running `epubcheck --mode exp {{30-valid-test}} --save`
+			Then the return code is 0
+			And file '30-valid-test.epub' was created
+
+
+
+	Rule: Configure the verbosity
+
+		Example: quiet mode prevents extra output
+			When running `epubcheck -mode exp {{20-warning-tester}} --quiet`
+			Then stdout is empty
+
+		Example: quiet mode does not conflict with saving a report
+			When running `epubcheck {{valid.epub}} --quiet -out {{report.xml}}`
+			Then the return code is 0
+			And stdout is empty
+			But file 'report.xml' was created
+
+		Example: USAGE messages are not reported by default
+			When running `epubcheck -mode exp {{20-severity-tester}}`
+			And stdout does not contain 'USAGE'
+
+		Example: USAGE messages are reported with option `-u`
+			When running `epubcheck -u -mode exp {{20-severity-tester}}`
+			And stdout contains 'USAGE'
+
+		Example: WARNING messages are reported with option `-w`
+			When running `epubcheck -w -mode exp {{20-severity-tester}}`
+			Then the return code is 1
+			And stdout does not contain 'USAGE'
+			And stderr contains 'WARNING'
+			And stderr contains 'ERROR'
+
+		Example: WARNING messages are silenced with option `-e`
+			When running `epubcheck -e -mode exp {{20-severity-tester}}`
+			Then the return code is 1
+			And stdout does not contain 'USAGE'
+			And stderr does not contain 'WARNING'
+			And stderr contains 'ERROR'
+
+		Example: ERROR messages are silenced with option `-f`
+			When running `epubcheck -f -mode exp {{20-severity-tester}}`
+			Then the return code is 0
+			And stdout does not contain 'USAGE'
+			And stderr does not contain 'WARNING'
+			And stderr does not contain 'ERROR'
+
+
+
+	Rule: Configure the rejection level
+
+		Example: option `--faileonwarnings` make the command fail when a WARNING is reported
+			When running `epubcheck --failonwarnings -mode exp {{20-warning-tester}}`
+			Then the return code is 1
+			And stderr contains 'WARNING(PKG-010)'
+
+
+
+	Rule: Output reports
+
+		Example: save an XML report with the `-out` option
+			Given file 'report.xml' does not exist
+			When running `epubcheck {{valid.epub}} -out {{report.xml}}`
+			Then the return code is 0
+			And file 'report.xml' was created
+
+		Example: save an XML report with the `-o` option
+			Given file 'report.xml' does not exist
+			When running `epubcheck {{valid.epub}} -o {{report.xml}}`
+			Then the return code is 0
+			And file 'report.xml' was created
+
+		Example: save an XMP report with the `-x` option
+			Given file 'report.xmp' does not exist
+			When running `epubcheck {{valid.epub}} -out {{report.xmp}}`
+			Then the return code is 0
+			And file 'report.xmp' was created
+
+		Example: save a JSON report with the `-j` option
+			Given file 'report.json' does not exist
+			When running `epubcheck {{valid.epub}} -j {{report.json}}`
+			Then the return code is 0
+			And file 'report.json' was created
+
+		Example: conflicting report formats are rejected
+			When running `epubcheck {{valid.epub}} -o {{report.xml}} -j {{report.json}}`
+			Then the return code is 1
+			And stderr contains "Only one output format can be specified at a time."
+			And file 'report.json' does not exist
+			And file 'report.xml' does not exist
+
+
+
+	Rule: Localize messages
+
+		Example: `--locale` option can be used to localize messages
+			Given the default locale is 'en-EN'
+			When running `epubcheck {{valid.epub}} --locale fr-FR`
+			Then stdout contains 'Aucune erreur ou avertissement détecté.'
+
+		Example: unsupported locale falls back to the default locale
+			Given the default locale is 'fr-FR'
+			When running `epubcheck {{valid.epub}} --locale ar-eg`
+			Then stdout contains 'Aucune erreur ou avertissement détecté.'
+
+		Example: invalid locale falls back to the default locale
+			Given the default locale is 'fr-FR'
+			When running `epubcheck {{valid.epub}} --locale foobar`
+			Then stdout contains 'Aucune erreur ou avertissement détecté.'
+
+		Example: missing locale argument makes the command fail
+			When running `epubcheck {{valid.epub}} --locale -h`
+			Then the return code is 1
+			And stderr contains 'Argument "-h" to the --locale option is incorrect.'
+
+		Example: missing locale argument when last option makes the command fail
+			When running `epubcheck {{valid.epub}} --locale`
+			Then the return code is 1
+			And stderr contains 'Argument to the --locale option is missing.'
+
+
+
+	Rule: Override message severities
+
+		Example: messages and severities are overridden with the `-c` option
+			When running `epubcheck -u -mode exp {{20-severity-tester}} -c {{severity_override.txt}}`
+			Then the return code is 1
+			And stdout does not contain 'USAGE('
+			And stderr does not contain 'WARNING('
+			And stderr contains 'ERROR('
+			And stderr contains 'This is an overridden message'
+
+		Example: messages and severities overridden with the `-c` option
+			When running `epubcheck -u -mode exp {{20-severity-tester}} -c {{severity_override_missing.txt}}`
+			Then the return code is 1
+			And stderr contains 'ERROR(CHK-001)'
+
+		Example: report an error when overriding an unknown message ID
+			When running `epubcheck -u -mode exp {{20-severity-tester}} -c {{severity_override_bad_id.txt}}`
+			Then the return code is 1
+			And stderr contains 'ERROR(CHK-002)'
+
+		Example: report an error when overriding to an unknown severity
+			When running `epubcheck -u -mode exp {{20-severity-tester}} -c {{severity_override_bad_severity.txt}}`
+			Then the return code is 1
+			And stderr contains 'ERROR(CHK-003)'
+
+		Example: report an error when overriding to a message with parameters mismatch
+			When running `epubcheck -u -mode exp {{20-severity-tester}} -c {{severity_override_bad_message.txt}}`
+			Then the return code is 1
+			And stderr contains 'ERROR(CHK-004)'
+
+		Example: report an error when overriding to a message suggestion with parameters mismatch
+			When running `epubcheck -u -mode exp {{20-severity-tester}} -c {{severity_override_bad_suggestion.txt}}`
+			Then the return code is 1
+			And stderr contains 'ERROR(CHK-005)'
+
diff --git a/src/test/resources/com/adobe/epubcheck/tools/20-severity-tester/META-INF/container.xml b/src/test/resources/cli/files/20-severity-tester/META-INF/container.xml
similarity index 100%
rename from src/test/resources/com/adobe/epubcheck/tools/20-severity-tester/META-INF/container.xml
rename to src/test/resources/cli/files/20-severity-tester/META-INF/container.xml
diff --git a/src/test/resources/com/adobe/epubcheck/tools/20-severity-tester/OPS/content 001.xhtml b/src/test/resources/cli/files/20-severity-tester/OPS/content 001.xhtml
similarity index 100%
rename from src/test/resources/com/adobe/epubcheck/tools/20-severity-tester/OPS/content 001.xhtml
rename to src/test/resources/cli/files/20-severity-tester/OPS/content 001.xhtml
diff --git a/src/test/resources/com/adobe/epubcheck/tools/20-severity-tester/OPS/package.opf b/src/test/resources/cli/files/20-severity-tester/OPS/package.opf
similarity index 100%
rename from src/test/resources/com/adobe/epubcheck/tools/20-severity-tester/OPS/package.opf
rename to src/test/resources/cli/files/20-severity-tester/OPS/package.opf
diff --git a/src/test/resources/com/adobe/epubcheck/tools/20-severity-tester/OPS/toc.ncx b/src/test/resources/cli/files/20-severity-tester/OPS/toc.ncx
similarity index 100%
rename from src/test/resources/com/adobe/epubcheck/tools/20-severity-tester/OPS/toc.ncx
rename to src/test/resources/cli/files/20-severity-tester/OPS/toc.ncx
diff --git a/src/test/resources/com/adobe/epubcheck/tools/20-severity-tester/mimetype b/src/test/resources/cli/files/20-severity-tester/mimetype
similarity index 100%
rename from src/test/resources/com/adobe/epubcheck/tools/20-severity-tester/mimetype
rename to src/test/resources/cli/files/20-severity-tester/mimetype
diff --git a/src/test/resources/com/adobe/epubcheck/tools/20-warning-tester/META-INF/container.xml b/src/test/resources/cli/files/20-warning-tester/META-INF/container.xml
similarity index 100%
rename from src/test/resources/com/adobe/epubcheck/tools/20-warning-tester/META-INF/container.xml
rename to src/test/resources/cli/files/20-warning-tester/META-INF/container.xml
diff --git a/src/test/resources/com/adobe/epubcheck/tools/20-warning-tester/OPS/content 001.xhtml b/src/test/resources/cli/files/20-warning-tester/OPS/content 001.xhtml
similarity index 100%
rename from src/test/resources/com/adobe/epubcheck/tools/20-warning-tester/OPS/content 001.xhtml
rename to src/test/resources/cli/files/20-warning-tester/OPS/content 001.xhtml
diff --git a/src/test/resources/com/adobe/epubcheck/tools/20-warning-tester/OPS/package.opf b/src/test/resources/cli/files/20-warning-tester/OPS/package.opf
similarity index 100%
rename from src/test/resources/com/adobe/epubcheck/tools/20-warning-tester/OPS/package.opf
rename to src/test/resources/cli/files/20-warning-tester/OPS/package.opf
diff --git a/src/test/resources/com/adobe/epubcheck/tools/20-warning-tester/OPS/toc.ncx b/src/test/resources/cli/files/20-warning-tester/OPS/toc.ncx
similarity index 100%
rename from src/test/resources/com/adobe/epubcheck/tools/20-warning-tester/OPS/toc.ncx
rename to src/test/resources/cli/files/20-warning-tester/OPS/toc.ncx
diff --git a/src/test/resources/com/adobe/epubcheck/tools/20-warning-tester/mimetype b/src/test/resources/cli/files/20-warning-tester/mimetype
similarity index 100%
rename from src/test/resources/com/adobe/epubcheck/tools/20-warning-tester/mimetype
rename to src/test/resources/cli/files/20-warning-tester/mimetype
diff --git a/src/test/resources/com/adobe/epubcheck/tools/30-mimetype-invalid/META-INF/container.xml b/src/test/resources/cli/files/30-mimetype-invalid/META-INF/container.xml
similarity index 100%
rename from src/test/resources/com/adobe/epubcheck/tools/30-mimetype-invalid/META-INF/container.xml
rename to src/test/resources/cli/files/30-mimetype-invalid/META-INF/container.xml
diff --git a/src/test/resources/com/adobe/epubcheck/tools/30-mimetype-invalid/OPS/content_001.xhtml b/src/test/resources/cli/files/30-mimetype-invalid/OPS/content_001.xhtml
similarity index 100%
rename from src/test/resources/com/adobe/epubcheck/tools/30-mimetype-invalid/OPS/content_001.xhtml
rename to src/test/resources/cli/files/30-mimetype-invalid/OPS/content_001.xhtml
diff --git a/src/test/resources/com/adobe/epubcheck/tools/30-mimetype-invalid/OPS/nav.xhtml b/src/test/resources/cli/files/30-mimetype-invalid/OPS/nav.xhtml
similarity index 100%
rename from src/test/resources/com/adobe/epubcheck/tools/30-mimetype-invalid/OPS/nav.xhtml
rename to src/test/resources/cli/files/30-mimetype-invalid/OPS/nav.xhtml
diff --git a/src/test/resources/com/adobe/epubcheck/tools/30-mimetype-invalid/OPS/package.opf b/src/test/resources/cli/files/30-mimetype-invalid/OPS/package.opf
similarity index 100%
rename from src/test/resources/com/adobe/epubcheck/tools/30-mimetype-invalid/OPS/package.opf
rename to src/test/resources/cli/files/30-mimetype-invalid/OPS/package.opf
diff --git a/src/test/resources/com/adobe/epubcheck/tools/30-mimetype-invalid/mimetype b/src/test/resources/cli/files/30-mimetype-invalid/mimetype
similarity index 100%
rename from src/test/resources/com/adobe/epubcheck/tools/30-mimetype-invalid/mimetype
rename to src/test/resources/cli/files/30-mimetype-invalid/mimetype
diff --git a/src/test/resources/com/adobe/epubcheck/tools/30-valid-test/META-INF/container.xml b/src/test/resources/cli/files/30-valid-test/META-INF/container.xml
similarity index 100%
rename from src/test/resources/com/adobe/epubcheck/tools/30-valid-test/META-INF/container.xml
rename to src/test/resources/cli/files/30-valid-test/META-INF/container.xml
diff --git a/src/test/resources/com/adobe/epubcheck/tools/30-valid-test/OPS/content_001.xhtml b/src/test/resources/cli/files/30-valid-test/OPS/content_001.xhtml
similarity index 100%
rename from src/test/resources/com/adobe/epubcheck/tools/30-valid-test/OPS/content_001.xhtml
rename to src/test/resources/cli/files/30-valid-test/OPS/content_001.xhtml
diff --git a/src/test/resources/com/adobe/epubcheck/tools/30-valid-test/OPS/nav.xhtml b/src/test/resources/cli/files/30-valid-test/OPS/nav.xhtml
similarity index 100%
rename from src/test/resources/com/adobe/epubcheck/tools/30-valid-test/OPS/nav.xhtml
rename to src/test/resources/cli/files/30-valid-test/OPS/nav.xhtml
diff --git a/src/test/resources/com/adobe/epubcheck/tools/30-valid-test/OPS/package.opf b/src/test/resources/cli/files/30-valid-test/OPS/package.opf
similarity index 100%
rename from src/test/resources/com/adobe/epubcheck/tools/30-valid-test/OPS/package.opf
rename to src/test/resources/cli/files/30-valid-test/OPS/package.opf
diff --git a/src/test/resources/com/adobe/epubcheck/tools/30-valid-test/mimetype b/src/test/resources/cli/files/30-valid-test/mimetype
similarity index 100%
rename from src/test/resources/com/adobe/epubcheck/tools/30-valid-test/mimetype
rename to src/test/resources/cli/files/30-valid-test/mimetype
diff --git a/src/test/resources/com/adobe/epubcheck/tools/nav-no-toc.xhtml b/src/test/resources/cli/files/nav-no-toc.xhtml
similarity index 100%
rename from src/test/resources/com/adobe/epubcheck/tools/nav-no-toc.xhtml
rename to src/test/resources/cli/files/nav-no-toc.xhtml
diff --git a/src/test/resources/com/adobe/epubcheck/tools/severity_override.txt b/src/test/resources/cli/files/severity_override.txt
similarity index 100%
rename from src/test/resources/com/adobe/epubcheck/tools/severity_override.txt
rename to src/test/resources/cli/files/severity_override.txt
diff --git a/src/test/resources/com/adobe/epubcheck/tools/severity_override_bad_id.txt b/src/test/resources/cli/files/severity_override_bad_id.txt
similarity index 100%
rename from src/test/resources/com/adobe/epubcheck/tools/severity_override_bad_id.txt
rename to src/test/resources/cli/files/severity_override_bad_id.txt
diff --git a/src/test/resources/com/adobe/epubcheck/tools/severity_override_bad_message.txt b/src/test/resources/cli/files/severity_override_bad_message.txt
similarity index 100%
rename from src/test/resources/com/adobe/epubcheck/tools/severity_override_bad_message.txt
rename to src/test/resources/cli/files/severity_override_bad_message.txt
diff --git a/src/test/resources/com/adobe/epubcheck/tools/severity_override_bad_severity.txt b/src/test/resources/cli/files/severity_override_bad_severity.txt
similarity index 100%
rename from src/test/resources/com/adobe/epubcheck/tools/severity_override_bad_severity.txt
rename to src/test/resources/cli/files/severity_override_bad_severity.txt
diff --git a/src/test/resources/com/adobe/epubcheck/tools/severity_override_bad_suggestion.txt b/src/test/resources/cli/files/severity_override_bad_suggestion.txt
similarity index 100%
rename from src/test/resources/com/adobe/epubcheck/tools/severity_override_bad_suggestion.txt
rename to src/test/resources/cli/files/severity_override_bad_suggestion.txt
diff --git a/src/test/resources/com/adobe/epubcheck/tools/valid.epub b/src/test/resources/cli/files/valid.epub
similarity index 100%
rename from src/test/resources/com/adobe/epubcheck/tools/valid.epub
rename to src/test/resources/cli/files/valid.epub
diff --git a/src/test/resources/com/adobe/epubcheck/tools/wrong_extension b/src/test/resources/cli/files/wrong_extension
similarity index 100%
rename from src/test/resources/com/adobe/epubcheck/tools/wrong_extension
rename to src/test/resources/cli/files/wrong_extension
diff --git a/src/test/resources/com/adobe/epubcheck/tools/wrong_extension.ePub b/src/test/resources/cli/files/wrong_extension.ePub
similarity index 100%
rename from src/test/resources/com/adobe/epubcheck/tools/wrong_extension.ePub
rename to src/test/resources/cli/files/wrong_extension.ePub
diff --git a/src/test/resources/com/adobe/epubcheck/tools/wrong_extension.zip b/src/test/resources/cli/files/wrong_extension.zip
similarity index 100%
rename from src/test/resources/com/adobe/epubcheck/tools/wrong_extension.zip
rename to src/test/resources/cli/files/wrong_extension.zip
diff --git a/src/test/resources/css/custom-properties.css b/src/test/resources/css/custom-properties.css
index 55a9e719..eb69d257 100644
--- a/src/test/resources/css/custom-properties.css
+++ b/src/test/resources/css/custom-properties.css
@@ -4,6 +4,7 @@
 
 :root {
   --main-bg-color: brown;
+  --empty:;
 }
 
 .one {
diff --git a/src/test/resources/epub-accessibility/accessibility.feature b/src/test/resources/epub-accessibility/accessibility.feature
index be6f1abf..7b84fcfd 100644
--- a/src/test/resources/epub-accessibility/accessibility.feature
+++ b/src/test/resources/epub-accessibility/accessibility.feature
@@ -23,3 +23,7 @@ Feature: EPUB 3 — Accessibility
   Scenario: Verify an 'a11y:certifierCredential' property can be defined as a link
     When checking file 'link-rel-a11y-certifierCredential-valid.opf'
     Then no errors or warnings are reported
+
+  Scenario: Allow using the 'a11y:exemption' property in package metadata
+    When checking file 'metadata-a11y-exemption-valid.opf'
+    Then no errors or warnings are reported
diff --git a/src/test/resources/epub-accessibility/files/metadata-a11y-exemption-valid.opf b/src/test/resources/epub-accessibility/files/metadata-a11y-exemption-valid.opf
new file mode 100644
index 00000000..4f158877
--- /dev/null
+++ b/src/test/resources/epub-accessibility/files/metadata-a11y-exemption-valid.opf
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<package xmlns="http://www.idpf.org/2007/opf" version="3.0" unique-identifier="uid"
+    xmlns:dc="http://purl.org/dc/elements/1.1/">
+    <metadata>
+        <dc:title>Title</dc:title>
+        <dc:language>en</dc:language>
+        <dc:identifier id="uid">NOID</dc:identifier>
+        <meta property="dcterms:modified">2019-01-01T12:00:00Z</meta>
+        <!-- using the a11y:exemption property -->
+        <meta property="dcterms:conformsTo">none</meta>
+        <meta property="a11y:exemption">eaa-microenterprise</meta>
+    </metadata>
+    <manifest>
+        <item id="t001" href="contents.xhtml" properties="nav" media-type="application/xhtml+xml"/>
+    </manifest>
+    <spine>
+        <itemref idref="t001"/>
+    </spine>
+</package>
diff --git a/src/test/resources/epub2/files/ops-document-xhtml/entities-unknown-error.xhtml b/src/test/resources/epub2/files/ops-document-xhtml/entities-unknown-error.xhtml
new file mode 100644
index 00000000..733801f6
--- /dev/null
+++ b/src/test/resources/epub2/files/ops-document-xhtml/entities-unknown-error.xhtml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+	<head>
+        <title>Test</title>
+	</head>
+	<body>
+		<h1>Loomings</h1>
+        <!-- this entity reference uses an unknown entity name -->
+        &foo; 
+	</body>
+</html>
diff --git a/src/test/resources/epub2/ops-content-document-xhtml.feature b/src/test/resources/epub2/ops-content-document-xhtml.feature
index e8c7501c..5253ef19 100644
--- a/src/test/resources/epub2/ops-content-document-xhtml.feature
+++ b/src/test/resources/epub2/ops-content-document-xhtml.feature
@@ -49,6 +49,12 @@ Feature: EPUB 2 ▸ Open Publication Structure ▸ XHTML Document Checks
     When checking document 'entities-character-references-valid.xhtml'
     Then no errors or warnings are reported
 
+  Scenario: Report unknown entity references
+    When checking document 'entities-unknown-error.xhtml'
+    Then fatal error RSC-016 is reported
+    And the message contains 'was referenced, but not declared'
+    And no other errors or warnings are reported
+
   Scenario: Report HTML5 elements used in OPS XHTML Content Documents
     When checking document 'html5-elements-error.xhtml'
     Then error RSC-005 is reported
diff --git a/src/test/resources/epub3/00-minimal/files/minimal.svg b/src/test/resources/epub3/00-minimal/files/minimal.svg
new file mode 100644
index 00000000..a4404608
--- /dev/null
+++ b/src/test/resources/epub3/00-minimal/files/minimal.svg
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" version="1.1">
+	<title>Test</title>
+	<desc>Description</desc>
+	<circle r="10"/>
+</svg>
diff --git a/src/test/resources/epub3/00-minimal/minimal.feature b/src/test/resources/epub3/00-minimal/minimal.feature
index 9739651c..d776fdc8 100644
--- a/src/test/resources/epub3/00-minimal/minimal.feature
+++ b/src/test/resources/epub3/00-minimal/minimal.feature
@@ -23,3 +23,7 @@
   Scenario: Verify a minimal XHTML content document 
     When checking file 'minimal.xhtml'
     Then no errors or warnings are reported
+
+  Scenario: Verify a minimal SVG content document 
+    When checking file 'minimal.svg'
+    Then no errors or warnings are reported
diff --git a/src/test/resources/epub3/03-resources/files/data-url-with-unescaped-query-valid/EPUB/content_001.xhtml b/src/test/resources/epub3/03-resources/files/data-url-with-unescaped-query-valid/EPUB/content_001.xhtml
new file mode 100644
index 00000000..5ad0c107
--- /dev/null
+++ b/src/test/resources/epub3/03-resources/files/data-url-with-unescaped-query-valid/EPUB/content_001.xhtml
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html xmlns:epub="http://www.idpf.org/2007/ops"
+	xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+	<head>
+		<meta charset="utf-8" />
+		<title>Minimal EPUB</title>
+	</head>
+	<body>
+		<h1>Loomings</h1>
+		<p>Call me Ishmael.</p>
+		<img src="data:image/gif;base64,XXX??aaa" alt="" />
+	</body>
+</html>
diff --git a/src/test/resources/epub3/03-resources/files/data-url-with-unescaped-query-valid/EPUB/nav.xhtml b/src/test/resources/epub3/03-resources/files/data-url-with-unescaped-query-valid/EPUB/nav.xhtml
new file mode 100644
index 00000000..240745e6
--- /dev/null
+++ b/src/test/resources/epub3/03-resources/files/data-url-with-unescaped-query-valid/EPUB/nav.xhtml
@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<html xmlns="http://www.w3.org/1999/xhtml" xmlns:epub="http://www.idpf.org/2007/ops" xml:lang="en" lang="en">
+	<head>
+		<meta charset="utf-8"/>
+		<title>Minimal Nav</title>
+	</head>
+	<body>
+		<nav epub:type="toc">
+			<ol>
+				<li><a href="content_001.xhtml">content 001</a></li>
+			</ol>
+		</nav>
+	</body>
+</html>
diff --git a/src/test/resources/epub3/03-resources/files/data-url-with-unescaped-query-valid/EPUB/package.opf b/src/test/resources/epub3/03-resources/files/data-url-with-unescaped-query-valid/EPUB/package.opf
new file mode 100644
index 00000000..0d1eec6e
--- /dev/null
+++ b/src/test/resources/epub3/03-resources/files/data-url-with-unescaped-query-valid/EPUB/package.opf
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<package xmlns="http://www.idpf.org/2007/opf" version="3.0" xml:lang="en" unique-identifier="q">
+<metadata xmlns:dc="http://purl.org/dc/elements/1.1/">
+  <dc:title id="title">Minimal EPUB 3.0</dc:title>
+  <dc:language>en</dc:language>
+  <dc:identifier id="q">NOID</dc:identifier>
+  <meta property="dcterms:modified">2017-06-14T00:00:01Z</meta>
+</metadata>
+<manifest>
+  <item id="content_001"  href="content_001.xhtml" media-type="application/xhtml+xml"/>
+  <item id="nav"  href="nav.xhtml" media-type="application/xhtml+xml" properties="nav"/>
+</manifest>
+<spine>
+  <itemref idref="content_001" />
+</spine>
+</package>
\ No newline at end of file
diff --git a/src/test/resources/epub3/03-resources/files/data-url-with-unescaped-query-valid/META-INF/container.xml b/src/test/resources/epub3/03-resources/files/data-url-with-unescaped-query-valid/META-INF/container.xml
new file mode 100644
index 00000000..31878217
--- /dev/null
+++ b/src/test/resources/epub3/03-resources/files/data-url-with-unescaped-query-valid/META-INF/container.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<container version="1.0" xmlns="urn:oasis:names:tc:opendocument:xmlns:container">
+	<rootfiles>
+		<rootfile full-path="EPUB/package.opf" media-type="application/oebps-package+xml"/>
+	</rootfiles>
+</container>
diff --git a/src/test/resources/epub3/03-resources/files/data-url-with-unescaped-query-valid/mimetype b/src/test/resources/epub3/03-resources/files/data-url-with-unescaped-query-valid/mimetype
new file mode 100644
index 00000000..57ef03f2
--- /dev/null
+++ b/src/test/resources/epub3/03-resources/files/data-url-with-unescaped-query-valid/mimetype
@@ -0,0 +1,1 @@
+application/epub+zip
\ No newline at end of file
diff --git a/src/test/resources/epub3/03-resources/resources.feature b/src/test/resources/epub3/03-resources/resources.feature
index 51a20fb0..d9cac61c 100644
--- a/src/test/resources/epub3/03-resources/resources.feature
+++ b/src/test/resources/epub3/03-resources/resources.feature
@@ -549,6 +549,12 @@
     Then error RSC-032 is reported
     And no other errors or warnings are reported
 
+  @spec @xref:sec-data-urls
+  Scenario: Verify a data URL having unesapced query-like component
+    See https://github.com/w3c/epubcheck/issues/1536
+    When checking EPUB 'data-url-with-unescaped-query-valid'
+    Then no other errors or warnings are reported
+
   ## 3.8 File URLs
 
   @spec @xref:sec-file-urls
diff --git a/src/test/resources/epub3/06-content-document/content-document-svg.feature b/src/test/resources/epub3/06-content-document/content-document-svg.feature
index 413677a9..20f65df4 100644
--- a/src/test/resources/epub3/06-content-document/content-document-svg.feature
+++ b/src/test/resources/epub3/06-content-document/content-document-svg.feature
@@ -51,6 +51,13 @@ Feature: EPUB 3 — Content Documents — SVG
     And the message contains '"epub:type" not allowed'
     And no other errors or warnings are reported
 
+  Scenario: Report an unknown `epub:*` attribute used in SVG
+    Given the reporting level is set to usage
+    When checking document 'unknown-epub-attribute-error.svg'
+    Then error RSC-005 is reported
+    And the message contains '"epub:unknown" not allowed'
+    And no other errors or warnings are reported
+
   
   ### 6.2.2 SVG requirements
 
diff --git a/src/test/resources/epub3/06-content-document/content-document-xhtml.feature b/src/test/resources/epub3/06-content-document/content-document-xhtml.feature
index c417d939..21b9042d 100644
--- a/src/test/resources/epub3/06-content-document/content-document-xhtml.feature
+++ b/src/test/resources/epub3/06-content-document/content-document-xhtml.feature
@@ -141,14 +141,6 @@ Feature: EPUB 3 — Content Documents — XHTML
     Then error RSC-005 is reported
     And the message contains 'attribute "aria-describedat" not allowed here'
     And no other errors or warnings are reported
-    
-  Scenario: Verify the DPUB-ARIA roles allowed on `a`
-    When checking document 'aria-roles-img-valid.xhtml'
-    Then no errors or warnings are reported
-
-  Scenario: Verify the DPUB-ARIA roles allowed on `aside`
-    When checking document 'aria-roles-img-valid.xhtml'
-    Then no errors or warnings are reported
 
   Scenario: Verify the DPUB-ARIA roles allowed on `footer`
     When checking document 'aria-roles-footer-valid.xhtml'
@@ -162,10 +154,6 @@ Feature: EPUB 3 — Content Documents — XHTML
     When checking document 'aria-roles-header-valid.xhtml'
     Then no errors or warnings are reported
 
-  Scenario: Verify the DPUB-ARIA roles allowed on `hr`
-    When checking document 'aria-roles-header-valid.xhtml'
-    Then no errors or warnings are reported
-
   Scenario: Verify the DPUB-ARIA roles allowed on `img`
     When checking document 'aria-roles-img-valid.xhtml'
     Then no errors or warnings are reported
@@ -694,7 +682,6 @@ Feature: EPUB 3 — Content Documents — XHTML
     When checking document 'epubtype-reserved-vocab-valid.xhtml'
     Then no errors or warnings are reported
 
-  @spec @xref:sec-prefix-attr
   Scenario: Verify `epub:type` attribute with author-declared vocabulary
     When checking document 'epubtype-declared-vocab-valid.xhtml'
     Then no errors or warnings are reported
@@ -717,13 +704,6 @@ Feature: EPUB 3 — Content Documents — XHTML
     Then usage OPF-087 is reported 7 times
     And no other errors or warnings are reported
 
-  @spec @xref:sec-prefix-attr
-  Scenario: Report `epub:type` attribute with a semantic from an undeclared vocabulary
-    When checking document 'epubtype-prefix-undeclared-error.xhtml'
-    Then error OPF-028 is reported
-    And no other errors or warnings are reported
-
-
   #### 6.1.3.2 RDFa
 
   @spec @xref:sec-xhtml-rdfa
diff --git a/src/test/resources/epub3/06-content-document/files/aria-roles-img-valid.xhtml b/src/test/resources/epub3/06-content-document/files/aria-roles-img-valid.xhtml
index 30f144d8..76c1b327 100644
--- a/src/test/resources/epub3/06-content-document/files/aria-roles-img-valid.xhtml
+++ b/src/test/resources/epub3/06-content-document/files/aria-roles-img-valid.xhtml
@@ -5,6 +5,7 @@
 		<title>DPUB-ARIA roles allowed on img</title>
 	</head>
 	<body>
-		<img role="doc-cover" src="test.jpg" alt="okay"/>
+		<img role="doc-cover" src="cover.jpg" alt="cover"/>
+		<img role="math" src="math.jpg" alt="math"/>
 	</body>
 </html>
diff --git a/src/test/resources/epub3/06-content-document/files/entities-external-error.xhtml b/src/test/resources/epub3/06-content-document/files/entities-external-error.xhtml
index 71148116..8fee8d5d 100644
--- a/src/test/resources/epub3/06-content-document/files/entities-external-error.xhtml
+++ b/src/test/resources/epub3/06-content-document/files/entities-external-error.xhtml
@@ -7,6 +7,5 @@
         <title>Entites declared in the internal subset are allowed</title>
     </head>
     <body>
-        &foo; 
     </body>
 </html>
diff --git a/src/test/resources/epub3/06-content-document/files/epubtype-declared-vocab-valid.xhtml b/src/test/resources/epub3/06-content-document/files/epubtype-declared-vocab-valid.xhtml
index 97e75680..993e1fa8 100644
--- a/src/test/resources/epub3/06-content-document/files/epubtype-declared-vocab-valid.xhtml
+++ b/src/test/resources/epub3/06-content-document/files/epubtype-declared-vocab-valid.xhtml
@@ -6,10 +6,6 @@
 		<title>epub:type with declared vocabulary</title>
 	</head>
 	<!-- this property is part of a declared vocab -->
-	<!-- 
-    note: 'subchatper' is deprecated in the SSV,
-          but not here as it comes from another vocab
-  -->
 	<body epub:type="my:subchapter">
 		<h1>Test</h1>
 	</body>
diff --git a/src/test/resources/epub3/06-content-document/files/unknown-epub-attribute-error.svg b/src/test/resources/epub3/06-content-document/files/unknown-epub-attribute-error.svg
new file mode 100644
index 00000000..aee63074
--- /dev/null
+++ b/src/test/resources/epub3/06-content-document/files/unknown-epub-attribute-error.svg
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:epub="http://www.idpf.org/2007/ops" epub:unknown=""
+	version="1.1">
+	<title>Test</title>
+	<desc>Description</desc>
+	<circle r="10"/>
+</svg>
diff --git a/src/test/resources/epub3/09-media-overlays/media-overlays.feature b/src/test/resources/epub3/09-media-overlays/media-overlays.feature
index 9d63ea91..34e16d54 100644
--- a/src/test/resources/epub3/09-media-overlays/media-overlays.feature
+++ b/src/test/resources/epub3/09-media-overlays/media-overlays.feature
@@ -188,12 +188,6 @@ Feature: EPUB 3 — Media Overlays
     When checking document 'epubtype-prefix-declared-valid.smil'
     Then no errors or warnings are reported
 
-  Scenario: Report an epub:type property with an undeclared prefix
-    When checking document 'epubtype-prefix-undeclared-error.smil'
-    Then error OPF-028 is reported
-    And the message contains 'Undeclared prefix: "my"'
-    And no other errors or warnings are reported
-
   Scenario: Allow unknown epub:type properties in the default vocabulary
     Given the reporting level set to usage
     When checking document 'epubtype-unknown-usage.smil'
diff --git a/src/test/resources/epub3/D-vocabularies/files/prefix-attribute-no-namespace-unrecognized-error.smil b/src/test/resources/epub3/D-vocabularies/files/prefix-attribute-no-namespace-unrecognized-error.smil
new file mode 100644
index 00000000..61b593eb
--- /dev/null
+++ b/src/test/resources/epub3/D-vocabularies/files/prefix-attribute-no-namespace-unrecognized-error.smil
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<smil xmlns="http://www.w3.org/ns/SMIL" xmlns:epub="http://www.idpf.org/2007/ops"
+    prefix="my: https://example.org/vocab/#" version="3.0">
+    <body epub:type="my:type">
+        <par id="par1">
+            <text src="chapter1.xhtml#text1"/>
+            <audio src="chapter1.mp3" clipBegin="0s" clipEnd="10s"/>
+        </par>
+        <par id="par2">
+            <text src="chapter1.xhtml#text2"/>
+            <audio src="chapter1.mp3" clipBegin="10s" clipEnd="20s"/>
+        </par>
+    </body>
+</smil>
diff --git a/src/test/resources/epub3/D-vocabularies/files/prefix-attribute-no-namespace-unrecognized-error.xhtml b/src/test/resources/epub3/D-vocabularies/files/prefix-attribute-no-namespace-unrecognized-error.xhtml
new file mode 100644
index 00000000..2ed20a4e
--- /dev/null
+++ b/src/test/resources/epub3/D-vocabularies/files/prefix-attribute-no-namespace-unrecognized-error.xhtml
@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<html xmlns="http://www.w3.org/1999/xhtml"
+	xmlns:epub="http://www.idpf.org/2007/ops"
+	prefix="my: https://example.org/vocab/#"
+	xml:lang="en" lang="en">
+	<head>
+		<meta charset="utf-8" />
+		<title>Test</title>
+	</head>
+	<body epub:type="my:type">
+		<h1>Loomings</h1>
+		<p>Call me Ishmael.</p>
+	</body>
+</html>
diff --git a/src/test/resources/epub3/D-vocabularies/files/prefix-attribute-on-head-error.xhtml b/src/test/resources/epub3/D-vocabularies/files/prefix-attribute-on-head-error.xhtml
new file mode 100644
index 00000000..d2ee6dde
--- /dev/null
+++ b/src/test/resources/epub3/D-vocabularies/files/prefix-attribute-on-head-error.xhtml
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html xmlns="http://www.w3.org/1999/xhtml" xmlns:epub="http://www.idpf.org/2007/ops" xml:lang="en"
+	lang="en">
+	<head epub:prefix="ex1: http://example.org/vocab1
+		ex2: http://example.org/vocab2">
+		<meta charset="utf-8" />
+		<title>Test</title>
+	</head>
+	<body>
+		<h1>Loomings</h1>
+		<p>Call me Ishmael.</p>
+	</body>
+</html>
diff --git a/src/test/resources/epub3/D-vocabularies/files/prefix-attribute-on-svg-error.xhtml b/src/test/resources/epub3/D-vocabularies/files/prefix-attribute-on-svg-error.xhtml
new file mode 100644
index 00000000..20e0e45f
--- /dev/null
+++ b/src/test/resources/epub3/D-vocabularies/files/prefix-attribute-on-svg-error.xhtml
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<html xmlns="http://www.w3.org/1999/xhtml" xmlns:epub="http://www.idpf.org/2007/ops" xml:lang="en"
+	lang="en">
+	<head>
+		<meta charset="utf-8" />
+		<title>Test</title>
+	</head>
+	<body>
+		<h1>Loomings</h1>
+		<p>Call me Ishmael.</p>
+		<svg xmlns="http://www.w3.org/2000/svg" epub:prefix="my: http://example.org/vocab/">
+			<title>SVG</title>
+			<desc>Description</desc>
+			<circle r="10"/>
+		</svg>
+	</body>
+</html>
diff --git a/src/test/resources/epub3/D-vocabularies/files/property-prefix-declaration-syntax-error.opf b/src/test/resources/epub3/D-vocabularies/files/prefix-attribute-syntax-error.opf
similarity index 100%
rename from src/test/resources/epub3/D-vocabularies/files/property-prefix-declaration-syntax-error.opf
rename to src/test/resources/epub3/D-vocabularies/files/prefix-attribute-syntax-error.opf
diff --git a/src/test/resources/epub3/D-vocabularies/files/property-prefix-declaration-valid.opf b/src/test/resources/epub3/D-vocabularies/files/prefix-attribute-valid.opf
similarity index 100%
rename from src/test/resources/epub3/D-vocabularies/files/property-prefix-declaration-valid.opf
rename to src/test/resources/epub3/D-vocabularies/files/prefix-attribute-valid.opf
diff --git a/src/test/resources/epub3/D-vocabularies/files/prefix-attribute-valid.smil b/src/test/resources/epub3/D-vocabularies/files/prefix-attribute-valid.smil
new file mode 100644
index 00000000..6e92caad
--- /dev/null
+++ b/src/test/resources/epub3/D-vocabularies/files/prefix-attribute-valid.smil
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<smil xmlns="http://www.w3.org/ns/SMIL" xmlns:epub="http://www.idpf.org/2007/ops"
+    epub:prefix="my: https://example.org/vocab/#" version="3.0">
+    <body>
+        <par id="par1">
+            <text src="chapter1.xhtml#text1"/>
+            <audio src="chapter1.mp3" clipBegin="0s" clipEnd="10s"/>
+        </par>
+        <par id="par2">
+            <text src="chapter1.xhtml#text2"/>
+            <audio src="chapter1.mp3" clipBegin="10s" clipEnd="20s"/>
+        </par>
+    </body>
+</smil>
diff --git a/src/test/resources/epub3/D-vocabularies/files/prefix-attribute-valid.svg b/src/test/resources/epub3/D-vocabularies/files/prefix-attribute-valid.svg
new file mode 100644
index 00000000..ac8cd1fc
--- /dev/null
+++ b/src/test/resources/epub3/D-vocabularies/files/prefix-attribute-valid.svg
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" 
+	xmlns:epub="http://www.idpf.org/2007/ops"
+	epub:prefix="ex1: http://example.org/vocab1
+	             ex2: http://example.org/vocab2"
+	version="1.1">
+	<title>Test</title>
+	<desc>Description</desc>
+	<circle r="10"/>
+</svg>
diff --git a/src/test/resources/epub3/D-vocabularies/files/prefix-attribute-valid.xhtml b/src/test/resources/epub3/D-vocabularies/files/prefix-attribute-valid.xhtml
new file mode 100644
index 00000000..253a9273
--- /dev/null
+++ b/src/test/resources/epub3/D-vocabularies/files/prefix-attribute-valid.xhtml
@@ -0,0 +1,15 @@
+<!DOCTYPE html>
+<html xmlns="http://www.w3.org/1999/xhtml"
+	xmlns:epub="http://www.idpf.org/2007/ops"
+	epub:prefix="ex1: http://example.org/vocab1
+               ex2: http://example.org/vocab2"
+	xml:lang="en" lang="en">
+	<head>
+		<meta charset="utf-8" />
+		<title>Test</title>
+	</head>
+	<body>
+		<h1>Loomings</h1>
+		<p>Call me Ishmael.</p>
+	</body>
+</html>
diff --git a/src/test/resources/epub3/D-vocabularies/files/prefix-in-epub-type-valid.xhtml b/src/test/resources/epub3/D-vocabularies/files/prefix-in-epub-type-valid.xhtml
new file mode 100644
index 00000000..c36c3514
--- /dev/null
+++ b/src/test/resources/epub3/D-vocabularies/files/prefix-in-epub-type-valid.xhtml
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<html xmlns="http://www.w3.org/1999/xhtml"
+	xmlns:epub="http://www.idpf.org/2007/ops"
+	epub:prefix="my: https://example.org/vocab/#"
+	xml:lang="en" lang="en">
+	<head>
+		<meta charset="utf-8" />
+		<title>Test</title>
+	</head>
+	<!-- 
+    note: 'subchatper' is deprecated in the SSV,
+          but not here as it comes from another vocab
+  -->
+	<body epub:type="my:subchapter">
+		<h1>Loomings</h1>
+		<p>Call me Ishmael.</p>
+	</body>
+</html>
diff --git a/src/test/resources/epub3/D-vocabularies/files/prefix-mapping-dc-elements-error.opf b/src/test/resources/epub3/D-vocabularies/files/prefix-mapping-dc-elements-error.opf
new file mode 100644
index 00000000..f848b309
--- /dev/null
+++ b/src/test/resources/epub3/D-vocabularies/files/prefix-mapping-dc-elements-error.opf
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<package xmlns="http://www.idpf.org/2007/opf" version="3.0" unique-identifier="uid"
+    xmlns:dc="http://purl.org/dc/elements/1.1/"
+    prefix="dc: http://purl.org/dc/elements/1.1/">
+    <metadata>
+        <dc:title>Title</dc:title>
+        <dc:language>en</dc:language>
+        <dc:identifier id="uid">NOID</dc:identifier>
+        <meta property="dcterms:modified">2019-01-01T12:00:00Z</meta>
+    </metadata>
+    <manifest>
+        <item id="t001" href="contents.xhtml" properties="nav" media-type="application/xhtml+xml"/>
+    </manifest>
+    <spine>
+        <itemref idref="t001"/>
+    </spine>
+</package>
diff --git a/src/test/resources/epub3/D-vocabularies/files/property-prefix-declaration-default-vocabs-error.opf b/src/test/resources/epub3/D-vocabularies/files/prefix-mapping-default-vocabs-error.opf
similarity index 100%
rename from src/test/resources/epub3/D-vocabularies/files/property-prefix-declaration-default-vocabs-error.opf
rename to src/test/resources/epub3/D-vocabularies/files/prefix-mapping-default-vocabs-error.opf
diff --git a/src/test/resources/epub3/D-vocabularies/files/prefix-mapping-default-vocaps-error.xhtml b/src/test/resources/epub3/D-vocabularies/files/prefix-mapping-default-vocaps-error.xhtml
new file mode 100644
index 00000000..d093dfd2
--- /dev/null
+++ b/src/test/resources/epub3/D-vocabularies/files/prefix-mapping-default-vocaps-error.xhtml
@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<html xmlns="http://www.w3.org/1999/xhtml"
+	xmlns:epub="http://www.idpf.org/2007/ops"
+	epub:prefix="my: https://example.org/vocab/#"
+	xml:lang="en" lang="en">
+	<head>
+		<meta charset="utf-8" />
+		<title>Valid `epub:prefix` attribute</title>
+	</head>
+	<body>
+		<h1>Loomings</h1>
+		<p>Call me Ishmael.</p>
+	</body>
+</html>
diff --git a/src/test/resources/epub3/D-vocabularies/files/property-prefix-declaration-reserved-explicit-valid.opf b/src/test/resources/epub3/D-vocabularies/files/prefix-mapping-reserved-valid.opf
similarity index 100%
rename from src/test/resources/epub3/D-vocabularies/files/property-prefix-declaration-reserved-explicit-valid.opf
rename to src/test/resources/epub3/D-vocabularies/files/prefix-mapping-reserved-valid.opf
diff --git a/src/test/resources/epub3/D-vocabularies/files/prefix-mapping-reserved-valid.xhtml b/src/test/resources/epub3/D-vocabularies/files/prefix-mapping-reserved-valid.xhtml
new file mode 100644
index 00000000..c1383118
--- /dev/null
+++ b/src/test/resources/epub3/D-vocabularies/files/prefix-mapping-reserved-valid.xhtml
@@ -0,0 +1,15 @@
+<!DOCTYPE html>
+<html xmlns="http://www.w3.org/1999/xhtml"
+	xmlns:epub="http://www.idpf.org/2007/ops"
+	epub:prefix="msv:   http://www.idpf.org/epub/vocab/structure/magazine/#
+	             prism: http://www.prismstandard.org/specifications/3.0/PRISM_CV_Spec_3.0.htm#"
+	xml:lang="en" lang="en">
+	<head>
+		<meta charset="utf-8" />
+		<title>Test</title>
+	</head>
+	<body>
+		<h1>Loomings</h1>
+		<p>Call me Ishmael.</p>
+	</body>
+</html>
diff --git a/src/test/resources/epub3/D-vocabularies/files/property-prefix-declaration-reserved-overridden-warning.opf b/src/test/resources/epub3/D-vocabularies/files/prefix-reserved-overridden-warning.opf
similarity index 100%
rename from src/test/resources/epub3/D-vocabularies/files/property-prefix-declaration-reserved-overridden-warning.opf
rename to src/test/resources/epub3/D-vocabularies/files/prefix-reserved-overridden-warning.opf
diff --git a/src/test/resources/epub3/D-vocabularies/files/prefix-reserved-overridden-warning.xhtml b/src/test/resources/epub3/D-vocabularies/files/prefix-reserved-overridden-warning.xhtml
new file mode 100644
index 00000000..3fd2bddc
--- /dev/null
+++ b/src/test/resources/epub3/D-vocabularies/files/prefix-reserved-overridden-warning.xhtml
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html xmlns="http://www.w3.org/1999/xhtml" xmlns:epub="http://www.idpf.org/2007/ops" xml:lang="en"
+	epub:prefix="msv:   http://example.org/msv/
+	             prism: http://example.org/prism/" lang="en">
+	<head>
+		<meta charset="utf-8" />
+		<title>Test</title>
+	</head>
+	<body>
+		<h1 epub:type="prism:title">Loomings</h1>
+		<p>Call me Ishmael.</p>
+	</body>
+</html>
diff --git a/src/test/resources/epub3/D-vocabularies/files/prefix-reserved-prism-undeclared-valid.xhtml b/src/test/resources/epub3/D-vocabularies/files/prefix-reserved-prism-undeclared-valid.xhtml
new file mode 100644
index 00000000..54e9f6d1
--- /dev/null
+++ b/src/test/resources/epub3/D-vocabularies/files/prefix-reserved-prism-undeclared-valid.xhtml
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<html xmlns="http://www.w3.org/1999/xhtml" xmlns:epub="http://www.idpf.org/2007/ops" xml:lang="en"
+	lang="en">
+	<head>
+		<meta charset="utf-8" />
+		<title>Test</title>
+	</head>
+	<body>
+		<h1 epub:type="prism:title">Loomings</h1>
+		<p>Call me Ishmael.</p>
+	</body>
+</html>
diff --git a/src/test/resources/epub3/D-vocabularies/files/property-prefix-schema-not-declared-valid.opf b/src/test/resources/epub3/D-vocabularies/files/prefix-reserved-schema-undeclared-valid.opf
similarity index 100%
rename from src/test/resources/epub3/D-vocabularies/files/property-prefix-schema-not-declared-valid.opf
rename to src/test/resources/epub3/D-vocabularies/files/prefix-reserved-schema-undeclared-valid.opf
diff --git a/src/test/resources/epub3/D-vocabularies/files/property-prefix-declaration-missing-error.opf b/src/test/resources/epub3/D-vocabularies/files/prefix-undeclared-error.opf
similarity index 100%
rename from src/test/resources/epub3/D-vocabularies/files/property-prefix-declaration-missing-error.opf
rename to src/test/resources/epub3/D-vocabularies/files/prefix-undeclared-error.opf
diff --git a/src/test/resources/epub3/09-media-overlays/files/epubtype-prefix-undeclared-error.smil b/src/test/resources/epub3/D-vocabularies/files/prefix-undeclared-error.smil
similarity index 100%
rename from src/test/resources/epub3/09-media-overlays/files/epubtype-prefix-undeclared-error.smil
rename to src/test/resources/epub3/D-vocabularies/files/prefix-undeclared-error.smil
diff --git a/src/test/resources/epub3/06-content-document/files/epubtype-prefix-undeclared-error.xhtml b/src/test/resources/epub3/D-vocabularies/files/prefix-undeclared-error.xhtml
similarity index 100%
rename from src/test/resources/epub3/06-content-document/files/epubtype-prefix-undeclared-error.xhtml
rename to src/test/resources/epub3/D-vocabularies/files/prefix-undeclared-error.xhtml
diff --git a/src/test/resources/epub3/D-vocabularies/files/prefix-underscore-error.xhtml b/src/test/resources/epub3/D-vocabularies/files/prefix-underscore-error.xhtml
new file mode 100644
index 00000000..2099c509
--- /dev/null
+++ b/src/test/resources/epub3/D-vocabularies/files/prefix-underscore-error.xhtml
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<html xmlns="http://www.w3.org/1999/xhtml" xmlns:epub="http://www.idpf.org/2007/ops"
+	epub:prefix="_: http://example.org/vocab#" xml:lang="en" lang="en">
+	<head>
+		<meta charset="utf-8" />
+		<title>Test</title>
+	</head>
+	<body>
+		<h1>Loomings</h1>
+		<p>Call me Ishmael.</p>
+	</body>
+</html>
diff --git a/src/test/resources/epub3/D-vocabularies/media-overlays.feature b/src/test/resources/epub3/D-vocabularies/media-overlays-vocab.feature
similarity index 100%
rename from src/test/resources/epub3/D-vocabularies/media-overlays.feature
rename to src/test/resources/epub3/D-vocabularies/media-overlays-vocab.feature
diff --git a/src/test/resources/epub3/D-vocabularies/meta-properties.feature b/src/test/resources/epub3/D-vocabularies/meta-properties-vocab.feature
similarity index 100%
rename from src/test/resources/epub3/D-vocabularies/meta-properties.feature
rename to src/test/resources/epub3/D-vocabularies/meta-properties-vocab.feature
diff --git a/src/test/resources/epub3/D-vocabularies/metadata-link.feature b/src/test/resources/epub3/D-vocabularies/metadata-link-vocab.feature
similarity index 100%
rename from src/test/resources/epub3/D-vocabularies/metadata-link.feature
rename to src/test/resources/epub3/D-vocabularies/metadata-link-vocab.feature
diff --git a/src/test/resources/epub3/D-vocabularies/vocabularies.feature b/src/test/resources/epub3/D-vocabularies/vocabularies.feature
new file mode 100644
index 00000000..44442746
--- /dev/null
+++ b/src/test/resources/epub3/D-vocabularies/vocabularies.feature
@@ -0,0 +1,184 @@
+Feature: EPUB 3 — Vocabularies — Vocabulary association
+
+
+	Checks conformance to the "Vocabulary association" section of the EPUB 3.3 specification:
+	https://www.w3.org/TR/epub-33/#sec-vocab-assoc
+
+
+	Background:
+		Given EPUB test files located at '/epub3/D-vocabularies/files/'
+		And EPUBCheck with default settings
+
+
+	# D.1 Vocabulary association mechanisms
+
+	## D.1.3 Default vocabularies
+
+	Rule: EPUB creators MUST NOT assign a prefix to the URLs associated with default vocabularies using the `prefix` attribute.
+
+		@spec @xref:sec-default-vocab
+		Example: Report `prefix` re-mapping default vocabularies in a package document
+			When checking file 'prefix-mapping-default-vocabs-error.opf'
+			Then error OPF-007b is reported 4 times (once for each default vocabulary)
+			And no other errors or warnings are reported
+
+		@spec @xref:sec-default-vocab
+		Example: Report `prefix` re-mapping default vocabularies in XHTML
+			When checking file 'prefix-mapping-default-vocabs-error.opf'
+			Then error OPF-007b is reported 4 times (once for each default vocabulary)
+			And no errors or warnings are reported
+
+
+	## D.1.4 The prefix attribute
+
+	Rule: The value of the `prefix` attribute is a whitespace-separated list of one or more prefix-to-URL mappings
+
+		@spec @xref:sec-prefix-attr
+		Example: Allow valid `prefix` attribute mappings in package documents
+			When checking file 'prefix-attribute-valid.opf'
+			Then no errors or warnings are reported
+
+		@spec @xref:sec-prefix-attr
+		Example: Allow valid `prefix` attribute mappings in XHTML content documents
+			When checking file 'prefix-attribute-valid.xhtml'
+			Then no errors or warnings are reported
+
+		@spec @xref:sec-prefix-attr
+		Example: Allow valid `prefix` attribute mappings in media overlays
+			When checking file 'prefix-attribute-valid.smil'
+			Then no errors or warnings are reported
+
+		@spec @xref:sec-prefix-attr
+		Example: Allow valid `prefix` attribute mappings in SVG content documents
+			When checking file 'prefix-attribute-valid.svg'
+			Then no errors or warnings are reported
+
+		@spec @xref:sec-prefix-attr
+		Example: Report syntax errors in the `prefix` attribute value
+			When checking file 'prefix-attribute-syntax-error.opf'
+			Then error OPF-004c is reported 2 times (the test file contains 2 syntax errors)
+			And no other errors or warnings are reported
+			Then no errors or warnings are reported
+
+
+	Rule: With the exception of reserved prefixes, EPUB creators MUST declare all prefixes used in a document
+
+		@spec @xref:sec-prefix-attr
+		Example: Allow declared prefixes used in the XHTML `epub:type` attribute
+			When checking document 'prefix-in-epub-type-valid.xhtml'
+			Then no errors or warnings are reported
+
+		@spec @xref:sec-prefix-attr
+		Scenario: Report undeclared prefixes used in the XHTML `epub:type` attribute
+			When checking document 'prefix-undeclared-error.xhtml'
+			Then error OPF-028 is reported
+			And no other errors or warnings are reported
+
+		@spec @xref:sec-prefix-attr
+		Example: Report undeclared prefixes used in package metadata properties
+			When checking file 'prefix-undeclared-error.opf'
+			Then error OPF-028 is reported
+			And no errors or warnings are reported
+
+		@spec @xref:sec-prefix-attr
+		Scenario: Report undeclared prefixes used in a Media Overlays `epub:type` attribute
+			When checking document 'prefix-undeclared-error.smil'
+			Then error OPF-028 is reported
+			And no other errors or warnings are reported
+
+
+	Rule: EPUB creators MUST declare the `prefix` attribute in the namespace `http://www.idpf.org/2007/ops` in EPUB content documents and media overlay documents.
+
+		@spec @xref:sec-prefix-attr
+		Example: Report a `epub:type` using a prefix declared in a `prefix` attribute with no namespace, in an XHTML content document.
+			When checking file 'prefix-attribute-no-namespace-unrecognized-error.xhtml'
+			Then error OPF-028 is reported (undeclared prefix)
+			And no other errors or warnings are reported
+
+		@spec @xref:sec-prefix-attr
+		Example: Report an `epub:type` using a prefix declared in a `prefix` attribute with no namespace, in a media overlay.
+			When checking file 'prefix-attribute-no-namespace-unrecognized-error.smil'
+			Then error RSC-005 is reported
+			And the message contains 'attribute "prefix" not allowed here'
+			Then error OPF-028 is reported (undeclared prefix)
+			And no other errors or warnings are reported
+
+
+	Rule: EPUB creators MUST only specify the `prefix` attribute on the root element of the respective format.
+
+		Note that for SVG embedded by inclusion, prefixes MUST be declared on the root html element.
+
+		@spec @xref:sec-prefix-attr
+		Example: Report a `prefix` attribute on an XHTML `head` element
+			When checking file 'prefix-attribute-on-head-error.xhtml'
+			Then error RSC-005 is reported
+			And the message contains 'attribute "epub:prefix" not allowed here'
+			And no other errors or warnings are reported
+
+		@spec @xref:sec-prefix-attr
+		Example: Report a `prefix` attribute on an XHTML embedded `svg` element
+			When checking file 'prefix-attribute-on-svg-error.xhtml'
+			Then error RSC-005 is reported
+		  And the message contains 'attribute "epub:prefix" not allowed here'
+			And no other errors or warnings are reported
+
+
+	Rule: EPUB creators MUST NOT declare the prefix '_'.
+
+		@spec @xref:sec-prefix-attr
+		Example: Report a declaration of the prefix '_' in XHTML content documents
+			When checking file 'prefix-underscore-error.xhtml'
+			Then error OPF-007a is reported
+			And no other errors or warnings are reported
+
+
+	Rule: EPUB creators MUST NOT declare a prefix for the Dublin Core `/elements/1.1/` namespace.
+
+		@spec @xref:sec-prefix-attr
+		Example: Report a prefix mapping to the Dublic Core elements namespace
+			When checking file 'prefix-mapping-dc-elements-error.opf'
+			Then error OPF-007c is reported
+			And no other errors or warnings are reported
+
+
+	## D.1.5 Reserved prefixes
+
+	Rule: (IMPLIED) EPUB creators can explicitly declare reserved prefixes.
+
+		@spec @xref:sec-reserved-prefixes
+		Example: Allow explicit declarations of reserved prefixes in package documents
+			When checking file 'prefix-mapping-reserved-valid.opf'
+			Then no errors or warnings are reported
+
+		@spec @xref:sec-reserved-prefixes
+		Example: Allow explicit declarations of reserved prefixes in content documents
+			When checking file 'prefix-mapping-reserved-valid.xhtml'
+			Then no errors or warnings are reported
+
+
+	Rule: EPUB creators MAY use reserved prefixes in attributes that expect a property value without declaring them in a `prefix` attribute.
+
+		@spec @xref:sec-reserved-prefixes
+		Example: Allow the reserved 'schema' prefix in package metadata properties
+			When checking file 'prefix-reserved-schema-undeclared-valid.opf'
+			Then no errors or warnings are reported
+
+		@spec @xref:sec-reserved-prefixes
+		Example: Allow the reserved 'prism' prefix in XHTML `epub:type` attributes
+			When checking file 'prefix-reserved-prism-undeclared-valid.xhtml'
+			Then no errors or warnings are reported
+
+
+	Rule: EPUB creators SHOULD NOT override reserved prefixes in the `prefix` attribute.
+
+		@spec @xref:sec-reserved-prefixes
+		Example: Report overriding of reserved prefixes in package documents
+			When checking file 'prefix-reserved-overridden-warning.opf'
+			Then warning OPF-007 is reported 8 times (once for each reserved prefix)
+			And no other errors or warnings are reported
+
+		@spec @xref:sec-reserved-prefixes
+		Example: Report overriding of reserved prefixes in XHTML content documents
+			When checking file 'prefix-reserved-overridden-warning.xhtml'
+			Then warning OPF-007 is reported 2 times (once for each reserved prefix)
+			And no other errors or warnings are reported
diff --git a/src/test/resources/epub3/D-vocabularies/vocabulary-association.feature b/src/test/resources/epub3/D-vocabularies/vocabulary-association.feature
deleted file mode 100644
index 20deee10..00000000
--- a/src/test/resources/epub3/D-vocabularies/vocabulary-association.feature
+++ /dev/null
@@ -1,54 +0,0 @@
-Feature: EPUB 3 — Vocabularies — Vocabulary association
-
-
-  Checks conformance to the "Vocabulary association" section of the EPUB 3.3 specification:
-    https://www.w3.org/TR/epub-33/#sec-vocab-assoc
-
-
-  Background: 
-    Given EPUB test files located at '/epub3/D-vocabularies/files/'
-    And EPUBCheck with default settings
-
-
-  # D.1 Vocabulary association mechanisms
-
-  ## D.1.4 The prefix attribute
-  
-  @spec @xref:sec-prefix-attr
-  Scenario: the 'prefix' attribute can be used to define new prefix mappings 
-    When checking file 'property-prefix-declaration-valid.opf'
-    Then no errors or warnings are reported
-
-  @spec @xref:sec-prefix-attr
-  Scenario: syntax errors in the 'prefix' attribute are reported
-    When checking file 'property-prefix-declaration-syntax-error.opf'
-    Then error OPF-004c is reported 2 times (the test file contains 2 syntax errors)
-    And no other errors or warnings are reported
-
-  @spec @xref:sec-prefix-attr
-  Scenario: default vocabularies must not be assigned a prefix
-    When checking file 'property-prefix-declaration-default-vocabs-error.opf'
-    Then error OPF-007b is reported 4 times (once for each default vocabulary)
-    And no other errors or warnings are reported
-
-  @spec @xref:sec-prefix-attr
-  Scenario: A metadata property with an unknown prefix is reported
-    When checking file 'property-prefix-declaration-missing-error.opf'
-    Then error OPF-028 is reported
-    And no errors or warnings are reported
-
-
-  ## D.1.5 Reserved prefixes
-  
-  Scenario: reserved prefixes can be explicitly declared
-    When checking file 'property-prefix-declaration-reserved-explicit-valid.opf'
-    Then no errors or warnings are reported
-
-  Scenario: reserved prefixes should not be overridden to other vocabularies 
-    When checking file 'property-prefix-declaration-reserved-overridden-warning.opf'
-    Then warning OPF-007 is reported 8 times (once for each reserved prefix)
-    And no other errors or warnings are reported
-
-  Scenario: The 'schema' prefix can be used in metadata properties without being declared
-    When checking file 'property-prefix-schema-not-declared-valid.opf'
-    Then no errors or warnings are reported
diff --git a/src/test/resources/reporting/xml-report.feature b/src/test/resources/reporting/xml-report.feature
new file mode 100644
index 00000000..7606676d
--- /dev/null
+++ b/src/test/resources/reporting/xml-report.feature
@@ -0,0 +1,21 @@
+ Feature: EPUBCheck - XML Report tests
+  
+  Checks the XML (JHove) report format
+
+
+  Background: 
+    Given EPUB test files located at '/reporting/files/'
+    And the reporting format is set to XML
+    And EPUBCheck with default settings
+    Given the default namespace is 'http://schema.openpreservation.org/ois/xml/ns/jhove'
+
+  Scenario: Basic well-formedness checks
+    When checking EPUB 'minimal'
+    Then the XML report is well-formed
+    And XPath '//repInfo' exists
+
+  Scenario: Creation date is set
+    When checking EPUB 'minimal'
+    And XPath '//repInfo/created' exists
+
+