├── .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 |
--------------------------------------------------------------------------------