2016-06-28 09:19:01 +12:00
|
|
|
|
|
|
|
import JSZip from 'jszip'
|
|
|
|
import escapeStringRegexp from 'escape-string-regexp'
|
|
|
|
import zeroFill from 'zero-fill'
|
2021-05-25 11:42:43 +12:00
|
|
|
import { decode } from 'html-entities'
|
2016-08-15 21:11:20 +12:00
|
|
|
import sanitize from 'sanitize-filename'
|
2019-10-08 22:45:47 +13:00
|
|
|
import { URL } from 'url'
|
2016-06-28 09:19:01 +12:00
|
|
|
import isNode from 'detect-node'
|
2020-01-16 23:09:13 +13:00
|
|
|
import FileType from 'file-type'
|
2017-06-12 23:53:17 +12:00
|
|
|
import isSvg from 'is-svg'
|
2016-08-24 02:32:55 +12:00
|
|
|
import sizeOf from 'image-size'
|
2018-03-15 06:04:50 +13:00
|
|
|
import EventEmitter from 'events'
|
2018-03-16 22:55:04 +13:00
|
|
|
import { buf as crc32 } from 'crc-32'
|
2016-06-28 09:19:01 +12:00
|
|
|
|
|
|
|
import { cleanMarkup } from './cleanMarkup'
|
2016-08-23 19:19:01 +12:00
|
|
|
import fetch from './fetch'
|
2020-09-08 03:08:49 +12:00
|
|
|
import fetchRemote from './fetchRemote'
|
2016-06-28 09:19:01 +12:00
|
|
|
import * as template from './templates'
|
2018-03-13 10:09:26 +13:00
|
|
|
import { styleCss, coverstyleCss, titlestyleCss, iconsCss, navstyleCss, paragraphsCss } from './styles'
|
|
|
|
import * as utils from './utils'
|
2018-03-27 07:32:02 +13:00
|
|
|
import kepubify from './kepubify'
|
2018-03-13 10:09:26 +13:00
|
|
|
import subsetFont from './subsetFont'
|
2016-06-28 09:19:01 +12:00
|
|
|
|
2016-08-22 21:50:11 +12:00
|
|
|
import { containerXml } from './constants'
|
2016-06-28 09:19:01 +12:00
|
|
|
|
2021-05-25 11:42:43 +12:00
|
|
|
const fontAwesomeCodes = require('../build/font-awesome-codes.json')
|
2016-06-28 09:19:01 +12:00
|
|
|
|
2017-06-12 23:53:17 +12:00
|
|
|
const trimWhitespace = /^\s*(<br\s*\/?\s*>)+|(<br\s*\/?\s*>)+\s*$/ig
|
|
|
|
|
2018-03-15 06:04:50 +13:00
|
|
|
class FimFic2Epub extends EventEmitter {
|
2016-08-17 02:33:52 +12:00
|
|
|
static getStoryId (id) {
|
|
|
|
if (isNaN(id)) {
|
2019-10-08 22:45:47 +13:00
|
|
|
const url = new URL(id)
|
2016-08-16 01:12:20 +12:00
|
|
|
if (url.hostname === 'www.fimfiction.net' || url.hostname === 'fimfiction.net') {
|
2019-10-08 22:31:42 +13:00
|
|
|
const m = url.pathname.match(/^\/story\/(\d+)/)
|
2016-08-16 01:12:20 +12:00
|
|
|
if (m) {
|
2016-08-17 02:33:52 +12:00
|
|
|
id = m[1]
|
2016-08-16 01:12:20 +12:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2016-08-17 02:33:52 +12:00
|
|
|
return id
|
|
|
|
}
|
|
|
|
|
|
|
|
static getFilename (storyInfo) {
|
2020-09-08 03:08:49 +12:00
|
|
|
return sanitize(storyInfo.author.name + ' - ' + storyInfo.title + '.epub')
|
2016-08-17 02:33:52 +12:00
|
|
|
}
|
|
|
|
|
2016-08-19 21:24:20 +12:00
|
|
|
static fetchStoryInfo (storyId, raw = false) {
|
2016-08-17 02:33:52 +12:00
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
storyId = FimFic2Epub.getStoryId(storyId)
|
2019-10-08 22:31:42 +13:00
|
|
|
const url = '/api/story.php?story=' + storyId
|
2016-08-23 19:19:01 +12:00
|
|
|
fetch(url).then((content) => {
|
2016-08-17 02:33:52 +12:00
|
|
|
let data
|
|
|
|
try {
|
2016-08-19 21:24:20 +12:00
|
|
|
data = JSON.parse(content)
|
2016-08-17 02:33:52 +12:00
|
|
|
} catch (e) {}
|
|
|
|
if (!data) {
|
2017-10-19 01:59:30 +13:00
|
|
|
reject(new Error('Unable to fetch story info'))
|
2016-08-17 02:33:52 +12:00
|
|
|
return
|
|
|
|
}
|
|
|
|
if (data.error) {
|
2018-03-14 01:58:03 +13:00
|
|
|
reject(new Error(data.error + ' (id: ' + storyId + ')'))
|
2016-08-17 02:33:52 +12:00
|
|
|
return
|
|
|
|
}
|
2019-10-08 22:31:42 +13:00
|
|
|
const story = data.story
|
2016-08-19 21:24:20 +12:00
|
|
|
if (raw) {
|
|
|
|
resolve(story)
|
|
|
|
return
|
|
|
|
}
|
2016-08-17 20:37:41 +12:00
|
|
|
// this is so the metadata can be cached.
|
|
|
|
if (!story.chapters) story.chapters = []
|
|
|
|
delete story.likes
|
|
|
|
delete story.dislikes
|
|
|
|
delete story.views
|
|
|
|
delete story.total_views
|
|
|
|
delete story.comments
|
|
|
|
story.chapters.forEach((ch) => {
|
|
|
|
delete ch.views
|
|
|
|
})
|
2016-08-19 21:24:20 +12:00
|
|
|
// Add version number
|
|
|
|
story.FIMFIC2EPUB_VERSION = FIMFIC2EPUB_VERSION
|
|
|
|
resolve(story)
|
2016-08-17 02:33:52 +12:00
|
|
|
})
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2018-03-13 10:09:26 +13:00
|
|
|
constructor (storyId, options = {}) {
|
2016-08-24 02:32:55 +12:00
|
|
|
super()
|
|
|
|
|
2016-08-17 02:33:52 +12:00
|
|
|
this.storyId = FimFic2Epub.getStoryId(storyId)
|
|
|
|
|
2018-03-13 10:09:26 +13:00
|
|
|
this.defaultOptions = {
|
2018-03-16 20:57:23 +13:00
|
|
|
typogrify: false,
|
2016-08-24 09:49:27 +12:00
|
|
|
addCommentsLink: true,
|
|
|
|
includeAuthorNotes: true,
|
2016-08-30 02:20:20 +12:00
|
|
|
useAuthorNotesIndex: false,
|
2018-07-31 20:27:49 +12:00
|
|
|
showChapterHeadings: true,
|
|
|
|
showChapterWordCount: true,
|
|
|
|
showChapterDuration: true,
|
2016-08-25 00:47:48 +12:00
|
|
|
includeExternal: true,
|
2017-06-07 08:15:05 +12:00
|
|
|
paragraphStyle: 'spaced',
|
2018-03-27 07:32:02 +13:00
|
|
|
kepubify: false,
|
2018-03-13 23:10:04 +13:00
|
|
|
joinSubjects: false,
|
2018-03-14 21:50:59 +13:00
|
|
|
calculateReadingEase: true,
|
2018-03-16 04:14:57 +13:00
|
|
|
readingEaseWakeupInterval: isNode ? 50 : 200, // lower for node, to not slow down thread
|
|
|
|
wordsPerMinute: 200, // 0 to disable
|
|
|
|
addChapterBars: true
|
2016-08-24 09:49:27 +12:00
|
|
|
}
|
|
|
|
|
2018-03-13 10:09:26 +13:00
|
|
|
this.options = Object.assign(this.defaultOptions, options)
|
|
|
|
|
2016-08-25 00:47:48 +12:00
|
|
|
// promise cache
|
|
|
|
this.pcache = {
|
|
|
|
metadata: null,
|
|
|
|
chapters: null,
|
|
|
|
remoteResources: null,
|
|
|
|
coverImage: null,
|
|
|
|
fetchAll: null
|
|
|
|
}
|
2016-08-23 07:57:19 +12:00
|
|
|
|
|
|
|
this.storyInfo = null
|
2016-08-24 02:32:55 +12:00
|
|
|
this.description = ''
|
2016-08-24 08:04:38 +12:00
|
|
|
this.subjects = []
|
2016-08-23 19:19:01 +12:00
|
|
|
this.chapters = []
|
2016-08-25 00:47:48 +12:00
|
|
|
this.chaptersHtml = []
|
2016-08-30 02:20:20 +12:00
|
|
|
this.notesHtml = []
|
|
|
|
this.hasAuthorNotes = false
|
|
|
|
this.chaptersWithNotes = []
|
2018-03-13 10:09:26 +13:00
|
|
|
this.pages = {}
|
2016-08-25 00:47:48 +12:00
|
|
|
this.remoteResourcesCached = false
|
2016-06-28 09:19:01 +12:00
|
|
|
this.remoteResources = new Map()
|
2018-03-13 10:09:26 +13:00
|
|
|
this.usedIcons = new Set()
|
|
|
|
this.iconsFont = null
|
2016-08-24 08:04:38 +12:00
|
|
|
this.coverUrl = ''
|
2016-08-24 02:32:55 +12:00
|
|
|
this.coverImage = null
|
|
|
|
this.coverFilename = ''
|
|
|
|
this.coverType = ''
|
2019-10-08 19:37:27 +13:00
|
|
|
this.coverImageDimensions = { width: 0, height: 0 }
|
2018-03-14 21:33:46 +13:00
|
|
|
this.readingEase = null
|
2016-08-23 07:57:19 +12:00
|
|
|
|
2016-08-25 00:47:48 +12:00
|
|
|
this.hasRemoteResources = {
|
|
|
|
titlePage: false
|
|
|
|
}
|
|
|
|
|
2016-08-15 21:11:20 +12:00
|
|
|
this.cachedFile = null
|
2016-08-24 02:32:55 +12:00
|
|
|
this.tags = []
|
2016-06-28 09:19:01 +12:00
|
|
|
|
2016-08-24 02:32:55 +12:00
|
|
|
this.zip = null
|
|
|
|
}
|
|
|
|
|
2016-08-25 00:47:48 +12:00
|
|
|
fetchAll () {
|
|
|
|
if (this.pcache.fetchAll) {
|
|
|
|
return this.pcache.fetchAll
|
2016-08-24 02:32:55 +12:00
|
|
|
}
|
2016-08-25 00:47:48 +12:00
|
|
|
|
2018-03-14 01:58:03 +13:00
|
|
|
this.progress(0, 0, '')
|
2016-08-25 00:47:48 +12:00
|
|
|
|
|
|
|
this.pcache.fetchAll = this.fetchMetadata()
|
|
|
|
.then(this.fetchChapters.bind(this))
|
|
|
|
.then(this.fetchCoverImage.bind(this))
|
2016-08-25 01:57:05 +12:00
|
|
|
.then(this.buildChapters.bind(this))
|
2018-03-13 23:10:04 +13:00
|
|
|
.then(this.buildPages.bind(this))
|
2018-03-13 10:09:26 +13:00
|
|
|
.then(this.findIcons.bind(this))
|
2016-08-25 00:47:48 +12:00
|
|
|
.then(this.fetchRemoteFiles.bind(this))
|
|
|
|
.then(() => {
|
|
|
|
this.progress(0, 0.95)
|
|
|
|
this.pcache.fetchAll = null
|
|
|
|
})
|
|
|
|
|
|
|
|
return this.pcache.fetchAll
|
2016-06-28 19:39:31 +12:00
|
|
|
}
|
2016-06-28 09:19:01 +12:00
|
|
|
|
2016-08-24 08:04:38 +12:00
|
|
|
fetchMetadata () {
|
2016-08-25 00:47:48 +12:00
|
|
|
if (this.pcache.metadata) {
|
|
|
|
return this.pcache.metadata
|
|
|
|
}
|
|
|
|
if (this.storyInfo) {
|
|
|
|
return Promise.resolve()
|
|
|
|
}
|
2016-08-24 08:04:38 +12:00
|
|
|
this.storyInfo = null
|
|
|
|
this.description = ''
|
2016-08-25 00:47:48 +12:00
|
|
|
this.subjects = []
|
2016-08-24 08:04:38 +12:00
|
|
|
|
2016-08-25 00:47:48 +12:00
|
|
|
this.progress(0, 0, 'Fetching metadata...')
|
|
|
|
|
2018-03-13 10:09:26 +13:00
|
|
|
this.pcache.metadata = FimFic2Epub.fetchStoryInfo(this.storyId)
|
|
|
|
.then((storyInfo) => {
|
|
|
|
this.storyInfo = storyInfo
|
2018-03-16 04:14:57 +13:00
|
|
|
this.storyInfo.uuid = 'urn:fimfiction:' + this.storyInfo.id
|
2018-03-13 10:09:26 +13:00
|
|
|
this.filename = FimFic2Epub.getFilename(this.storyInfo)
|
|
|
|
this.storyInfo.chapters.forEach((chapter) => {
|
|
|
|
if (chapter.date_modified > this.storyInfo.date_modified) {
|
|
|
|
this.storyInfo.date_modified = chapter.date_modified
|
|
|
|
}
|
|
|
|
})
|
|
|
|
this.progress(0, 0.5)
|
|
|
|
})
|
|
|
|
.then(this.fetchTitlePage.bind(this))
|
|
|
|
.then(() => {
|
|
|
|
this.progress(0, 1)
|
|
|
|
})
|
|
|
|
.then(() => cleanMarkup(this.description)).then((html) => {
|
|
|
|
this.storyInfo.description = html
|
|
|
|
}).then(() => {
|
|
|
|
this.pcache.metadata = null
|
|
|
|
})
|
2016-08-25 00:47:48 +12:00
|
|
|
return this.pcache.metadata
|
2016-08-24 08:04:38 +12:00
|
|
|
}
|
|
|
|
|
2016-08-25 00:47:48 +12:00
|
|
|
fetchChapters () {
|
|
|
|
if (this.pcache.chapters) {
|
|
|
|
return this.pcache.chapters
|
|
|
|
}
|
|
|
|
// chapters have already been fetched
|
|
|
|
if (this.chapters.length !== 0) {
|
|
|
|
return Promise.resolve()
|
|
|
|
}
|
|
|
|
this.chapters.length = 0
|
|
|
|
this.chaptersHtml.length = 0
|
2016-08-30 02:20:20 +12:00
|
|
|
this.hasAuthorNotes = false
|
|
|
|
this.chaptersWithNotes.length = 0
|
2016-08-25 00:47:48 +12:00
|
|
|
|
|
|
|
this.progress(0, 0, 'Fetching chapters...')
|
2017-06-12 23:53:17 +12:00
|
|
|
|
2019-10-08 22:31:42 +13:00
|
|
|
const chapterCount = this.storyInfo.chapters.length
|
2020-09-07 23:40:21 +12:00
|
|
|
const url = '/story/download/' + this.storyInfo.id + '/html'
|
2017-06-12 23:53:17 +12:00
|
|
|
|
|
|
|
this.pcache.chapters = fetch(url).then((html) => {
|
|
|
|
let p = Promise.resolve()
|
2019-10-08 22:31:42 +13:00
|
|
|
const matchChapter = /<article class="chapter">[\s\S]*?<\/header>([\s\S]*?)<\/article>/g
|
2017-06-12 23:53:17 +12:00
|
|
|
for (let ma, i = 0; (ma = matchChapter.exec(html)); i++) {
|
2018-03-15 23:33:52 +13:00
|
|
|
const ch = this.storyInfo.chapters[i]
|
2017-06-12 23:53:17 +12:00
|
|
|
let chapterContent = ma[1]
|
|
|
|
chapterContent = chapterContent.replace(/<footer>[\s\S]*?<\/footer>/g, '').trim()
|
|
|
|
|
2019-10-08 22:31:42 +13:00
|
|
|
const authorNotesPos = chapterContent.indexOf('<aside ')
|
2017-06-12 23:53:17 +12:00
|
|
|
let notesContent = ''
|
2019-10-08 22:31:42 +13:00
|
|
|
const notesFirst = authorNotesPos === 0
|
2017-06-12 23:53:17 +12:00
|
|
|
if (authorNotesPos !== -1) {
|
|
|
|
chapterContent = chapterContent.replace(/<aside class="authors-note">([\s\S]*?)<\/aside>/, (match, content, pos) => {
|
|
|
|
content = content.replace(/<header><h1>.*?<\/h1><\/header>/, '')
|
|
|
|
notesContent = content.trim().replace(trimWhitespace, '')
|
|
|
|
return ''
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
chapterContent = chapterContent.trim().replace(trimWhitespace, '')
|
2019-10-08 19:37:27 +13:00
|
|
|
const chapter = { content: chapterContent, notes: notesContent, notesFirst }
|
2018-03-15 23:33:52 +13:00
|
|
|
ch.realWordCount = utils.htmlWordCount(chapter.content)
|
|
|
|
|
2018-03-26 18:49:15 +13:00
|
|
|
p = p.then(() => cleanMarkup(chapter.content).then((content) => {
|
2017-06-12 23:53:17 +12:00
|
|
|
chapter.content = content
|
|
|
|
}))
|
2018-03-26 18:49:15 +13:00
|
|
|
if (chapter.notes) {
|
|
|
|
p = p.then(() => cleanMarkup(notesContent).then((notes) => {
|
|
|
|
if (!notes) {
|
|
|
|
console.log('error notes:', i, notesContent)
|
|
|
|
}
|
2017-06-12 23:53:17 +12:00
|
|
|
chapter.notes = notes
|
|
|
|
}))
|
|
|
|
}
|
|
|
|
p = p.then(() => {
|
2018-03-14 01:58:03 +13:00
|
|
|
this.progress(0, (i + 1) / chapterCount, 'Parsed chapter ' + (i + 1) + ' / ' + chapterCount)
|
2017-06-12 23:53:17 +12:00
|
|
|
if (chapter.notes) {
|
|
|
|
this.hasAuthorNotes = true
|
|
|
|
this.chaptersWithNotes.push(i)
|
|
|
|
}
|
|
|
|
this.chapters[i] = chapter
|
2018-03-15 23:33:52 +13:00
|
|
|
return utils.sleep(0)
|
|
|
|
})
|
2017-06-12 23:53:17 +12:00
|
|
|
}
|
|
|
|
return p
|
|
|
|
}).then(() => {
|
2018-03-16 04:14:57 +13:00
|
|
|
this.totalWordCount = this.storyInfo.chapters.reduce((count, ch) => count + ch.realWordCount, 0)
|
2017-06-12 23:53:17 +12:00
|
|
|
this.pcache.chapters = null
|
2020-03-15 06:15:02 +13:00
|
|
|
}).catch((err) => {
|
|
|
|
console.error(err)
|
2017-06-12 23:53:17 +12:00
|
|
|
})
|
|
|
|
|
2016-08-25 00:47:48 +12:00
|
|
|
return this.pcache.chapters
|
2016-08-24 09:49:27 +12:00
|
|
|
}
|
|
|
|
|
2016-08-25 00:47:48 +12:00
|
|
|
fetchRemoteFiles () {
|
2018-03-14 01:58:03 +13:00
|
|
|
if (!this.options.includeExternal || this.remoteResources.size === 0) {
|
2016-08-25 00:47:48 +12:00
|
|
|
return Promise.resolve()
|
|
|
|
}
|
|
|
|
if (this.pcache.remoteResources) {
|
|
|
|
return this.pcache.remoteResources
|
|
|
|
}
|
|
|
|
if (this.remoteResourcesCached) {
|
|
|
|
return Promise.resolve()
|
2016-08-23 07:57:19 +12:00
|
|
|
}
|
2016-06-28 19:39:31 +12:00
|
|
|
|
2019-10-08 22:31:42 +13:00
|
|
|
const checksums = new Map()
|
2018-03-16 22:55:04 +13:00
|
|
|
|
2016-08-25 00:47:48 +12:00
|
|
|
this.progress(0, 0, 'Fetching remote files...')
|
|
|
|
this.pcache.remoteResources = new Promise((resolve, reject) => {
|
2019-10-08 22:31:42 +13:00
|
|
|
const iter = this.remoteResources.entries()
|
2016-08-25 00:47:48 +12:00
|
|
|
let completeCount = 0
|
2016-08-23 19:19:01 +12:00
|
|
|
|
2019-10-08 22:31:42 +13:00
|
|
|
const next = (r) => {
|
2018-03-13 10:09:26 +13:00
|
|
|
completeCount++
|
|
|
|
if (r.data) {
|
|
|
|
this.progress(0, completeCount / this.remoteResources.size, 'Fetched remote file ' + completeCount + ' / ' + this.remoteResources.size)
|
|
|
|
} else {
|
|
|
|
this.progress(0, completeCount / this.remoteResources.size, 'Fetching remote files...')
|
|
|
|
}
|
|
|
|
recursive()
|
|
|
|
}
|
|
|
|
|
2019-10-08 22:31:42 +13:00
|
|
|
const recursive = () => {
|
2016-08-25 00:47:48 +12:00
|
|
|
let r = iter.next().value
|
|
|
|
if (!r) {
|
|
|
|
if (completeCount === this.remoteResources.size) {
|
|
|
|
resolve()
|
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
2019-10-08 22:31:42 +13:00
|
|
|
const url = r[0]
|
2016-08-25 00:47:48 +12:00
|
|
|
r = r[1]
|
2018-03-13 10:09:26 +13:00
|
|
|
if (r.data) {
|
|
|
|
next(r)
|
|
|
|
return
|
|
|
|
}
|
2016-06-28 19:39:31 +12:00
|
|
|
|
2020-08-10 20:02:17 +12:00
|
|
|
console.log('Remote file URL: ' + url)
|
|
|
|
|
2020-09-08 03:08:49 +12:00
|
|
|
fetchRemote(url, 'arraybuffer').then(async (data) => {
|
2016-08-25 00:47:48 +12:00
|
|
|
r.dest = null
|
2020-01-16 23:09:13 +13:00
|
|
|
let info = await FileType.fromBuffer(isNode ? data : new Uint8Array(data))
|
2018-03-13 10:09:26 +13:00
|
|
|
if (!info || info.mime === 'application/xml') {
|
2017-06-12 23:53:17 +12:00
|
|
|
// file-type doesn't support SVG, extra check:
|
|
|
|
if (isSvg(Buffer.from(data).toString('utf8'))) {
|
|
|
|
info = {
|
|
|
|
mime: 'image/svg+xml',
|
|
|
|
ext: 'svg'
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2016-08-25 00:47:48 +12:00
|
|
|
if (info) {
|
2019-10-08 22:31:42 +13:00
|
|
|
const checksum = crc32(isNode ? data : new Uint8Array(data))
|
2018-03-16 22:55:04 +13:00
|
|
|
if (checksums.has(checksum)) {
|
2019-10-08 22:31:42 +13:00
|
|
|
const sameFile = this.remoteResources.get(checksums.get(checksum))
|
2018-03-16 22:55:04 +13:00
|
|
|
r.dest = sameFile.dest
|
|
|
|
r.filename = sameFile.dest
|
|
|
|
r.type = sameFile.type
|
|
|
|
r.data = sameFile.data
|
|
|
|
} else {
|
|
|
|
checksums.set(checksum, url)
|
2019-10-08 22:31:42 +13:00
|
|
|
const type = info.mime
|
2018-03-16 22:55:04 +13:00
|
|
|
r.type = type
|
2019-10-08 22:31:42 +13:00
|
|
|
const isImage = type.startsWith('image/')
|
|
|
|
const folder = isImage ? 'Images' : 'Misc'
|
|
|
|
const dest = folder + '/*.' + info.ext
|
2018-03-16 22:55:04 +13:00
|
|
|
r.dest = dest.replace('*', r.filename)
|
|
|
|
r.data = data
|
2018-03-13 10:09:26 +13:00
|
|
|
}
|
2016-08-25 00:47:48 +12:00
|
|
|
}
|
2018-03-13 10:09:26 +13:00
|
|
|
next(r)
|
2018-03-16 22:55:04 +13:00
|
|
|
}).catch((err) => { console.error(err); next(r) })
|
2016-08-25 00:47:48 +12:00
|
|
|
}
|
|
|
|
|
2018-03-16 22:55:04 +13:00
|
|
|
// no concurrent downloads!
|
2016-08-25 00:47:48 +12:00
|
|
|
recursive()
|
|
|
|
}).then(() => {
|
|
|
|
this.pcache.remoteResources = null
|
|
|
|
})
|
|
|
|
return this.pcache.remoteResources
|
|
|
|
}
|
|
|
|
|
2018-03-13 10:09:26 +13:00
|
|
|
async buildPages () {
|
|
|
|
this.pages.cover = await template.createCoverPage(this)
|
|
|
|
this.pages.title = await template.createTitlePage(this)
|
|
|
|
this.findRemoteResources('titlepage', 'titlepage', this.pages.title)
|
|
|
|
this.pages.nav = await template.createNav(this)
|
|
|
|
delete this.pages.notesnav
|
|
|
|
if (this.options.includeAuthorNotes && this.options.useAuthorNotesIndex && this.hasAuthorNotes) {
|
|
|
|
this.pages.notesnav = await template.createNotesNav(this)
|
|
|
|
}
|
2018-03-27 07:32:02 +13:00
|
|
|
if (this.options.kepubify) {
|
|
|
|
this.pages.nav = kepubify(this.pages.nav)
|
|
|
|
this.pages.title = kepubify(this.pages.title)
|
|
|
|
if (this.pages.notesnav) {
|
|
|
|
this.pages.notesnav = kepubify(this.pages.notesnav)
|
|
|
|
}
|
|
|
|
}
|
2018-03-13 10:09:26 +13:00
|
|
|
}
|
|
|
|
|
2016-08-25 00:47:48 +12:00
|
|
|
buildChapters () {
|
|
|
|
let chain = Promise.resolve()
|
|
|
|
this.chaptersHtml.length = 0
|
2016-08-30 02:20:20 +12:00
|
|
|
this.notesHtml.length = 0
|
2016-08-25 00:47:48 +12:00
|
|
|
|
|
|
|
for (let i = 0; i < this.chapters.length; i++) {
|
2019-10-08 22:31:42 +13:00
|
|
|
const ch = this.storyInfo.chapters[i]
|
|
|
|
const chapter = this.chapters[i]
|
2018-03-16 05:43:11 +13:00
|
|
|
let content = chapter.content
|
2018-03-24 04:33:02 +13:00
|
|
|
|
2018-03-16 05:43:11 +13:00
|
|
|
if (this.options.typogrify) {
|
2018-03-24 04:33:02 +13:00
|
|
|
content = utils.typogrify(content)
|
2018-03-16 05:43:11 +13:00
|
|
|
}
|
|
|
|
|
2018-03-16 04:14:57 +13:00
|
|
|
chain = chain.then(template.createChapter.bind(null, this, {
|
2018-05-09 07:11:50 +12:00
|
|
|
title: ch.title,
|
2018-07-31 20:27:49 +12:00
|
|
|
showHeadings: this.options.showChapterHeadings,
|
|
|
|
showWordCount: this.options.showChapterWordCount,
|
|
|
|
showDuration: this.options.showChapterDuration,
|
2016-08-30 02:20:20 +12:00
|
|
|
link: this.options.addCommentsLink ? ch.link : null,
|
|
|
|
linkNotes: this.options.includeAuthorNotes && this.options.useAuthorNotesIndex && chapter.notes ? 'note_' + zeroFill(3, i + 1) + '.xhtml' : null,
|
2018-03-16 05:43:11 +13:00
|
|
|
content: content,
|
2020-08-20 19:26:29 +12:00
|
|
|
notes: this.options.includeAuthorNotes && !this.options.useAuthorNotesIndex ? chapter.notes : '',
|
2018-03-16 04:14:57 +13:00
|
|
|
notesFirst: chapter.notesFirst,
|
|
|
|
index: i
|
2016-08-30 02:20:20 +12:00
|
|
|
})).then((html) => {
|
2019-10-08 19:37:27 +13:00
|
|
|
this.findRemoteResources('ch_' + zeroFill(3, i + 1), { chapter: i }, html)
|
2018-03-27 07:32:02 +13:00
|
|
|
if (this.options.kepubify) {
|
|
|
|
html = kepubify(html)
|
|
|
|
}
|
2016-08-25 00:47:48 +12:00
|
|
|
this.chaptersHtml[i] = html
|
2016-08-23 07:57:19 +12:00
|
|
|
})
|
2016-08-30 02:20:20 +12:00
|
|
|
if (this.options.includeAuthorNotes && this.options.useAuthorNotesIndex && chapter.notes) {
|
2018-03-16 04:14:57 +13:00
|
|
|
chain = chain.then(template.createChapter.bind(null, this, {
|
2016-08-30 02:20:20 +12:00
|
|
|
title: 'Author\'s Note: ' + ch.title,
|
2018-07-31 20:27:49 +12:00
|
|
|
showHeadings: true,
|
2018-03-26 19:50:20 +13:00
|
|
|
content: chapter.notes,
|
|
|
|
index: i
|
2018-03-16 04:14:57 +13:00
|
|
|
}, true)).then((html) => {
|
2019-10-08 19:37:27 +13:00
|
|
|
this.findRemoteResources('note_' + zeroFill(3, i + 1), { note: i }, html)
|
2018-03-27 07:32:02 +13:00
|
|
|
if (this.options.kepubify) {
|
|
|
|
html = kepubify(html)
|
|
|
|
}
|
2016-08-30 02:20:20 +12:00
|
|
|
this.notesHtml[i] = html
|
|
|
|
})
|
|
|
|
}
|
2018-03-14 21:33:46 +13:00
|
|
|
chain = chain
|
|
|
|
.then(() => {
|
2018-03-14 22:28:36 +13:00
|
|
|
this.progress(0, ((i + 1) / this.chapters.length) * 0.99, 'Processed chapter ' + (i + 1) + ' / ' + this.chapters.length)
|
2018-03-15 23:33:52 +13:00
|
|
|
return utils.sleep(0)
|
2018-03-14 21:33:46 +13:00
|
|
|
})
|
2016-08-25 00:47:48 +12:00
|
|
|
}
|
2016-08-15 21:11:20 +12:00
|
|
|
|
2018-03-14 21:33:46 +13:00
|
|
|
chain = chain.then(async () => {
|
|
|
|
if (this.options.calculateReadingEase && !this.readingEase) {
|
|
|
|
const content = this.chapters.reduce((str, ch) => {
|
|
|
|
return str + utils.htmlToText(ch.content) + '\n\n'
|
|
|
|
}, '')
|
|
|
|
this.progress(0, 0, 'Calculating Flesch reading ease...')
|
|
|
|
this.readingEase = await utils.readingEase(
|
2018-03-14 22:28:20 +13:00
|
|
|
content, this.options.readingEaseWakeupInterval,
|
2018-03-14 21:33:46 +13:00
|
|
|
(progress) => {
|
2018-03-14 22:28:36 +13:00
|
|
|
this.progress(0, progress * 0.99, 'Calculating Flesch reading ease ' + Math.round(progress * 100) + '%')
|
2018-03-14 21:33:46 +13:00
|
|
|
}
|
|
|
|
)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
2016-08-25 00:47:48 +12:00
|
|
|
return chain
|
2016-08-23 07:57:19 +12:00
|
|
|
}
|
2016-06-28 19:39:31 +12:00
|
|
|
|
2018-03-13 10:09:26 +13:00
|
|
|
async build () {
|
2016-08-24 02:32:55 +12:00
|
|
|
this.cachedFile = null
|
|
|
|
this.zip = null
|
|
|
|
|
2016-08-25 01:57:05 +12:00
|
|
|
this.replaceRemoteResources()
|
2016-08-24 02:32:55 +12:00
|
|
|
|
2016-08-25 01:57:05 +12:00
|
|
|
this.zip = new JSZip()
|
2016-08-24 02:32:55 +12:00
|
|
|
|
2019-10-08 19:37:27 +13:00
|
|
|
this.zip.file('mimetype', 'application/epub+zip', { compression: 'STORE' })
|
2016-08-25 01:57:05 +12:00
|
|
|
this.zip.file('META-INF/container.xml', containerXml)
|
2016-08-24 02:32:55 +12:00
|
|
|
|
2018-03-13 10:09:26 +13:00
|
|
|
this.zip.file('OEBPS/content.opf', Buffer.from(await template.createOpf(this), 'utf8'))
|
2016-08-24 02:32:55 +12:00
|
|
|
|
2016-08-25 01:57:05 +12:00
|
|
|
if (this.coverImage) {
|
2018-03-13 10:09:26 +13:00
|
|
|
this.zip.file('OEBPS/' + this.coverFilename, Buffer.from(this.coverImage))
|
2016-08-25 01:57:05 +12:00
|
|
|
}
|
2019-04-11 01:34:19 +12:00
|
|
|
|
2018-03-13 10:09:26 +13:00
|
|
|
this.zip.file('OEBPS/Text/cover.xhtml', Buffer.from(this.pages.cover, 'utf8'))
|
2017-10-19 02:02:07 +13:00
|
|
|
this.zip.file('OEBPS/Styles/coverstyle.css', Buffer.from(coverstyleCss, 'utf8'))
|
2016-08-24 02:32:55 +12:00
|
|
|
|
2018-03-13 10:09:26 +13:00
|
|
|
this.zip.file('OEBPS/Text/title.xhtml', Buffer.from(this.pages.title, 'utf8'))
|
2017-10-19 02:02:07 +13:00
|
|
|
this.zip.file('OEBPS/Styles/titlestyle.css', Buffer.from(titlestyleCss, 'utf8'))
|
2016-08-24 02:32:55 +12:00
|
|
|
|
2018-03-13 10:09:26 +13:00
|
|
|
this.zip.file('OEBPS/nav.xhtml', Buffer.from(this.pages.nav, 'utf8'))
|
|
|
|
this.zip.file('OEBPS/toc.ncx', Buffer.from(await template.createNcx(this), 'utf8'))
|
|
|
|
this.zip.file('OEBPS/Styles/navstyle.css', Buffer.from(navstyleCss, 'utf8'))
|
2016-08-24 02:32:55 +12:00
|
|
|
|
2016-08-25 01:57:05 +12:00
|
|
|
for (let i = 0; i < this.chapters.length; i++) {
|
2019-10-08 22:31:42 +13:00
|
|
|
const filename = 'OEBPS/Text/chapter_' + zeroFill(3, i + 1) + '.xhtml'
|
|
|
|
const html = this.chaptersHtml[i]
|
2017-10-19 02:02:07 +13:00
|
|
|
this.zip.file(filename, Buffer.from(html, 'utf8'))
|
2016-08-25 01:57:05 +12:00
|
|
|
}
|
2016-08-24 02:32:55 +12:00
|
|
|
|
2016-08-30 02:20:20 +12:00
|
|
|
if (this.options.includeAuthorNotes && this.options.useAuthorNotesIndex && this.hasAuthorNotes) {
|
2018-03-13 10:09:26 +13:00
|
|
|
this.zip.file('OEBPS/notesnav.xhtml', Buffer.from(this.pages.notesnav, 'utf8'))
|
2016-08-30 02:20:20 +12:00
|
|
|
|
|
|
|
for (let i = 0; i < this.chapters.length; i++) {
|
|
|
|
if (!this.chapters[i].notes) continue
|
2019-10-08 22:31:42 +13:00
|
|
|
const filename = 'OEBPS/Text/note_' + zeroFill(3, i + 1) + '.xhtml'
|
|
|
|
const html = this.notesHtml[i]
|
2017-10-19 02:02:07 +13:00
|
|
|
this.zip.file(filename, Buffer.from(html, 'utf8'))
|
2016-08-30 02:20:20 +12:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-03-13 10:09:26 +13:00
|
|
|
if (this.iconsFont) {
|
|
|
|
this.zip.file('OEBPS/Fonts/fontawesome-webfont-subset.ttf', this.iconsFont)
|
|
|
|
}
|
|
|
|
|
|
|
|
this.zip.file('OEBPS/Styles/style.css', Buffer.from(
|
|
|
|
styleCss + '\n\n' + this.iconsStyle() + '\n\n' +
|
|
|
|
(paragraphsCss[this.options.paragraphStyle] || '')
|
|
|
|
, 'utf8'))
|
2016-08-24 02:32:55 +12:00
|
|
|
|
2019-10-08 22:31:42 +13:00
|
|
|
const remoteDestCache = new Set()
|
2016-08-25 01:57:05 +12:00
|
|
|
this.remoteResources.forEach((r) => {
|
2018-03-16 22:55:04 +13:00
|
|
|
if (r.dest && !remoteDestCache.has(r.dest)) {
|
2016-08-25 01:57:05 +12:00
|
|
|
this.zip.file('OEBPS/' + r.dest, r.data)
|
2018-03-16 22:55:04 +13:00
|
|
|
remoteDestCache.add(r.dest)
|
2016-08-25 01:57:05 +12:00
|
|
|
}
|
2016-08-24 02:32:55 +12:00
|
|
|
})
|
2018-03-16 22:55:04 +13:00
|
|
|
remoteDestCache.clear()
|
2016-08-24 02:32:55 +12:00
|
|
|
}
|
|
|
|
|
|
|
|
// for node, resolve a Buffer, in browser resolve a Blob
|
|
|
|
getFile () {
|
|
|
|
if (!this.zip) {
|
2017-10-19 01:59:30 +13:00
|
|
|
return Promise.reject(new Error('Not downloaded.'))
|
2016-08-24 02:32:55 +12:00
|
|
|
}
|
|
|
|
if (this.cachedFile) {
|
|
|
|
return Promise.resolve(this.cachedFile)
|
|
|
|
}
|
2018-03-16 04:14:57 +13:00
|
|
|
|
2017-10-19 02:02:07 +13:00
|
|
|
this.progress(0, 0, 'Compressing...')
|
|
|
|
|
|
|
|
let lastPercent = -1
|
2016-08-24 02:32:55 +12:00
|
|
|
|
|
|
|
return this.zip
|
|
|
|
.generateAsync({
|
|
|
|
type: isNode ? 'nodebuffer' : 'blob',
|
|
|
|
mimeType: 'application/epub+zip',
|
|
|
|
compression: 'DEFLATE',
|
2019-10-08 19:37:27 +13:00
|
|
|
compressionOptions: { level: 9 }
|
2017-10-19 02:02:07 +13:00
|
|
|
}, (metadata) => { // onUpdate
|
2019-10-08 22:31:42 +13:00
|
|
|
const currentPercent = Math.round(metadata.percent / 10) * 10
|
2017-10-19 02:02:07 +13:00
|
|
|
if (lastPercent !== currentPercent) {
|
|
|
|
lastPercent = currentPercent
|
|
|
|
this.progress(0, currentPercent / 100, 'Compressing...')
|
|
|
|
}
|
2016-08-24 02:32:55 +12:00
|
|
|
})
|
|
|
|
.then((file) => {
|
2016-08-25 00:47:48 +12:00
|
|
|
this.progress(0, 1, 'Complete!')
|
2016-08-24 02:32:55 +12:00
|
|
|
this.cachedFile = file
|
|
|
|
return file
|
2016-08-23 19:19:01 +12:00
|
|
|
})
|
2018-03-13 10:09:26 +13:00
|
|
|
.catch((err) => {
|
|
|
|
console.error(err)
|
|
|
|
})
|
2016-06-28 09:19:01 +12:00
|
|
|
}
|
|
|
|
|
2016-08-24 02:32:55 +12:00
|
|
|
// example usage: .pipe(fs.createWriteStream(filename))
|
2017-10-19 02:02:07 +13:00
|
|
|
streamFile (onUpdate) {
|
2016-08-24 02:32:55 +12:00
|
|
|
if (!this.zip) {
|
|
|
|
return null
|
|
|
|
}
|
2018-03-16 04:14:57 +13:00
|
|
|
this.progress(0, 0, 'Compressing...')
|
|
|
|
|
|
|
|
let lastPercent = -1
|
|
|
|
|
2016-08-24 02:32:55 +12:00
|
|
|
return this.zip
|
|
|
|
.generateNodeStream({
|
|
|
|
type: 'nodebuffer',
|
|
|
|
streamFiles: false,
|
|
|
|
mimeType: 'application/epub+zip',
|
|
|
|
compression: 'DEFLATE',
|
2019-10-08 19:37:27 +13:00
|
|
|
compressionOptions: { level: 9 }
|
2018-03-16 04:14:57 +13:00
|
|
|
}, (metadata) => {
|
|
|
|
if (onUpdate) onUpdate(metadata)
|
2019-10-08 22:31:42 +13:00
|
|
|
const currentPercent = Math.round(metadata.percent / 20) * 20
|
2018-03-16 04:14:57 +13:00
|
|
|
if (lastPercent !== currentPercent) {
|
|
|
|
lastPercent = currentPercent
|
|
|
|
this.progress(0, currentPercent / 100, 'Compressing...')
|
|
|
|
}
|
|
|
|
})
|
2016-08-24 02:32:55 +12:00
|
|
|
}
|
|
|
|
|
2016-08-25 00:47:48 +12:00
|
|
|
setTitle (title) {
|
|
|
|
this.storyInfo.title = title.trim()
|
|
|
|
this.filename = FimFic2Epub.getFilename(this.storyInfo)
|
|
|
|
}
|
2019-10-08 22:31:42 +13:00
|
|
|
|
2016-08-25 00:47:48 +12:00
|
|
|
setAuthorName (name) {
|
|
|
|
this.storyInfo.author.name = name.trim()
|
|
|
|
this.filename = FimFic2Epub.getFilename(this.storyInfo)
|
|
|
|
}
|
2019-10-08 22:31:42 +13:00
|
|
|
|
2020-01-16 23:09:13 +13:00
|
|
|
async setCoverImage (buffer) {
|
2019-04-11 01:31:29 +12:00
|
|
|
buffer = isNode ? buffer : Buffer.from(new Uint8Array(buffer))
|
2020-01-16 23:09:13 +13:00
|
|
|
const info = await FileType.fromBuffer(buffer)
|
2018-03-13 10:09:26 +13:00
|
|
|
if (!info || !info.mime.startsWith('image/')) {
|
2016-08-25 00:47:48 +12:00
|
|
|
throw new Error('Invalid image')
|
|
|
|
}
|
|
|
|
this.coverImage = buffer
|
|
|
|
this.coverFilename = 'Images/cover.' + info.ext
|
|
|
|
this.coverType = info.mime
|
2017-10-19 01:59:30 +13:00
|
|
|
this.coverImageDimensions = sizeOf(Buffer.from(buffer))
|
2020-01-16 23:09:13 +13:00
|
|
|
return this.coverImage
|
2016-08-25 00:47:48 +12:00
|
|
|
}
|
|
|
|
|
2016-08-24 02:32:55 +12:00
|
|
|
// Internal/private methods
|
2017-06-08 02:17:40 +12:00
|
|
|
progress (part, percent, status = '') {
|
2016-08-25 00:47:48 +12:00
|
|
|
// let parts = 6.3
|
|
|
|
// let partsize = 1 / parts
|
|
|
|
// percent = (part / parts) + percent * partsize
|
2017-06-08 02:17:40 +12:00
|
|
|
try {
|
2018-03-15 06:04:50 +13:00
|
|
|
this.emit('progress', percent, status)
|
2017-06-08 02:17:40 +12:00
|
|
|
} catch (err) {
|
|
|
|
console.error(err)
|
|
|
|
}
|
|
|
|
if (status) {
|
|
|
|
console.log(status)
|
|
|
|
}
|
2016-08-24 02:32:55 +12:00
|
|
|
}
|
|
|
|
|
|
|
|
findRemoteResources (prefix, where, html) {
|
|
|
|
let remoteCounter = 1
|
2019-10-08 22:31:42 +13:00
|
|
|
const matchUrl = /<img.*?src="([^">]*\/([^">]*?))".*?>/g
|
|
|
|
const emoticonUrl = /static\.fimfiction\.net\/images\/emoticons\/([a-z_]*)\.[a-z]*$/
|
2016-08-24 02:32:55 +12:00
|
|
|
|
|
|
|
for (let ma; (ma = matchUrl.exec(html));) {
|
2019-10-08 22:31:42 +13:00
|
|
|
const url = ma[1]
|
2021-05-25 11:42:43 +12:00
|
|
|
const cleanurl = decode(url, { level: 'xml' })
|
2016-08-24 02:32:55 +12:00
|
|
|
if (this.remoteResources.has(cleanurl)) {
|
2019-10-08 22:31:42 +13:00
|
|
|
const r = this.remoteResources.get(cleanurl)
|
2016-08-24 02:32:55 +12:00
|
|
|
if (r.where.indexOf(where) === -1) {
|
|
|
|
r.where.push(where)
|
|
|
|
}
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
let filename = prefix + '_' + remoteCounter
|
2019-10-08 22:31:42 +13:00
|
|
|
const emoticon = url.match(emoticonUrl)
|
2016-08-24 02:32:55 +12:00
|
|
|
if (emoticon) {
|
|
|
|
filename = 'emoticon_' + emoticon[1]
|
|
|
|
}
|
|
|
|
remoteCounter++
|
2019-10-08 19:37:27 +13:00
|
|
|
this.remoteResources.set(cleanurl, { filename: filename, where: [where], originalUrl: url })
|
2016-08-24 02:32:55 +12:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-03-13 10:09:26 +13:00
|
|
|
async findIcons () {
|
2019-10-08 22:31:42 +13:00
|
|
|
const matchIcon = /<i class="fa fa-fw fa-(.*?)"/g
|
2018-03-13 10:09:26 +13:00
|
|
|
this.usedIcons.clear()
|
|
|
|
|
|
|
|
const scan = (html) => {
|
|
|
|
if (!html) return
|
|
|
|
for (let ma; (ma = matchIcon.exec(html));) {
|
|
|
|
if (ma[1] in fontAwesomeCodes) {
|
|
|
|
this.usedIcons.add(ma[1])
|
|
|
|
} else {
|
|
|
|
console.warn('Unknown icon:', ma[1])
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
scan(this.pages.title)
|
|
|
|
this.chaptersHtml.forEach(scan)
|
|
|
|
this.notesHtml.forEach(scan)
|
|
|
|
|
|
|
|
if (this.usedIcons.size === 0) {
|
|
|
|
this.iconsFont = null
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2019-10-08 22:31:42 +13:00
|
|
|
const glyphs = [...this.usedIcons].map((name) => {
|
2018-03-13 10:09:26 +13:00
|
|
|
return fontAwesomeCodes[name].charCodeAt(0)
|
|
|
|
})
|
2019-10-08 22:31:42 +13:00
|
|
|
const fontFile = require('font-awesome/fonts/fontawesome-webfont.ttf')
|
2019-10-08 19:37:27 +13:00
|
|
|
this.iconsFont = await subsetFont(fontFile, glyphs, { local: isNode })
|
2018-03-13 10:09:26 +13:00
|
|
|
}
|
|
|
|
|
|
|
|
iconsStyle () {
|
|
|
|
if (this.usedIcons.size === 0) return ''
|
|
|
|
let style = iconsCss.trim() + '\n'
|
|
|
|
this.usedIcons.forEach((name) => {
|
|
|
|
style += '.fa-' + name + ':before { content: "\\' + fontAwesomeCodes[name].charCodeAt(0).toString(16) + '"; }\n'
|
|
|
|
})
|
|
|
|
return style
|
|
|
|
}
|
|
|
|
|
2016-08-24 02:32:55 +12:00
|
|
|
fetchCoverImage () {
|
2016-08-25 00:47:48 +12:00
|
|
|
if (this.pcache.coverImage) {
|
|
|
|
return this.pcache.coverImage
|
|
|
|
}
|
2019-04-11 01:34:19 +12:00
|
|
|
|
2016-08-24 02:32:55 +12:00
|
|
|
if (this.coverImage) {
|
2016-08-25 00:47:48 +12:00
|
|
|
return Promise.resolve(this.coverImage)
|
2016-08-24 02:32:55 +12:00
|
|
|
}
|
|
|
|
this.coverImage = null
|
2019-10-08 22:31:42 +13:00
|
|
|
const url = this.coverUrl || this.storyInfo.full_image
|
2016-08-24 02:32:55 +12:00
|
|
|
if (!url) {
|
2019-04-11 01:34:19 +12:00
|
|
|
console.warn('Story has no image. Generating one...')
|
|
|
|
let canvas
|
|
|
|
if (isNode) {
|
|
|
|
canvas = require('canvas').createCanvas(1080, 1440)
|
|
|
|
} else {
|
|
|
|
canvas = document.createElement('canvas')
|
|
|
|
canvas.width = 1080
|
|
|
|
canvas.height = 1440
|
|
|
|
}
|
2019-10-08 22:31:42 +13:00
|
|
|
const ctx = canvas.getContext('2d')
|
2019-04-11 01:34:19 +12:00
|
|
|
ctx.fillStyle = 'white'
|
|
|
|
ctx.fillRect(0, 0, canvas.width, canvas.height)
|
|
|
|
|
|
|
|
ctx.fillStyle = 'black'
|
|
|
|
ctx.textAlign = 'center'
|
|
|
|
ctx.strokeStyle = 'black'
|
|
|
|
|
|
|
|
ctx.strokeRect(20, 20, canvas.width - 40, canvas.height - 40)
|
|
|
|
ctx.strokeRect(12, 12, canvas.width - 24, canvas.height - 24)
|
|
|
|
|
2019-10-08 22:31:42 +13:00
|
|
|
const title = this.storyInfo.title
|
|
|
|
const author = this.storyInfo.author.name
|
2019-04-11 01:34:19 +12:00
|
|
|
|
|
|
|
let fontSize = 150
|
|
|
|
let width
|
|
|
|
do {
|
2019-10-08 19:37:27 +13:00
|
|
|
ctx.font = 'bold ' + fontSize + 'px sans-serif'
|
2019-04-11 01:34:19 +12:00
|
|
|
width = ctx.measureText(title).width
|
|
|
|
fontSize -= 5
|
|
|
|
} while (width > canvas.width * 0.85)
|
|
|
|
|
|
|
|
ctx.fillText(title, canvas.width / 2, canvas.height * 0.2)
|
|
|
|
fontSize = 75
|
|
|
|
do {
|
2019-10-08 19:37:27 +13:00
|
|
|
ctx.font = fontSize + 'px sans-serif'
|
2019-04-11 01:34:19 +12:00
|
|
|
width = ctx.measureText(author).width
|
|
|
|
fontSize -= 5
|
|
|
|
} while (width > canvas.width * 0.7)
|
|
|
|
|
|
|
|
ctx.fillText(author, canvas.width / 2, canvas.height * 0.9)
|
|
|
|
|
2020-01-16 23:09:13 +13:00
|
|
|
return this.setCoverImage(Buffer.from(canvas.toDataURL('image/jpeg').split(',')[1], 'base64'))
|
2016-08-24 02:32:55 +12:00
|
|
|
}
|
|
|
|
|
2016-08-25 00:47:48 +12:00
|
|
|
this.progress(0, 0, 'Fetching cover image...')
|
2016-08-24 02:32:55 +12:00
|
|
|
|
2020-09-08 03:08:49 +12:00
|
|
|
this.pcache.coverImage = fetchRemote(url, 'arraybuffer').then(async (data) => {
|
2018-03-13 10:09:26 +13:00
|
|
|
data = isNode ? data : new Uint8Array(data)
|
2020-01-16 23:09:13 +13:00
|
|
|
const info = await FileType.fromBuffer(data)
|
2016-08-24 02:32:55 +12:00
|
|
|
if (info) {
|
2019-10-08 22:31:42 +13:00
|
|
|
const type = info.mime
|
|
|
|
const isImage = type.startsWith('image/')
|
2016-08-24 02:32:55 +12:00
|
|
|
if (!isImage) {
|
|
|
|
return null
|
|
|
|
}
|
2019-10-08 22:31:42 +13:00
|
|
|
const filename = 'Images/cover.' + info.ext
|
2016-08-24 02:32:55 +12:00
|
|
|
this.coverFilename = filename
|
|
|
|
this.coverType = type
|
|
|
|
|
2017-10-19 01:59:30 +13:00
|
|
|
this.coverImageDimensions = sizeOf(Buffer.from(data))
|
2016-08-24 02:32:55 +12:00
|
|
|
this.coverImage = data
|
|
|
|
this.coverFilename = filename
|
|
|
|
return this.coverImage
|
|
|
|
} else {
|
|
|
|
return null
|
|
|
|
}
|
2019-04-11 01:34:19 +12:00
|
|
|
}).then((data) => {
|
2016-08-25 00:47:48 +12:00
|
|
|
this.pcache.coverImage = null
|
2019-04-11 01:34:19 +12:00
|
|
|
return data
|
2016-08-24 02:32:55 +12:00
|
|
|
})
|
2016-08-25 00:47:48 +12:00
|
|
|
return this.pcache.coverImage
|
2016-08-24 02:32:55 +12:00
|
|
|
}
|
|
|
|
|
2016-08-23 07:57:19 +12:00
|
|
|
fetchTitlePage () {
|
2018-03-13 10:09:26 +13:00
|
|
|
let viewMature = true
|
2019-10-08 22:31:42 +13:00
|
|
|
const isStoryMature = this.storyInfo.content_rating === 2
|
2018-03-13 10:09:26 +13:00
|
|
|
if (!isNode) {
|
|
|
|
viewMature = document.cookie.split('; ').includes('view_mature=true')
|
|
|
|
if (!viewMature && isStoryMature) {
|
|
|
|
if (window.setCookie) {
|
|
|
|
window.setCookie('view_mature', true, 365)
|
|
|
|
} else {
|
|
|
|
document.cookie = 'view_mature=true; path=/'
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return fetch(this.storyInfo.url).then((data) => {
|
|
|
|
if (!viewMature && isStoryMature) {
|
|
|
|
// Delete cookie
|
|
|
|
document.cookie = 'view_mature=false; path=/; Max-Age=0'
|
|
|
|
}
|
|
|
|
return data
|
|
|
|
}).then(this.extractTitlePageInfo.bind(this))
|
2016-06-28 09:19:01 +12:00
|
|
|
}
|
|
|
|
|
2016-06-28 19:39:31 +12:00
|
|
|
extractTitlePageInfo (html) {
|
2017-06-07 08:15:05 +12:00
|
|
|
let startTagsPos = html.indexOf('<div class="story_content_box"')
|
|
|
|
startTagsPos += html.substring(startTagsPos).indexOf('<ul class="story-tags">') + 23
|
|
|
|
let tagsHtml = html.substring(startTagsPos)
|
|
|
|
|
2019-10-08 22:31:42 +13:00
|
|
|
const endTagsPos = tagsHtml.indexOf('</ul>')
|
2017-06-07 08:15:05 +12:00
|
|
|
tagsHtml = tagsHtml.substring(0, endTagsPos)
|
2016-08-23 19:19:01 +12:00
|
|
|
|
2019-10-08 22:31:42 +13:00
|
|
|
const tags = []
|
2018-03-13 10:09:26 +13:00
|
|
|
let c
|
2017-06-07 08:15:05 +12:00
|
|
|
tags.byImage = {}
|
2016-08-25 06:18:01 +12:00
|
|
|
this.subjects.length = 0
|
2016-08-24 08:04:38 +12:00
|
|
|
this.subjects.push('Fimfiction')
|
2016-08-25 06:18:01 +12:00
|
|
|
this.subjects.push(this.storyInfo.content_rating_text)
|
2018-03-13 10:09:26 +13:00
|
|
|
|
2019-10-08 22:31:42 +13:00
|
|
|
const matchTag = /<a href="([^"]*?)" class="([^"]*?)" title="[^"]*?" data-tag="([^"]*?)".*?>(.*?)<\/a>/g
|
2018-03-13 10:09:26 +13:00
|
|
|
for (;(c = matchTag.exec(tagsHtml));) {
|
2019-10-08 22:31:42 +13:00
|
|
|
const cat = {
|
2020-09-07 23:40:21 +12:00
|
|
|
url: 'https://www.fimfiction.net' + c[1],
|
2018-03-13 10:09:26 +13:00
|
|
|
className: 'story-tag ' + c[2],
|
2021-05-25 11:42:43 +12:00
|
|
|
name: decode(c[4], { level: 'xml' }),
|
2018-03-13 10:09:26 +13:00
|
|
|
type: c[2].replace('tag-', '')
|
2017-06-07 18:02:43 +12:00
|
|
|
}
|
2018-03-13 10:09:26 +13:00
|
|
|
tags.push(cat)
|
2017-06-07 18:02:43 +12:00
|
|
|
this.subjects.push(cat.name)
|
|
|
|
}
|
2018-03-13 10:09:26 +13:00
|
|
|
this.tags = tags
|
2016-08-23 19:19:01 +12:00
|
|
|
|
2017-06-07 08:15:05 +12:00
|
|
|
html = html.substring(endTagsPos + 5)
|
|
|
|
html = html.substring(html.indexOf('<span class="description-text bbcode">') + 38)
|
|
|
|
|
|
|
|
let ma = html.match(/This story is a sequel to <a href="([^"]*)">(.*?)<\/a>/)
|
2016-08-23 19:19:01 +12:00
|
|
|
if (ma) {
|
|
|
|
this.storyInfo.prequel = {
|
2020-09-07 23:40:21 +12:00
|
|
|
url: 'https://www.fimfiction.net' + ma[1],
|
2021-05-25 11:42:43 +12:00
|
|
|
title: decode(ma[2], { level: 'xml' })
|
2016-06-28 19:39:31 +12:00
|
|
|
}
|
2016-08-23 19:19:01 +12:00
|
|
|
html = html.substring(html.indexOf('<hr />') + 6)
|
|
|
|
}
|
2017-06-07 08:15:05 +12:00
|
|
|
|
2019-10-08 22:31:42 +13:00
|
|
|
const endDescPos = html.indexOf('</span>\n')
|
|
|
|
const description = html.substring(0, endDescPos).trim()
|
2016-08-24 02:32:55 +12:00
|
|
|
this.description = description
|
2016-08-23 19:19:01 +12:00
|
|
|
|
|
|
|
html = html.substring(endDescPos + 7)
|
2019-10-08 22:31:42 +13:00
|
|
|
const extraPos = html.indexOf('<div class="extra_story_data">')
|
2016-08-23 19:19:01 +12:00
|
|
|
html = html.substring(extraPos + 30)
|
|
|
|
|
2018-03-13 23:10:54 +13:00
|
|
|
ma = html.match(/<span class="approved-date">.*?data-time="(.*?)".*?<\/span>/)
|
2016-08-23 19:19:01 +12:00
|
|
|
if (ma) {
|
2018-03-13 23:10:54 +13:00
|
|
|
this.storyInfo.publishDate = +ma[1]
|
2016-08-23 19:19:01 +12:00
|
|
|
}
|
2016-06-28 09:19:01 +12:00
|
|
|
|
2016-08-23 19:19:01 +12:00
|
|
|
html = html.substring(0, html.indexOf('<div class="button-group"'))
|
|
|
|
}
|
2016-06-28 09:19:01 +12:00
|
|
|
|
2016-08-24 09:49:27 +12:00
|
|
|
parseChapterPage (html) {
|
2019-10-08 22:31:42 +13:00
|
|
|
const trimWhitespace = /^\s*(<br\s*\/?\s*>)+|(<br\s*\/?\s*>)+\s*$/ig
|
2016-08-24 09:49:27 +12:00
|
|
|
|
|
|
|
let authorNotesPos = html.indexOf('<div class="authors-note"')
|
|
|
|
let authorNotes = ''
|
2016-08-25 00:47:48 +12:00
|
|
|
if (authorNotesPos !== -1) {
|
2016-08-24 09:49:27 +12:00
|
|
|
authorNotesPos = authorNotesPos + html.substring(authorNotesPos).indexOf('<b>Author\'s Note:</b>')
|
|
|
|
authorNotes = html.substring(authorNotesPos + 22)
|
|
|
|
authorNotes = authorNotes.substring(0, authorNotes.indexOf('\t\n\t</div>'))
|
|
|
|
authorNotes = authorNotes.trim()
|
|
|
|
authorNotes = authorNotes.replace(trimWhitespace, '')
|
|
|
|
}
|
|
|
|
|
2019-10-08 22:31:42 +13:00
|
|
|
const chapterPos = html.indexOf('<div class="bbcode">')
|
2017-06-07 08:15:05 +12:00
|
|
|
let chapter = html.substring(chapterPos + 20)
|
2016-08-24 09:49:27 +12:00
|
|
|
|
2019-10-08 22:31:42 +13:00
|
|
|
const pos = chapter.indexOf('\t\t</div>\n\t</div>\t\t\n\t\t\t\t\t</div>\n')
|
2016-08-24 09:49:27 +12:00
|
|
|
|
|
|
|
chapter = chapter.substring(0, pos).trim()
|
|
|
|
|
|
|
|
// remove leading and trailing <br /> tags and whitespace
|
|
|
|
chapter = chapter.replace(trimWhitespace, '')
|
2019-10-08 19:37:27 +13:00
|
|
|
return { content: chapter, notes: authorNotes, notesFirst: authorNotesPos < chapterPos }
|
2016-08-24 09:49:27 +12:00
|
|
|
}
|
|
|
|
|
2016-08-25 00:47:48 +12:00
|
|
|
replaceRemoteResources () {
|
2018-03-16 21:32:45 +13:00
|
|
|
if (this.remoteResources.size === 0) return
|
2016-08-25 00:47:48 +12:00
|
|
|
if (!this.options.includeExternal) {
|
|
|
|
this.remoteResources.forEach((r, url) => {
|
|
|
|
if (r.originalUrl && r.where) {
|
2019-10-08 22:31:42 +13:00
|
|
|
const ourl = new RegExp(escapeStringRegexp(r.originalUrl), 'g')
|
2016-08-25 00:47:48 +12:00
|
|
|
for (var i = 0; i < r.where.length; i++) {
|
2019-10-08 22:31:42 +13:00
|
|
|
const w = r.where[i]
|
2016-08-25 00:47:48 +12:00
|
|
|
if (typeof w === 'number') {
|
|
|
|
if (ourl.test(this.chapters[w])) {
|
|
|
|
this.storyInfo.chapters[w].remote = true
|
|
|
|
}
|
2018-03-13 10:09:26 +13:00
|
|
|
} else if (w === 'titlepage') {
|
|
|
|
if (ourl.test(this.pages.title)) {
|
2016-08-25 00:47:48 +12:00
|
|
|
this.hasRemoteResources.titlePage = true
|
|
|
|
}
|
2016-08-24 02:32:55 +12:00
|
|
|
}
|
2016-08-23 19:19:01 +12:00
|
|
|
}
|
2016-08-23 07:57:19 +12:00
|
|
|
}
|
2016-08-25 00:47:48 +12:00
|
|
|
})
|
|
|
|
} else {
|
|
|
|
this.remoteResources.forEach((r, url) => {
|
|
|
|
if (r.dest && r.originalUrl && r.where) {
|
2019-10-08 22:31:42 +13:00
|
|
|
const dest = '../' + r.dest
|
|
|
|
const ourl = new RegExp(escapeStringRegexp(r.originalUrl), 'g')
|
2016-08-25 00:47:48 +12:00
|
|
|
for (var i = 0; i < r.where.length; i++) {
|
2019-10-08 22:31:42 +13:00
|
|
|
const w = r.where[i]
|
2016-08-30 02:20:20 +12:00
|
|
|
if (typeof w === 'object' && w.chapter !== undefined && this.chaptersHtml[w.chapter]) {
|
|
|
|
this.chaptersHtml[w.chapter] = this.chaptersHtml[w.chapter].replace(ourl, dest)
|
|
|
|
} else if (typeof w === 'object' && w.note !== undefined && this.notesHtml[w.note]) {
|
|
|
|
this.notesHtml[w.note] = this.notesHtml[w.note].replace(ourl, dest)
|
2018-03-13 10:09:26 +13:00
|
|
|
} else if (w === 'titlepage') {
|
|
|
|
this.pages.title = this.pages.title.replace(ourl, dest)
|
2016-08-25 00:47:48 +12:00
|
|
|
}
|
2016-08-24 08:04:38 +12:00
|
|
|
}
|
2018-03-16 22:55:04 +13:00
|
|
|
} else {
|
|
|
|
console.log('bad remote', r, url)
|
2016-08-24 08:04:38 +12:00
|
|
|
}
|
2016-08-25 00:47:48 +12:00
|
|
|
})
|
|
|
|
}
|
2016-08-24 08:04:38 +12:00
|
|
|
}
|
2016-06-28 09:19:01 +12:00
|
|
|
}
|
2016-08-24 08:04:38 +12:00
|
|
|
|
2019-04-11 01:31:29 +12:00
|
|
|
export default FimFic2Epub
|