├── .gitignore ├── normalize-xhtml.js ├── LICENSE ├── package.json ├── CHANGELOG.md ├── README.md └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | .#* 3 | node_modules 4 | -------------------------------------------------------------------------------- /normalize-xhtml.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | module.exports = normalizeXHTML 3 | var Bluebird = require('bluebird') 4 | var parse5 = require('parse5') 5 | var xmlserializer = require('xmlserializer') 6 | 7 | function normalizeXHTML (xhtml) { 8 | return Bluebird.resolve(xmlserializer.serializeToString(parse5.parse(xhtml))) 9 | } 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015, Rebecca Turner 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any 4 | purpose with or without fee is hereby granted, provided that the above 5 | copyright notice and this permission notice appear in all copies. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 8 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 9 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 10 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 11 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 12 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 13 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 14 | 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "streampub", 3 | "version": "1.9.0", 4 | "description": "A streaming EPUB3 writer/generator.", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "standard" 8 | }, 9 | "keywords": [ 10 | "epub", 11 | "epub3", 12 | "writer" 13 | ], 14 | "author": "Rebecca Turner (http://re-becca.org/)", 15 | "contributors": [ 16 | "Meinaart van Straalen (https://github.com/meinaart)" 17 | ], 18 | "license": "ISC", 19 | "repository": { 20 | "type": "git", 21 | "url": "https://github.com/iarna/streampub" 22 | }, 23 | "bugs": { 24 | "url": "https://github.com/iarna/streampub/issues" 25 | }, 26 | "homepage": "https://github.com/iarna/streampub", 27 | "dependencies": { 28 | "bluebird": "^3.4.6", 29 | "buffer-signature": "^1.0.0", 30 | "concat-stream": "^1.6.0", 31 | "mime-types": "^2.1.12", 32 | "parse5": "^3.0.1", 33 | "readable-stream": "^2.1.5", 34 | "uuid": "^3.0.1", 35 | "xml": "^1.0.1", 36 | "xmlserializer": "^0.6.0", 37 | "zip-stream": "^1.1.0" 38 | }, 39 | "files": [ 40 | "index.js", 41 | "normalize-xhtml.js" 42 | ], 43 | "devDependencies": { 44 | "standard": "^10.0.2" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # v1.9.0 (2017-12-13) 2 | 3 | * A bunch of README improvements and fixes thanks to [@ari7](https://github.com/ari7)! 4 | * Support for EPUB v2 compatible TOCs, again thanks to [@ari7](https://github.com/ari7)! 5 | 6 | # v1.8.0 (2017-04-06) 7 | 8 | * Add support for optionally passing through calibre metadata 9 | * Add setters for includeTOC and numberTOC 10 | * Stop exporting a calibre authormap. While calibre generates this, it does 11 | not import it and will not (according to the authors of calibre). 12 | 13 | # v1.7.0 (2017-01-02) 14 | 15 | * Ditch `htmltidy` as it has C requirements. 16 | 17 | # v1.6.0 (2016-12-20) 18 | 19 | * Detect mime-types based on content 20 | 21 | # v1.5.0 (2016-12-02) 22 | 23 | * Add support for manually numbered tables of contents and generation of 24 | table of contents pages. 25 | 26 | # v1.4.3 (2016-12-02) 27 | 28 | * Allow "proprietary" attributes through. `epub` prefixed attributes count as proprietary. 29 | 30 | # v1.4.2 (2016-12-01) 31 | 32 | * Include items in the spine on the basis of mime-type not chapter name. 33 | This let's us have HTML pages that aren't in the index but ARE in the 34 | spine. 35 | 36 | # v1.4.1 (2016-11-13) 37 | 38 | * Fix bug that was causing browsing order to not match table of contents 39 | order if chapters were added out-of-order. 40 | 41 | # v1.4.0 (2016-10-09) 42 | 43 | * Only pass content through normalizeXHTML if it has an XHTML mimetype. 44 | * Allow zero as an index order value. 45 | 46 | # v1.3.0 (2016-10-04) 47 | 48 | * Author URLs are now included in such a way that Calibre will import them 49 | * In `stream.newFile`: Make filename optional when content is a stream. 50 | * Improve documentation around producing objects for Streampub to consume. 51 | 52 | # v1.2.0 (2016-09-25) 53 | 54 | * Added changelog ([@meinaart](https://github.com/meinaart)) 55 | * Added support for adding files/assets (for example images or stylesheets) ([@meinaart](https://github.com/meinaart)) 56 | * Support for cover image ([@meinaart](https://github.com/meinaart)) 57 | * Much improved documentation ([@meinaart](https://github.com/meinaart)) ([@iarna](https://github.com/iarna)) 58 | * The interface to newChapter was changed to match the documentation. ([@iarna](https://github.com/iarna)) 59 | * Added `setId` interface to match constructor option ([@iarna](https://github.com/iarna)) 60 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Streampub 2 | --------- 3 | 4 | A streaming EPUB3 writer. 5 | 6 | ## EXAMPLE 7 | 8 | ```js 9 | var Streampub = require('streampub') 10 | var fs = require('fs') 11 | var epub = new Streampub({title: 'My Example'}) 12 | epub.setAuthor('Example User') 13 | epub.pipe(fs.createWriteStream('example.epub')) 14 | epub.write(Streampub.newChapter('Chapter 1', 'doc content', 0, 'chapter-1.xhtml')) 15 | epub.end() 16 | ``` 17 | 18 | ## USAGE 19 | 20 | ### var epub = new Streampub(*opts*) 21 | 22 | *opts* is an object that optionally has the following properties: 23 | 24 | * **id** _String_ - _Default: url:**source** or a UUID_ A unique identifier for this work. Note that URLs for this field must be prefixed by "url:". 25 | * **title** _String_ - _Default: "Untitled"_ The title of the epub. 26 | * **author** _String_ - _Optional_ The name of the author of the epub. 27 | * **authorUrl** _String_ - _Optional_ Only used if an author name is used as 28 | well. Adds a related `foaf:homepage` link to the author record. Also a 29 | Calibre link_map, but as yet, Calibre seems unwilling to import this. 30 | * **modified** _Date_ - _Default: new Date()_ When the epub was last modified. 31 | * **published** _Date_ - _Optional_ When the source material was published. 32 | * **source** _String_ - _Optional_ The original URL or URN of the source material. "The described resource may be derived from the related resource in whole or in part. Recommended best practice is to identify the related resource by means of a string conforming to a formal identification system." 33 | * **language** _String_ - _Default: "en"_ Identifies the language used in the book content. The content has to comply with [RFC 3066](http://www.ietf.org/rfc/rfc3066.txt). [List of language codes](http://www.loc.gov/standards/iso639-2/php/code_list.php). 34 | * **description** _String_ - _Optional_ A brief description or summary of the material. 35 | * **publisher** _String_ - _Optional_ "An entity responsible for making the resource available." 36 | * **subject** _String_ - _Optional_ Calibre treats this field as a comma separated list of tag names. "Typically, the subject will be represented using keywords, key phrases, or classification codes. Recommended best practice is to use a controlled vocabulary." 37 | * **includeTOC** _Boolean_ - If true, generate a separate Table of Contents page distinct from the one the ereader uses for navigation. 38 | * **numberTOC** _Boolean_ - If true, suppress the `ol` based list numbering and put our own as text. Necessary to have numbers in front of each TOC entry with most readers. 39 | * **calibre** _Object_ - _Optional_ If set, an object containing Calibre user fields which will be filled in on import to Calibre. 40 | 41 | A note on the calibre object: The format of this object requires a little 42 | discussion. In order for Calibre to import into a custom filed you have to 43 | provide both a matching name and a matching type. This is best explained 44 | via example. For our example, let's assume you have a Calibre field named 45 | `#words` that contains the number of words in a work. To make that 46 | available to Calibre you'd pass in an object like: 47 | 48 | ```js 49 | {words: {'#value#': wordCount, datatype: 'int'}} 50 | ``` 51 | 52 | Other useful datatypes are `text` and `enumeration`. 53 | 54 | All of the options can be set after object creation with obvious setters: 55 | 56 | * `epub.setId(id)` 57 | * `epub.setTitle(title)` 58 | * `epub.setAuthor(author)` 59 | * `epub.setAuthorUrl(author)` 60 | * `epub.setModified(modified)` 61 | * `epub.setPublished(published)` 62 | * `epub.setSource(source)` 63 | * `epub.setLanguage(language)` 64 | * `epub.setDescription(description)` 65 | * `epub.setPublisher(publisher)` 66 | * `epub.setSubject(subject)` 67 | * `epub.setIncudeTOC(includeTOC)` 68 | * `epub.setNumberTOC(numberTOC)` 69 | * `epub.setCalibre(calibre)` 70 | 71 | ### The Streampub Object 72 | 73 | The Streampub object is a transform stream that takes chapter information as 74 | input and outputs binary chunks of an epub file. It's an ordinary stream so you 75 | can pipe into it or write to it and call `.end()` when you're done. 76 | 77 | ### var epub.write(*obj*, *callback*) 78 | 79 | This is the usual stream write function. The object can either be constructed with: 80 | 81 | ```js 82 | Streampub.newChapter(chapterName, content, index, fileName, mime) 83 | Streampub.newCoverImage(content, mime) 84 | Streampub.newFile(fileName, content, mime) 85 | ``` 86 | 87 | Or by hand by creating an object with the following keys: 88 | 89 | * **id** _String_ - _Optional_ Internal ID of object, if omited `streampub` will generate one. 90 | * **chapterName** _String_ - _Required_ The name of the chapter in the index. 91 | * **content** _String_ or _stream.Readable_ - _Required_ The content of item being added. If this is HTML then 92 | it will be run through `parse5` and `xmlserializer` to make it valid XHTML. 93 | * **index** _Number_ - _Optional_ Where the chapter should show up in the index. These numbers 94 | can have gaps and are used for ordering ONLY. Duplicate index values will 95 | result in the earlier chapter being excluded from the index. If not specified will 96 | be added after any where it _was_ specified, in the order written. 97 | * **fileName** _String_ - _Optional_ The filename to use *inside* the epub. For chapters this is only needed 98 | if you want to inter-chapter linking. Uses are more obvious for CSS and images. If content is an `fs` stream 99 | then this will default to a value inferred from the original filename. 100 | * **mime** _String_ - _Optional_ Mimetype of content, if not supplied `streampub` will try to determine type. 101 | 102 | *If you include indexes then you can add chapters in any order.* 103 | 104 | #### Example 105 | 106 | ```js 107 | var Streampub = require('./index') 108 | var fs = require('fs') 109 | 110 | var epub = new Streampub({title: 'My Example'}) 111 | epub.setAuthor('Example author') 112 | epub.pipe(fs.createWriteStream('example.epub')) 113 | epub.write(Streampub.newFile(fs.createReadStream('author.jpg'))) 114 | epub.write(Streampub.newFile('stylesheet.css', fs.createReadStream('styles.css'))) 115 | epub.write(Streampub.newChapter('Chapter 1', '

Chapter 1

doc content')) 116 | epub.write(Streampub.newChapter('Chapter 2', '

Chapter 2

doc content')) 117 | epub.end() 118 | ``` 119 | 120 | or equivalently 121 | 122 | ```js 123 | var epub = new Streampub({title: 'My Example'}) 124 | epub.setAuthor('Example author') 125 | epub.pipe(fs.createWriteStream('example.epub')) 126 | epub.write({content: fs.createReadStream('author.jpg')}) 127 | epub.write({fileName: 'stylesheet.css', content: fs.createReadStream('styles.css')}) 128 | epub.write({chapterName: 'Chapter 1', content: '

Chapter 1

doc content'}) 129 | epub.write({chapterName: 'Chapter 2', content: '

Chapter 2

doc content'}) 130 | epub.end() 131 | ``` 132 | 133 | ## Cover image 134 | 135 | The epub specification does not contain a standarized way to include book covers. There is however a "best practice" that will work in most reader applications. `streampub` has some magic under the hood to correctly add a cover image. The only requirements are that the file needs to be in JPEG format and should be max 1000x1000 pixels. 136 | 137 | ### Example 138 | 139 | 140 | ```js 141 | var Streampub = require('./index') 142 | var fs = require('fs') 143 | 144 | var epub = new Streampub({title: 'My Example'}) 145 | epub.setAuthor('Example author') 146 | epub.pipe(fs.createWriteStream('example.epub')) 147 | // Using this specific ID causes cover magic to kick in 148 | epub.write(Streampub.newCoverImage(fs.createReadStream('cover.jpg'))) 149 | epub.write(Streampub.newChapter('Chapter 1', '

Chapter 1

doc content')) 150 | epub.write(Streampub.newChapter('Chapter 2', '

Chapter 2

doc content')) 151 | epub.end() 152 | ``` 153 | 154 | or equivalently 155 | 156 | ```js 157 | var Streampub = require('./index') 158 | var fs = require('fs') 159 | 160 | var epub = new Streampub({title: 'My Example'}) 161 | epub.setAuthor('Example author') 162 | epub.pipe(fs.createWriteStream('example.epub')) 163 | // Using this specific ID causes cover magic to kick in 164 | epub.write({id: 'cover-image', content: fs.createReadStream('cover.jpg')}) 165 | epub.write({chapterName: 'Chapter 1', content: '

Chapter 1

doc content'}) 166 | epub.write({chapterName: 'Chapter 2', content: '

Chapter 2

doc content'}) 167 | epub.end() 168 | ``` 169 | 170 | ## VALIDATION 171 | 172 | This takes care to generate only valid XML using programmatic generators and 173 | not templates 174 | 175 | Epubs produced by this have been validated with 176 | [epubcheck](https://github.com/idpf/epubcheck). No warnings outside of 177 | content warnings should be present. 178 | 179 | Content warnings ordinarily only happen if your content contains broken links–usually relative links to resources 180 | that don't exist in the epub. 181 | 182 | ## PRIOR ART 183 | 184 | There are a bunch of epub generators already available. Many are pre EPUB3. 185 | Most work off of files on disk rather than in memory constructs. Only one 186 | other provides a stream that I was able to find was 187 | [epub-generator](https://npmjs.com/package/epub-generator) and it only 188 | provides a read stream. I wanted to be able to build a full pipeline for, 189 | for example, backpressure reasons. I also very much wanted to be able to 190 | set epub metadata after object construction time. 191 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | var Bluebird = require('bluebird') 3 | var Transform = require('readable-stream').Transform 4 | var zlib = require('zlib') 5 | var ZipStream = require('zip-stream') 6 | var inherits = require('util').inherits 7 | var xml = require('xml') 8 | var uuid = require('uuid') 9 | var normalizeXHTML = require('./normalize-xhtml.js') 10 | var mime = require('mime-types') 11 | var stream = require('stream') 12 | var path = require('path') 13 | var identifyStream = require('buffer-signature').identifyStream 14 | var identifyBuffer = require('buffer-signature').identify 15 | var concatStream = require('concat-stream') 16 | 17 | module.exports = Streampub 18 | module.exports.newChapter = Chapter 19 | module.exports.newCoverImage = CoverImage 20 | module.exports.newFile = File 21 | 22 | var container = {container: [ 23 | {_attr: {version: '1.0', xmlns: 'urn:oasis:names:tc:opendocument:xmlns:container'}}, 24 | {rootfiles: [ 25 | {rootfile: [ 26 | {_attr: {'full-path': 'OEBPS/content.opf', 'media-type': 'application/oebps-package+xml'}} 27 | ]} 28 | ]} 29 | ]} 30 | 31 | var MIME_XHTML = 'application/xhtml+xml' 32 | var TYPE_COVER = 'cover' 33 | var TYPE_COVER_IMAGE = 'cover-image' 34 | var FILENAME_COVER = 'cover.xhtml' 35 | var FILENAME_COVER_IMAGE = 'images/cover.jpg' 36 | 37 | function Streampub (opts) { 38 | var self = this 39 | Transform.call(this, {objectMode: true}) 40 | if (!opts) opts = {} 41 | this.zip = new ZipStream({level: zlib.Z_BEST_COMPRESSION}) 42 | this.zip.entry = Bluebird.promisify(this.zip.entry) 43 | this.zip.on('data', function (data, enc) { 44 | self.push(data, enc) 45 | }) 46 | this.chapters = [] 47 | this.files = [] 48 | this.meta = {} 49 | this.meta.id = opts.id 50 | this.meta.title = opts.title || 'Untitled' 51 | this.meta.author = opts.author 52 | this.meta.authorUrl = opts.authorUrl 53 | this.setModified(opts.modified || new Date()) 54 | if (opts.published) this.setPublished(opts.published) 55 | this.meta.source = opts.source 56 | this.meta.language = opts.language || 'en' 57 | this.meta.description = opts.description 58 | this.meta.publisher = opts.publisher 59 | this.meta.subject = opts.subject 60 | this.meta.calibre = opts.calibre 61 | this.numberTOC = opts.numberTOC 62 | this.includeTOC = opts.includeTOC 63 | this.maxId = 0 64 | this.header = self.zip.entry('application/epub+zip', {name: 'mimetype'}).then(function () { 65 | return self.zip.entry(xml(container, {declaration: true}), {name: 'META-INF/container.xml'}) 66 | }) 67 | } 68 | inherits(Streampub, Transform) 69 | 70 | Streampub.prototype._flush = function (done) { 71 | var self = this 72 | 73 | var toZip = [] 74 | if (this.includeTOC) { 75 | this.files.push({chapterName: 'Table of Contents', fileName: 'toc.xhtml', id: '__index', order: -1, mime: MIME_XHTML}) 76 | var title = this.chapters.shift() 77 | this.chapters.unshift({index: -1, chapterName: 'Table of Contents', fileName: 'toc.xhtml', mime: MIME_XHTML}) 78 | this.chapters.unshift(title) 79 | toZip.push([xml([{html: self._generateTOC()}], {declaration: true}), {name: 'OEBPS/toc.xhtml'}]) 80 | } 81 | 82 | this.files.push({fileName: 'toc.ncx', id: 'ncx', order: -2, mime: 'application/x-dtbncx+xml'}) 83 | toZip.push([self._generateVersion2TOC(), {name: 'OEBPS/toc.ncx'}]) 84 | 85 | var pkg = [] 86 | pkg.push({_attr: { 87 | version: '3.0', 88 | 'unique-identifier': 'pub-id', 89 | 'xmlns': 'http://www.idpf.org/2007/opf', 90 | 'prefix': 'foaf: http://xmlns.com/foaf/spec/ ' + 91 | 'calibre: https://calibre-ebook.com' 92 | }}) 93 | pkg.push({metadata: self._generateMetadata()}) 94 | pkg.push({manifest: self._generateManifest()}) 95 | pkg.push({spine: self._generateSpine()}) 96 | if (self.hasCover) { 97 | pkg.push({guide: [{reference: {_attr: {href: FILENAME_COVER, type: 'cover', title: self.meta.title || 'Cover'}}}]}) 98 | } 99 | 100 | toZip.push([xml([{'package': pkg}], {declaration: true}), {name: 'OEBPS/content.opf'}]) 101 | toZip.push([xml([{html: self._generateTOC(self.numberTOC)}], {declaration: true}), {name: 'OEBPS/ereader-toc.xhtml'}]) 102 | 103 | self.header.then(function () { 104 | return Bluebird.each(toZip, function (item) { return self.zip.entry.apply(self.zip, item) }) 105 | }).then(function () { 106 | self.zip.once('finish', done) 107 | self.zip.finalize() 108 | return null 109 | }) 110 | } 111 | 112 | function Chapter (chapterName, content, index, fileName, mime) { 113 | return {index: index, chapterName: chapterName, fileName: fileName, content: content, mime: mime} 114 | } 115 | 116 | function CoverImage (content, mime) { 117 | return {id: 'cover-image', content: content, mime: mime} 118 | } 119 | 120 | function File (fileName, content, mime) { 121 | if (fileName instanceof stream.Stream) { 122 | mime = content 123 | content = fileName 124 | fileName = null 125 | } 126 | return {content: content, fileName: fileName, mime: mime} 127 | } 128 | 129 | Streampub.prototype._injectCover = function (cb) { 130 | var self = this 131 | var title = self.meta.title || 'Cover' 132 | self._transform({ 133 | id: TYPE_COVER, 134 | fileName: FILENAME_COVER, 135 | mime: MIME_XHTML, 136 | content: 137 | '' + title + '' + 138 | '' + 139 | '', 140 | index: -100 141 | }, 'utf8', cb) 142 | } 143 | 144 | Streampub.prototype._transform = function (data, encoding, done) { 145 | var self = this 146 | var id = data.id || ++self.maxId 147 | var index = data.index == null ? (100000 + id) : data.index 148 | 149 | if (data.id === TYPE_COVER_IMAGE) { 150 | self.hasCoverImage = true 151 | data.fileName = FILENAME_COVER_IMAGE 152 | } else if (data.id === TYPE_COVER) { 153 | self.hasCover = true 154 | } 155 | 156 | var contentIsStream = data.content instanceof stream.Stream 157 | var sourceFilename = contentIsStream && typeof data.content.path === 'string' ? path.basename(data.content.path) : undefined 158 | 159 | data.fileName = data.fileName || 160 | (data.chapterName ? 'chapter-' + id + '.xhtml' : sourceFilename || 'asset-' + id) 161 | 162 | data.mime = data.mime || mime.lookup(sourceFilename || data.fileName) 163 | 164 | function addContent (content) { 165 | if (data.chapterName) { 166 | self.chapters[index] = {index: index, chapterName: data.chapterName, fileName: data.fileName} 167 | self.chapters[index] = {index: index, chapterName: data.chapterName, fileName: data.fileName} 168 | } 169 | self.files.push({chapterName: data.chapterName, fileName: data.fileName, mime: data.mime, id: data.id || 'file' + id, order: index}) 170 | return self.header.then(function () { 171 | return self.zip.entry(content, {name: 'OEBPS/' + data.fileName}) 172 | }) 173 | } 174 | 175 | function setMimeType (info) { 176 | if (info.unknown) return 177 | data.mime = info.mimeType 178 | } 179 | 180 | if (contentIsStream) { 181 | // we don't actually support streaming XHTML as we don't support 182 | // normalizing an XHTML stream, so concat it up and then treat it as 183 | // normal. 184 | if (data.mime === MIME_XHTML) { 185 | var errored = false 186 | data.content.on('error', function (err) { 187 | done(err) 188 | errored = true 189 | }).pipe(concatStream(function (content) { 190 | if (errored) return 191 | normalizeXHTML(content).catch(done).then(addContent).finally(done) 192 | })) 193 | } else { 194 | addContent(data.content.pipe(identifyStream(setMimeType))).then(function () { 195 | if (self.hasCoverImage && !self.hasCover) { 196 | self._injectCover(done) 197 | } else { 198 | done() 199 | } 200 | }).catch(done) 201 | } 202 | } else if (data.mime === MIME_XHTML) { 203 | normalizeXHTML(data.content).catch(done).then(addContent).finally(done) 204 | } else { 205 | setMimeType(identifyBuffer(data.content)) 206 | addContent(data.content).finally(done) 207 | } 208 | } 209 | 210 | Streampub.prototype.setId = function (id) { 211 | this.meta.id = id 212 | } 213 | 214 | Streampub.prototype.setTitle = function (title) { 215 | this.meta.title = title 216 | } 217 | 218 | Streampub.prototype.setAuthor = function (author) { 219 | this.meta.author = author 220 | } 221 | 222 | Streampub.prototype.setAuthorUrl = function (authorUrl) { 223 | this.meta.authorUrl = authorUrl 224 | } 225 | 226 | Streampub.prototype.setModified = function (modified) { 227 | if (!(modified instanceof Date)) modified = new Date(modified) 228 | this.meta.modified = modified 229 | } 230 | 231 | Streampub.prototype.setPublished = function (published) { 232 | if (!(published instanceof Date)) published = new Date(published) 233 | this.meta.published = published 234 | } 235 | 236 | Streampub.prototype.setSource = function (src) { 237 | this.meta.source = src 238 | } 239 | 240 | Streampub.prototype.setLanguage = function (language) { 241 | this.meta.language = language 242 | } 243 | 244 | Streampub.prototype.setDescription = function (description) { 245 | this.meta.description = description 246 | } 247 | 248 | Streampub.prototype.setPublisher = function (publisher) { 249 | this.meta.publisher = publisher 250 | } 251 | 252 | Streampub.prototype.setSubject = function (subject) { 253 | this.meta.subject = subject 254 | } 255 | 256 | Streampub.prototype.setIncludeTOC = function (includeTOC) { 257 | this.includeTOC = includeTOC 258 | } 259 | 260 | Streampub.prototype.setNumberTOC = function (numberTOC) { 261 | this.numberTOC = numberTOC 262 | } 263 | 264 | Streampub.prototype.setCalibre = function (calibre) { 265 | this.meta.calibre = calibre 266 | } 267 | 268 | function w3cdtc (date) { 269 | try { 270 | return date.toISOString().replace(/[.]\d{1,3}Z/, 'Z') 271 | } catch (e) { 272 | console.error('WAT', date, '!!') 273 | throw e 274 | } 275 | } 276 | 277 | Streampub.prototype._generateMetadata = function () { 278 | var metadata = [{_attr: {'xmlns:dc': 'http://purl.org/dc/elements/1.1/'}}] 279 | var id = this.meta.id || 'url:' + this.meta.source || 'urn:uuid:' + uuid.v4() 280 | metadata.push({'dc:identifier': [{_attr: {id: 'pub-id'}}, id]}) 281 | metadata.push({'dc:language': this.meta.language}) 282 | metadata.push({'dc:title': this.meta.title}) 283 | metadata.push({'meta': [{_attr: {property: 'dcterms:modified'}}, w3cdtc(this.meta.modified)]}) 284 | if (this.meta.source) { 285 | metadata.push({'dc:source': this.meta.source}) 286 | metadata.push({'link': [{_attr: {href: this.meta.source, rel: 'foaf:homepage'}}]}) 287 | } 288 | if (this.meta.author) { 289 | metadata.push({'dc:creator': [{_attr: {id: 'author'}}, this.meta.author]}) 290 | metadata.push({'meta': [{_attr: {refines: '#author', property: 'role', scheme: 'marc:relators', id: 'role'}}, 'aut']}) 291 | metadata.push({'meta': [{_attr: {'property': 'file-as', refines: '#author'}}, this.meta.author]}) 292 | if (this.meta.authorUrl) { 293 | metadata.push({'link': [{_attr: {href: this.meta.authorUrl, rel: 'foaf:homepage', refines: '#author'}}]}) 294 | } 295 | } 296 | if (this.meta.description) { 297 | metadata.push({'dc:description': this.meta.description}) 298 | } 299 | if (this.meta.published) { 300 | metadata.push({'dc:date': w3cdtc(this.meta.published)}) 301 | } 302 | if (this.meta.publisher) { 303 | metadata.push({'dc:publisher': this.meta.publisher}) 304 | } 305 | if (this.hasCoverImage) { 306 | metadata.push({'meta': [{_attr: {name: 'cover', content: 'cover-image'}}]}) 307 | } 308 | if (this.meta.subject) { 309 | metadata.push({'dc:subject': this.meta.subject}) 310 | } 311 | if (this.meta.calibre) { 312 | var self = this 313 | Object.keys(this.meta.calibre).forEach(function (name) { 314 | if (!self.meta.calibre[name]) return 315 | metadata.push({'meta': [{_attr: {name: 'calibre:user_metadata:#' + name, content: JSON.stringify(self.meta.calibre[name])}}]}) 316 | }) 317 | } 318 | return metadata 319 | } 320 | 321 | function cmp (aa, bb) { 322 | if (aa > bb) return 1 323 | if (bb > aa) return -1 324 | return 0 325 | } 326 | 327 | function fileOrder (aa, bb) { 328 | return cmp((aa.order || 0), (bb.order || 0)) || cmp(aa.id, bb.id) 329 | } 330 | 331 | Streampub.prototype._generateManifest = function () { 332 | var manifest = [] 333 | // epub2: 334 | // epub3: 335 | var item 336 | manifest.push({'item': [{_attr: {id: 'nav', href: 'ereader-toc.xhtml', properties: 'nav', 'media-type': MIME_XHTML}}]}) 337 | this.files.sort(fileOrder).forEach(function (file) { 338 | item = {'item': [{_attr: {id: file.id, href: file.fileName, 'media-type': file.mime}}]} 339 | if (file.id === TYPE_COVER_IMAGE) { 340 | manifest.unshift(item) 341 | } else { 342 | manifest.push(item) 343 | } 344 | }) 345 | return manifest 346 | } 347 | 348 | Streampub.prototype._generateSpine = function () { 349 | var spine = [{_attr: {toc: 'ncx'}}] 350 | this.files.sort(fileOrder).forEach(function (file) { 351 | if (file.id === TYPE_COVER) { 352 | spine.unshift({'itemref': [{_attr: {idref: file.id, linear: 'no'}}]}) 353 | } else if (file.mime === MIME_XHTML) { 354 | spine.push({'itemref': [{_attr: {idref: file.id}}]}) 355 | } 356 | }) 357 | return spine 358 | } 359 | 360 | Streampub.prototype._generateTOC = function (numberTOC) { 361 | var html = [{_attr: {'xmlns': 'http://www.w3.org/1999/xhtml', 'xmlns:epub': 'http://www.idpf.org/2007/ops'}}] 362 | var header = [] 363 | html.push({'head': header}) 364 | if (numberTOC) { 365 | // we were asked to include numbers in the index because not all devices 366 | // show the autogenerated ones. 367 | header.push({'style': 'ol { list-style-type: none; }'}) 368 | } 369 | var body = [] 370 | html.push({'body': body}) 371 | var nav = [{_attr: {'epub:type': 'toc'}}] 372 | body.push({'nav': nav}) 373 | var ol = [] 374 | nav.push({'ol': ol}) 375 | var chapterNum = 0 376 | this.chapters.forEach(function (chapter) { 377 | var chapterLine = [] 378 | chapterLine.push({'a': [{_attr: {'href': chapter.fileName}}, 379 | (numberTOC ? (++chapterNum) + '. ' : '') + chapter.chapterName]}) 380 | ol.push({'li': chapterLine}) 381 | }) 382 | return html 383 | } 384 | 385 | Streampub.prototype._generateVersion2TOC = function () { 386 | let inner = [{ 387 | _attr: { 388 | xmlns: 'http://www.daisy.org/z3986/2005/ncx/', 389 | version: '2005-1' 390 | } 391 | }] 392 | inner.push({ 393 | head: [ 394 | {meta: {_attr: {name: 'dtb:uid', content: ''}}}, 395 | {meta: {_attr: {name: 'dtb:depth', content: '1'}}}, 396 | {meta: {_attr: {name: 'dtb:totalPageCount', content: '0'}}}, 397 | {meta: {_attr: {name: 'dtb:maxPageNumber', content: '0'}}} 398 | ] 399 | }) 400 | inner.push({ 401 | docTitle: [ 402 | {text: this.meta.title} 403 | ] 404 | }) 405 | let i = 0 406 | inner.push({ 407 | navMap: this.chapters.map((chapter) => ({ 408 | navPoint: [ 409 | {_attr: {id: 'navpoint' + (++i), playOrder: i}}, 410 | {navLabel: [{text: chapter.chapterName}]}, 411 | {content: {_attr: {src: chapter.fileName}}} 412 | ] 413 | })) 414 | }) 415 | let decl = ( 416 | '\n' 417 | ) 418 | return decl + xml({ncx: inner}, {indent: ' '}) 419 | } 420 | --------------------------------------------------------------------------------