1
0
Fork 0
mirror of https://github.com/futurepress/epub.js.git synced 2025-10-03 14:59:18 +02:00
This commit is contained in:
kmacshane 2023-05-15 22:03:08 -07:00 committed by GitHub
commit 184520769d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 1024 additions and 21196 deletions

325
espark_reader/reader.css Normal file
View file

@ -0,0 +1,325 @@
body {
margin: 0;
background: #fafafa;
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
color: #333;
/*position: absolute;*/
height: 100%;
width: 100%;
/*min-height: 800px;*/
}
#title {
width: 900px;
min-height: 18px;
margin: 10px auto;
text-align: center;
font-size: 16px;
color: #E2E2E2;
font-weight: 400;
}
#title:hover {
color: #777;
}
#viewer {
width: 900px;
height: 600px;
box-shadow: 0 0 4px #ccc;
padding: 10px 10px 0px 10px;
margin: 5px auto;
background: white;
}
#viewer.spreads {
width: 900px;
height: 600px;
box-shadow: 0 0 4px #ccc;
border-radius: 5px;
padding: 0;
position: relative;
margin: 10px auto;
background: white;
top: calc(50vh - 400px);
}
/* Smartphone - Portrait */
@media only screen and (min-width: 320px) and (max-width: 667px) and (orientation: portrait) {
#viewer {
width: 320px;
height: 667px;
}
}
/* Smartphone - Landscape */
@media only screen and (min-width: 320px) and (max-width: 667px) and (orientation: landscape) {
#viewer {
width: 667px;
height: 320px;
}
}
/* Tablet - Portrait and Landscape */
@media only screen and (min-width: 667px) and (max-width: 1024px) {
#viewer {
width: 600px;
height: 600px;
}
}
#viewer.spreads .epub-view>iframe {
background: white;
}
#viewer.scrolled {
overflow: hidden;
width: 800px;
margin: 0 auto;
position: relative;
background: url('ajax-loader.gif') center center no-repeat;
box-shadow: 0 0 4px #ccc;
padding: 20px;
background: white;
}
#viewer.scrolled .epub-view>iframe {
background: white;
}
#prev {
left: 0;
}
#next {
right: 0;
}
#toc {
display: block;
margin: 10px auto;
}
@media (min-width: 1000px) {
/*#viewer.spreads:after {
position: absolute;
width: 1px;
border-right: 1px #000 solid;
height: 90%;
z-index: 1;
left: 50%;
margin-left: -1px;
top: 5%;
opacity: .15;
box-shadow: -2px 0 15px rgba(0, 0, 0, 1);
content: "";
}
#viewer.spreads.single:after {
display: none;
}*/
#prev {
left: 40px;
}
#next {
right: 40px;
}
}
.arrow {
position: fixed;
top: 50%;
margin-top: -32px;
font-size: 64px;
color: #E2E2E2;
font-family: arial, sans-serif;
font-weight: bold;
cursor: pointer;
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
text-decoration: none;
}
.navlink {
margin: 14px;
display: block;
text-align: center;
text-decoration: none;
color: #ccc;
}
.arrow:hover,
.navlink:hover {
color: #777;
}
.arrow:active,
.navlink:hover {
color: #000;
}
#book-wrapper {
width: 480px;
height: 640px;
overflow: hidden;
border: 1px solid #ccc;
margin: 28px auto;
background: #fff;
border-radius: 0 5px 5px 0;
position: absolute;
}
#book-viewer {
width: 480px;
height: 660px;
margin: -30px auto;
-moz-box-shadow: inset 10px 0 20px rgba(0, 0, 0, .1);
-webkit-box-shadow: inset 10px 0 20px rgba(0, 0, 0, .1);
box-shadow: inset 10px 0 20px rgba(0, 0, 0, .1);
}
#book-viewer iframe {
padding: 40px 40px;
}
#controls {
position: absolute;
bottom: 16px;
left: 50%;
width: 400px;
margin-left: -200px;
text-align: center;
display: none;
}
#controls>input[type=range] {
width: 400px;
}
#navigation {
width: 400px;
height: 100vh;
position: absolute;
overflow: auto;
top: 0;
left: 0;
background: #777;
-webkit-transition: -webkit-transform .25s ease-out;
-moz-transition: -moz-transform .25s ease-out;
-ms-transition: -moz-transform .25s ease-out;
transition: transform .25s ease-out;
}
#navigation.fixed {
position: fixed;
}
#navigation h1 {
width: 200px;
font-size: 16px;
font-weight: normal;
color: #fff;
margin-bottom: 10px;
}
#navigation h2 {
font-size: 14px;
font-weight: normal;
color: #B0B0B0;
margin-bottom: 20px;
}
#navigation ul {
padding-left: 36px;
margin-left: 0;
margin-top: 12px;
margin-bottom: 12px;
width: 340px;
}
#navigation ul li {
list-style: decimal;
margin-bottom: 10px;
color: #cccddd;
font-size: 12px;
padding-left: 0;
margin-left: 0;
}
#navigation ul li a {
color: #ccc;
text-decoration: none;
}
#navigation ul li a:hover {
color: #fff;
text-decoration: underline;
}
#navigation ul li a.active {
color: #fff;
}
#navigation #cover {
display: block;
margin: 24px auto;
}
#navigation #closer {
position: absolute;
top: 0;
right: 0;
padding: 12px;
color: #cccddd;
width: 24px;
}
#navigation.closed {
-webkit-transform: translate(-400px, 0);
-moz-transform: translate(-400px, 0);
-ms-transform: translate(-400px, 0);
transform: translate(-400px, 0);
}
svg {
display: block;
}
.close-x {
stroke: #cccddd;
fill: transparent;
stroke-linecap: round;
stroke-width: 5;
}
.close-x:hover {
stroke: #fff;
}
#opener {
position: absolute;
top: 0;
left: 0;
padding: 10px;
stroke: #E2E2E2;
fill: #E2E2E2;
}
#opener:hover {
stroke: #777;
fill: #777;
}
#audioPlayer {
margin: 10px auto;
text-align: center;
font-size: 16px;
color: #E2E2E2;
font-weight: 400;
}

38
espark_reader/reader.html Normal file
View file

@ -0,0 +1,38 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>EPUB.js Input Example</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.1.5/jszip.min.js"></script>
<script src="../dist/epub.js"></script>
<script src="../howler.js/src/howler.core.js"></script>
<script src="reader.js"></script>
<link rel="stylesheet" type="text/css" href="reader.css">
</head>
<body>
<div id="title">
<input type="file" id="input">
<div id="audioplayer"><button id="playButton" onclick="playPause()">Play</button><button id="resetButton"
onclick="resetAudioToStart()">Reset</button></div>
</div>
<div id="viewer"></div>
<a id="prev" href="#prev" class="arrow"></a>
<a id="next" href="#next" class="arrow"></a>
<script>
init();
</script>
</body>
</html>

186
espark_reader/reader.js Normal file
View file

@ -0,0 +1,186 @@
var book = ePub();
var rendition;
var inputElement;
var audioClips = [];
var currentWord = 0;
var isPlaying = false;
function init() {
inputElement = document.getElementById("input");
inputElement.addEventListener('change', function (e) {
var file = e.target.files[0];
if (window.FileReader) {
var reader = new FileReader();
reader.onload = openBook;
reader.readAsArrayBuffer(file);
}
});
}
function openBook(e) {
var bookData = e.target.result;
var title = document.getElementById("title");
var next = document.getElementById("next");
var prev = document.getElementById("prev");
book.open(bookData, "binary");
/* console.log("opened book")
console.log(book) */
var rendition = book.renderTo("viewer", {
//manager: "continuous",
flow: "paginated",
width: "100%",
height: "100%",
snap: true
});
rendition.display();
var keyListener = function (e) {
// Left Key
if ((e.keyCode || e.which) == 37) {
rendition.prev();
}
// Right Key
if ((e.keyCode || e.which) == 39) {
rendition.next();
}
};
rendition.on("keyup", keyListener);
rendition.on("relocated", function (location) {
console.log(location);
initAudio();
});
next.addEventListener("click", function (e) {
rendition.next();
e.preventDefault();
}, false);
prev.addEventListener("click", function (e) {
rendition.prev();
e.preventDefault();
}, false);
document.addEventListener("keyup", keyListener, false);
}
function initAudio() {
var iframeList = document.getElementsByTagName("iframe");
audioClips = [];
for (let iframe of iframeList) {
var iframeDoc = iframe.contentDocument;
//get audio information
iframeDoc.querySelectorAll('par').forEach(par => {
var text = par.getElementsByTagName("text")[0]; //should only be one
var audio = par.getElementsByTagName("audio")[0]; //should only be one
var textId = text.getAttribute("src").split("#")[1];//text source format is page.xhtml#word
var textRef = iframeDoc.getElementById(textId);
var audioSrc = audio.getAttribute("src");
const contentType = "audio/mp3";
/*var sound = new Howl({
src: [`data:${contentType};base64,${audioSrc}`]
});*/
audioClips.push({
textId: textId,
text: textRef,
audio: audio,
clipBegin: parseFloat(audio.getAttribute("clipbegin")),
clipEnd: parseFloat(audio.getAttribute("clipend"))
//duration: 1000 * (parseFloat(audio.getAttribute("clipend")) - parseFloat(audio.getAttribute("clipbegin")))
})
});
//add next clip's start to the duration to account for pauses
for (let i = 0; i < audioClips.length - 1; i++) {
audioClips[i].duration = 1000 * (audioClips[i + 1].clipBegin - audioClips[i].clipBegin);
}
if (audioClips.length > 0) {
document.getElementById("audioplayer").style.visibility = "visible";
resetAudioToStart();
}
else {
document.getElementById("audioplayer").style.visibility = "hidden";
}
}
}
/**
* This is where the audio play/pause function should load and play everything on the page.
* Needs to loop through all available iframes because there may be 1-2 pages loaded separately on the page.
* I believe the audio tags will be in order per epub spec. Could look at play start and check if that's not the case.
* To play audio and go through highlighting the text and stopping at the right point, need to look at the clipBegin/clipEnd properties
* You could go through each audio tag and play/stop for each one but I think that'd cause some choppy behavior. Probably would be smoother to get the start/end for the page,
* and then track the time so that you can track the word for highlighting and pausing.
*
*
* Something to consider: will tracking the pages like this get messed up if the device rotates and only shows one page?
*/
function playPause() {
if (audioClips.length > 0) {
if (isPlaying) {
pauseAudio();
}
else {
playAudio();
}
}
}
/**
* The audio is the same for the whole book, need to play/pause the same audio tag
*/
function playAudio() {
isPlaying = true;
highlightWord();
audioClips[0].audio.play();
document.getElementById("playButton").textContent = "Pause";
}
function pauseAudio() {
isPlaying = false;
audioClips[0].audio.pause();
document.getElementById("playButton").textContent = "Play";
}
function resetAudioToStart() {
if (currentWord > 0)
audioClips[currentWord - 1].text.setAttribute("style", "");
currentWord = 0;
pauseAudio();
audioClips[0].audio.currentTime = audioClips[currentWord].clipBegin;
}
function resetAudioToWord() {
audioClips[0].audio.currentTime = audioClips[currentWord].clipBegin;
}
/**
* To highlight text with audio, search for the span with the id that matches the #id in the text src wrapping the audio. Each par tag has a text with source and the audio.
* Add a css highlight class to the span, then remove when moving to the next word.
*/
function highlightWord() {
//don't increment words if the audio is ended
if (currentWord >= audioClips.length) {
resetAudioToStart();
return;
}
if (!isPlaying) {
return;
}
/**
* note: using class add/remove doesn't work with iframes because the css is separate
*/
//highlight current word
if (currentWord > 0)
audioClips[currentWord - 1].text.setAttribute("style", "");
audioClips[currentWord].text.setAttribute("style", "background-color:yellow;");
//setup timer for when word is done being read
const myTimeout = setTimeout(highlightWord, audioClips[currentWord].duration);
//increment word index
currentWord++;
}

3
espark_reader/view.css Normal file
View file

@ -0,0 +1,3 @@
.highlight {
background: yellow;
}

View file

@ -1,5 +1,6 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
@ -11,6 +12,7 @@
<link rel="stylesheet" type="text/css" href="examples.css"> <link rel="stylesheet" type="text/css" href="examples.css">
</head> </head>
<body> <body>
<div id="title"><input type="file" id="input"></div> <div id="title"><input type="file" id="input"></div>
<div id="viewer" class="spreads"></div> <div id="viewer" class="spreads"></div>
@ -33,7 +35,7 @@
} }
}); });
function openBook(e){ function openBook(e) {
var bookData = e.target.result; var bookData = e.target.result;
var title = document.getElementById("title"); var title = document.getElementById("title");
var next = document.getElementById("next"); var next = document.getElementById("next");
@ -43,12 +45,12 @@
rendition = book.renderTo("viewer", { rendition = book.renderTo("viewer", {
width: "100%", width: "100%",
height: 600 height: "100%"
}); });
rendition.display(); rendition.display();
var keyListener = function(e){ var keyListener = function (e) {
// Left Key // Left Key
if ((e.keyCode || e.which) == 37) { if ((e.keyCode || e.which) == 37) {
@ -63,16 +65,16 @@
}; };
rendition.on("keyup", keyListener); rendition.on("keyup", keyListener);
rendition.on("relocated", function(location){ rendition.on("relocated", function (location) {
console.log(location); console.log(location);
}); });
next.addEventListener("click", function(e){ next.addEventListener("click", function (e) {
rendition.next(); rendition.next();
e.preventDefault(); e.preventDefault();
}, false); }, false);
prev.addEventListener("click", function(e){ prev.addEventListener("click", function (e) {
rendition.prev(); rendition.prev();
e.preventDefault(); e.preventDefault();
}, false); }, false);
@ -88,4 +90,5 @@
</script> </script>
</body> </body>
</html> </html>

20921
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -59,6 +59,7 @@
"@xmldom/xmldom": "^0.7.5", "@xmldom/xmldom": "^0.7.5",
"core-js": "^3.18.3", "core-js": "^3.18.3",
"event-emitter": "^0.3.5", "event-emitter": "^0.3.5",
"howler": "^2.2.3",
"jszip": "^3.7.1", "jszip": "^3.7.1",
"localforage": "^1.10.0", "localforage": "^1.10.0",
"lodash": "^4.17.21", "lodash": "^4.17.21",

View file

@ -1,4 +1,4 @@
import {qs, qsa, qsp, indexOfElementNode} from "./utils/core"; import { qs, qsa, qsp, indexOfElementNode } from "./utils/core";
/** /**
* Open Packaging Format Parser * Open Packaging Format Parser
@ -25,25 +25,25 @@ class Packaging {
* @param {document} packageDocument OPF XML * @param {document} packageDocument OPF XML
* @return {object} parsed package parts * @return {object} parsed package parts
*/ */
parse(packageDocument){ parse(packageDocument) {
var metadataNode, manifestNode, spineNode; var metadataNode, manifestNode, spineNode;
if(!packageDocument) { if (!packageDocument) {
throw new Error("Package File Not Found"); throw new Error("Package File Not Found");
} }
metadataNode = qs(packageDocument, "metadata"); metadataNode = qs(packageDocument, "metadata");
if(!metadataNode) { if (!metadataNode) {
throw new Error("No Metadata Found"); throw new Error("No Metadata Found");
} }
manifestNode = qs(packageDocument, "manifest"); manifestNode = qs(packageDocument, "manifest");
if(!manifestNode) { if (!manifestNode) {
throw new Error("No Manifest Found"); throw new Error("No Manifest Found");
} }
spineNode = qs(packageDocument, "spine"); spineNode = qs(packageDocument, "spine");
if(!spineNode) { if (!spineNode) {
throw new Error("No Spine Found"); throw new Error("No Spine Found");
} }
@ -53,7 +53,7 @@ class Packaging {
this.coverPath = this.findCoverPath(packageDocument); this.coverPath = this.findCoverPath(packageDocument);
this.spineNodeIndex = indexOfElementNode(spineNode); this.spineNodeIndex = indexOfElementNode(spineNode);
//KEM: spine creation/reading
this.spine = this.parseSpine(spineNode, this.manifest); this.spine = this.parseSpine(spineNode, this.manifest);
this.uniqueIdentifier = this.findUniqueIdentifier(packageDocument); this.uniqueIdentifier = this.findUniqueIdentifier(packageDocument);
@ -62,13 +62,13 @@ class Packaging {
this.metadata.direction = spineNode.getAttribute("page-progression-direction"); this.metadata.direction = spineNode.getAttribute("page-progression-direction");
return { return {
"metadata" : this.metadata, "metadata": this.metadata,
"spine" : this.spine, "spine": this.spine,
"manifest" : this.manifest, "manifest": this.manifest,
"navPath" : this.navPath, "navPath": this.navPath,
"ncxPath" : this.ncxPath, "ncxPath": this.ncxPath,
"coverPath": this.coverPath, "coverPath": this.coverPath,
"spineNodeIndex" : this.spineNodeIndex "spineNodeIndex": this.spineNodeIndex
}; };
} }
@ -78,7 +78,7 @@ class Packaging {
* @param {node} xml * @param {node} xml
* @return {object} metadata * @return {object} metadata
*/ */
parseMetadata(xml){ parseMetadata(xml) {
var metadata = {}; var metadata = {};
metadata.title = this.getElementText(xml, "title"); metadata.title = this.getElementText(xml, "title");
@ -112,7 +112,7 @@ class Packaging {
* @param {node} manifestXml * @param {node} manifestXml
* @return {object} manifest * @return {object} manifest
*/ */
parseManifest(manifestXml){ parseManifest(manifestXml) {
var manifest = {}; var manifest = {};
//-- Turn items into an array //-- Turn items into an array
@ -121,7 +121,7 @@ class Packaging {
var items = Array.prototype.slice.call(selected); var items = Array.prototype.slice.call(selected);
//-- Create an object with the id as key //-- Create an object with the id as key
items.forEach(function(item){ items.forEach(function (item) {
var id = item.getAttribute("id"), var id = item.getAttribute("id"),
href = item.getAttribute("href") || "", href = item.getAttribute("href") || "",
type = item.getAttribute("media-type") || "", type = item.getAttribute("media-type") || "",
@ -129,11 +129,11 @@ class Packaging {
properties = item.getAttribute("properties") || ""; properties = item.getAttribute("properties") || "";
manifest[id] = { manifest[id] = {
"href" : href, "href": href,
// "url" : href, // "url" : href,
"type" : type, "type": type,
"overlay" : overlay, "overlay": overlay,
"properties" : properties.length ? properties.split(" ") : [] "properties": properties.length ? properties.split(" ") : []
}; };
}); });
@ -149,7 +149,7 @@ class Packaging {
* @param {Packaging.manifest} manifest * @param {Packaging.manifest} manifest
* @return {object} spine * @return {object} spine
*/ */
parseSpine(spineXml, manifest){ parseSpine(spineXml, manifest) {
var spine = []; var spine = [];
var selected = qsa(spineXml, "itemref"); var selected = qsa(spineXml, "itemref");
@ -158,7 +158,7 @@ class Packaging {
// var epubcfi = new EpubCFI(); // var epubcfi = new EpubCFI();
//-- Add to array to maintain ordering and cross reference with manifest //-- Add to array to maintain ordering and cross reference with manifest
items.forEach(function(item, index){ items.forEach(function (item, index) {
var idref = item.getAttribute("idref"); var idref = item.getAttribute("idref");
// var cfiBase = epubcfi.generateChapterComponent(spineNodeIndex, index, Id); // var cfiBase = epubcfi.generateChapterComponent(spineNodeIndex, index, Id);
var props = item.getAttribute("properties") || ""; var props = item.getAttribute("properties") || "";
@ -167,13 +167,13 @@ class Packaging {
// var manifestPropArray = manifestProps.length ? manifestProps.split(" ") : []; // var manifestPropArray = manifestProps.length ? manifestProps.split(" ") : [];
var itemref = { var itemref = {
"id" : item.getAttribute("id"), "id": item.getAttribute("id"),
"idref" : idref, "idref": idref,
"linear" : item.getAttribute("linear") || "yes", "linear": item.getAttribute("linear") || "yes",
"properties" : propArray, "properties": propArray,
// "href" : manifest[Id].href, // "href" : manifest[Id].href,
// "url" : manifest[Id].url, // "url" : manifest[Id].url,
"index" : index "index": index
// "cfiBase" : cfiBase // "cfiBase" : cfiBase
}; };
spine.push(itemref); spine.push(itemref);
@ -188,13 +188,13 @@ class Packaging {
* @param {node} packageXml * @param {node} packageXml
* @return {string} Unique Identifier text * @return {string} Unique Identifier text
*/ */
findUniqueIdentifier(packageXml){ findUniqueIdentifier(packageXml) {
var uniqueIdentifierId = packageXml.documentElement.getAttribute("unique-identifier"); var uniqueIdentifierId = packageXml.documentElement.getAttribute("unique-identifier");
if (! uniqueIdentifierId) { if (!uniqueIdentifierId) {
return ""; return "";
} }
var identifier = packageXml.getElementById(uniqueIdentifierId); var identifier = packageXml.getElementById(uniqueIdentifierId);
if (! identifier) { if (!identifier) {
return ""; return "";
} }
@ -211,11 +211,11 @@ class Packaging {
* @param {element} manifestNode * @param {element} manifestNode
* @return {string} * @return {string}
*/ */
findNavPath(manifestNode){ findNavPath(manifestNode) {
// Find item with property "nav" // Find item with property "nav"
// Should catch nav regardless of order // Should catch nav regardless of order
// var node = manifestNode.querySelector("item[properties$='nav'], item[properties^='nav '], item[properties*=' nav ']"); // var node = manifestNode.querySelector("item[properties$='nav'], item[properties^='nav '], item[properties*=' nav ']");
var node = qsp(manifestNode, "item", {"properties":"nav"}); var node = qsp(manifestNode, "item", { "properties": "nav" });
return node ? node.getAttribute("href") : false; return node ? node.getAttribute("href") : false;
} }
@ -227,9 +227,9 @@ class Packaging {
* @param {element} spineNode * @param {element} spineNode
* @return {string} * @return {string}
*/ */
findNcxPath(manifestNode, spineNode){ findNcxPath(manifestNode, spineNode) {
// var node = manifestNode.querySelector("item[media-type='application/x-dtbncx+xml']"); // var node = manifestNode.querySelector("item[media-type='application/x-dtbncx+xml']");
var node = qsp(manifestNode, "item", {"media-type":"application/x-dtbncx+xml"}); var node = qsp(manifestNode, "item", { "media-type": "application/x-dtbncx+xml" });
var tocId; var tocId;
// If we can't find the toc by media-type then try to look for id of the item in the spine attributes as // If we can't find the toc by media-type then try to look for id of the item in the spine attributes as
@ -237,7 +237,7 @@ class Packaging {
// "The item that describes the NCX must be referenced by the spine toc attribute." // "The item that describes the NCX must be referenced by the spine toc attribute."
if (!node) { if (!node) {
tocId = spineNode.getAttribute("toc"); tocId = spineNode.getAttribute("toc");
if(tocId) { if (tocId) {
// node = manifestNode.querySelector("item[id='" + tocId + "']"); // node = manifestNode.querySelector("item[id='" + tocId + "']");
node = manifestNode.querySelector(`#${tocId}`); node = manifestNode.querySelector(`#${tocId}`);
} }
@ -254,17 +254,17 @@ class Packaging {
* @param {node} packageXml * @param {node} packageXml
* @return {string} href * @return {string} href
*/ */
findCoverPath(packageXml){ findCoverPath(packageXml) {
var pkg = qs(packageXml, "package"); var pkg = qs(packageXml, "package");
var epubVersion = pkg.getAttribute("version"); var epubVersion = pkg.getAttribute("version");
// Try parsing cover with epub 3. // Try parsing cover with epub 3.
// var node = packageXml.querySelector("item[properties='cover-image']"); // var node = packageXml.querySelector("item[properties='cover-image']");
var node = qsp(packageXml, "item", {"properties":"cover-image"}); var node = qsp(packageXml, "item", { "properties": "cover-image" });
if (node) return node.getAttribute("href"); if (node) return node.getAttribute("href");
// Fallback to epub 2. // Fallback to epub 2.
var metaCover = qsp(packageXml, "meta", {"name":"cover"}); var metaCover = qsp(packageXml, "meta", { "name": "cover" });
if (metaCover) { if (metaCover) {
var coverId = metaCover.getAttribute("content"); var coverId = metaCover.getAttribute("content");
@ -284,15 +284,15 @@ class Packaging {
* @param {string} tag * @param {string} tag
* @return {string} text * @return {string} text
*/ */
getElementText(xml, tag){ getElementText(xml, tag) {
var found = xml.getElementsByTagNameNS("http://purl.org/dc/elements/1.1/", tag); var found = xml.getElementsByTagNameNS("http://purl.org/dc/elements/1.1/", tag);
var el; var el;
if(!found || found.length === 0) return ""; if (!found || found.length === 0) return "";
el = found[0]; el = found[0];
if(el.childNodes.length){ if (el.childNodes.length) {
return el.childNodes[0].nodeValue; return el.childNodes[0].nodeValue;
} }
@ -307,10 +307,10 @@ class Packaging {
* @param {string} property * @param {string} property
* @return {string} text * @return {string} text
*/ */
getPropertyText(xml, property){ getPropertyText(xml, property) {
var el = qsp(xml, "meta", {"property":property}); var el = qsp(xml, "meta", { "property": property });
if(el && el.childNodes.length){ if (el && el.childNodes.length) {
return el.childNodes[0].nodeValue; return el.childNodes[0].nodeValue;
} }
@ -326,7 +326,7 @@ class Packaging {
this.metadata = json.metadata; this.metadata = json.metadata;
let spine = json.readingOrder || json.spine; let spine = json.readingOrder || json.spine;
this.spine = spine.map((item, index) =>{ this.spine = spine.map((item, index) => {
item.index = index; item.index = index;
item.linear = item.linear || "yes"; item.linear = item.linear || "yes";
return item; return item;
@ -342,20 +342,20 @@ class Packaging {
this.spineNodeIndex = 0; this.spineNodeIndex = 0;
this.toc = json.toc.map((item, index) =>{ this.toc = json.toc.map((item, index) => {
item.label = item.title; item.label = item.title;
return item; return item;
}); });
return { return {
"metadata" : this.metadata, "metadata": this.metadata,
"spine" : this.spine, "spine": this.spine,
"manifest" : this.manifest, "manifest": this.manifest,
"navPath" : this.navPath, "navPath": this.navPath,
"ncxPath" : this.ncxPath, "ncxPath": this.ncxPath,
"coverPath": this.coverPath, "coverPath": this.coverPath,
"spineNodeIndex" : this.spineNodeIndex, "spineNodeIndex": this.spineNodeIndex,
"toc" : this.toc "toc": this.toc
}; };
} }

View file

@ -64,7 +64,7 @@ class Rendition {
extend(this.settings, options); extend(this.settings, options);
if (typeof(this.settings.manager) === "object") { if (typeof (this.settings.manager) === "object") {
this.manager = this.settings.manager; this.manager = this.settings.manager;
} }
@ -176,6 +176,7 @@ class Rendition {
// If manager is a string, try to load from imported managers // If manager is a string, try to load from imported managers
if (typeof manager === "string" && manager === "default") { if (typeof manager === "string" && manager === "default") {
viewManager = DefaultViewManager; viewManager = DefaultViewManager;
} else if (typeof manager === "string" && manager === "continuous") { } else if (typeof manager === "string" && manager === "continuous") {
viewManager = ContinuousViewManager; viewManager = ContinuousViewManager;
} else { } else {
@ -209,11 +210,11 @@ class Rendition {
* Start the rendering * Start the rendering
* @return {Promise} rendering has started * @return {Promise} rendering has started
*/ */
start(){ start() {
if (!this.settings.layout && (this.book.package.metadata.layout === "pre-paginated" || this.book.displayOptions.fixedLayout === "true")) { if (!this.settings.layout && (this.book.package.metadata.layout === "pre-paginated" || this.book.displayOptions.fixedLayout === "true")) {
this.settings.layout = "pre-paginated"; this.settings.layout = "pre-paginated";
} }
switch(this.book.package.metadata.spread) { switch (this.book.package.metadata.spread) {
case 'none': case 'none':
this.settings.spread = 'none'; this.settings.spread = 'none';
break; break;
@ -222,7 +223,7 @@ class Rendition {
break; break;
} }
if(!this.manager) { if (!this.manager) {
this.ViewManager = this.requireManager(this.settings.manager); this.ViewManager = this.requireManager(this.settings.manager);
this.View = this.requireView(this.settings.view); this.View = this.requireView(this.settings.view);
@ -273,14 +274,14 @@ class Rendition {
* @param {element} element to attach to * @param {element} element to attach to
* @return {Promise} * @return {Promise}
*/ */
attachTo(element){ attachTo(element) {
return this.q.enqueue(function () { return this.q.enqueue(function () {
// Start rendering // Start rendering
this.manager.render(element, { this.manager.render(element, {
"width" : this.settings.width, "width": this.settings.width,
"height" : this.settings.height "height": this.settings.height
}); });
/** /**
@ -302,7 +303,7 @@ class Rendition {
* @param {string} target Url or EpubCFI * @param {string} target Url or EpubCFI
* @return {Promise} * @return {Promise}
*/ */
display(target){ display(target) {
if (this.displaying) { if (this.displaying) {
this.displaying.resolve(); this.displaying.resolve();
} }
@ -315,7 +316,7 @@ class Rendition {
* @param {string} target Url or EpubCFI * @param {string} target Url or EpubCFI
* @return {Promise} * @return {Promise}
*/ */
_display(target){ _display(target) {
if (!this.book) { if (!this.book) {
return; return;
} }
@ -331,10 +332,10 @@ class Rendition {
if (this.book.locations.length() && isFloat(target)) { if (this.book.locations.length() && isFloat(target)) {
target = this.book.locations.cfiFromPercentage(parseFloat(target)); target = this.book.locations.cfiFromPercentage(parseFloat(target));
} }
//KEM: Section display
section = this.book.spine.get(target); section = this.book.spine.get(target);
if(!section){ if (!section) {
displaying.reject(new Error("No Section Found")); displaying.reject(new Error("No Section Found"));
return displayed; return displayed;
} }
@ -415,7 +416,7 @@ class Rendition {
* @private * @private
* @param {*} view * @param {*} view
*/ */
afterDisplayed(view){ afterDisplayed(view) {
view.on(EVENTS.VIEWS.MARK_CLICKED, (cfiRange, data) => this.triggerMarkEvent(cfiRange, data, view.contents)); view.on(EVENTS.VIEWS.MARK_CLICKED, (cfiRange, data) => this.triggerMarkEvent(cfiRange, data, view.contents));
@ -444,7 +445,7 @@ class Rendition {
* @private * @private
* @param {*} view * @param {*} view
*/ */
afterRemoved(view){ afterRemoved(view) {
this.hooks.unloaded.trigger(view, this).then(() => { this.hooks.unloaded.trigger(view, this).then(() => {
/** /**
* Emit that a section has been removed * Emit that a section has been removed
@ -461,7 +462,7 @@ class Rendition {
* Report resize events and display the last seen location * Report resize events and display the last seen location
* @private * @private
*/ */
onResized(size, epubcfi){ onResized(size, epubcfi) {
/** /**
* Emit that the rendition has been resized * Emit that the rendition has been resized
@ -486,7 +487,7 @@ class Rendition {
* Report orientation events and display the last seen location * Report orientation events and display the last seen location
* @private * @private
*/ */
onOrientationChange(orientation){ onOrientationChange(orientation) {
/** /**
* Emit that the rendition has been rotated * Emit that the rendition has been rotated
* @event orientationchange * @event orientationchange
@ -501,7 +502,7 @@ class Rendition {
* Usually you would be better off calling display() * Usually you would be better off calling display()
* @param {object} offset * @param {object} offset
*/ */
moveTo(offset){ moveTo(offset) {
this.manager.moveTo(offset); this.manager.moveTo(offset);
} }
@ -511,7 +512,7 @@ class Rendition {
* @param {number} [height] * @param {number} [height]
* @param {string} [epubcfi] (optional) * @param {string} [epubcfi] (optional)
*/ */
resize(width, height, epubcfi){ resize(width, height, epubcfi) {
if (width) { if (width) {
this.settings.width = width; this.settings.width = width;
} }
@ -524,7 +525,7 @@ class Rendition {
/** /**
* Clear all rendered views * Clear all rendered views
*/ */
clear(){ clear() {
this.manager.clear(); this.manager.clear();
} }
@ -532,7 +533,7 @@ class Rendition {
* Go to the next "page" in the rendition * Go to the next "page" in the rendition
* @return {Promise} * @return {Promise}
*/ */
next(){ next() {
return this.q.enqueue(this.manager.next.bind(this.manager)) return this.q.enqueue(this.manager.next.bind(this.manager))
.then(this.reportLocation.bind(this)); .then(this.reportLocation.bind(this));
} }
@ -541,7 +542,7 @@ class Rendition {
* Go to the previous "page" in the rendition * Go to the previous "page" in the rendition
* @return {Promise} * @return {Promise}
*/ */
prev(){ prev() {
return this.q.enqueue(this.manager.prev.bind(this.manager)) return this.q.enqueue(this.manager.prev.bind(this.manager))
.then(this.reportLocation.bind(this)); .then(this.reportLocation.bind(this));
} }
@ -553,7 +554,7 @@ class Rendition {
* @param {object} metadata * @param {object} metadata
* @return {object} properties * @return {object} properties
*/ */
determineLayoutProperties(metadata){ determineLayoutProperties(metadata) {
var properties; var properties;
var layout = this.settings.layout || metadata.layout || "reflowable"; var layout = this.settings.layout || metadata.layout || "reflowable";
var spread = this.settings.spread || metadata.spread || "auto"; var spread = this.settings.spread || metadata.spread || "auto";
@ -569,12 +570,12 @@ class Rendition {
} }
properties = { properties = {
layout : layout, layout: layout,
spread : spread, spread: spread,
orientation : orientation, orientation: orientation,
flow : flow, flow: flow,
viewport : viewport, viewport: viewport,
minSpreadWidth : minSpreadWidth, minSpreadWidth: minSpreadWidth,
direction: direction direction: direction
}; };
@ -586,7 +587,7 @@ class Rendition {
* (scrolled-continuous vs scrolled-doc are handled by different view managers) * (scrolled-continuous vs scrolled-doc are handled by different view managers)
* @param {string} flow * @param {string} flow
*/ */
flow(flow){ flow(flow) {
var _flow = flow; var _flow = flow;
if (flow === "scrolled" || if (flow === "scrolled" ||
flow === "scrolled-doc" || flow === "scrolled-doc" ||
@ -622,7 +623,7 @@ class Rendition {
* Adjust the layout of the rendition to reflowable or pre-paginated * Adjust the layout of the rendition to reflowable or pre-paginated
* @param {object} settings * @param {object} settings
*/ */
layout(settings){ layout(settings) {
if (settings) { if (settings) {
this._layout = new Layout(settings); this._layout = new Layout(settings);
this._layout.spread(settings.spread, this.settings.minSpreadWidth); this._layout.spread(settings.spread, this.settings.minSpreadWidth);
@ -646,7 +647,7 @@ class Rendition {
* @param {string} spread none | auto (TODO: implement landscape, portrait, both) * @param {string} spread none | auto (TODO: implement landscape, portrait, both)
* @param {int} [min] min width to use spreads at * @param {int} [min] min width to use spreads at
*/ */
spread(spread, min){ spread(spread, min) {
this.settings.spread = spread; this.settings.spread = spread;
@ -667,7 +668,7 @@ class Rendition {
* Adjust the direction of the rendition * Adjust the direction of the rendition
* @param {string} dir * @param {string} dir
*/ */
direction(dir){ direction(dir) {
this.settings.direction = dir || "ltr"; this.settings.direction = dir || "ltr";
@ -686,12 +687,12 @@ class Rendition {
* @fires relocated * @fires relocated
* @fires locationChanged * @fires locationChanged
*/ */
reportLocation(){ reportLocation() {
return this.q.enqueue(function reportedLocation(){ return this.q.enqueue(function reportedLocation() {
requestAnimationFrame(function reportedLocationAfterRAF() { requestAnimationFrame(function reportedLocationAfterRAF() {
var location = this.manager.currentLocation(); var location = this.manager.currentLocation();
if (location && location.then && typeof location.then === "function") { if (location && location.then && typeof location.then === "function") {
location.then(function(result) { location.then(function (result) {
let located = this.located(result); let located = this.located(result);
if (!located || !located.start || !located.end) { if (!located || !located.start || !located.end) {
@ -753,10 +754,10 @@ class Rendition {
* Get the Current Location object * Get the Current Location object
* @return {displayedLocation | promise} location (may be a promise) * @return {displayedLocation | promise} location (may be a promise)
*/ */
currentLocation(){ currentLocation() {
var location = this.manager.currentLocation(); var location = this.manager.currentLocation();
if (location && location.then && typeof location.then === "function") { if (location && location.then && typeof location.then === "function") {
location.then(function(result) { location.then(function (result) {
let located = this.located(result); let located = this.located(result);
return located; return located;
}.bind(this)); }.bind(this));
@ -772,12 +773,12 @@ class Rendition {
* @returns {displayedLocation} * @returns {displayedLocation}
* @private * @private
*/ */
located(location){ located(location) {
if (!location.length) { if (!location.length) {
return {}; return {};
} }
let start = location[0]; let start = location[0];
let end = location[location.length-1]; let end = location[location.length - 1];
let located = { let located = {
start: { start: {
@ -794,7 +795,7 @@ class Rendition {
href: end.href, href: end.href,
cfi: end.mapping.end, cfi: end.mapping.end,
displayed: { displayed: {
page: end.pages[end.pages.length-1] || 1, page: end.pages[end.pages.length - 1] || 1,
total: end.totalPages total: end.totalPages
} }
} }
@ -838,7 +839,7 @@ class Rendition {
/** /**
* Remove and Clean Up the Rendition * Remove and Clean Up the Rendition
*/ */
destroy(){ destroy() {
// Clear the queue // Clear the queue
// this.q.clear(); // this.q.clear();
// this.q = undefined; // this.q = undefined;
@ -873,7 +874,7 @@ class Rendition {
* @private * @private
* @param {Contents} view contents * @param {Contents} view contents
*/ */
passEvents(contents){ passEvents(contents) {
DOM_EVENTS.forEach((e) => { DOM_EVENTS.forEach((e) => {
contents.on(e, (ev) => this.triggerViewEvent(ev, contents)); contents.on(e, (ev) => this.triggerViewEvent(ev, contents));
}); });
@ -886,7 +887,7 @@ class Rendition {
* @private * @private
* @param {event} e * @param {event} e
*/ */
triggerViewEvent(e, contents){ triggerViewEvent(e, contents) {
this.emit(e.type, e, contents); this.emit(e.type, e, contents);
} }
@ -895,7 +896,7 @@ class Rendition {
* @private * @private
* @param {string} cfirange * @param {string} cfirange
*/ */
triggerSelectedEvent(cfirange, contents){ triggerSelectedEvent(cfirange, contents) {
/** /**
* Emit that a text selection has occurred * Emit that a text selection has occurred
* @event selected * @event selected
@ -911,7 +912,7 @@ class Rendition {
* @private * @private
* @param {EpubCFI} cfirange * @param {EpubCFI} cfirange
*/ */
triggerMarkEvent(cfiRange, data, contents){ triggerMarkEvent(cfiRange, data, contents) {
/** /**
* Emit that a mark was clicked * Emit that a mark was clicked
* @event markClicked * @event markClicked
@ -929,10 +930,10 @@ class Rendition {
* @param {string} ignoreClass * @param {string} ignoreClass
* @return {range} * @return {range}
*/ */
getRange(cfi, ignoreClass){ getRange(cfi, ignoreClass) {
var _cfi = new EpubCFI(cfi); var _cfi = new EpubCFI(cfi);
var found = this.manager.visible().filter(function (view) { var found = this.manager.visible().filter(function (view) {
if(_cfi.spinePos === view.index) return true; if (_cfi.spinePos === view.index) return true;
}); });
// Should only every return 1 item // Should only every return 1 item
@ -949,7 +950,7 @@ class Rendition {
adjustImages(contents) { adjustImages(contents) {
if (this._layout.name === "pre-paginated") { if (this._layout.name === "pre-paginated") {
return new Promise(function(resolve){ return new Promise(function (resolve) {
resolve(); resolve();
}); });
} }
@ -959,7 +960,7 @@ class Rendition {
let horizontalPadding = parseFloat(computed.paddingLeft) + parseFloat(computed.paddingRight); let horizontalPadding = parseFloat(computed.paddingLeft) + parseFloat(computed.paddingRight);
contents.addStylesheetRules({ contents.addStylesheetRules({
"img" : { "img": {
"max-width": (this._layout.columnWidth ? (this._layout.columnWidth - horizontalPadding) + "px" : "100%") + "!important", "max-width": (this._layout.columnWidth ? (this._layout.columnWidth - horizontalPadding) + "px" : "100%") + "!important",
"max-height": height + "px" + "!important", "max-height": height + "px" + "!important",
"object-fit": "contain", "object-fit": "contain",
@ -967,7 +968,7 @@ class Rendition {
"break-inside": "avoid", "break-inside": "avoid",
"box-sizing": "border-box" "box-sizing": "border-box"
}, },
"svg" : { "svg": {
"max-width": (this._layout.columnWidth ? (this._layout.columnWidth - horizontalPadding) + "px" : "100%") + "!important", "max-width": (this._layout.columnWidth ? (this._layout.columnWidth - horizontalPadding) + "px" : "100%") + "!important",
"max-height": height + "px" + "!important", "max-height": height + "px" + "!important",
"page-break-inside": "avoid", "page-break-inside": "avoid",
@ -975,9 +976,9 @@ class Rendition {
} }
}); });
return new Promise(function(resolve, reject){ return new Promise(function (resolve, reject) {
// Wait to apply // Wait to apply
setTimeout(function() { setTimeout(function () {
resolve(); resolve();
}, 1); }, 1);
}); });
@ -987,7 +988,7 @@ class Rendition {
* Get the Contents object of each rendered view * Get the Contents object of each rendered view
* @returns {Contents[]} * @returns {Contents[]}
*/ */
getContents () { getContents() {
return this.manager ? this.manager.getContents() : []; return this.manager ? this.manager.getContents() : [];
} }
@ -995,7 +996,7 @@ class Rendition {
* Get the views member from the manager * Get the views member from the manager
* @returns {Views} * @returns {Views}
*/ */
views () { views() {
let views = this.manager ? this.manager.views : undefined; let views = this.manager ? this.manager.views : undefined;
return views || []; return views || [];
} }

View file

@ -1,5 +1,5 @@
import {substitute} from "./utils/replacements"; import { substitute } from "./utils/replacements";
import {createBase64Url, createBlobUrl, blob2base64} from "./utils/core"; import { createBase64Url, createBlobUrl, blob2base64 } from "./utils/core";
import Url from "./utils/url"; import Url from "./utils/url";
import mime from "./utils/mime"; import mime from "./utils/mime";
import Path from "./utils/path"; import Path from "./utils/path";
@ -30,16 +30,17 @@ class Resources {
* Process resources * Process resources
* @param {Manifest} manifest * @param {Manifest} manifest
*/ */
process(manifest){ process(manifest) {
this.manifest = manifest; this.manifest = manifest;
this.resources = Object.keys(manifest). this.resources = Object.keys(manifest).
map(function (key){ map(function (key) {
return manifest[key]; return manifest[key];
}); });
this.replacementUrls = []; this.replacementUrls = [];
this.html = []; this.html = [];
//KEM: assets and urls are where the urls in items from the manifest are replaced by "blobs"
this.assets = []; this.assets = [];
this.css = []; this.css = [];
@ -54,11 +55,11 @@ class Resources {
* Split resources by type * Split resources by type
* @private * @private
*/ */
split(){ split() {
// HTML // HTML
this.html = this.resources. this.html = this.resources.
filter(function (item){ filter(function (item) {
if (item.type === "application/xhtml+xml" || if (item.type === "application/xhtml+xml" ||
item.type === "text/html") { item.type === "text/html") {
return true; return true;
@ -67,7 +68,7 @@ class Resources {
// Exclude HTML // Exclude HTML
this.assets = this.resources. this.assets = this.resources.
filter(function (item){ filter(function (item) {
if (item.type !== "application/xhtml+xml" && if (item.type !== "application/xhtml+xml" &&
item.type !== "text/html") { item.type !== "text/html") {
return true; return true;
@ -76,7 +77,7 @@ class Resources {
// Only CSS // Only CSS
this.css = this.resources. this.css = this.resources.
filter(function (item){ filter(function (item) {
if (item.type === "text/css") { if (item.type === "text/css") {
return true; return true;
} }
@ -87,16 +88,16 @@ class Resources {
* Convert split resources into Urls * Convert split resources into Urls
* @private * @private
*/ */
splitUrls(){ splitUrls() {
// All Assets Urls // All Assets Urls
this.urls = this.assets. this.urls = this.assets.
map(function(item) { map(function (item) {
return item.href; return item.href;
}.bind(this)); }.bind(this));
// Css Urls // Css Urls
this.cssUrls = this.css.map(function(item) { this.cssUrls = this.css.map(function (item) {
return item.href; return item.href;
}); });
@ -107,12 +108,12 @@ class Resources {
* @param {string} url * @param {string} url
* @return {Promise<string>} Promise resolves with url string * @return {Promise<string>} Promise resolves with url string
*/ */
createUrl (url) { createUrl(url) {
var parsedUrl = new Url(url); var parsedUrl = new Url(url);
var mimeType = mime.lookup(parsedUrl.filename); var mimeType = mime.lookup(parsedUrl.filename);
//KEM: based on the printout of the parsedUrl here, the url is correct, but the smil file is referring to it differently. May need to modify the replacement to include the ../
if (this.settings.archive) { if (this.settings.archive) {
return this.settings.archive.createUrl(url, {"base64": (this.settings.replacements === "base64")}); return this.settings.archive.createUrl(url, { "base64": (this.settings.replacements === "base64") });
} else { } else {
if (this.settings.replacements === "base64") { if (this.settings.replacements === "base64") {
return this.settings.request(url, 'blob') return this.settings.request(url, 'blob')
@ -134,14 +135,14 @@ class Resources {
* Create blob urls for all the assets * Create blob urls for all the assets
* @return {Promise} returns replacement urls * @return {Promise} returns replacement urls
*/ */
replacements(){ replacements() {
if (this.settings.replacements === "none") { if (this.settings.replacements === "none") {
return new Promise(function(resolve) { return new Promise(function (resolve) {
resolve(this.urls); resolve(this.urls);
}.bind(this)); }.bind(this));
} }
var replacements = this.urls.map( (url) => { var replacements = this.urls.map((url) => {
var absolute = this.settings.resolver(url); var absolute = this.settings.resolver(url);
return this.createUrl(absolute). return this.createUrl(absolute).
@ -152,9 +153,9 @@ class Resources {
}); });
return Promise.all(replacements) return Promise.all(replacements)
.then( (replacementUrls) => { .then((replacementUrls) => {
this.replacementUrls = replacementUrls.filter((url) => { this.replacementUrls = replacementUrls.filter((url) => {
return (typeof(url) === "string"); return (typeof (url) === "string");
}); });
return replacementUrls; return replacementUrls;
}); });
@ -167,11 +168,11 @@ class Resources {
* @param {method} [resolver] * @param {method} [resolver]
* @return {Promise} * @return {Promise}
*/ */
replaceCss(archive, resolver){ replaceCss(archive, resolver) {
var replaced = []; var replaced = [];
archive = archive || this.settings.archive; archive = archive || this.settings.archive;
resolver = resolver || this.settings.resolver; resolver = resolver || this.settings.resolver;
this.cssUrls.forEach(function(href) { this.cssUrls.forEach(function (href) {
var replacement = this.createCssFile(href, archive, resolver) var replacement = this.createCssFile(href, archive, resolver)
.then(function (replacementUrl) { .then(function (replacementUrl) {
// switch the url in the replacementUrls // switch the url in the replacementUrls
@ -193,11 +194,11 @@ class Resources {
* @param {string} href the original css file * @param {string} href the original css file
* @return {Promise} returns a BlobUrl to the new CSS file or a data url * @return {Promise} returns a BlobUrl to the new CSS file or a data url
*/ */
createCssFile(href){ createCssFile(href) {
var newUrl; var newUrl;
if (path.isAbsolute(href)) { if (path.isAbsolute(href)) {
return new Promise(function(resolve){ return new Promise(function (resolve) {
resolve(); resolve();
}); });
} }
@ -214,7 +215,7 @@ class Resources {
} }
// Get asset links relative to css file // Get asset links relative to css file
var relUrls = this.urls.map( (assetHref) => { var relUrls = this.urls.map((assetHref) => {
var resolved = this.settings.resolver(assetHref); var resolved = this.settings.resolver(assetHref);
var relative = new Path(absolute).relative(resolved); var relative = new Path(absolute).relative(resolved);
@ -223,12 +224,12 @@ class Resources {
if (!textResponse) { if (!textResponse) {
// file not found, don't replace // file not found, don't replace
return new Promise(function(resolve){ return new Promise(function (resolve) {
resolve(); resolve();
}); });
} }
return textResponse.then( (text) => { return textResponse.then((text) => {
// Replacements in the css text // Replacements in the css text
text = substitute(text, relUrls, this.replacementUrls); text = substitute(text, relUrls, this.replacementUrls);
@ -242,7 +243,7 @@ class Resources {
return newUrl; return newUrl;
}, (err) => { }, (err) => {
// handle response errors // handle response errors
return new Promise(function(resolve){ return new Promise(function (resolve) {
resolve(); resolve();
}); });
}); });
@ -255,14 +256,18 @@ class Resources {
* @param {resolver} [resolver] * @param {resolver} [resolver]
* @return {string[]} array with relative Urls * @return {string[]} array with relative Urls
*/ */
relativeTo(absolute, resolver){ relativeTo(absolute, resolver) {
resolver = resolver || this.settings.resolver; resolver = resolver || this.settings.resolver;
// Get Urls relative to current sections // Get Urls relative to current sections
return this.urls. return this.urls.
map(function(href) { map(function (href) {
var resolved = resolver(href); var resolved = resolver(href);
var relative = new Path(absolute).relative(resolved); var relative = new Path(absolute).relative(resolved);
//KEM: hardcoding to fix audio links for testing
//KEM: these are the links that are searched for in the content
if (relative.includes("audio")) {
relative = "../" + relative;
}
return relative; return relative;
}.bind(this)); }.bind(this));
} }
@ -278,7 +283,7 @@ class Resources {
return; return;
} }
if (this.replacementUrls.length) { if (this.replacementUrls.length) {
return new Promise(function(resolve, reject) { return new Promise(function (resolve, reject) {
resolve(this.replacementUrls[indexInUrls]); resolve(this.replacementUrls[indexInUrls]);
}.bind(this)); }.bind(this));
} else { } else {
@ -300,6 +305,12 @@ class Resources {
} else { } else {
relUrls = this.urls; relUrls = this.urls;
} }
//KEM: this seems to be where the audio urls are going wrong, but it could be that there's a problem with the original file.
//KEM: The smil files have relative paths to the resources with an extra folder jump like ../, and the regular pages don't do that
//KEM: so there may need to be some kind of check to see where the files actually are before replacing them?
/**
* Goes through the content and replaces any instances of relUrls with this.replacementUrls
*/
return substitute(content, relUrls, this.replacementUrls); return substitute(content, relUrls, this.replacementUrls);
} }

View file

@ -14,7 +14,7 @@ import { DOMParser as XMLDOMSerializer } from "@xmldom/xmldom";
* @param {object} hooks hooks for serialize and content * @param {object} hooks hooks for serialize and content
*/ */
class Section { class Section {
constructor(item, hooks){ constructor(item, hooks) {
this.idref = item.idref; this.idref = item.idref;
this.linear = item.linear === "yes"; this.linear = item.linear === "yes";
this.properties = item.properties; this.properties = item.properties;
@ -24,7 +24,7 @@ class Section {
this.canonical = item.canonical; this.canonical = item.canonical;
this.next = item.next; this.next = item.next;
this.prev = item.prev; this.prev = item.prev;
this.overlay = item.overlay;
this.cfiBase = item.cfiBase; this.cfiBase = item.cfiBase;
if (hooks) { if (hooks) {
@ -38,6 +38,7 @@ class Section {
this.document = undefined; this.document = undefined;
this.contents = undefined; this.contents = undefined;
this.output = undefined; this.output = undefined;
this.mediaOverlay = undefined;
} }
/** /**
@ -45,30 +46,61 @@ class Section {
* @param {method} [_request] a request method to use for loading * @param {method} [_request] a request method to use for loading
* @return {document} a promise with the xml document * @return {document} a promise with the xml document
*/ */
load(_request){ load(_request) {
var request = _request || this.request || Request; var request = _request || this.request || Request;
var loading = new defer(); var loading = new defer();
var loaded = loading.promise; var loaded = loading.promise;
if(this.contents) { //KEM: this is where the file is loaded and turned into xml
//KEM: add in a load to the smil file and append it?
//KEM: try to load overlay
if (this.overlay) {
if (this.contents) {
loading.resolve(this.contents); loading.resolve(this.contents);
} else { } else {
request(this.url) request(this.overlay.url).then(function (overlayXml) {
.then(function(xml){ var div = document.createElement("div");
// var directory = new Url(this.url).directory; div.classList.add("audioContainer");
//overlay is returning as a string? possibly because xml instead of xhtml
var start = overlayXml.search("<smil");
var xmlStr = overlayXml.substring(start);
div.insertAdjacentHTML('beforeend', xmlStr);
this.mediaOverlay = div;
return request(this.url).then(function (xml) {
this.document = xml; this.document = xml;
this.contents = xml.documentElement; this.contents = xml.documentElement;
this.contents.appendChild(div);
return this.hooks.content.trigger(this.document, this); return this.hooks.content.trigger(this.document, this);
}.bind(this))
.then(function(){ }.bind(this)).then(function () {
loading.resolve(this.contents); loading.resolve(this.contents);
}.bind(this)) }.bind(this)).catch(function (error) {
.catch(function(error){
loading.reject(error); loading.reject(error);
}); });
} }
.bind(this)).catch(function (error) {
loading.reject(error);
});
}
}
else {
if (this.contents) {
loading.resolve(this.contents);
} else {
request(this.url).then(function (xml) {
// var directory = new Url(this.url).directory;
this.document = xml;
this.contents = xml.documentElement;
return this.hooks.content.trigger(this.document, this);
}.bind(this)).then(function () {
loading.resolve(this.contents);
}.bind(this)).catch(function (error) {
loading.reject(error);
});
}
}
return loaded; return loaded;
} }
@ -77,7 +109,7 @@ class Section {
* Adds a base tag for resolving urls in the section * Adds a base tag for resolving urls in the section
* @private * @private
*/ */
base(){ base() {
return replaceBase(this.document, this); return replaceBase(this.document, this);
} }
@ -86,13 +118,13 @@ class Section {
* @param {method} [_request] a request method to use for loading * @param {method} [_request] a request method to use for loading
* @return {string} output a serialized XML Document * @return {string} output a serialized XML Document
*/ */
render(_request){ render(_request) {
var rendering = new defer(); var rendering = new defer();
var rendered = rendering.promise; var rendered = rendering.promise;
this.output; // TODO: better way to return this from hooks? this.output; // TODO: better way to return this from hooks?
//console.log("rendering section");
this.load(_request). this.load(_request).
then(function(contents){ then(function (contents) {
var userAgent = (typeof navigator !== 'undefined' && navigator.userAgent) || ''; var userAgent = (typeof navigator !== 'undefined' && navigator.userAgent) || '';
var isIE = userAgent.indexOf('Trident') >= 0; var isIE = userAgent.indexOf('Trident') >= 0;
var Serializer; var Serializer;
@ -105,13 +137,13 @@ class Section {
this.output = serializer.serializeToString(contents); this.output = serializer.serializeToString(contents);
return this.output; return this.output;
}.bind(this)). }.bind(this)).
then(function(){ then(function () {
return this.hooks.serialize.trigger(this.output, this); return this.hooks.serialize.trigger(this.output, this);
}.bind(this)). }.bind(this)).
then(function(){ then(function () {
rendering.resolve(this.output); rendering.resolve(this.output);
}.bind(this)) }.bind(this))
.catch(function(error){ .catch(function (error) {
rendering.reject(error); rendering.reject(error);
}); });
@ -123,11 +155,11 @@ class Section {
* @param {string} _query The query string to find * @param {string} _query The query string to find
* @return {object[]} A list of matches, with form {cfi, excerpt} * @return {object[]} A list of matches, with form {cfi, excerpt}
*/ */
find(_query){ find(_query) {
var section = this; var section = this;
var matches = []; var matches = [];
var query = _query.toLowerCase(); var query = _query.toLowerCase();
var find = function(node){ var find = function (node) {
var text = node.textContent.toLowerCase(); var text = node.textContent.toLowerCase();
var range = section.document.createRange(); var range = section.document.createRange();
var cfi; var cfi;
@ -153,7 +185,7 @@ class Section {
excerpt = node.textContent; excerpt = node.textContent;
} }
else { else {
excerpt = node.textContent.substring(pos - limit/2, pos + limit/2); excerpt = node.textContent.substring(pos - limit / 2, pos + limit / 2);
excerpt = "..." + excerpt + "..."; excerpt = "..." + excerpt + "...";
} }
@ -168,7 +200,7 @@ class Section {
} }
}; };
sprint(section.document, function(node) { sprint(section.document, function (node) {
find(node); find(node);
}); });
@ -182,43 +214,43 @@ class Section {
* @param {int} maxSeqEle The maximum number of Element that are combined for search, default value is 5. * @param {int} maxSeqEle The maximum number of Element that are combined for search, default value is 5.
* @return {object[]} A list of matches, with form {cfi, excerpt} * @return {object[]} A list of matches, with form {cfi, excerpt}
*/ */
search(_query , maxSeqEle = 5){ search(_query, maxSeqEle = 5) {
if (typeof(document.createTreeWalker) == "undefined") { if (typeof (document.createTreeWalker) == "undefined") {
return this.find(_query); return this.find(_query);
} }
let matches = []; let matches = [];
const excerptLimit = 150; const excerptLimit = 150;
const section = this; const section = this;
const query = _query.toLowerCase(); const query = _query.toLowerCase();
const search = function(nodeList){ const search = function (nodeList) {
const textWithCase = nodeList.reduce((acc ,current)=>{ const textWithCase = nodeList.reduce((acc, current) => {
return acc + current.textContent; return acc + current.textContent;
},""); }, "");
const text = textWithCase.toLowerCase(); const text = textWithCase.toLowerCase();
const pos = text.indexOf(query); const pos = text.indexOf(query);
if (pos != -1){ if (pos != -1) {
const startNodeIndex = 0 , endPos = pos + query.length; const startNodeIndex = 0, endPos = pos + query.length;
let endNodeIndex = 0 , l = 0; let endNodeIndex = 0, l = 0;
if (pos < nodeList[startNodeIndex].length){ if (pos < nodeList[startNodeIndex].length) {
let cfi; let cfi;
while( endNodeIndex < nodeList.length - 1 ){ while (endNodeIndex < nodeList.length - 1) {
l += nodeList[endNodeIndex].length; l += nodeList[endNodeIndex].length;
if ( endPos <= l){ if (endPos <= l) {
break; break;
} }
endNodeIndex += 1; endNodeIndex += 1;
} }
let startNode = nodeList[startNodeIndex] , endNode = nodeList[endNodeIndex]; let startNode = nodeList[startNodeIndex], endNode = nodeList[endNodeIndex];
let range = section.document.createRange(); let range = section.document.createRange();
range.setStart(startNode,pos); range.setStart(startNode, pos);
let beforeEndLengthCount = nodeList.slice(0, endNodeIndex).reduce((acc,current)=>{return acc+current.textContent.length;},0) ; let beforeEndLengthCount = nodeList.slice(0, endNodeIndex).reduce((acc, current) => { return acc + current.textContent.length; }, 0);
range.setEnd(endNode, beforeEndLengthCount > endPos ? endPos : endPos - beforeEndLengthCount ); range.setEnd(endNode, beforeEndLengthCount > endPos ? endPos : endPos - beforeEndLengthCount);
cfi = section.cfiFromRange(range); cfi = section.cfiFromRange(range);
let excerpt = nodeList.slice(0, endNodeIndex+1).reduce((acc,current)=>{return acc+current.textContent ;},""); let excerpt = nodeList.slice(0, endNodeIndex + 1).reduce((acc, current) => { return acc + current.textContent; }, "");
if (excerpt.length > excerptLimit){ if (excerpt.length > excerptLimit) {
excerpt = excerpt.substring(pos - excerptLimit/2, pos + excerptLimit/2); excerpt = excerpt.substring(pos - excerptLimit / 2, pos + excerptLimit / 2);
excerpt = "..." + excerpt + "..."; excerpt = "..." + excerpt + "...";
} }
matches.push({ matches.push({
@ -230,15 +262,15 @@ class Section {
} }
const treeWalker = document.createTreeWalker(section.document, NodeFilter.SHOW_TEXT, null, false); const treeWalker = document.createTreeWalker(section.document, NodeFilter.SHOW_TEXT, null, false);
let node , nodeList = []; let node, nodeList = [];
while (node = treeWalker.nextNode()) { while (node = treeWalker.nextNode()) {
nodeList.push(node); nodeList.push(node);
if (nodeList.length == maxSeqEle){ if (nodeList.length == maxSeqEle) {
search(nodeList.slice(0 , maxSeqEle)); search(nodeList.slice(0, maxSeqEle));
nodeList = nodeList.slice(1, maxSeqEle); nodeList = nodeList.slice(1, maxSeqEle);
} }
} }
if (nodeList.length > 0){ if (nodeList.length > 0) {
search(nodeList); search(nodeList);
} }
return matches; return matches;
@ -250,23 +282,23 @@ class Section {
* @param {object} globalLayout The global layout settings object, chapter properties string * @param {object} globalLayout The global layout settings object, chapter properties string
* @return {object} layoutProperties Object with layout properties * @return {object} layoutProperties Object with layout properties
*/ */
reconcileLayoutSettings(globalLayout){ reconcileLayoutSettings(globalLayout) {
//-- Get the global defaults //-- Get the global defaults
var settings = { var settings = {
layout : globalLayout.layout, layout: globalLayout.layout,
spread : globalLayout.spread, spread: globalLayout.spread,
orientation : globalLayout.orientation orientation: globalLayout.orientation
}; };
//-- Get the chapter's display type //-- Get the chapter's display type
this.properties.forEach(function(prop){ this.properties.forEach(function (prop) {
var rendition = prop.replace("rendition:", ""); var rendition = prop.replace("rendition:", "");
var split = rendition.indexOf("-"); var split = rendition.indexOf("-");
var property, value; var property, value;
if(split != -1){ if (split != -1) {
property = rendition.slice(0, split); property = rendition.slice(0, split);
value = rendition.slice(split+1); value = rendition.slice(split + 1);
settings[property] = value; settings[property] = value;
} }
@ -299,6 +331,7 @@ class Section {
this.document = undefined; this.document = undefined;
this.contents = undefined; this.contents = undefined;
this.output = undefined; this.output = undefined;
this.mediaOverlay = undefined;
} }
destroy() { destroy() {

View file

@ -1,7 +1,7 @@
import EpubCFI from "./epubcfi"; import EpubCFI from "./epubcfi";
import Hook from "./utils/hook"; import Hook from "./utils/hook";
import Section from "./section"; import Section from "./section";
import {replaceBase, replaceCanonical, replaceMeta} from "./utils/replacements"; import { replaceBase, replaceCanonical, replaceMeta } from "./utils/replacements";
/** /**
* A collection of Spine Items * A collection of Spine Items
@ -45,8 +45,8 @@ class Spine {
this.spineNodeIndex = _package.spineNodeIndex; this.spineNodeIndex = _package.spineNodeIndex;
this.baseUrl = _package.baseUrl || _package.basePath || ""; this.baseUrl = _package.baseUrl || _package.basePath || "";
this.length = this.items.length; this.length = this.items.length;
//KEM: unpacking opf file
this.items.forEach( (item, index) => { this.items.forEach((item, index) => {
var manifestItem = this.manifest[item.idref]; var manifestItem = this.manifest[item.idref];
var spineItem; var spineItem;
@ -58,21 +58,27 @@ class Spine {
item.canonical = canonical(item.href); item.canonical = canonical(item.href);
} }
if(manifestItem) { if (manifestItem) {
//KEM: This is where spine items get the URL from the manifest, this is where to connect the manifest media-overlay and get SMIL URL
item.href = manifestItem.href; item.href = manifestItem.href;
item.url = resolver(item.href, true); item.url = resolver(item.href, true);
item.canonical = canonical(item.href); item.canonical = canonical(item.href);
//KEM: added overlay so can load later in section
if(manifestItem.properties.length){ item.overlay = this.manifest[manifestItem.overlay];
if (item.overlay) {
item.overlay.url = resolver(item.overlay.href, true);
item.overlay.canonical = canonical(item.overlay.href);
}
if (manifestItem.properties.length) {
item.properties.push.apply(item.properties, manifestItem.properties); item.properties.push.apply(item.properties, manifestItem.properties);
} }
} }
if (item.linear === "yes") { if (item.linear === "yes") {
item.prev = function() { item.prev = function () {
let prevIndex = item.index; let prevIndex = item.index;
while (prevIndex > 0) { while (prevIndex > 0) {
let prev = this.get(prevIndex-1); let prev = this.get(prevIndex - 1);
if (prev && prev.linear) { if (prev && prev.linear) {
return prev; return prev;
} }
@ -80,10 +86,10 @@ class Spine {
} }
return; return;
}.bind(this); }.bind(this);
item.next = function() { item.next = function () {
let nextIndex = item.index; let nextIndex = item.index;
while (nextIndex < this.spineItems.length-1) { while (nextIndex < this.spineItems.length - 1) {
let next = this.get(nextIndex+1); let next = this.get(nextIndex + 1);
if (next && next.linear) { if (next && next.linear) {
return next; return next;
} }
@ -92,15 +98,15 @@ class Spine {
return; return;
}.bind(this); }.bind(this);
} else { } else {
item.prev = function() { item.prev = function () {
return; return;
} }
item.next = function() { item.next = function () {
return; return;
} }
} }
//KEM: section creation
spineItem = new Section(item, this.hooks); spineItem = new Section(item, this.hooks);
this.append(spineItem); this.append(spineItem);
@ -131,14 +137,14 @@ class Spine {
} }
index += 1; index += 1;
} }
} else if(this.epubcfi.isCfiString(target)) { } else if (this.epubcfi.isCfiString(target)) {
let cfi = new EpubCFI(target); let cfi = new EpubCFI(target);
index = cfi.spinePos; index = cfi.spinePos;
} else if(typeof target === "number" || isNaN(target) === false){ } else if (typeof target === "number" || isNaN(target) === false) {
index = target; index = target;
} else if(typeof target === "string" && target.indexOf("#") === 0) { } else if (typeof target === "string" && target.indexOf("#") === 0) {
index = this.spineById[target.substring(1)]; index = this.spineById[target.substring(1)];
} else if(typeof target === "string") { } else if (typeof target === "string") {
// Remove fragments // Remove fragments
target = target.split("#")[0]; target = target.split("#")[0];
index = this.spineByHref[target] || this.spineByHref[encodeURI(target)]; index = this.spineByHref[target] || this.spineByHref[encodeURI(target)];
@ -180,7 +186,7 @@ class Spine {
this.spineById[section.idref] = 0; this.spineById[section.idref] = 0;
// Re-index // Re-index
this.spineItems.forEach(function(item, index){ this.spineItems.forEach(function (item, index) {
item.index = index; item.index = index;
}); });
@ -199,7 +205,7 @@ class Spine {
remove(section) { remove(section) {
var index = this.spineItems.indexOf(section); var index = this.spineItems.indexOf(section);
if(index > -1) { if (index > -1) {
delete this.spineByHref[section.href]; delete this.spineByHref[section.href];
delete this.spineById[section.idref]; delete this.spineById[section.idref];
@ -229,7 +235,7 @@ class Spine {
return next; return next;
} }
index += 1; index += 1;
} while (index < this.spineItems.length) ; } while (index < this.spineItems.length);
} }
/** /**
@ -237,7 +243,7 @@ class Spine {
* @return {Section} last section * @return {Section} last section
*/ */
last() { last() {
let index = this.spineItems.length-1; let index = this.spineItems.length - 1;
do { do {
let prev = this.get(index); let prev = this.get(index);

85
src/test.html Normal file
View file

@ -0,0 +1,85 @@
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:epub="http://www.idpf.org/2007/ops" xml:lang="en-US">
<head>
<title>Me and My Cat</title>
<link href="CSS/default.css" rel="stylesheet" type="text/css" />
<link href="CSS/css_1.css" rel="stylesheet" type="text/css" />
<meta name="viewport" content="width=1400, height=1685" />
</head>
<body epub:type="frontmatter" lang="en-US" xml:lang="en-US">
<div class="pg1">
<section epub:type="titlepage">
<img src="images/1.jpg" class="img" alt="written " />
<p id="para1">
<span id="p1s1"><span id="written">written</span></span>
<span id="Me"></span>
<span id="and"></span>
<span id="My"></span>
<span id="Cat"></span>
<span id="p1s2"><span id="by">by</span></span> <br />
<span id="p1s4"><span id="Michael">Michael</span></span>
<span id="p1s5"><span id="Dahl">Dahl</span></span> <br />
<span id="p1s6"><span id="art">art</span></span>
<span id="p1s7"><span id="by1">by</span></span> <br />
<span id="p1s9"><span id="Zoe">Zoe</span></span>
<span id="p1s10"><span id="Persico">Persico</span></span> <br />
</p>
</section>
</div>
</body>
<smil xmlns="http://www.w3.org/ns/SMIL" xmlns:epub="http://www.idpf.org/2007/ops" version="3.0">
<body>
<par id="id0">
<text src="../P1.xhtml#Me">
</text>
<audio clipBegin="4.15" clipEnd="4.479" src="../audio/mmp_mecat_f16_masteraudio.mp3">
</audio>
</par>
<par id="id1">
<text src="../P1.xhtml#and">
</text>
<audio clipBegin="4.479" clipEnd="4.674" src="../audio/mmp_mecat_f16_masteraudio.mp3">
</audio>
</par>
<par id="id2">
<text src="../P1.xhtml#My">
</text>
<audio clipBegin="4.674" clipEnd="4.895" src="../audio/mmp_mecat_f16_masteraudio.mp3">
</audio>
</par>
<par id="id3">
<text src="../P1.xhtml#Cat">
</text>
<audio clipBegin="4.895" clipEnd="5.31" src="../audio/mmp_mecat_f16_masteraudio.mp3">
</audio>
</par>
<par id="id4">
<text src="../P1.xhtml#by">
</text>
<audio clipBegin="5.76" clipEnd="5.919" src="../audio/mmp_mecat_f16_masteraudio.mp3">
</audio>
</par>
<par id="id5">
<text src="../P1.xhtml#Michael">
</text>
<audio clipBegin="5.919" clipEnd="6.37" src="../audio/mmp_mecat_f16_masteraudio.mp3">
</audio>
</par>
<par id="id6">
<text src="../P1.xhtml#Dahl">
</text>
<audio clipBegin="6.371" clipEnd="6.778" src="../audio/mmp_mecat_f16_masteraudio.mp3">
</audio>
</par>
</body>
</smil>
</html>

View file

@ -2,20 +2,20 @@ import { qs, qsa } from "./core";
import Url from "./url"; import Url from "./url";
import Path from "./path"; import Path from "./path";
export function replaceBase(doc, section){ export function replaceBase(doc, section) {
var base; var base;
var head; var head;
var url = section.url; var url = section.url;
var absolute = (url.indexOf("://") > -1); var absolute = (url.indexOf("://") > -1);
if(!doc){ if (!doc) {
return; return;
} }
head = qs(doc, "head"); head = qs(doc, "head");
base = qs(head, "base"); base = qs(head, "base");
if(!base) { if (!base) {
base = doc.createElement("base"); base = doc.createElement("base");
head.insertBefore(base, head.firstChild); head.insertBefore(base, head.firstChild);
} }
@ -28,12 +28,12 @@ export function replaceBase(doc, section){
base.setAttribute("href", url); base.setAttribute("href", url);
} }
export function replaceCanonical(doc, section){ export function replaceCanonical(doc, section) {
var head; var head;
var link; var link;
var url = section.canonical; var url = section.canonical;
if(!doc){ if (!doc) {
return; return;
} }
@ -50,11 +50,11 @@ export function replaceCanonical(doc, section){
} }
} }
export function replaceMeta(doc, section){ export function replaceMeta(doc, section) {
var head; var head;
var meta; var meta;
var id = section.idref; var id = section.idref;
if(!doc){ if (!doc) {
return; return;
} }
@ -79,35 +79,34 @@ export function replaceLinks(contents, fn) {
if (!links.length) { if (!links.length) {
return; return;
} }
var base = qs(contents.ownerDocument, "base"); var base = qs(contents.ownerDocument, "base");
var location = base ? base.getAttribute("href") : undefined; var location = base ? base.getAttribute("href") : undefined;
var replaceLink = function(link){ var replaceLink = function (link) {
var href = link.getAttribute("href"); var href = link.getAttribute("href");
if(href.indexOf("mailto:") === 0){ if (href.indexOf("mailto:") === 0) {
return; return;
} }
var absolute = (href.indexOf("://") > -1); var absolute = (href.indexOf("://") > -1);
if(absolute){ if (absolute) {
link.setAttribute("target", "_blank"); link.setAttribute("target", "_blank");
}else{ } else {
var linkUrl; var linkUrl;
try { try {
linkUrl = new Url(href, location); linkUrl = new Url(href, location);
} catch(error) { } catch (error) {
// NOOP // NOOP
} }
link.onclick = function(){ link.onclick = function () {
if(linkUrl && linkUrl.hash) { if (linkUrl && linkUrl.hash) {
fn(linkUrl.Path.path + linkUrl.hash); fn(linkUrl.Path.path + linkUrl.hash);
} else if(linkUrl){ } else if (linkUrl) {
fn(linkUrl.Path.path); fn(linkUrl.Path.path);
} else { } else {
fn(href); fn(href);
@ -126,7 +125,7 @@ export function replaceLinks(contents, fn) {
} }
export function substitute(content, urls, replacements) { export function substitute(content, urls, replacements) {
urls.forEach(function(url, i){ urls.forEach(function (url, i) {
if (url && replacements[i]) { if (url && replacements[i]) {
// Account for special characters in the file name. // Account for special characters in the file name.
// See https://stackoverflow.com/a/6318729. // See https://stackoverflow.com/a/6318729.