diff --git a/extension/manifest.json b/extension/manifest.json index a77345a..fc6001c 100644 --- a/extension/manifest.json +++ b/extension/manifest.json @@ -4,7 +4,7 @@ "name": "fimfic2epub", "short_name": "ff2epub", "description": "Improved EPUB exporter for Fimfiction", - "version": "1.0.2", + "version": "1.0.3", "icons": { "128": "icon-128.png" diff --git a/package.json b/package.json index 86cb323..b229ed4 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { - "name": "fimfic2epub.js", + "name": "fimfic2epub", "private": true, - "version": "1.0.1", + "version": "1.0.3", "description": "", "author": "djazz", "scripts": { diff --git a/src/constants.js b/src/constants.js new file mode 100644 index 0000000..5e8ba51 --- /dev/null +++ b/src/constants.js @@ -0,0 +1,36 @@ + +export const NS = { + OPF: 'http://www.idpf.org/2007/opf', + OPS: 'http://www.idpf.org/2007/ops', + DC: 'http://purl.org/dc/elements/1.1/', + DAISY: 'http://www.daisy.org/z3986/2005/ncx/', + XHTML: 'http://www.w3.org/1999/xhtml', + SVG: 'http://www.w3.org/2000/svg', + XLINK: 'http://www.w3.org/1999/xlink' +} + +export const tidyOptions = { + 'indent': 'auto', + 'numeric-entities': 'yes', + 'output-xhtml': 'yes', + 'alt-text': 'Image', + 'wrap': '0', + 'quiet': 'yes', + 'show-warnings': 0, + 'newline': 'LF', + 'tidy-mark': 'no' +} + +export const mimeMap = { + 'image/jpeg': 'Images/*.jpg', + 'image/png': 'Images/*.png', + 'image/gif': 'Images/*.gif' +} + +export const containerXml = ` + + + + + +` diff --git a/src/eventPage.js b/src/eventPage.js index ca2966e..444bcf3 100644 --- a/src/eventPage.js +++ b/src/eventPage.js @@ -1,28 +1,11 @@ /* global chrome, safari */ -function fetch (url, cb, type) { - if (url.indexOf('//') === 0) { - url = 'http:' + url - } - let x = new XMLHttpRequest() - x.open('get', url, true) - if (type) { - x.responseType = type - } - x.onload = function () { - cb(x.response, x.getResponseHeader('content-type')) - } - x.onerror = function () { - console.error('error') - cb(null) - } - x.send() -} +import fetch from './fetch' if (typeof safari !== 'undefined') { safari.application.addEventListener('message', function (ev) { let url = ev.message - fetch(url, function (buffer, type) { + fetch(url, (buffer, type) => { console.log('Fetched ' + url + ' (' + type + ')') ev.target.page.dispatchMessage('remote', { input: url, @@ -35,7 +18,7 @@ if (typeof safari !== 'undefined') { let onMessage = chrome.extension.onMessage ? chrome.extension.onMessage : chrome.runtime.onMessage onMessage.addListener(function (request, sender, sendResponse) { - fetch(request, function (blob, type) { + fetch(request, (blob, type) => { sendResponse(URL.createObjectURL(blob), type) }, 'blob') return true diff --git a/src/fetch.js b/src/fetch.js new file mode 100644 index 0000000..a9ad5ac --- /dev/null +++ b/src/fetch.js @@ -0,0 +1,19 @@ + +export default function fetch (url, cb, type) { + if (url.indexOf('//') === 0) { + url = 'http:' + url + } + let x = new XMLHttpRequest() + x.open('get', url, true) + if (type) { + x.responseType = type + } + x.onload = function () { + cb(x.response, x.getResponseHeader('content-type')) + } + x.onerror = function () { + console.error('error') + cb(null) + } + x.send() +} diff --git a/src/main.js b/src/main.js index e784994..2ef6111 100644 --- a/src/main.js +++ b/src/main.js @@ -1,53 +1,24 @@ /* global chrome, safari */ import JSZip from 'jszip' -import m from 'mithril' -import render from './lib/mithril-node-render' -import { pd as pretty } from 'pretty-data' import escapeStringRegexp from 'escape-string-regexp' -import { XmlEntities } from 'html-entities' import { saveAs } from 'file-saver' -import tidy from 'exports?tidy_html5!tidy-html5' import zeroFill from 'zero-fill' import styleCss from './style' import coverstyleCss from './coverstyle' -const entities = new XmlEntities() - -const NS = { - OPF: 'http://www.idpf.org/2007/opf', - OPS: 'http://www.idpf.org/2007/ops', - DC: 'http://purl.org/dc/elements/1.1/', - DAISY: 'http://www.daisy.org/z3986/2005/ncx/', - XHTML: 'http://www.w3.org/1999/xhtml', - SVG: 'http://www.w3.org/2000/svg', - XLINK: 'http://www.w3.org/1999/xlink' -} - -let tidyOptions = { - 'indent': 'auto', - 'numeric-entities': 'yes', - 'output-xhtml': 'yes', - 'alt-text': 'Image', - 'wrap': '0', - 'quiet': 'yes', - 'show-warnings': 0, - 'newline': 'LF', - 'tidy-mark': 'no' -} - -let mimeMap = { - 'image/jpeg': 'Images/*.jpg', - 'image/png': 'Images/*.png', - 'image/gif': 'Images/*.gif' -} +import fetch from './fetch' +import parseChapter from './parseChapter' +import * as template from './templates' +import { mimeMap, containerXml } from './constants' const STORY_ID = document.location.pathname.match(/^\/story\/(\d*)/)[1] let storyInfo let remoteResources = new Map() let chapterContent = [] +let safariQueue = {} let epubButton = document.querySelector('.story_container ul.chapters li.bottom a[title="Download Story (.epub)"]') let isDownloading = false @@ -68,47 +39,25 @@ if (epubButton) { }, false) } -function fetch (url, cb, type) { - if (url.indexOf('//') === 0) { - url = 'http:' + url - } - let x = new XMLHttpRequest() - x.open('get', url, true) - if (type) { - x.responseType = type - } - x.onload = function () { - cb(x.response, x.getResponseHeader('content-type')) - } - x.onerror = function () { - cb(null) - } - x.send() +function blobToDataURL (blob, callback) { + let a = new FileReader() + a.onloadend = function (e) { callback(a.result) } + a.readAsDataURL(blob) } -function fetchChapters (cb) { - let chapters = storyInfo.chapters - let chapterCount = storyInfo.chapters.length - let currentChapter = 0 - function recursive () { - let ch = chapters[currentChapter] - console.log('Fetching chapter ' + (currentChapter + 1) + ' of ' + chapters.length + ': ' + ch.title) - fetchRemote(ch.link.replace('http', 'https'), function (html) { - html = parseChapter(currentChapter, ch, html) - chapterContent[currentChapter] = html - currentChapter++ - if (currentChapter < chapterCount) { - recursive() - } else { - cb() - } +function saveStory () { + console.log('Saving epub...') + if (typeof safari !== 'undefined') { + blobToDataURL(cachedBlob, (dataurl) => { + document.location.href = dataurl + alert('Rename downloaded file to .epub') }) + } else { + saveAs(cachedBlob, storyInfo.title + ' by ' + storyInfo.author.name + '.epub') } - recursive() } -let safariQueue = {} - +// messaging with the safari extension global page function safariHandler (ev) { let type = ev.message.type let url = ev.message.input @@ -133,14 +82,6 @@ function safariHandler (ev) { cb(fr.result, type) } fr.readAsText(blob) - /* - let str = '' - let arr = new Uint8Array(data) - for (let i = 0; i < arr.length; i++) { - str += String.fromCharCode(arr[i]) - } - cb(str, type) - */ } else { cb(data, type) } @@ -150,7 +91,7 @@ if (typeof safari !== 'undefined') { safari.self.addEventListener('message', safariHandler, false) } -function fetchRemote (url, cb, responseType) { +function fetchBackground (url, cb, responseType) { if (typeof chrome !== 'undefined' && chrome.runtime.sendMessage) { chrome.runtime.sendMessage(url, function (objurl) { fetch(objurl, cb, responseType) @@ -162,6 +103,23 @@ function fetchRemote (url, cb, responseType) { } } +function fetchRemote (url, cb, responseType) { + if (url.indexOf('//') === 0) { + url = 'http:' + url + } + if (document.location.protocol === 'https:' && url.indexOf('http:') === 0) { + fetchBackground(url, cb, responseType) + return + } + fetch(url, (data, type) => { + if (!data) { + fetchBackground(url, cb, responseType) + } else { + cb(data, type) + } + }, responseType) +} + function fetchRemoteFiles (zip, cb) { let iter = remoteResources.entries() let counter = 0 @@ -175,7 +133,7 @@ function fetchRemoteFiles (zip, cb) { let url = r[0] r = r[1] console.log('Fetching remote file ' + (counter + 1) + ' of ' + remoteResources.size + ': ' + r.filename, url) - fetchRemote(url, function (data, type) { + fetchRemote(url, (data, type) => { r.dest = null r.type = type let dest = mimeMap[type] @@ -191,22 +149,38 @@ function fetchRemoteFiles (zip, cb) { recursive() } +function fetchChapters (cb) { + let chapters = storyInfo.chapters + let chapterCount = storyInfo.chapters.length + let currentChapter = 0 + function recursive () { + let ch = chapters[currentChapter] + console.log('Fetching chapter ' + (currentChapter + 1) + ' of ' + chapters.length + ': ' + ch.title) + fetchRemote(ch.link.replace('http', 'https'), (html) => { + parseChapter(currentChapter, ch, html, remoteResources, (html) => { + chapterContent[currentChapter] = html + currentChapter++ + if (currentChapter < chapterCount) { + recursive() + } else { + cb() + } + }) + }) + } + recursive() +} + function downloadStory () { isDownloading = true const zip = new JSZip() zip.file('mimetype', 'application/epub+zip') - zip.folder('META-INF').file('container.xml', ` - - - - - -`) + zip.folder('META-INF').file('container.xml', containerXml) console.log('Fetching story metadata...') - fetchRemote('https://www.fimfiction.net/api/story.php?story=' + STORY_ID, function (raw, type) { + fetchRemote('https://www.fimfiction.net/api/story.php?story=' + STORY_ID, (raw, type) => { let data try { data = JSON.parse(raw) @@ -225,20 +199,22 @@ function downloadStory () { zip.file('style.css', styleCss) zip.file('coverstyle.css', coverstyleCss) - coverImage.addEventListener('load', function () { - zip.file('toc.ncx', createNcx()) - zip.file('nav.xhtml', createNav()) + coverImage.addEventListener('load', () => { + zip.file('toc.ncx', template.createNcx(storyInfo)) + zip.file('nav.xhtml', template.createNav(storyInfo)) - fetchChapters(function () { - fetchRemoteFiles(zip, function () { + fetchChapters(() => { + fetchRemoteFiles(zip, () => { + let coverFilename = '' remoteResources.forEach((r, url) => { if (typeof r.chapter !== 'undefined' && r.originalUrl && r.dest) { chapterContent[r.chapter] = chapterContent[r.chapter].replace( new RegExp(escapeStringRegexp(r.originalUrl), 'g'), r.dest ) - } else { - r.remote = true + } + if (r.filename === 'cover') { + coverFilename = r.dest } }) @@ -248,25 +224,10 @@ function downloadStory () { zip.file(filename, html) } - zip.file('cover.xhtml', createCoverPage(coverImage.width, coverImage.height)) - zip.file('content.opf', createOpf()) + chapterContent.length = 0 - /* - zip - .generateNodeStream({ - type: 'nodebuffer', - streamFiles: true, - mimeType: 'application/epub+zip', - compression: 'DEFLATE', - compressionOptions: {level: 9} - }) - .pipe(fs.createWriteStream('out.epub')) - .on('finish', function () { - // JSZip generates a readable stream with a "end" event, - // but is piped here in a writable stream which emits a "finish" event. - console.log("out.epub written."); - }) - */ + zip.file('cover.xhtml', template.createCoverPage(coverFilename, coverImage.width, coverImage.height)) + zip.file('content.opf', template.createOpf(storyInfo, remoteResources)) console.log('Packaging epub...') @@ -287,231 +248,3 @@ function downloadStory () { }, false) }) } - -function blobToDataURL (blob, callback) { - let a = new FileReader() - a.onloadend = function (e) { callback(a.result) } - a.readAsDataURL(blob) -} - -function saveStory () { - console.log('Saving epub...') - if (typeof safari !== 'undefined') { - blobToDataURL(cachedBlob, function (dataurl) { - document.location.href = dataurl - alert('Rename downloaded file to .epub') - }) - } else { - saveAs(cachedBlob, storyInfo.title + ' by ' + storyInfo.author.name + '.epub') - } -} - -function parseChapter (num, ch, html) { - let chapterTitle = html.match(/]*id="chapter_title"[^>]*>(.*?)<\/a>/) - - if (!chapterTitle) { - return tidy('\n' + chapterPage, tidyOptions) - } - chapterTitle = chapterTitle[1] - - let chapterPos = html.indexOf('
') - let chapter = html.substring(chapterPos + 29) - - let pos = chapter.indexOf('\t
\t\t\n\t') - - let authorNotesPos = chapter.substring(pos).indexOf('Author\'s Note:') - let authorNotes = '' - if (authorNotesPos !== -1) { - authorNotes = chapter.substring(pos + authorNotesPos + 22) - authorNotes = authorNotes.substring(0, authorNotes.indexOf('\t\t\n\t')) - } - - chapter = chapter.substring(0, pos) - - let chapterPage = '' + render( - m('html', {xmlns: NS.XHTML}, [ - m('head', [ - m('meta', {charset: 'utf-8'}), - m('link', {rel: 'stylesheet', type: 'text/css', href: 'style.css'}), - m('title', ch.title) - ]), - m('body', [ - m('div#chapter_container', m.trust(chapter)), - authorNotes ? m('div#author_notes', m.trust(authorNotes)) : null - ]) - ]) - ) - - chapterPage = chapterPage.replace(/
/g, '
') - chapterPage = chapterPage.replace(/<\/center>/g, '
') - - chapterPage = chapterPage.replace(/
(.+?)<\/div>/g, function (match, contents, offset) { - // console.log(match, contents, offset) - let youtubeId = contents.match(/src="https:\/\/www.youtube.com\/embed\/(.+?)"/)[1] - let thumbnail = 'http://img.youtube.com/vi/' + youtubeId + '/hqdefault.jpg' - let youtubeUrl = 'https://youtube.com/watch?v=' + youtubeId - return render(m('a', {href: youtubeUrl, target: '_blank'}, - m('img', {src: thumbnail, alt: 'Youtube Video'}) - )) - }) - - chapterPage = chapterPage.replace('
', '
') - chapterPage = chapterPage.replace('
', '
') - - chapterPage = tidy(`\n` + chapterPage, tidyOptions) - - let remoteCounter = 1 - chapterPage = chapterPage.replace(/(]*>)/g, function (match, first, url, last) { - let cleanurl = decodeURI(entities.decode(url)) - if (remoteResources.has(cleanurl)) { - return match - } - let filename = 'ch_' + zeroFill(3, num + 1) + '_' + remoteCounter - remoteCounter++ - remoteResources.set(cleanurl, {filename: filename, chapter: num, originalUrl: url}) - return match - }) - - return chapterPage -} - -function subjects (s) { - let list = [] - for (let i = 0; i < s.length; i++) { - list.push(m('dc:subject', s[i])) - } - return list -} - -function createOpf () { - let remotes = [] - remoteResources.forEach((r, url) => { - if (!r.dest) { - return - } - let attrs = {id: r.filename, href: r.dest, 'media-type': r.type} - if (r.filename === 'cover') { - attrs.properties = 'cover-image' - } - remotes.push(m('item', attrs)) - }) - - let contentOpf = '\n' + pretty.xml(render( - m('package', {xmlns: NS.OPF, version: '3.0', 'unique-identifier': 'BookId'}, [ - m('metadata', {'xmlns:dc': NS.DC, 'xmlns:opf': NS.OPF}, [ - m('dc:identifier#BookId', storyInfo.uuid), - m('dc:title', storyInfo.title), - m('dc:creator#cre', storyInfo.author.name), - m('meta', {refines: '#cre', property: 'role', scheme: 'marc:relators'}, 'aut'), - m('dc:date', storyInfo.publishDate), - m('dc:publisher', 'Fimfiction'), - m('dc:description', storyInfo.description), - m('dc:source', storyInfo.url), - m('dc:language', 'en'), - m('meta', {name: 'cover', content: 'cover'}), - m('meta', {property: 'dcterms:modified'}, new Date(storyInfo.date_modified * 1000).toISOString().replace('.000', '')) - ].concat(subjects(['Fiction', 'Pony']))), - - m('manifest', [ - m('item', {id: 'ncx', href: 'toc.ncx', 'media-type': 'application/x-dtbncx+xml'}), - m('item', {id: 'nav', 'href': 'nav.xhtml', 'media-type': 'application/xhtml+xml', properties: 'nav'}), - m('item', {id: 'style', href: 'style.css', 'media-type': 'text/css'}), - m('item', {id: 'coverstyle', href: 'coverstyle.css', 'media-type': 'text/css'}), - m('item', {id: 'coverpage', href: 'cover.xhtml', 'media-type': 'application/xhtml+xml', properties: 'svg'}) - ].concat(storyInfo.chapters.map((ch, num) => - m('item', {id: 'chapter_' + zeroFill(3, num + 1), href: 'chapter_' + zeroFill(3, num + 1) + '.xhtml', 'media-type': 'application/xhtml+xml'}) - ), remotes)), - - m('spine', {toc: 'ncx'}, [ - m('itemref', {idref: 'coverpage'}), - m('itemref', {idref: 'nav'}) - ].concat(storyInfo.chapters.map((ch, num) => - m('itemref', {idref: 'chapter_' + zeroFill(3, num + 1)}) - ))), - - false ? m('guide', [ - - ]) : null - ]) - )) - // console.log(contentOpf) - return contentOpf -} - -function navPoints (list) { - let arr = [] - for (let i = 0; i < list.length; i++) { - list[i] - arr.push(m('navPoint', {id: 'navPoint-' + (i + 1), playOrder: i + 1}, [ - m('navLabel', m('text', list[i][0])), - m('content', {src: list[i][1]}) - ])) - } - return arr -} - -function createNcx () { - let tocNcx = '\n' + pretty.xml(render( - m('ncx', {version: '2005-1', xmlns: NS.DAISY}, [ - m('head', [ - m('meta', {content: storyInfo.uuid, name: 'dtb:uid'}), - m('meta', {content: 0, name: 'dtb:depth'}), - m('meta', {content: 0, name: 'dtb:totalPageCount'}), - m('meta', {content: 0, name: 'dtb:maxPageNumber'}) - ]), - m('docTitle', m('text', storyInfo.title)), - m('navMap', navPoints([ - ['Cover', 'cover.xhtml'], - ['Contents', 'nav.xhtml'] - ].concat(storyInfo.chapters.map((ch, num) => - [ch.title, 'chapter_' + zeroFill(3, num + 1) + '.xhtml'] - )))) - ]) - )) - // console.log(tocNcx) - return tocNcx -} - -function createNav () { - let navDocument = '\n\n' + pretty.xml(render( - m('html', {xmlns: NS.XHTML, 'xmlns:epub': NS.OPS, lang: 'en', 'xml:lang': 'en'}, [ - m('head', [ - m('meta', {charset: 'utf-8'}), - m('link', {rel: 'stylesheet', type: 'text/css', href: 'style.css'}), - m('title', 'Contents') - ]), - m('body', [ - m('nav#toc', {'epub:type': 'toc'}, [ - m('h1', 'Contents'), - m('ol', [ - m('li', {hidden: ''}, m('a', {href: 'cover.xhtml'}, 'Cover')), - m('li', {hidden: ''}, m('a', {href: 'nav.xhtml'}, 'Contents')) - ].concat(storyInfo.chapters.map((ch, num) => - m('li', m('a', {href: 'chapter_' + zeroFill(3, num + 1) + '.xhtml'}, ch.title)) - ))) - ]) - ]) - ]) - )) - // console.log(navDocument) - return navDocument -} - -function createCoverPage (w, h) { - let coverPage = '\n\n' + pretty.xml(render( - m('html', {xmlns: NS.XHTML, 'xmlns:epub': NS.OPS}, [ - m('head', [ - m('meta', {name: 'viewport', content: 'width=' + w + ', height=' + h}), - m('title', 'Cover'), - m('link', {rel: 'stylesheet', type: 'text/css', href: 'coverstyle.css'}) - ]), - m('body', {'epub:type': 'cover'}, [ - m('svg#cover', {xmlns: NS.SVG, 'xmlns:xlink': NS.XLINK, version: '1.1', viewBox: '0 0 ' + w + ' ' + h}, - m('image', {width: w, height: h, 'xlink:href': 'Images/cover.jpg'}) - ) - ]) - ]) - )) - // console.log(coverPage) - return coverPage -} diff --git a/src/parseChapter.js b/src/parseChapter.js new file mode 100644 index 0000000..b4fffdd --- /dev/null +++ b/src/parseChapter.js @@ -0,0 +1,81 @@ + +import m from 'mithril' +import render from './lib/mithril-node-render' +import { XmlEntities } from 'html-entities' +import tidy from 'exports?tidy_html5!tidy-html5' +import zeroFill from 'zero-fill' + +import { NS, tidyOptions } from './constants' + +const entities = new XmlEntities() + +export default function parseChapter (num, ch, html, remoteResources, callback) { + let chapterTitle = html.match(/]*id="chapter_title"[^>]*>(.*?)<\/a>/) + + if (!chapterTitle) { + return tidy('\n' + chapterPage, tidyOptions) + } + chapterTitle = chapterTitle[1] + + let chapterPos = html.indexOf('
') + let chapter = html.substring(chapterPos + 29) + + let pos = chapter.indexOf('\t
\t\t\n\t') + + let authorNotesPos = chapter.substring(pos).indexOf('Author\'s Note:') + let authorNotes = '' + if (authorNotesPos !== -1) { + authorNotes = chapter.substring(pos + authorNotesPos + 22) + authorNotes = authorNotes.substring(0, authorNotes.indexOf('\t\t\n\t
')) + } + + chapter = chapter.substring(0, pos) + + let chapterPage = '' + render( + m('html', {xmlns: NS.XHTML}, [ + m('head', [ + m('meta', {charset: 'utf-8'}), + m('link', {rel: 'stylesheet', type: 'text/css', href: 'style.css'}), + m('title', ch.title) + ]), + m('body', [ + m('div#chapter_container', m.trust(chapter)), + authorNotes ? m('div#author_notes', m.trust(authorNotes)) : null + ]) + ]) + ) + + chapterPage = chapterPage.replace(/
/g, '
') + chapterPage = chapterPage.replace(/<\/center>/g, '
') + + chapterPage = chapterPage.replace(/
(.+?)<\/div>/g, (match, contents) => { + // console.log(match, contents) + let youtubeId = contents.match(/src="https:\/\/www.youtube.com\/embed\/(.+?)"/)[1] + let thumbnail = 'http://img.youtube.com/vi/' + youtubeId + '/hqdefault.jpg' + let youtubeUrl = 'https://youtube.com/watch?v=' + youtubeId + return render(m('a', {href: youtubeUrl, target: '_blank'}, + m('img', {src: thumbnail, alt: 'Youtube Video'}) + )) + }) + + chapterPage = chapterPage.replace('
', '
') + chapterPage = chapterPage.replace('
', '
') + + chapterPage = tidy(`\n` + chapterPage, tidyOptions) + + let remoteCounter = 1 + let matchUrl = /]*>/g + + for (let ma; (ma = matchUrl.exec(chapterPage));) { + let url = ma[1] + let cleanurl = decodeURI(entities.decode(url)) + if (remoteResources.has(cleanurl)) { + continue + } + let filename = 'ch_' + zeroFill(3, num + 1) + '_' + remoteCounter + remoteCounter++ + remoteResources.set(cleanurl, {filename: filename, chapter: num, originalUrl: url}) + } + + callback(chapterPage) +} diff --git a/src/templates.js b/src/templates.js new file mode 100644 index 0000000..6c576ef --- /dev/null +++ b/src/templates.js @@ -0,0 +1,148 @@ + +import m from 'mithril' +import render from './lib/mithril-node-render' +import { pd as pretty } from 'pretty-data' +import zeroFill from 'zero-fill' + +import { NS } from './constants' + +function subjects (s) { + let list = [] + for (let i = 0; i < s.length; i++) { + list.push(m('dc:subject', s[i])) + } + return list +} + +export function createOpf (storyInfo, remoteResources) { + let remotes = [] + remoteResources.forEach((r, url) => { + if (!r.dest) { + return + } + let attrs = {id: r.filename, href: r.dest, 'media-type': r.type} + if (r.filename === 'cover') { + attrs.properties = 'cover-image' + } + remotes.push(m('item', attrs)) + }) + + let contentOpf = '\n' + pretty.xml(render( + m('package', {xmlns: NS.OPF, version: '3.0', 'unique-identifier': 'BookId'}, [ + m('metadata', {'xmlns:dc': NS.DC, 'xmlns:opf': NS.OPF}, [ + m('dc:identifier#BookId', storyInfo.uuid), + m('dc:title', storyInfo.title), + m('dc:creator#cre', storyInfo.author.name), + m('meta', {refines: '#cre', property: 'role', scheme: 'marc:relators'}, 'aut'), + m('dc:date', storyInfo.publishDate), + m('dc:publisher', 'Fimfiction'), + m('dc:description', storyInfo.description), + m('dc:source', storyInfo.url), + m('dc:language', 'en'), + m('meta', {name: 'cover', content: 'cover'}), + m('meta', {property: 'dcterms:modified'}, new Date(storyInfo.date_modified * 1000).toISOString().replace('.000', '')) + ].concat(subjects(['Pony']))), + + m('manifest', [ + m('item', {id: 'ncx', href: 'toc.ncx', 'media-type': 'application/x-dtbncx+xml'}), + m('item', {id: 'nav', 'href': 'nav.xhtml', 'media-type': 'application/xhtml+xml', properties: 'nav'}), + m('item', {id: 'style', href: 'style.css', 'media-type': 'text/css'}), + m('item', {id: 'coverstyle', href: 'coverstyle.css', 'media-type': 'text/css'}), + m('item', {id: 'coverpage', href: 'cover.xhtml', 'media-type': 'application/xhtml+xml', properties: 'svg'}) + ].concat(storyInfo.chapters.map((ch, num) => + m('item', {id: 'chapter_' + zeroFill(3, num + 1), href: 'chapter_' + zeroFill(3, num + 1) + '.xhtml', 'media-type': 'application/xhtml+xml'}) + ), remotes)), + + m('spine', {toc: 'ncx'}, [ + m('itemref', {idref: 'coverpage'}), + m('itemref', {idref: 'nav'}) + ].concat(storyInfo.chapters.map((ch, num) => + m('itemref', {idref: 'chapter_' + zeroFill(3, num + 1)}) + ))), + + false ? m('guide', [ + + ]) : null + ]) + )) + // console.log(contentOpf) + return contentOpf +} + +function navPoints (list) { + let arr = [] + for (let i = 0; i < list.length; i++) { + list[i] + arr.push(m('navPoint', {id: 'navPoint-' + (i + 1), playOrder: i + 1}, [ + m('navLabel', m('text', list[i][0])), + m('content', {src: list[i][1]}) + ])) + } + return arr +} + +export function createNcx (storyInfo) { + let tocNcx = '\n' + pretty.xml(render( + m('ncx', {version: '2005-1', xmlns: NS.DAISY}, [ + m('head', [ + m('meta', {content: storyInfo.uuid, name: 'dtb:uid'}), + m('meta', {content: 0, name: 'dtb:depth'}), + m('meta', {content: 0, name: 'dtb:totalPageCount'}), + m('meta', {content: 0, name: 'dtb:maxPageNumber'}) + ]), + m('docTitle', m('text', storyInfo.title)), + m('navMap', navPoints([ + ['Cover', 'cover.xhtml'], + ['Contents', 'nav.xhtml'] + ].concat(storyInfo.chapters.map((ch, num) => + [ch.title, 'chapter_' + zeroFill(3, num + 1) + '.xhtml'] + )))) + ]) + )) + // console.log(tocNcx) + return tocNcx +} + +export function createNav (storyInfo) { + let navDocument = '\n\n' + pretty.xml(render( + m('html', {xmlns: NS.XHTML, 'xmlns:epub': NS.OPS, lang: 'en', 'xml:lang': 'en'}, [ + m('head', [ + m('meta', {charset: 'utf-8'}), + m('link', {rel: 'stylesheet', type: 'text/css', href: 'style.css'}), + m('title', 'Contents') + ]), + m('body', [ + m('nav#toc', {'epub:type': 'toc'}, [ + m('h1', 'Contents'), + m('ol', [ + m('li', {hidden: ''}, m('a', {href: 'cover.xhtml'}, 'Cover')), + m('li', {hidden: ''}, m('a', {href: 'nav.xhtml'}, 'Contents')) + ].concat(storyInfo.chapters.map((ch, num) => + m('li', m('a', {href: 'chapter_' + zeroFill(3, num + 1) + '.xhtml'}, ch.title)) + ))) + ]) + ]) + ]) + )) + // console.log(navDocument) + return navDocument +} + +export function createCoverPage (coverFilename, w, h) { + let coverPage = '\n\n' + pretty.xml(render( + m('html', {xmlns: NS.XHTML, 'xmlns:epub': NS.OPS}, [ + m('head', [ + m('meta', {name: 'viewport', content: 'width=' + w + ', height=' + h}), + m('title', 'Cover'), + m('link', {rel: 'stylesheet', type: 'text/css', href: 'coverstyle.css'}) + ]), + m('body', {'epub:type': 'cover'}, [ + m('svg#cover', {xmlns: NS.SVG, 'xmlns:xlink': NS.XLINK, version: '1.1', viewBox: '0 0 ' + w + ' ' + h}, + m('image', {width: w, height: h, 'xlink:href': coverFilename}) + ) + ]) + ]) + )) + // console.log(coverPage) + return coverPage +}