aboutsummaryrefslogtreecommitdiffstats
path: root/vendor/mikespub/php-epub-meta/src/Tools
diff options
context:
space:
mode:
Diffstat (limited to 'vendor/mikespub/php-epub-meta/src/Tools')
-rw-r--r--vendor/mikespub/php-epub-meta/src/Tools/HtmlTools.php97
-rw-r--r--vendor/mikespub/php-epub-meta/src/Tools/ZipEdit.php450
-rw-r--r--vendor/mikespub/php-epub-meta/src/Tools/ZipFile.php343
-rw-r--r--vendor/mikespub/php-epub-meta/src/Tools/htmlBlockLevelElements.php42
-rw-r--r--vendor/mikespub/php-epub-meta/src/Tools/htmlEntityMap.php260
5 files changed, 1192 insertions, 0 deletions
diff --git a/vendor/mikespub/php-epub-meta/src/Tools/HtmlTools.php b/vendor/mikespub/php-epub-meta/src/Tools/HtmlTools.php
new file mode 100644
index 000000000..f905d265a
--- /dev/null
+++ b/vendor/mikespub/php-epub-meta/src/Tools/HtmlTools.php
@@ -0,0 +1,97 @@
+<?php
+
+namespace SebLucas\EPubMeta\Tools;
+
+/**
+ * From Epubli\Common\Tools - see https://github.com/epubli/common
+ * @author Epubli Developers <devs@epubli.com>
+ */
+class HtmlTools
+{
+ /**
+ * @param string $html
+ * @return string
+ */
+ public static function convertEntitiesNamedToNumeric($html)
+ {
+ return strtr($html, include(__DIR__ . '/htmlEntityMap.php'));
+ }
+
+ /**
+ * @param string $name
+ * @return bool
+ */
+ public static function isBlockLevelElement($name)
+ {
+ return in_array($name, include(__DIR__ . '/htmlBlockLevelElements.php'));
+ }
+
+ /**
+ * performs a tag-aware truncation of (html-) strings, preserving tag integrity
+ * @param array<string>|string $html
+ * @param int|string $length
+ * @return bool|string
+ */
+ public static function truncate($html, $length = "20%")
+ {
+ $htmls = is_array($html) ? $html : [$html];
+ foreach ($htmls as &$htmlString) {
+ if (is_string($length)) {
+ $length = trim($length);
+ /* interpret percentage value */
+ if (substr($length, -1) == '%') {
+ $length = (int) (strlen($htmlString) * intval(substr($length, 0, -1)) / 100);
+ }
+ }
+ $htmlString = substr($htmlString, 0, $length);
+ /* eliminate trailing truncated tag fragment if present */
+ $htmlString = preg_replace('/<[^>]*$/is', '', $htmlString);
+ }
+
+ return is_array($html) ? $htmls : array_pop($htmls);
+ }
+
+ /**
+ * strips all occurring html tags from $html (which can either be a string or an array of strings),
+ * preserving all content enclosed by all tags in $keep and
+ * dumping the content residing in all tags listed in $drop
+ * @param array<string>|string $html
+ * @param array<string> $keep
+ * @param array<string> $drop
+ * @return array<string>|string
+ */
+ public static function stripHtmlTags(
+ $html,
+ $keep =
+ ['title', 'br', 'p', 'h1','h2','h3','h4','h5','span','div','i','strong','b', 'table', 'td', 'th', 'tr'],
+ $drop =
+ ['head','style']
+ ) {
+ $htmls = is_array($html) ? $html : [$html];
+ foreach ($htmls as &$htmlString) {
+ foreach ($drop as $dumpTag) {
+ $htmlString = preg_replace("/<$dumpTag.*$dumpTag>/is", "\n", $htmlString);
+ }
+ $htmlString = preg_replace("/[\n\r ]{2,}/i", "\n", $htmlString);
+ $htmlString = preg_replace("/[\n|\r]/i", '<br />', $htmlString);
+
+ /* @TODO: remove style tags and only keep body content (drop head) */
+ $tempFunc = function ($matches) use ($keep) {
+ $htmlNode = "<" . $matches[1] . ">" . strip_tags($matches[2]) . "</" . $matches[1] . ">";
+ if (in_array($matches[1], $keep)) {
+ return " " . $htmlNode . " ";
+ } else {
+ return "";
+ }
+ };
+
+ $allowedTags = implode("|", array_values($keep));
+ $regExp = '@<(' . $allowedTags . ')[^>]*?>(.*?)<\/\1>@i';
+ $htmlString = preg_replace_callback($regExp, $tempFunc, $htmlString);
+
+ $htmlString = strip_tags($htmlString, "<" . implode("><", $keep) . ">");
+ }
+ /* preserve injected variable cast type (string|array) when returning processed entity */
+ return is_array($html) ? $htmls : array_pop($htmls);
+ }
+}
diff --git a/vendor/mikespub/php-epub-meta/src/Tools/ZipEdit.php b/vendor/mikespub/php-epub-meta/src/Tools/ZipEdit.php
new file mode 100644
index 000000000..ff9f607cf
--- /dev/null
+++ b/vendor/mikespub/php-epub-meta/src/Tools/ZipEdit.php
@@ -0,0 +1,450 @@
+<?php
+/**
+ * ZipEdit class
+ *
+ * @author mikespub
+ */
+
+namespace SebLucas\EPubMeta\Tools;
+
+use ZipStream\ZipStream;
+use DateTime;
+use Exception;
+use ZipArchive;
+
+/**
+ * ZipEdit class allows to edit zip files on the fly and stream them afterwards
+ */
+class ZipEdit
+{
+ public const DOWNLOAD = 1; // download (default)
+ public const NOHEADER = 4; // option to use with DOWNLOAD: no header is sent
+ public const FILE = 8; // output to file , or add from file
+ public const STRING = 32; // output to string, or add from string
+ public const MIME_TYPE = 'application/epub+zip';
+
+ /** @var ZipArchive|null */
+ private $mZip;
+ /** @var array<string, mixed>|null */
+ private $mEntries;
+ /** @var array<string, mixed> */
+ private $mChanges = [];
+ /** @var string|null */
+ private $mFileName;
+ private bool $mSaveMe = false;
+
+ public function __construct()
+ {
+ $this->mZip = null;
+ $this->mEntries = null;
+ $this->mFileName = null;
+ }
+
+ /**
+ * Destructor
+ */
+ public function __destruct()
+ {
+ $this->Close();
+ }
+
+ /**
+ * Open a zip file and read it's entries
+ *
+ * @param string $inFileName
+ * @param int|null $inFlags
+ * @return boolean True if zip file has been correctly opended, else false
+ */
+ public function Open($inFileName, $inFlags = 0) // ZipArchive::RDONLY)
+ {
+ $this->Close();
+
+ $this->mZip = new ZipArchive();
+ $result = $this->mZip->open($inFileName, ZipArchive::RDONLY);
+ if ($result !== true) {
+ return false;
+ }
+
+ $this->mFileName = $inFileName;
+
+ $this->mEntries = [];
+ $this->mChanges = [];
+
+ for ($i = 0; $i < $this->mZip->numFiles; $i++) {
+ $entry = $this->mZip->statIndex($i);
+ $fileName = $entry['name'];
+ $this->mEntries[$fileName] = $entry;
+ $this->mChanges[$fileName] = ['status' => 'unchanged'];
+ }
+
+ return true;
+ }
+
+ /**
+ * Check if a file exist in the zip entries
+ *
+ * @param string $inFileName File to search
+ *
+ * @return boolean True if the file exist, else false
+ */
+ public function FileExists($inFileName)
+ {
+ if (!isset($this->mZip)) {
+ return false;
+ }
+
+ if (!isset($this->mEntries[$inFileName])) {
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Read the content of a file in the zip entries
+ *
+ * @param string $inFileName File to search
+ *
+ * @return mixed File content the file exist, else false
+ */
+ public function FileRead($inFileName)
+ {
+ if (!isset($this->mZip)) {
+ return false;
+ }
+
+ if (!isset($this->mEntries[$inFileName])) {
+ return false;
+ }
+
+ $data = false;
+
+ $changes = $this->mChanges[$inFileName] ?? ['status' => 'unchanged'];
+ switch ($changes['status']) {
+ case 'unchanged':
+ $data = $this->mZip->getFromName($inFileName);
+ break;
+ case 'added':
+ case 'modified':
+ if (isset($changes['data'])) {
+ $data = $changes['data'];
+ } elseif (isset($changes['path'])) {
+ $data = file_get_contents($changes['path']);
+ }
+ break;
+ case 'deleted':
+ default:
+ break;
+ }
+ return $data;
+ }
+
+ /**
+ * Get a file handler to a file in the zip entries (read-only)
+ *
+ * @param string $inFileName File to search
+ *
+ * @return resource|bool File handler if the file exist, else false
+ */
+ public function FileStream($inFileName)
+ {
+ if (!isset($this->mZip)) {
+ return false;
+ }
+
+ if (!isset($this->mEntries[$inFileName])) {
+ return false;
+ }
+
+ // @todo streaming of added/modified data?
+ return $this->mZip->getStream($inFileName);
+ }
+
+ /**
+ * Summary of FileAdd
+ * @param string $inFileName
+ * @param mixed $inData
+ * @return bool
+ */
+ public function FileAdd($inFileName, $inData)
+ {
+ if (!isset($this->mZip)) {
+ return false;
+ }
+
+ $this->mEntries[$inFileName] = [
+ 'name' => $inFileName, // 'foobar/baz',
+ 'size' => strlen($inData),
+ 'mtime' => time(), // 1123164748,
+ ];
+ $this->mChanges[$inFileName] = ['status' => 'added', 'data' => $inData];
+ return true;
+ }
+
+ /**
+ * Summary of FileAddPath
+ * @param string $inFileName
+ * @param string $inFilePath
+ * @return bool
+ */
+ public function FileAddPath($inFileName, $inFilePath)
+ {
+ if (!isset($this->mZip)) {
+ return false;
+ }
+
+ $this->mEntries[$inFileName] = [
+ 'name' => $inFileName, // 'foobar/baz',
+ 'size' => filesize($inFilePath),
+ 'mtime' => filemtime($inFilePath), // 1123164748,
+ ];
+ $this->mChanges[$inFileName] = ['status' => 'added', 'path' => $inFilePath];
+ return true;
+ }
+
+ /**
+ * Summary of FileDelete
+ * @param string $inFileName
+ * @return bool
+ */
+ public function FileDelete($inFileName)
+ {
+ if (!$this->FileExists($inFileName)) {
+ return false;
+ }
+
+ $this->mEntries[$inFileName]['size'] = 0;
+ $this->mEntries[$inFileName]['mtime'] = time();
+ $this->mChanges[$inFileName] = ['status' => 'deleted'];
+ return true;
+ }
+
+ /**
+ * Replace the content of a file in the zip entries
+ *
+ * @param string $inFileName File with content to replace
+ * @param string|bool $inData Data content to replace, or false to delete
+ * @return bool
+ */
+ public function FileReplace($inFileName, $inData)
+ {
+ if (!isset($this->mZip)) {
+ return false;
+ }
+
+ if ($inData === false) {
+ return $this->FileDelete($inFileName);
+ }
+
+ $this->mEntries[$inFileName] ??= [];
+ $this->mEntries[$inFileName]['name'] = $inFileName;
+ $this->mEntries[$inFileName]['size'] = strlen($inData);
+ $this->mEntries[$inFileName]['mtime'] = time();
+ $this->mChanges[$inFileName] = ['status' => 'modified', 'data' => $inData];
+ return true;
+ }
+
+ /**
+ * Return the state of the file.
+ * @param mixed $inFileName
+ * @return string|bool 'u'=unchanged, 'm'=modified, 'd'=deleted, 'a'=added, false=unknown
+ */
+ public function FileGetState($inFileName)
+ {
+ $changes = $this->mChanges[$inFileName] ?? ['status' => false];
+ return $changes['status'];
+ }
+
+ /**
+ * Summary of FileCancelModif
+ * @param string $inFileName
+ * @param bool $ReplacedAndDeleted
+ * @return int
+ */
+ public function FileCancelModif($inFileName, $ReplacedAndDeleted = true)
+ {
+ // cancel added, modified or deleted modifications on a file in the archive
+ // return the number of cancels
+
+ $nbr = 0;
+
+ $this->mChanges[$inFileName] = ['status' => 'unchanged'];
+ $nbr += 1;
+ return $nbr;
+ }
+
+ /**
+ * Close the zip file
+ *
+ * @return void
+ * @throws Exception
+ */
+ public function Close()
+ {
+ if (!isset($this->mZip)) {
+ return;
+ }
+
+ $outFileName = $this->mFileName . '.copy';
+ if ($this->mSaveMe) {
+ $outFileStream = fopen($outFileName, 'wb+');
+ if ($outFileStream === false) {
+ throw new Exception('Unable to open zip copy ' . $outFileName);
+ }
+ $this->Flush(self::DOWNLOAD, $this->mFileName, self::MIME_TYPE, false, $outFileStream);
+ $result = fclose($outFileStream);
+ if ($result === false) {
+ throw new Exception('Unable to close zip copy ' . $outFileName);
+ }
+ }
+
+ if (!$this->mZip->close()) {
+ $status = $this->mZip->getStatusString();
+ $this->mZip = null;
+ throw new Exception($status);
+ }
+ if ($this->mSaveMe) {
+ $result = rename($outFileName, $this->mFileName);
+ if ($result === false) {
+ throw new Exception('Unable to rename zip copy ' . $outFileName);
+ }
+ $this->mSaveMe = false;
+ }
+ $this->mZip = null;
+ }
+
+ /**
+ * Summary of SaveBeforeClose
+ * @return void
+ */
+ public function SaveBeforeClose()
+ {
+ // Coming from EPub()->download() without fileName, called in EPub()->save()
+ // This comes right before EPub()->zip->close(), at which point we're lost
+ $this->mSaveMe = true;
+ }
+
+ /**
+ * Summary of Flush
+ * @param mixed $render
+ * @param mixed $outFileName
+ * @param mixed $contentType
+ * @param bool $sendHeaders
+ * @param resource|null $outFileStream
+ * @return void
+ */
+ public function Flush($render = self::DOWNLOAD, $outFileName = '', $contentType = '', $sendHeaders = true, $outFileStream = null)
+ {
+ // we don't want to close the zip file to save all changes here - probably what you needed :-)
+ //$this->Close();
+
+ $outFileName = $outFileName ?: $this->mFileName;
+ $contentType = $contentType ?: self::MIME_TYPE;
+ if ($outFileStream) {
+ $sendHeaders = false;
+ }
+ if (!$sendHeaders) {
+ $render = $render | self::NOHEADER;
+ }
+ if (($render & self::NOHEADER) !== self::NOHEADER) {
+ $sendHeaders = true;
+ } else {
+ $sendHeaders = false;
+ }
+
+ $outZipStream = new ZipStream(
+ outputName: basename($outFileName),
+ outputStream: $outFileStream,
+ sendHttpHeaders: $sendHeaders,
+ contentType: $contentType,
+ );
+ foreach ($this->mEntries as $fileName => $entry) {
+ $changes = $this->mChanges[$fileName];
+ switch ($changes['status']) {
+ case 'unchanged':
+ // Automatic binding of $this
+ $callback = function () use ($fileName) {
+ // this expects a stream as result, not the actual data
+ return $this->mZip->getStream($fileName);
+ };
+ $date = new DateTime();
+ $date->setTimestamp($entry['mtime']);
+ $outZipStream->addFileFromCallback(
+ fileName: $fileName,
+ exactSize: $entry['size'],
+ lastModificationDateTime: $date,
+ callback: $callback,
+ );
+ break;
+ case 'added':
+ case 'modified':
+ if (isset($changes['data'])) {
+ $outZipStream->addFile(
+ fileName: $fileName,
+ data: $changes['data'],
+ );
+ } elseif (isset($changes['path'])) {
+ $outZipStream->addFileFromPath(
+ fileName: $fileName,
+ path: $changes['path'],
+ );
+ }
+ break;
+ case 'deleted':
+ default:
+ break;
+ }
+ }
+
+ $outZipStream->finish();
+ }
+
+ /**
+ * Summary of copyTest
+ * @param string $inFileName
+ * @param string $outFileName
+ * @return void
+ */
+ public static function copyTest($inFileName, $outFileName)
+ {
+ $inZipFile = new ZipArchive();
+ $result = $inZipFile->open($inFileName, ZipArchive::RDONLY);
+ if ($result !== true) {
+ throw new Exception('Unable to open zip file ' . $inFileName);
+ }
+
+ $entries = [];
+ for ($i = 0; $i < $inZipFile->numFiles; $i++) {
+ $entry = $inZipFile->statIndex($i);
+ $fileName = $entry['name'];
+ $entries[$fileName] = $entry;
+ }
+
+ // see ZipStreamTest.php
+ $outFileStream = fopen($outFileName, 'wb+');
+
+ $outZipStream = new ZipStream(
+ outputName: basename($outFileName),
+ outputStream: $outFileStream,
+ sendHttpHeaders: false,
+ );
+ foreach ($entries as $fileName => $entry) {
+ $date = new DateTime();
+ $date->setTimestamp($entry['mtime']);
+ // does not work in v2 - the zip stream is not seekable, but ZipStream checks for it in Stream.php
+ // does work in v3 - implemented using addFileFromCallback, so we might as well use that :-)
+ $outZipStream->addFileFromCallback(
+ fileName: $fileName,
+ exactSize: $entry['size'],
+ lastModificationDateTime: $date,
+ callback: function () use ($inZipFile, $fileName) {
+ // this expects a stream as result, not the actual data
+ return $inZipFile->getStream($fileName);
+ },
+ );
+ }
+
+ $outZipStream->finish();
+ fclose($outFileStream);
+ }
+}
diff --git a/vendor/mikespub/php-epub-meta/src/Tools/ZipFile.php b/vendor/mikespub/php-epub-meta/src/Tools/ZipFile.php
new file mode 100644
index 000000000..b083b0c4f
--- /dev/null
+++ b/vendor/mikespub/php-epub-meta/src/Tools/ZipFile.php
@@ -0,0 +1,343 @@
+<?php
+/**
+ * ZipFile class
+ *
+ * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
+ * @author Didier Corbière <contact@atoll-digital-library.org>
+ * @author mikespub
+ */
+
+namespace SebLucas\EPubMeta\Tools;
+
+use Exception;
+use ZipArchive;
+
+/**
+ * ZipFile class allows to open files inside a zip file with the standard php zip functions
+ *
+ * This class also supports adding/replacing/deleting files inside the zip file, but changes
+ * will *not* be reflected correctly until you close the zip file, and open it again if needed
+ *
+ * Note: this is not meant to handle a massive amount of files inside generic archive files.
+ * It is specifically meant for EPUB files which typically contain hundreds/thousands of pages,
+ * not millions or more. And any changes you make are kept in memory, so don't re-write every
+ * page of War and Peace either - better to unzip that locally and then re-zip it afterwards.
+ */
+class ZipFile
+{
+ public const DOWNLOAD = 1; // download (default)
+ public const NOHEADER = 4; // option to use with DOWNLOAD: no header is sent
+ public const FILE = 8; // output to file , or add from file
+ public const STRING = 32; // output to string, or add from string
+ public const MIME_TYPE = 'application/epub+zip';
+
+ /** @var ZipArchive|null */
+ protected $mZip;
+ /** @var array<string, mixed>|null */
+ protected $mEntries;
+ /** @var array<string, mixed> */
+ protected $mChanges = [];
+ /** @var string|null */
+ protected $mFileName;
+
+ public function __construct()
+ {
+ $this->mZip = null;
+ $this->mEntries = null;
+ $this->mFileName = null;
+ }
+
+ /**
+ * Destructor
+ */
+ public function __destruct()
+ {
+ $this->Close();
+ }
+
+ /**
+ * Open a zip file and read it's entries
+ *
+ * @param string $inFileName
+ * @param int|null $inFlags
+ * @return boolean True if zip file has been correctly opended, else false
+ */
+ public function Open($inFileName, $inFlags = 0) // ZipArchive::RDONLY)
+ {
+ $this->Close();
+ $inFileName = realpath($inFileName);
+
+ $this->mZip = new ZipArchive();
+ $result = $this->mZip->open($inFileName, $inFlags);
+ if ($result !== true) {
+ return false;
+ }
+
+ $this->mFileName = $inFileName;
+
+ $this->mEntries = [];
+ $this->mChanges = [];
+
+ for ($i = 0; $i < $this->mZip->numFiles; $i++) {
+ $entry = $this->mZip->statIndex($i);
+ $fileName = $entry['name'];
+ $this->mEntries[$fileName] = $entry;
+ $this->mChanges[$fileName] = ['status' => 'unchanged'];
+ }
+
+ return true;
+ }
+
+ /**
+ * Check if a file exist in the zip entries
+ *
+ * @param string $inFileName File to search
+ *
+ * @return boolean True if the file exist, else false
+ */
+ public function FileExists($inFileName)
+ {
+ if (!isset($this->mZip)) {
+ return false;
+ }
+
+ if (!isset($this->mEntries[$inFileName])) {
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Read the content of a file in the zip entries
+ *
+ * @param string $inFileName File to search
+ *
+ * @return mixed File content the file exist, else false
+ */
+ public function FileRead($inFileName)
+ {
+ if (!isset($this->mZip)) {
+ return false;
+ }
+
+ if (!isset($this->mEntries[$inFileName])) {
+ return false;
+ }
+
+ $data = false;
+
+ $changes = $this->mChanges[$inFileName] ?? ['status' => 'unchanged'];
+ switch ($changes['status']) {
+ case 'unchanged':
+ $data = $this->mZip->getFromName($inFileName);
+ break;
+ case 'added':
+ case 'modified':
+ if (isset($changes['data'])) {
+ $data = $changes['data'];
+ } elseif (isset($changes['path'])) {
+ $data = file_get_contents($changes['path']);
+ }
+ break;
+ case 'deleted':
+ default:
+ break;
+ }
+ return $data;
+ }
+
+ /**
+ * Get a file handler to a file in the zip entries (read-only)
+ *
+ * @param string $inFileName File to search
+ *
+ * @return resource|bool File handler if the file exist, else false
+ */
+ public function FileStream($inFileName)
+ {
+ if (!isset($this->mZip)) {
+ return false;
+ }
+
+ if (!isset($this->mEntries[$inFileName])) {
+ return false;
+ }
+
+ // @todo streaming of added/modified data?
+ return $this->mZip->getStream($inFileName);
+ }
+
+ /**
+ * Summary of FileAdd
+ * @param string $inFileName
+ * @param mixed $inData
+ * @return bool
+ */
+ public function FileAdd($inFileName, $inData)
+ {
+ if (!isset($this->mZip)) {
+ return false;
+ }
+
+ if (!$this->mZip->addFromString($inFileName, $inData)) {
+ return false;
+ }
+ $this->mEntries[$inFileName] = $this->mZip->statName($inFileName);
+ $this->mChanges[$inFileName] = ['status' => 'added', 'data' => $inData];
+ return true;
+ }
+
+ /**
+ * Summary of FileAddPath
+ * @param string $inFileName
+ * @param string $inFilePath
+ * @return mixed
+ */
+ public function FileAddPath($inFileName, $inFilePath)
+ {
+ if (!isset($this->mZip)) {
+ return false;
+ }
+
+ if (!$this->mZip->addFile($inFilePath, $inFileName)) {
+ return false;
+ }
+ $this->mEntries[$inFileName] = $this->mZip->statName($inFileName);
+ $this->mChanges[$inFileName] = ['status' => 'added', 'path' => $inFilePath];
+ return true;
+ }
+
+ /**
+ * Summary of FileDelete
+ * @param string $inFileName
+ * @return bool
+ */
+ public function FileDelete($inFileName)
+ {
+ if (!$this->FileExists($inFileName)) {
+ return false;
+ }
+
+ if (!$this->mZip->deleteName($inFileName)) {
+ return false;
+ }
+ unset($this->mEntries[$inFileName]);
+ $this->mChanges[$inFileName] = ['status' => 'deleted'];
+ return true;
+ }
+
+ /**
+ * Replace the content of a file in the zip entries
+ *
+ * @param string $inFileName File with content to replace
+ * @param string|bool $inData Data content to replace, or false to delete
+ * @return bool
+ */
+ public function FileReplace($inFileName, $inData)
+ {
+ if (!isset($this->mZip)) {
+ return false;
+ }
+
+ if ($inData === false) {
+ return $this->FileDelete($inFileName);
+ }
+
+ if (!$this->mZip->addFromString($inFileName, $inData)) {
+ return false;
+ }
+ $this->mEntries[$inFileName] = $this->mZip->statName($inFileName);
+ $this->mChanges[$inFileName] = ['status' => 'modified', 'data' => $inData];
+ return true;
+ }
+
+ /**
+ * Return the state of the file.
+ * @param mixed $inFileName
+ * @return string|bool 'u'=unchanged, 'm'=modified, 'd'=deleted, 'a'=added, false=unknown
+ */
+ public function FileGetState($inFileName)
+ {
+ $changes = $this->mChanges[$inFileName] ?? ['status' => false];
+ return $changes['status'];
+ }
+
+ /**
+ * Summary of FileCancelModif
+ * @param string $inFileName
+ * @param bool $ReplacedAndDeleted
+ * @return int
+ */
+ public function FileCancelModif($inFileName, $ReplacedAndDeleted = true)
+ {
+ // cancel added, modified or deleted modifications on a file in the archive
+ // return the number of cancels
+
+ $nbr = 0;
+
+ if (!$this->mZip->unchangeName($inFileName)) {
+ return $nbr;
+ }
+ $nbr += 1;
+ $this->mChanges[$inFileName] = ['status' => 'unchanged'];
+ return $nbr;
+ }
+
+ /**
+ * Close the zip file
+ *
+ * @return void
+ * @throws Exception
+ */
+ public function Close()
+ {
+ if (!isset($this->mZip)) {
+ return;
+ }
+
+ if (!$this->mZip->close()) {
+ $status = $this->mZip->getStatusString();
+ $this->mZip = null;
+ throw new Exception($status);
+ }
+ $this->mZip = null;
+ }
+
+ /**
+ * Summary of Flush
+ * @param mixed $render
+ * @param mixed $outFileName
+ * @param mixed $contentType
+ * @param bool $sendHeaders
+ * @return void
+ */
+ public function Flush($render = self::DOWNLOAD, $outFileName = '', $contentType = '', $sendHeaders = true)
+ {
+ // we need to close the zip file to save all changes here - probably not what you wanted :-()
+ $this->Close();
+
+ $outFileName = $outFileName ?: $this->mFileName;
+ $contentType = $contentType ?: static::MIME_TYPE;
+ if (!$sendHeaders) {
+ $render = $render | static::NOHEADER;
+ }
+ $inFilePath = realpath($this->mFileName);
+
+ if (($render & static::NOHEADER) !== static::NOHEADER) {
+ $expires = 60 * 60 * 24 * 14;
+ header('Pragma: public');
+ header('Cache-Control: max-age=' . $expires);
+ header('Expires: ' . gmdate('D, d M Y H:i:s', time() + $expires) . ' GMT');
+
+ header('Content-Type: ' . $contentType);
+ header('Content-Disposition: attachment; filename="' . basename($outFileName) . '"');
+
+ // see fetch.php for use of Config::get('x_accel_redirect')
+ header('Content-Length: ' . filesize($inFilePath));
+ //header(Config::get('x_accel_redirect') . ': ' . $inFilePath);
+ }
+
+ readfile($inFilePath);
+ }
+}
diff --git a/vendor/mikespub/php-epub-meta/src/Tools/htmlBlockLevelElements.php b/vendor/mikespub/php-epub-meta/src/Tools/htmlBlockLevelElements.php
new file mode 100644
index 000000000..6e96a6b0f
--- /dev/null
+++ b/vendor/mikespub/php-epub-meta/src/Tools/htmlBlockLevelElements.php
@@ -0,0 +1,42 @@
+<?php
+/**
+ * List of HTML block level elements.
+ * Source: https://developer.mozilla.org/en-US/docs/Web/HTML/Block-level_elements
+ */
+return [
+ 'address',
+ 'article',
+ 'aside',
+ 'audio',
+ 'video',
+ 'blockquote',
+ 'canvas',
+ 'dd',
+ 'div',
+ 'dl',
+ 'fieldset',
+ 'figcaption',
+ 'figure',
+ 'footer',
+ 'form',
+ 'h1',
+ 'h2',
+ 'h3',
+ 'h4',
+ 'h5',
+ 'h6',
+ 'header',
+ 'hgroup',
+ 'hr',
+ 'li',
+ 'noscript',
+ 'ol',
+ 'output',
+ 'p',
+ 'pre',
+ 'section',
+ 'table',
+ 'tfoot',
+ 'ul',
+ 'video',
+];
diff --git a/vendor/mikespub/php-epub-meta/src/Tools/htmlEntityMap.php b/vendor/mikespub/php-epub-meta/src/Tools/htmlEntityMap.php
new file mode 100644
index 000000000..6bc3f4653
--- /dev/null
+++ b/vendor/mikespub/php-epub-meta/src/Tools/htmlEntityMap.php
@@ -0,0 +1,260 @@
+<?php
+/**
+ * Maps every HTML named entity to its numeric equivalent.
+ * Source: http://stackoverflow.com/questions/11176752/converting-named-html-entities-to-numeric-html-entities#answer-11179875
+ */
+return [
+'&nbsp;' => '&#160;', # no-break space = non-breaking space, U+00A0 ISOnum
+'&iexcl;' => '&#161;', # inverted exclamation mark, U+00A1 ISOnum
+'&cent;' => '&#162;', # cent sign, U+00A2 ISOnum
+'&pound;' => '&#163;', # pound sign, U+00A3 ISOnum
+'&curren;' => '&#164;', # currency sign, U+00A4 ISOnum
+'&yen;' => '&#165;', # yen sign = yuan sign, U+00A5 ISOnum
+'&brvbar;' => '&#166;', # broken bar = broken vertical bar, U+00A6 ISOnum
+'&sect;' => '&#167;', # section sign, U+00A7 ISOnum
+'&uml;' => '&#168;', # diaeresis = spacing diaeresis, U+00A8 ISOdia
+'&copy;' => '&#169;', # copyright sign, U+00A9 ISOnum
+'&ordf;' => '&#170;', # feminine ordinal indicator, U+00AA ISOnum
+'&laquo;' => '&#171;', # left-pointing double angle quotation mark = left pointing guillemet, U+00AB ISOnum
+'&not;' => '&#172;', # not sign, U+00AC ISOnum
+'&shy;' => '&#173;', # soft hyphen = discretionary hyphen, U+00AD ISOnum
+'&reg;' => '&#174;', # registered sign = registered trade mark sign, U+00AE ISOnum
+'&macr;' => '&#175;', # macron = spacing macron = overline = APL overbar, U+00AF ISOdia
+'&deg;' => '&#176;', # degree sign, U+00B0 ISOnum
+'&plusmn;' => '&#177;', # plus-minus sign = plus-or-minus sign, U+00B1 ISOnum
+'&sup2;' => '&#178;', # superscript two = superscript digit two = squared, U+00B2 ISOnum
+'&sup3;' => '&#179;', # superscript three = superscript digit three = cubed, U+00B3 ISOnum
+'&acute;' => '&#180;', # acute accent = spacing acute, U+00B4 ISOdia
+'&micro;' => '&#181;', # micro sign, U+00B5 ISOnum
+'&para;' => '&#182;', # pilcrow sign = paragraph sign, U+00B6 ISOnum
+'&middot;' => '&#183;', # middle dot = Georgian comma = Greek middle dot, U+00B7 ISOnum
+'&cedil;' => '&#184;', # cedilla = spacing cedilla, U+00B8 ISOdia
+'&sup1;' => '&#185;', # superscript one = superscript digit one, U+00B9 ISOnum
+'&ordm;' => '&#186;', # masculine ordinal indicator, U+00BA ISOnum
+'&raquo;' => '&#187;', # right-pointing double angle quotation mark = right pointing guillemet, U+00BB ISOnum
+'&frac14;' => '&#188;', # vulgar fraction one quarter = fraction one quarter, U+00BC ISOnum
+'&frac12;' => '&#189;', # vulgar fraction one half = fraction one half, U+00BD ISOnum
+'&frac34;' => '&#190;', # vulgar fraction three quarters = fraction three quarters, U+00BE ISOnum
+'&iquest;' => '&#191;', # inverted question mark = turned question mark, U+00BF ISOnum
+'&Agrave;' => '&#192;', # latin capital letter A with grave = latin capital letter A grave, U+00C0 ISOlat1
+'&Aacute;' => '&#193;', # latin capital letter A with acute, U+00C1 ISOlat1
+'&Acirc;' => '&#194;', # latin capital letter A with circumflex, U+00C2 ISOlat1
+'&Atilde;' => '&#195;', # latin capital letter A with tilde, U+00C3 ISOlat1
+'&Auml;' => '&#196;', # latin capital letter A with diaeresis, U+00C4 ISOlat1
+'&Aring;' => '&#197;', # latin capital letter A with ring above = latin capital letter A ring, U+00C5 ISOlat1
+'&AElig;' => '&#198;', # latin capital letter AE = latin capital ligature AE, U+00C6 ISOlat1
+'&Ccedil;' => '&#199;', # latin capital letter C with cedilla, U+00C7 ISOlat1
+'&Egrave;' => '&#200;', # latin capital letter E with grave, U+00C8 ISOlat1
+'&Eacute;' => '&#201;', # latin capital letter E with acute, U+00C9 ISOlat1
+'&Ecirc;' => '&#202;', # latin capital letter E with circumflex, U+00CA ISOlat1
+'&Euml;' => '&#203;', # latin capital letter E with diaeresis, U+00CB ISOlat1
+'&Igrave;' => '&#204;', # latin capital letter I with grave, U+00CC ISOlat1
+'&Iacute;' => '&#205;', # latin capital letter I with acute, U+00CD ISOlat1
+'&Icirc;' => '&#206;', # latin capital letter I with circumflex, U+00CE ISOlat1
+'&Iuml;' => '&#207;', # latin capital letter I with diaeresis, U+00CF ISOlat1
+'&ETH;' => '&#208;', # latin capital letter ETH, U+00D0 ISOlat1
+'&Ntilde;' => '&#209;', # latin capital letter N with tilde, U+00D1 ISOlat1
+'&Ograve;' => '&#210;', # latin capital letter O with grave, U+00D2 ISOlat1
+'&Oacute;' => '&#211;', # latin capital letter O with acute, U+00D3 ISOlat1
+'&Ocirc;' => '&#212;', # latin capital letter O with circumflex, U+00D4 ISOlat1
+'&Otilde;' => '&#213;', # latin capital letter O with tilde, U+00D5 ISOlat1
+'&Ouml;' => '&#214;', # latin capital letter O with diaeresis, U+00D6 ISOlat1
+'&times;' => '&#215;', # multiplication sign, U+00D7 ISOnum
+'&Oslash;' => '&#216;', # latin capital letter O with stroke = latin capital letter O slash, U+00D8 ISOlat1
+'&Ugrave;' => '&#217;', # latin capital letter U with grave, U+00D9 ISOlat1
+'&Uacute;' => '&#218;', # latin capital letter U with acute, U+00DA ISOlat1
+'&Ucirc;' => '&#219;', # latin capital letter U with circumflex, U+00DB ISOlat1
+'&Uuml;' => '&#220;', # latin capital letter U with diaeresis, U+00DC ISOlat1
+'&Yacute;' => '&#221;', # latin capital letter Y with acute, U+00DD ISOlat1
+'&THORN;' => '&#222;', # latin capital letter THORN, U+00DE ISOlat1
+'&szlig;' => '&#223;', # latin small letter sharp s = ess-zed, U+00DF ISOlat1
+'&agrave;' => '&#224;', # latin small letter a with grave = latin small letter a grave, U+00E0 ISOlat1
+'&aacute;' => '&#225;', # latin small letter a with acute, U+00E1 ISOlat1
+'&acirc;' => '&#226;', # latin small letter a with circumflex, U+00E2 ISOlat1
+'&atilde;' => '&#227;', # latin small letter a with tilde, U+00E3 ISOlat1
+'&auml;' => '&#228;', # latin small letter a with diaeresis, U+00E4 ISOlat1
+'&aring;' => '&#229;', # latin small letter a with ring above = latin small letter a ring, U+00E5 ISOlat1
+'&aelig;' => '&#230;', # latin small letter ae = latin small ligature ae, U+00E6 ISOlat1
+'&ccedil;' => '&#231;', # latin small letter c with cedilla, U+00E7 ISOlat1
+'&egrave;' => '&#232;', # latin small letter e with grave, U+00E8 ISOlat1
+'&eacute;' => '&#233;', # latin small letter e with acute, U+00E9 ISOlat1
+'&ecirc;' => '&#234;', # latin small letter e with circumflex, U+00EA ISOlat1
+'&euml;' => '&#235;', # latin small letter e with diaeresis, U+00EB ISOlat1
+'&igrave;' => '&#236;', # latin small letter i with grave, U+00EC ISOlat1
+'&iacute;' => '&#237;', # latin small letter i with acute, U+00ED ISOlat1
+'&icirc;' => '&#238;', # latin small letter i with circumflex, U+00EE ISOlat1
+'&iuml;' => '&#239;', # latin small letter i with diaeresis, U+00EF ISOlat1
+'&eth;' => '&#240;', # latin small letter eth, U+00F0 ISOlat1
+'&ntilde;' => '&#241;', # latin small letter n with tilde, U+00F1 ISOlat1
+'&ograve;' => '&#242;', # latin small letter o with grave, U+00F2 ISOlat1
+'&oacute;' => '&#243;', # latin small letter o with acute, U+00F3 ISOlat1
+'&ocirc;' => '&#244;', # latin small letter o with circumflex, U+00F4 ISOlat1
+'&otilde;' => '&#245;', # latin small letter o with tilde, U+00F5 ISOlat1
+'&ouml;' => '&#246;', # latin small letter o with diaeresis, U+00F6 ISOlat1
+'&divide;' => '&#247;', # division sign, U+00F7 ISOnum
+'&oslash;' => '&#248;', # latin small letter o with stroke, = latin small letter o slash, U+00F8 ISOlat1
+'&ugrave;' => '&#249;', # latin small letter u with grave, U+00F9 ISOlat1
+'&uacute;' => '&#250;', # latin small letter u with acute, U+00FA ISOlat1
+'&ucirc;' => '&#251;', # latin small letter u with circumflex, U+00FB ISOlat1
+'&uuml;' => '&#252;', # latin small letter u with diaeresis, U+00FC ISOlat1
+'&yacute;' => '&#253;', # latin small letter y with acute, U+00FD ISOlat1
+'&thorn;' => '&#254;', # latin small letter thorn, U+00FE ISOlat1
+'&yuml;' => '&#255;', # latin small letter y with diaeresis, U+00FF ISOlat1
+'&fnof;' => '&#402;', # latin small f with hook = function = florin, U+0192 ISOtech
+'&Alpha;' => '&#913;', # greek capital letter alpha, U+0391
+'&Beta;' => '&#914;', # greek capital letter beta, U+0392
+'&Gamma;' => '&#915;', # greek capital letter gamma, U+0393 ISOgrk3
+'&Delta;' => '&#916;', # greek capital letter delta, U+0394 ISOgrk3
+'&Epsilon;' => '&#917;', # greek capital letter epsilon, U+0395
+'&Zeta;' => '&#918;', # greek capital letter zeta, U+0396
+'&Eta;' => '&#919;', # greek capital letter eta, U+0397
+'&Theta;' => '&#920;', # greek capital letter theta, U+0398 ISOgrk3
+'&Iota;' => '&#921;', # greek capital letter iota, U+0399
+'&Kappa;' => '&#922;', # greek capital letter kappa, U+039A
+'&Lambda;' => '&#923;', # greek capital letter lambda, U+039B ISOgrk3
+'&Mu;' => '&#924;', # greek capital letter mu, U+039C
+'&Nu;' => '&#925;', # greek capital letter nu, U+039D
+'&Xi;' => '&#926;', # greek capital letter xi, U+039E ISOgrk3
+'&Omicron;' => '&#927;', # greek capital letter omicron, U+039F
+'&Pi;' => '&#928;', # greek capital letter pi, U+03A0 ISOgrk3
+'&Rho;' => '&#929;', # greek capital letter rho, U+03A1
+'&Sigma;' => '&#931;', # greek capital letter sigma, U+03A3 ISOgrk3
+'&Tau;' => '&#932;', # greek capital letter tau, U+03A4
+'&Upsilon;' => '&#933;', # greek capital letter upsilon, U+03A5 ISOgrk3
+'&Phi;' => '&#934;', # greek capital letter phi, U+03A6 ISOgrk3
+'&Chi;' => '&#935;', # greek capital letter chi, U+03A7
+'&Psi;' => '&#936;', # greek capital letter psi, U+03A8 ISOgrk3
+'&Omega;' => '&#937;', # greek capital letter omega, U+03A9 ISOgrk3
+'&alpha;' => '&#945;', # greek small letter alpha, U+03B1 ISOgrk3
+'&beta;' => '&#946;', # greek small letter beta, U+03B2 ISOgrk3
+'&gamma;' => '&#947;', # greek small letter gamma, U+03B3 ISOgrk3
+'&delta;' => '&#948;', # greek small letter delta, U+03B4 ISOgrk3
+'&epsilon;' => '&#949;', # greek small letter epsilon, U+03B5 ISOgrk3
+'&zeta;' => '&#950;', # greek small letter zeta, U+03B6 ISOgrk3
+'&eta;' => '&#951;', # greek small letter eta, U+03B7 ISOgrk3
+'&theta;' => '&#952;', # greek small letter theta, U+03B8 ISOgrk3
+'&iota;' => '&#953;', # greek small letter iota, U+03B9 ISOgrk3
+'&kappa;' => '&#954;', # greek small letter kappa, U+03BA ISOgrk3
+'&lambda;' => '&#955;', # greek small letter lambda, U+03BB ISOgrk3
+'&mu;' => '&#956;', # greek small letter mu, U+03BC ISOgrk3
+'&nu;' => '&#957;', # greek small letter nu, U+03BD ISOgrk3
+'&xi;' => '&#958;', # greek small letter xi, U+03BE ISOgrk3
+'&omicron;' => '&#959;', # greek small letter omicron, U+03BF NEW
+'&pi;' => '&#960;', # greek small letter pi, U+03C0 ISOgrk3
+'&rho;' => '&#961;', # greek small letter rho, U+03C1 ISOgrk3
+'&sigmaf;' => '&#962;', # greek small letter final sigma, U+03C2 ISOgrk3
+'&sigma;' => '&#963;', # greek small letter sigma, U+03C3 ISOgrk3
+'&tau;' => '&#964;', # greek small letter tau, U+03C4 ISOgrk3
+'&upsilon;' => '&#965;', # greek small letter upsilon, U+03C5 ISOgrk3
+'&phi;' => '&#966;', # greek small letter phi, U+03C6 ISOgrk3
+'&chi;' => '&#967;', # greek small letter chi, U+03C7 ISOgrk3
+'&psi;' => '&#968;', # greek small letter psi, U+03C8 ISOgrk3
+'&omega;' => '&#969;', # greek small letter omega, U+03C9 ISOgrk3
+'&thetasym;' => '&#977;', # greek small letter theta symbol, U+03D1 NEW
+'&upsih;' => '&#978;', # greek upsilon with hook symbol, U+03D2 NEW
+'&piv;' => '&#982;', # greek pi symbol, U+03D6 ISOgrk3
+'&bull;' => '&#8226;', # bullet = black small circle, U+2022 ISOpub
+'&hellip;' => '&#8230;', # horizontal ellipsis = three dot leader, U+2026 ISOpub
+'&prime;' => '&#8242;', # prime = minutes = feet, U+2032 ISOtech
+'&Prime;' => '&#8243;', # double prime = seconds = inches, U+2033 ISOtech
+'&oline;' => '&#8254;', # overline = spacing overscore, U+203E NEW
+'&frasl;' => '&#8260;', # fraction slash, U+2044 NEW
+'&weierp;' => '&#8472;', # script capital P = power set = Weierstrass p, U+2118 ISOamso
+'&image;' => '&#8465;', # blackletter capital I = imaginary part, U+2111 ISOamso
+'&real;' => '&#8476;', # blackletter capital R = real part symbol, U+211C ISOamso
+'&trade;' => '&#8482;', # trade mark sign, U+2122 ISOnum
+'&alefsym;' => '&#8501;', # alef symbol = first transfinite cardinal, U+2135 NEW
+'&larr;' => '&#8592;', # leftwards arrow, U+2190 ISOnum
+'&uarr;' => '&#8593;', # upwards arrow, U+2191 ISOnum
+'&rarr;' => '&#8594;', # rightwards arrow, U+2192 ISOnum
+'&darr;' => '&#8595;', # downwards arrow, U+2193 ISOnum
+'&harr;' => '&#8596;', # left right arrow, U+2194 ISOamsa
+'&crarr;' => '&#8629;', # downwards arrow with corner leftwards = carriage return, U+21B5 NEW
+'&lArr;' => '&#8656;', # leftwards double arrow, U+21D0 ISOtech
+'&uArr;' => '&#8657;', # upwards double arrow, U+21D1 ISOamsa
+'&rArr;' => '&#8658;', # rightwards double arrow, U+21D2 ISOtech
+'&dArr;' => '&#8659;', # downwards double arrow, U+21D3 ISOamsa
+'&hArr;' => '&#8660;', # left right double arrow, U+21D4 ISOamsa
+'&forall;' => '&#8704;', # for all, U+2200 ISOtech
+'&part;' => '&#8706;', # partial differential, U+2202 ISOtech
+'&exist;' => '&#8707;', # there exists, U+2203 ISOtech
+'&empty;' => '&#8709;', # empty set = null set = diameter, U+2205 ISOamso
+'&nabla;' => '&#8711;', # nabla = backward difference, U+2207 ISOtech
+'&isin;' => '&#8712;', # element of, U+2208 ISOtech
+'&notin;' => '&#8713;', # not an element of, U+2209 ISOtech
+'&ni;' => '&#8715;', # contains as member, U+220B ISOtech
+'&prod;' => '&#8719;', # n-ary product = product sign, U+220F ISOamsb
+'&sum;' => '&#8721;', # n-ary sumation, U+2211 ISOamsb
+'&minus;' => '&#8722;', # minus sign, U+2212 ISOtech
+'&lowast;' => '&#8727;', # asterisk operator, U+2217 ISOtech
+'&radic;' => '&#8730;', # square root = radical sign, U+221A ISOtech
+'&prop;' => '&#8733;', # proportional to, U+221D ISOtech
+'&infin;' => '&#8734;', # infinity, U+221E ISOtech
+'&ang;' => '&#8736;', # angle, U+2220 ISOamso
+'&and;' => '&#8743;', # logical and = wedge, U+2227 ISOtech
+'&or;' => '&#8744;', # logical or = vee, U+2228 ISOtech
+'&cap;' => '&#8745;', # intersection = cap, U+2229 ISOtech
+'&cup;' => '&#8746;', # union = cup, U+222A ISOtech
+'&int;' => '&#8747;', # integral, U+222B ISOtech
+'&there4;' => '&#8756;', # therefore, U+2234 ISOtech
+'&sim;' => '&#8764;', # tilde operator = varies with = similar to, U+223C ISOtech
+'&cong;' => '&#8773;', # approximately equal to, U+2245 ISOtech
+'&asymp;' => '&#8776;', # almost equal to = asymptotic to, U+2248 ISOamsr
+'&ne;' => '&#8800;', # not equal to, U+2260 ISOtech
+'&equiv;' => '&#8801;', # identical to, U+2261 ISOtech
+'&le;' => '&#8804;', # less-than or equal to, U+2264 ISOtech
+'&ge;' => '&#8805;', # greater-than or equal to, U+2265 ISOtech
+'&sub;' => '&#8834;', # subset of, U+2282 ISOtech
+'&sup;' => '&#8835;', # superset of, U+2283 ISOtech
+'&nsub;' => '&#8836;', # not a subset of, U+2284 ISOamsn
+'&sube;' => '&#8838;', # subset of or equal to, U+2286 ISOtech
+'&supe;' => '&#8839;', # superset of or equal to, U+2287 ISOtech
+'&oplus;' => '&#8853;', # circled plus = direct sum, U+2295 ISOamsb
+'&otimes;' => '&#8855;', # circled times = vector product, U+2297 ISOamsb
+'&perp;' => '&#8869;', # up tack = orthogonal to = perpendicular, U+22A5 ISOtech
+'&sdot;' => '&#8901;', # dot operator, U+22C5 ISOamsb
+'&lceil;' => '&#8968;', # left ceiling = apl upstile, U+2308 ISOamsc
+'&rceil;' => '&#8969;', # right ceiling, U+2309 ISOamsc
+'&lfloor;' => '&#8970;', # left floor = apl downstile, U+230A ISOamsc
+'&rfloor;' => '&#8971;', # right floor, U+230B ISOamsc
+'&lang;' => '&#9001;', # left-pointing angle bracket = bra, U+2329 ISOtech
+'&rang;' => '&#9002;', # right-pointing angle bracket = ket, U+232A ISOtech
+'&loz;' => '&#9674;', # lozenge, U+25CA ISOpub
+'&spades;' => '&#9824;', # black spade suit, U+2660 ISOpub
+'&clubs;' => '&#9827;', # black club suit = shamrock, U+2663 ISOpub
+'&hearts;' => '&#9829;', # black heart suit = valentine, U+2665 ISOpub
+'&diams;' => '&#9830;', # black diamond suit, U+2666 ISOpub
+'&quot;' => '&#34;', # quotation mark = APL quote, U+0022 ISOnum
+'&amp;' => '&#38;', # ampersand, U+0026 ISOnum
+'&lt;' => '&#60;', # less-than sign, U+003C ISOnum
+'&gt;' => '&#62;', # greater-than sign, U+003E ISOnum
+'&OElig;' => '&#338;', # latin capital ligature OE, U+0152 ISOlat2
+'&oelig;' => '&#339;', # latin small ligature oe, U+0153 ISOlat2
+'&Scaron;' => '&#352;', # latin capital letter S with caron, U+0160 ISOlat2
+'&scaron;' => '&#353;', # latin small letter s with caron, U+0161 ISOlat2
+'&Yuml;' => '&#376;', # latin capital letter Y with diaeresis, U+0178 ISOlat2
+'&circ;' => '&#710;', # modifier letter circumflex accent, U+02C6 ISOpub
+'&tilde;' => '&#732;', # small tilde, U+02DC ISOdia
+'&ensp;' => '&#8194;', # en space, U+2002 ISOpub
+'&emsp;' => '&#8195;', # em space, U+2003 ISOpub
+'&thinsp;' => '&#8201;', # thin space, U+2009 ISOpub
+'&zwnj;' => '&#8204;', # zero width non-joiner, U+200C NEW RFC 2070
+'&zwj;' => '&#8205;', # zero width joiner, U+200D NEW RFC 2070
+'&lrm;' => '&#8206;', # left-to-right mark, U+200E NEW RFC 2070
+'&rlm;' => '&#8207;', # right-to-left mark, U+200F NEW RFC 2070
+'&ndash;' => '&#8211;', # en dash, U+2013 ISOpub
+'&mdash;' => '&#8212;', # em dash, U+2014 ISOpub
+'&lsquo;' => '&#8216;', # left single quotation mark, U+2018 ISOnum
+'&rsquo;' => '&#8217;', # right single quotation mark, U+2019 ISOnum
+'&sbquo;' => '&#8218;', # single low-9 quotation mark, U+201A NEW
+'&ldquo;' => '&#8220;', # left double quotation mark, U+201C ISOnum
+'&rdquo;' => '&#8221;', # right double quotation mark, U+201D ISOnum
+'&bdquo;' => '&#8222;', # double low-9 quotation mark, U+201E NEW
+'&dagger;' => '&#8224;', # dagger, U+2020 ISOpub
+'&Dagger;' => '&#8225;', # double dagger, U+2021 ISOpub
+'&permil;' => '&#8240;', # per mille sign, U+2030 ISOtech
+'&lsaquo;' => '&#8249;', # single left-pointing angle quotation mark, U+2039 ISO proposed
+'&rsaquo;' => '&#8250;', # single right-pointing angle quotation mark, U+203A ISO proposed
+'&euro;' => '&#8364;', # euro sign, U+20AC NEW
+'&apos;' => '&#39;', # apostrophe = APL quote, U+0027 ISOnum
+];