This commit is contained in:
daniel-j 2019-10-08 11:31:42 +02:00
parent 19194dea31
commit 9a2e60c1c9
15 changed files with 175 additions and 174 deletions

View file

@ -33,7 +33,7 @@ if (isStandalone) {
webpackConfig.pop() webpackConfig.pop()
} }
let watchOpts = { const watchOpts = {
readDelay: 500, readDelay: 500,
verbose: true, verbose: true,
read: false read: false
@ -41,12 +41,12 @@ let watchOpts = {
let packageVersion = require('./package.json').version let packageVersion = require('./package.json').version
let webpackDefines = new webpack.DefinePlugin({ const webpackDefines = new webpack.DefinePlugin({
FIMFIC2EPUB_VERSION: JSON.stringify(packageVersion) FIMFIC2EPUB_VERSION: JSON.stringify(packageVersion)
}) })
// No need to bloat the build with a list of all tlds... // No need to bloat the build with a list of all tlds...
let replaceTlds = new webpack.NormalModuleReplacementPlugin(/^tlds$/, '../../src/false') const replaceTlds = new webpack.NormalModuleReplacementPlugin(/^tlds$/, '../../src/false')
webpackConfig.forEach((c) => { webpackConfig.forEach((c) => {
c.plugins.push(webpackDefines) c.plugins.push(webpackDefines)
@ -89,8 +89,8 @@ function webpackTask () {
} }
function convertFontAwesomeVars (contents) { function convertFontAwesomeVars (contents) {
let vars = {} const vars = {}
let matchVar = /\$fa-var-(.*?): "\\(.*?)";/g const matchVar = /\$fa-var-(.*?): "\\(.*?)";/g
let ma let ma
for (;(ma = matchVar.exec(contents));) { for (;(ma = matchVar.exec(contents));) {
vars[ma[1]] = String.fromCharCode(parseInt(ma[2], 16)) vars[ma[1]] = String.fromCharCode(parseInt(ma[2], 16))

View file

@ -9,7 +9,7 @@
"url": "https://github.com/daniel-j/fimfic2epub.git" "url": "https://github.com/daniel-j/fimfic2epub.git"
}, },
"scripts": { "scripts": {
"build": "NODE_ENV=production gulp", "build": "gulp",
"dev": "NODE_ENV=development gulp" "dev": "NODE_ENV=development gulp"
}, },
"bin": { "bin": {

View file

@ -31,9 +31,9 @@ const trimWhitespace = /^\s*(<br\s*\/?\s*>)+|(<br\s*\/?\s*>)+\s*$/ig
class FimFic2Epub extends EventEmitter { class FimFic2Epub extends EventEmitter {
static getStoryId (id) { static getStoryId (id) {
if (isNaN(id)) { if (isNaN(id)) {
let url = URL(id) const url = URL(id)
if (url.hostname === 'www.fimfiction.net' || url.hostname === 'fimfiction.net') { if (url.hostname === 'www.fimfiction.net' || url.hostname === 'fimfiction.net') {
let m = url.pathname.match(/^\/story\/(\d+)/) const m = url.pathname.match(/^\/story\/(\d+)/)
if (m) { if (m) {
id = m[1] id = m[1]
} }
@ -49,7 +49,7 @@ class FimFic2Epub extends EventEmitter {
static fetchStoryInfo (storyId, raw = false) { static fetchStoryInfo (storyId, raw = false) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
storyId = FimFic2Epub.getStoryId(storyId) storyId = FimFic2Epub.getStoryId(storyId)
let url = '/api/story.php?story=' + storyId const url = '/api/story.php?story=' + storyId
fetch(url).then((content) => { fetch(url).then((content) => {
let data let data
try { try {
@ -63,7 +63,7 @@ class FimFic2Epub extends EventEmitter {
reject(new Error(data.error + ' (id: ' + storyId + ')')) reject(new Error(data.error + ' (id: ' + storyId + ')'))
return return
} }
let story = data.story const story = data.story
if (raw) { if (raw) {
resolve(story) resolve(story)
return return
@ -223,20 +223,20 @@ class FimFic2Epub extends EventEmitter {
this.progress(0, 0, 'Fetching chapters...') this.progress(0, 0, 'Fetching chapters...')
let chapterCount = this.storyInfo.chapters.length const chapterCount = this.storyInfo.chapters.length
let url = 'https://fimfiction.net/story/download/' + this.storyInfo.id + '/html' const url = 'https://fimfiction.net/story/download/' + this.storyInfo.id + '/html'
this.pcache.chapters = fetch(url).then((html) => { this.pcache.chapters = fetch(url).then((html) => {
let p = Promise.resolve() let p = Promise.resolve()
let matchChapter = /<article class="chapter">[\s\S]*?<\/header>([\s\S]*?)<\/article>/g const matchChapter = /<article class="chapter">[\s\S]*?<\/header>([\s\S]*?)<\/article>/g
for (let ma, i = 0; (ma = matchChapter.exec(html)); i++) { for (let ma, i = 0; (ma = matchChapter.exec(html)); i++) {
const ch = this.storyInfo.chapters[i] const ch = this.storyInfo.chapters[i]
let chapterContent = ma[1] let chapterContent = ma[1]
chapterContent = chapterContent.replace(/<footer>[\s\S]*?<\/footer>/g, '').trim() chapterContent = chapterContent.replace(/<footer>[\s\S]*?<\/footer>/g, '').trim()
let authorNotesPos = chapterContent.indexOf('<aside ') const authorNotesPos = chapterContent.indexOf('<aside ')
let notesContent = '' let notesContent = ''
let notesFirst = authorNotesPos === 0 const notesFirst = authorNotesPos === 0
if (authorNotesPos !== -1) { if (authorNotesPos !== -1) {
chapterContent = chapterContent.replace(/<aside class="authors-note">([\s\S]*?)<\/aside>/, (match, content, pos) => { chapterContent = chapterContent.replace(/<aside class="authors-note">([\s\S]*?)<\/aside>/, (match, content, pos) => {
content = content.replace(/<header><h1>.*?<\/h1><\/header>/, '') content = content.replace(/<header><h1>.*?<\/h1><\/header>/, '')
@ -290,14 +290,14 @@ class FimFic2Epub extends EventEmitter {
return Promise.resolve() return Promise.resolve()
} }
let checksums = new Map() const checksums = new Map()
this.progress(0, 0, 'Fetching remote files...') this.progress(0, 0, 'Fetching remote files...')
this.pcache.remoteResources = new Promise((resolve, reject) => { this.pcache.remoteResources = new Promise((resolve, reject) => {
let iter = this.remoteResources.entries() const iter = this.remoteResources.entries()
let completeCount = 0 let completeCount = 0
let next = (r) => { const next = (r) => {
completeCount++ completeCount++
if (r.data) { if (r.data) {
this.progress(0, completeCount / this.remoteResources.size, 'Fetched remote file ' + completeCount + ' / ' + this.remoteResources.size) this.progress(0, completeCount / this.remoteResources.size, 'Fetched remote file ' + completeCount + ' / ' + this.remoteResources.size)
@ -307,7 +307,7 @@ class FimFic2Epub extends EventEmitter {
recursive() recursive()
} }
let recursive = () => { const recursive = () => {
let r = iter.next().value let r = iter.next().value
if (!r) { if (!r) {
if (completeCount === this.remoteResources.size) { if (completeCount === this.remoteResources.size) {
@ -315,7 +315,7 @@ class FimFic2Epub extends EventEmitter {
} }
return return
} }
let url = r[0] const url = r[0]
r = r[1] r = r[1]
if (r.data) { if (r.data) {
next(r) next(r)
@ -335,9 +335,9 @@ class FimFic2Epub extends EventEmitter {
} }
} }
if (info) { if (info) {
let checksum = crc32(isNode ? data : new Uint8Array(data)) const checksum = crc32(isNode ? data : new Uint8Array(data))
if (checksums.has(checksum)) { if (checksums.has(checksum)) {
let sameFile = this.remoteResources.get(checksums.get(checksum)) const sameFile = this.remoteResources.get(checksums.get(checksum))
r.dest = sameFile.dest r.dest = sameFile.dest
r.filename = sameFile.dest r.filename = sameFile.dest
r.type = sameFile.type r.type = sameFile.type
@ -348,11 +348,11 @@ class FimFic2Epub extends EventEmitter {
data = await utils.webp2png(isNode ? data : new Uint8Array(data)) data = await utils.webp2png(isNode ? data : new Uint8Array(data))
info = fileType(data) info = fileType(data)
} }
let type = info.mime const type = info.mime
r.type = type r.type = type
let isImage = type.startsWith('image/') const isImage = type.startsWith('image/')
let folder = isImage ? 'Images' : 'Misc' const folder = isImage ? 'Images' : 'Misc'
let dest = folder + '/*.' + info.ext const dest = folder + '/*.' + info.ext
r.dest = dest.replace('*', r.filename) r.dest = dest.replace('*', r.filename)
r.data = data r.data = data
} }
@ -393,8 +393,8 @@ class FimFic2Epub extends EventEmitter {
this.notesHtml.length = 0 this.notesHtml.length = 0
for (let i = 0; i < this.chapters.length; i++) { for (let i = 0; i < this.chapters.length; i++) {
let ch = this.storyInfo.chapters[i] const ch = this.storyInfo.chapters[i]
let chapter = this.chapters[i] const chapter = this.chapters[i]
let content = chapter.content let content = chapter.content
if (this.options.typogrify) { if (this.options.typogrify) {
@ -486,8 +486,8 @@ class FimFic2Epub extends EventEmitter {
this.zip.file('OEBPS/Styles/navstyle.css', Buffer.from(navstyleCss, 'utf8')) this.zip.file('OEBPS/Styles/navstyle.css', Buffer.from(navstyleCss, 'utf8'))
for (let i = 0; i < this.chapters.length; i++) { for (let i = 0; i < this.chapters.length; i++) {
let filename = 'OEBPS/Text/chapter_' + zeroFill(3, i + 1) + '.xhtml' const filename = 'OEBPS/Text/chapter_' + zeroFill(3, i + 1) + '.xhtml'
let html = this.chaptersHtml[i] const html = this.chaptersHtml[i]
this.zip.file(filename, Buffer.from(html, 'utf8')) this.zip.file(filename, Buffer.from(html, 'utf8'))
} }
@ -496,8 +496,8 @@ class FimFic2Epub extends EventEmitter {
for (let i = 0; i < this.chapters.length; i++) { for (let i = 0; i < this.chapters.length; i++) {
if (!this.chapters[i].notes) continue if (!this.chapters[i].notes) continue
let filename = 'OEBPS/Text/note_' + zeroFill(3, i + 1) + '.xhtml' const filename = 'OEBPS/Text/note_' + zeroFill(3, i + 1) + '.xhtml'
let html = this.notesHtml[i] const html = this.notesHtml[i]
this.zip.file(filename, Buffer.from(html, 'utf8')) this.zip.file(filename, Buffer.from(html, 'utf8'))
} }
} }
@ -511,7 +511,7 @@ class FimFic2Epub extends EventEmitter {
(paragraphsCss[this.options.paragraphStyle] || '') (paragraphsCss[this.options.paragraphStyle] || '')
, 'utf8')) , 'utf8'))
let remoteDestCache = new Set() const remoteDestCache = new Set()
this.remoteResources.forEach((r) => { this.remoteResources.forEach((r) => {
if (r.dest && !remoteDestCache.has(r.dest)) { if (r.dest && !remoteDestCache.has(r.dest)) {
this.zip.file('OEBPS/' + r.dest, r.data) this.zip.file('OEBPS/' + r.dest, r.data)
@ -541,7 +541,7 @@ class FimFic2Epub extends EventEmitter {
compression: 'DEFLATE', compression: 'DEFLATE',
compressionOptions: { level: 9 } compressionOptions: { level: 9 }
}, (metadata) => { // onUpdate }, (metadata) => { // onUpdate
let currentPercent = Math.round(metadata.percent / 10) * 10 const currentPercent = Math.round(metadata.percent / 10) * 10
if (lastPercent !== currentPercent) { if (lastPercent !== currentPercent) {
lastPercent = currentPercent lastPercent = currentPercent
this.progress(0, currentPercent / 100, 'Compressing...') this.progress(0, currentPercent / 100, 'Compressing...')
@ -575,7 +575,7 @@ class FimFic2Epub extends EventEmitter {
compressionOptions: { level: 9 } compressionOptions: { level: 9 }
}, (metadata) => { }, (metadata) => {
if (onUpdate) onUpdate(metadata) if (onUpdate) onUpdate(metadata)
let currentPercent = Math.round(metadata.percent / 20) * 20 const currentPercent = Math.round(metadata.percent / 20) * 20
if (lastPercent !== currentPercent) { if (lastPercent !== currentPercent) {
lastPercent = currentPercent lastPercent = currentPercent
this.progress(0, currentPercent / 100, 'Compressing...') this.progress(0, currentPercent / 100, 'Compressing...')
@ -587,13 +587,15 @@ class FimFic2Epub extends EventEmitter {
this.storyInfo.title = title.trim() this.storyInfo.title = title.trim()
this.filename = FimFic2Epub.getFilename(this.storyInfo) this.filename = FimFic2Epub.getFilename(this.storyInfo)
} }
setAuthorName (name) { setAuthorName (name) {
this.storyInfo.author.name = name.trim() this.storyInfo.author.name = name.trim()
this.filename = FimFic2Epub.getFilename(this.storyInfo) this.filename = FimFic2Epub.getFilename(this.storyInfo)
} }
setCoverImage (buffer) { setCoverImage (buffer) {
buffer = isNode ? buffer : Buffer.from(new Uint8Array(buffer)) buffer = isNode ? buffer : Buffer.from(new Uint8Array(buffer))
let info = fileType(buffer) const info = fileType(buffer)
if (!info || !info.mime.startsWith('image/')) { if (!info || !info.mime.startsWith('image/')) {
throw new Error('Invalid image') throw new Error('Invalid image')
} }
@ -620,21 +622,21 @@ class FimFic2Epub extends EventEmitter {
findRemoteResources (prefix, where, html) { findRemoteResources (prefix, where, html) {
let remoteCounter = 1 let remoteCounter = 1
let matchUrl = /<img.*?src="([^">]*\/([^">]*?))".*?>/g const matchUrl = /<img.*?src="([^">]*\/([^">]*?))".*?>/g
let emoticonUrl = /static\.fimfiction\.net\/images\/emoticons\/([a-z_]*)\.[a-z]*$/ const emoticonUrl = /static\.fimfiction\.net\/images\/emoticons\/([a-z_]*)\.[a-z]*$/
for (let ma; (ma = matchUrl.exec(html));) { for (let ma; (ma = matchUrl.exec(html));) {
let url = ma[1] const url = ma[1]
let cleanurl = entities.decode(url) const cleanurl = entities.decode(url)
if (this.remoteResources.has(cleanurl)) { if (this.remoteResources.has(cleanurl)) {
let r = this.remoteResources.get(cleanurl) const r = this.remoteResources.get(cleanurl)
if (r.where.indexOf(where) === -1) { if (r.where.indexOf(where) === -1) {
r.where.push(where) r.where.push(where)
} }
continue continue
} }
let filename = prefix + '_' + remoteCounter let filename = prefix + '_' + remoteCounter
let emoticon = url.match(emoticonUrl) const emoticon = url.match(emoticonUrl)
if (emoticon) { if (emoticon) {
filename = 'emoticon_' + emoticon[1] filename = 'emoticon_' + emoticon[1]
} }
@ -644,7 +646,7 @@ class FimFic2Epub extends EventEmitter {
} }
async findIcons () { async findIcons () {
let matchIcon = /<i class="fa fa-fw fa-(.*?)"/g const matchIcon = /<i class="fa fa-fw fa-(.*?)"/g
this.usedIcons.clear() this.usedIcons.clear()
const scan = (html) => { const scan = (html) => {
@ -667,10 +669,10 @@ class FimFic2Epub extends EventEmitter {
return return
} }
let glyphs = [...this.usedIcons].map((name) => { const glyphs = [...this.usedIcons].map((name) => {
return fontAwesomeCodes[name].charCodeAt(0) return fontAwesomeCodes[name].charCodeAt(0)
}) })
let fontFile = require('font-awesome/fonts/fontawesome-webfont.ttf') const fontFile = require('font-awesome/fonts/fontawesome-webfont.ttf')
this.iconsFont = await subsetFont(fontFile, glyphs, { local: isNode }) this.iconsFont = await subsetFont(fontFile, glyphs, { local: isNode })
} }
@ -692,7 +694,7 @@ class FimFic2Epub extends EventEmitter {
return Promise.resolve(this.coverImage) return Promise.resolve(this.coverImage)
} }
this.coverImage = null this.coverImage = null
let url = this.coverUrl || this.storyInfo.full_image const url = this.coverUrl || this.storyInfo.full_image
if (!url) { if (!url) {
console.warn('Story has no image. Generating one...') console.warn('Story has no image. Generating one...')
let canvas let canvas
@ -703,7 +705,7 @@ class FimFic2Epub extends EventEmitter {
canvas.width = 1080 canvas.width = 1080
canvas.height = 1440 canvas.height = 1440
} }
let ctx = canvas.getContext('2d') const ctx = canvas.getContext('2d')
ctx.fillStyle = 'white' ctx.fillStyle = 'white'
ctx.fillRect(0, 0, canvas.width, canvas.height) ctx.fillRect(0, 0, canvas.width, canvas.height)
@ -714,8 +716,8 @@ class FimFic2Epub extends EventEmitter {
ctx.strokeRect(20, 20, canvas.width - 40, canvas.height - 40) ctx.strokeRect(20, 20, canvas.width - 40, canvas.height - 40)
ctx.strokeRect(12, 12, canvas.width - 24, canvas.height - 24) ctx.strokeRect(12, 12, canvas.width - 24, canvas.height - 24)
let title = this.storyInfo.title const title = this.storyInfo.title
let author = this.storyInfo.author.name const author = this.storyInfo.author.name
let fontSize = 150 let fontSize = 150
let width let width
@ -744,14 +746,14 @@ class FimFic2Epub extends EventEmitter {
this.pcache.coverImage = fetchRemote(url, 'arraybuffer').then((data) => { this.pcache.coverImage = fetchRemote(url, 'arraybuffer').then((data) => {
data = isNode ? data : new Uint8Array(data) data = isNode ? data : new Uint8Array(data)
let info = fileType(data) const info = fileType(data)
if (info) { if (info) {
let type = info.mime const type = info.mime
let isImage = type.startsWith('image/') const isImage = type.startsWith('image/')
if (!isImage) { if (!isImage) {
return null return null
} }
let filename = 'Images/cover.' + info.ext const filename = 'Images/cover.' + info.ext
this.coverFilename = filename this.coverFilename = filename
this.coverType = type this.coverType = type
@ -771,7 +773,7 @@ class FimFic2Epub extends EventEmitter {
fetchTitlePage () { fetchTitlePage () {
let viewMature = true let viewMature = true
let isStoryMature = this.storyInfo.content_rating === 2 const isStoryMature = this.storyInfo.content_rating === 2
if (!isNode) { if (!isNode) {
viewMature = document.cookie.split('; ').includes('view_mature=true') viewMature = document.cookie.split('; ').includes('view_mature=true')
if (!viewMature && isStoryMature) { if (!viewMature && isStoryMature) {
@ -796,19 +798,19 @@ class FimFic2Epub extends EventEmitter {
startTagsPos += html.substring(startTagsPos).indexOf('<ul class="story-tags">') + 23 startTagsPos += html.substring(startTagsPos).indexOf('<ul class="story-tags">') + 23
let tagsHtml = html.substring(startTagsPos) let tagsHtml = html.substring(startTagsPos)
let endTagsPos = tagsHtml.indexOf('</ul>') const endTagsPos = tagsHtml.indexOf('</ul>')
tagsHtml = tagsHtml.substring(0, endTagsPos) tagsHtml = tagsHtml.substring(0, endTagsPos)
let tags = [] const tags = []
let c let c
tags.byImage = {} tags.byImage = {}
this.subjects.length = 0 this.subjects.length = 0
this.subjects.push('Fimfiction') this.subjects.push('Fimfiction')
this.subjects.push(this.storyInfo.content_rating_text) this.subjects.push(this.storyInfo.content_rating_text)
let matchTag = /<a href="([^"]*?)" class="([^"]*?)" title="[^"]*?" data-tag="([^"]*?)".*?>(.*?)<\/a>/g const matchTag = /<a href="([^"]*?)" class="([^"]*?)" title="[^"]*?" data-tag="([^"]*?)".*?>(.*?)<\/a>/g
for (;(c = matchTag.exec(tagsHtml));) { for (;(c = matchTag.exec(tagsHtml));) {
let cat = { const cat = {
url: 'https://fimfiction.net' + c[1], url: 'https://fimfiction.net' + c[1],
className: 'story-tag ' + c[2], className: 'story-tag ' + c[2],
name: entities.decode(c[4]), name: entities.decode(c[4]),
@ -831,12 +833,12 @@ class FimFic2Epub extends EventEmitter {
html = html.substring(html.indexOf('<hr />') + 6) html = html.substring(html.indexOf('<hr />') + 6)
} }
let endDescPos = html.indexOf('</span>\n') const endDescPos = html.indexOf('</span>\n')
let description = html.substring(0, endDescPos).trim() const description = html.substring(0, endDescPos).trim()
this.description = description this.description = description
html = html.substring(endDescPos + 7) html = html.substring(endDescPos + 7)
let extraPos = html.indexOf('<div class="extra_story_data">') const extraPos = html.indexOf('<div class="extra_story_data">')
html = html.substring(extraPos + 30) html = html.substring(extraPos + 30)
ma = html.match(/<span class="approved-date">.*?data-time="(.*?)".*?<\/span>/) ma = html.match(/<span class="approved-date">.*?data-time="(.*?)".*?<\/span>/)
@ -848,7 +850,7 @@ class FimFic2Epub extends EventEmitter {
} }
parseChapterPage (html) { parseChapterPage (html) {
let trimWhitespace = /^\s*(<br\s*\/?\s*>)+|(<br\s*\/?\s*>)+\s*$/ig const trimWhitespace = /^\s*(<br\s*\/?\s*>)+|(<br\s*\/?\s*>)+\s*$/ig
let authorNotesPos = html.indexOf('<div class="authors-note"') let authorNotesPos = html.indexOf('<div class="authors-note"')
let authorNotes = '' let authorNotes = ''
@ -860,10 +862,10 @@ class FimFic2Epub extends EventEmitter {
authorNotes = authorNotes.replace(trimWhitespace, '') authorNotes = authorNotes.replace(trimWhitespace, '')
} }
let chapterPos = html.indexOf('<div class="bbcode">') const chapterPos = html.indexOf('<div class="bbcode">')
let chapter = html.substring(chapterPos + 20) let chapter = html.substring(chapterPos + 20)
let pos = chapter.indexOf('\t\t</div>\n\t</div>\t\t\n\t\t\t\t\t</div>\n') const pos = chapter.indexOf('\t\t</div>\n\t</div>\t\t\n\t\t\t\t\t</div>\n')
chapter = chapter.substring(0, pos).trim() chapter = chapter.substring(0, pos).trim()
@ -877,9 +879,9 @@ class FimFic2Epub extends EventEmitter {
if (!this.options.includeExternal) { if (!this.options.includeExternal) {
this.remoteResources.forEach((r, url) => { this.remoteResources.forEach((r, url) => {
if (r.originalUrl && r.where) { if (r.originalUrl && r.where) {
let ourl = new RegExp(escapeStringRegexp(r.originalUrl), 'g') const ourl = new RegExp(escapeStringRegexp(r.originalUrl), 'g')
for (var i = 0; i < r.where.length; i++) { for (var i = 0; i < r.where.length; i++) {
let w = r.where[i] const w = r.where[i]
if (typeof w === 'number') { if (typeof w === 'number') {
if (ourl.test(this.chapters[w])) { if (ourl.test(this.chapters[w])) {
this.storyInfo.chapters[w].remote = true this.storyInfo.chapters[w].remote = true
@ -895,10 +897,10 @@ class FimFic2Epub extends EventEmitter {
} else { } else {
this.remoteResources.forEach((r, url) => { this.remoteResources.forEach((r, url) => {
if (r.dest && r.originalUrl && r.where) { if (r.dest && r.originalUrl && r.where) {
let dest = '../' + r.dest const dest = '../' + r.dest
let ourl = new RegExp(escapeStringRegexp(r.originalUrl), 'g') const ourl = new RegExp(escapeStringRegexp(r.originalUrl), 'g')
for (var i = 0; i < r.where.length; i++) { for (var i = 0; i < r.where.length; i++) {
let w = r.where[i] const w = r.where[i]
if (typeof w === 'object' && w.chapter !== undefined && this.chaptersHtml[w.chapter]) { if (typeof w === 'object' && w.chapter !== undefined && this.chaptersHtml[w.chapter]) {
this.chaptersHtml[w.chapter] = this.chaptersHtml[w.chapter].replace(ourl, dest) this.chaptersHtml[w.chapter] = this.chaptersHtml[w.chapter].replace(ourl, dest)
} else if (typeof w === 'object' && w.note !== undefined && this.notesHtml[w.note]) { } else if (typeof w === 'object' && w.note !== undefined && this.notesHtml[w.note]) {

View file

@ -44,12 +44,12 @@ export async function cleanMarkup (html) {
html = html.replace('<blockquote style="margin: 10px 0px; box-sizing:border-box; -moz-box-sizing:border-box;margin-left:25px; padding: 15px;background-color: #F7F7F7;border: 1px solid #AAA;width: 50%;float:right;box-shadow: 5px 5px 0px #EEE;">', '<blockquote class="right_insert">') html = html.replace('<blockquote style="margin: 10px 0px; box-sizing:border-box; -moz-box-sizing:border-box;margin-left:25px; padding: 15px;background-color: #F7F7F7;border: 1px solid #AAA;width: 50%;float:right;box-shadow: 5px 5px 0px #EEE;">', '<blockquote class="right_insert">')
// add alt attributes to images that don't have them // add alt attributes to images that don't have them
let imageEmbed = /<img src="(.*?)" \/>/g const imageEmbed = /<img src="(.*?)" \/>/g
html = await replaceAsync(html, imageEmbed, (match, src) => render(m('img', { src: entities.decode(src), alt: 'Image' }), { strict: true })) html = await replaceAsync(html, imageEmbed, (match, src) => render(m('img', { src: entities.decode(src), alt: 'Image' }), { strict: true }))
// Fix links pointing to pages on fimfiction // Fix links pointing to pages on fimfiction
// Example: <a href="/user/djazz" rel="nofollow">djazz</a> // Example: <a href="/user/djazz" rel="nofollow">djazz</a>
let matchLink = /(<a .?href=")(.+?)(".+?>)/g const matchLink = /(<a .?href=")(.+?)(".+?>)/g
html = html.replace(matchLink, (match, head, url, tail) => { html = html.replace(matchLink, (match, head, url, tail) => {
if (url.substring(0, 1) !== '#' && url.substring(0, 2) !== '//' && url.substring(0, 4) !== 'http' && url.substring(0, 1) === '/') { if (url.substring(0, 1) !== '#' && url.substring(0, 2) !== '//' && url.substring(0, 4) !== 'http' && url.substring(0, 1) === '/') {
url = 'https://fimfiction.net' + url url = 'https://fimfiction.net' + url
@ -62,14 +62,14 @@ export async function cleanMarkup (html) {
const query = new Map() const query = new Map()
let completeCount = 0 let completeCount = 0
let matchYouTube = /<p><a class="embed" href="https:\/\/www\.youtube\.com\/watch\?v=(.*?)">.*?<\/a><\/p>/g const matchYouTube = /<p><a class="embed" href="https:\/\/www\.youtube\.com\/watch\?v=(.*?)">.*?<\/a><\/p>/g
for (let ma; (ma = matchYouTube.exec(html));) { for (let ma; (ma = matchYouTube.exec(html));) {
let youtubeId = ma[1].match(/^[^&]+/)[0] const youtubeId = ma[1].match(/^[^&]+/)[0]
cache.set(youtubeId, null) cache.set(youtubeId, null)
query.set(entities.decode(ma[1]), youtubeId) query.set(entities.decode(ma[1]), youtubeId)
} }
let matchSoundCloud = /<p><a class="embed" href="(https:\/\/soundcloud\.com\/.*?)">.*?<\/a><\/p>/g const matchSoundCloud = /<p><a class="embed" href="(https:\/\/soundcloud\.com\/.*?)">.*?<\/a><\/p>/g
html = await replaceAsync(html, matchSoundCloud, (match, url) => { html = await replaceAsync(html, matchSoundCloud, (match, url) => {
return render(m('.soundcloud.leftalign', [ return render(m('.soundcloud.leftalign', [
'SoundCloud: ', m('a', { href: entities.decode(url), rel: 'nofollow' }, url.replace('https://soundcloud.com/', '').replace(/[-_]/g, ' ').replace('/', ' - ').replace(/ {2}/g, ' ')) 'SoundCloud: ', m('a', { href: entities.decode(url), rel: 'nofollow' }, url.replace('https://soundcloud.com/', '').replace(/[-_]/g, ' ').replace('/', ' - ').replace(/ {2}/g, ' '))
@ -104,12 +104,12 @@ export async function cleanMarkup (html) {
function replaceYouTube (match, queryString) { function replaceYouTube (match, queryString) {
queryString = entities.decode(queryString) queryString = entities.decode(queryString)
let youtubeId = query.get(queryString) const youtubeId = query.get(queryString)
let thumbnail = 'https://img.youtube.com/vi/' + youtubeId + '/hqdefault.jpg' let thumbnail = 'https://img.youtube.com/vi/' + youtubeId + '/hqdefault.jpg'
let youtubeUrl = 'https://youtube.com/watch?v=' + queryString const youtubeUrl = 'https://youtube.com/watch?v=' + queryString
let title = 'Youtube Video' let title = 'Youtube Video'
let caption = '' let caption = ''
let data = cache.get(youtubeId) const data = cache.get(youtubeId)
if (data) { if (data) {
thumbnail = (data.thumbnails.standard || data.thumbnails.high || data.thumbnails.medium || data.thumbnails.default).url thumbnail = (data.thumbnails.standard || data.thumbnails.high || data.thumbnails.medium || data.thumbnails.default).url
@ -138,7 +138,7 @@ export function fixDoubleSpacing (html) {
export function fixParagraphIndent (html) { export function fixParagraphIndent (html) {
// from FimFictionConverter by Nyerguds // from FimFictionConverter by Nyerguds
let fixIndent = 2 const fixIndent = 2
if (fixIndent > 0) { if (fixIndent > 0) {
// only trigger indenting when finding as many whitespace characters in a row as indicated by the FixIndent setting. // only trigger indenting when finding as many whitespace characters in a row as indicated by the FixIndent setting.

View file

@ -39,7 +39,6 @@ if (outputStdout) {
const mock = require('mithril/test-utils/browserMock')(global) const mock = require('mithril/test-utils/browserMock')(global)
global.requestAnimationFrame = mock.requestAnimationFrame global.requestAnimationFrame = mock.requestAnimationFrame
const htmlToText = require('./utils').htmlToText const htmlToText = require('./utils').htmlToText
const FimFic2Epub = require('./FimFic2Epub').default const FimFic2Epub = require('./FimFic2Epub').default
const fs = require('fs') const fs = require('fs')

View file

@ -10,13 +10,13 @@ export const NS = {
} }
export const tidyOptions = { export const tidyOptions = {
'indent': 'auto', indent: 'auto',
'numeric-entities': 'yes', 'numeric-entities': 'yes',
'output-xhtml': 'yes', 'output-xhtml': 'yes',
'alt-text': 'Image', 'alt-text': 'Image',
'wrap': '0', wrap: '0',
'quiet': 'yes', quiet: 'yes',
'newline': 'LF', newline: 'LF',
'tidy-mark': 'no', 'tidy-mark': 'no',
'show-body-only': 'auto' 'show-body-only': 'auto'
} }

View file

@ -4,7 +4,7 @@ import fetch from './fetch'
if (typeof safari !== 'undefined') { if (typeof safari !== 'undefined') {
safari.application.addEventListener('message', function (ev) { safari.application.addEventListener('message', function (ev) {
let url = ev.message const url = ev.message
fetch(url, 'arraybuffer').then((buffer) => { fetch(url, 'arraybuffer').then((buffer) => {
console.log('Fetched ' + url) console.log('Fetched ' + url)
ev.target.page.dispatchMessage('remote', { ev.target.page.dispatchMessage('remote', {
@ -14,12 +14,12 @@ if (typeof safari !== 'undefined') {
}) })
}, false) }, false)
} else { } else {
let onMessage = chrome.extension.onMessage ? chrome.extension.onMessage : chrome.runtime.onMessage const onMessage = chrome.extension.onMessage ? chrome.extension.onMessage : chrome.runtime.onMessage
onMessage.addListener(function (request, sender, sendResponse) { onMessage.addListener(function (request, sender, sendResponse) {
if (typeof request === 'string') { if (typeof request === 'string') {
fetch(request, 'blob').then((blob) => { fetch(request, 'blob').then((blob) => {
let ourl = URL.createObjectURL(blob) const ourl = URL.createObjectURL(blob)
console.log('Fetched', request) console.log('Fetched', request)
sendResponse(ourl) sendResponse(ourl)
}) })

View file

@ -11,9 +11,9 @@ function fetchNode (url, responseType) {
url: url, url: url,
encoding: responseType ? null : 'utf8', encoding: responseType ? null : 'utf8',
headers: { headers: {
'referer': 'https://fimfiction.net/', referer: 'https://fimfiction.net/',
'cookie': 'view_mature=true', cookie: 'view_mature=true',
'accept': '*/*' accept: '*/*'
} }
}, (error, response, body) => { }, (error, response, body) => {
if (error) { if (error) {
@ -46,7 +46,7 @@ export default function fetch (url, responseType) {
cache: 'default', cache: 'default',
redirect: 'follow', redirect: 'follow',
headers: { headers: {
'accept': '*/*' // Fix for not getting webp images from Fimfiction accept: '*/*' // Fix for not getting webp images from Fimfiction
}, },
referrer: window.location.origin referrer: window.location.origin
}).then((response) => { }).then((response) => {
@ -61,7 +61,7 @@ export default function fetch (url, responseType) {
reject(new Error('Error fetching ' + url + ' (' + err + ')')) reject(new Error('Error fetching ' + url + ' (' + err + ')'))
}) })
} else { } else {
let x = new XMLHttpRequest() const x = new XMLHttpRequest()
x.withCredentials = true x.withCredentials = true
x.setRequestHeader('accept', '*/*') // Fix for not getting webp images from Fimfiction x.setRequestHeader('accept', '*/*') // Fix for not getting webp images from Fimfiction
x.open('get', url, true) x.open('get', url, true)

View file

@ -7,25 +7,25 @@ const safariQueue = {}
// messaging with the safari extension global page // messaging with the safari extension global page
function safariHandler (ev) { function safariHandler (ev) {
let type = ev.message.type const type = ev.message.type
let url = ev.message.input const url = ev.message.input
let data = ev.message.output // arraybuffer const data = ev.message.output // arraybuffer
if (!safariQueue[url]) { if (!safariQueue[url]) {
// console.error("Unable to get callback for " + url, JSON.stringify(safariQueue)) // console.error("Unable to get callback for " + url, JSON.stringify(safariQueue))
return return
} }
let cb = safariQueue[url].cb const cb = safariQueue[url].cb
let responseType = safariQueue[url].responseType const responseType = safariQueue[url].responseType
console.log(url, cb, responseType, data) console.log(url, cb, responseType, data)
delete safariQueue[url] delete safariQueue[url]
if (responseType === 'blob') { if (responseType === 'blob') {
let blob = new Blob([data], { type: type }) const blob = new Blob([data], { type: type })
cb(blob, type) cb(blob, type)
} else { } else {
if (!responseType) { if (!responseType) {
let blob = new Blob([data], { type: type }) const blob = new Blob([data], { type: type })
let fr = new FileReader() const fr = new FileReader()
fr.onloadend = function () { fr.onloadend = function () {
cb(fr.result, type) cb(fr.result, type)
} }

View file

@ -46,7 +46,7 @@ function textToSentences (text) {
.split(/\s*\0/) .split(/\s*\0/)
for (let i = 0; i < tokenSentences.length; i++) { for (let i = 0; i < tokenSentences.length; i++) {
let s = tokenSentences[i] const s = tokenSentences[i]
if (s.trim().length === 0) { if (s.trim().length === 0) {
if (i - 1 >= 0) tokenSentences[i - 1] += s if (i - 1 >= 0) tokenSentences[i - 1] += s
tokenSentences.splice(i, 1) tokenSentences.splice(i, 1)
@ -61,15 +61,15 @@ function textToSentences (text) {
function fixupTree (node, parent) { function fixupTree (node, parent) {
if (node.tag !== '#') { if (node.tag !== '#') {
if (node.text && !node.tag.match(specialTags)) { if (node.text && !node.tag.match(specialTags)) {
let el = et.Element('#') const el = et.Element('#')
el.text = node.text el.text = node.text
node._children.unshift(el) node._children.unshift(el)
delete node.text delete node.text
} }
if (node.tail) { if (node.tail) {
let el = et.Element('#') const el = et.Element('#')
el.text = node.tail el.text = node.tail
let pos = parent._children.indexOf(node) + 1 const pos = parent._children.indexOf(node) + 1
parent._children.splice(pos, 0, el) parent._children.splice(pos, 0, el)
delete node.tail delete node.tail
} }
@ -84,11 +84,11 @@ function addSpansToNode (node, parent, state) {
if (node.tag === '#') { if (node.tag === '#') {
state.segment++ state.segment++
let sentences = textToSentences(node.text) const sentences = textToSentences(node.text)
let pos let pos
sentences.forEach((sentence) => { sentences.forEach((sentence) => {
let span = createSpan(state.paragraph, state.segment++) const span = createSpan(state.paragraph, state.segment++)
span.text = sentence span.text = sentence
// insert the span before the text node // insert the span before the text node

View file

@ -8,15 +8,15 @@ import { saveAs } from 'file-saver'
import autosize from 'autosize' import autosize from 'autosize'
import { htmlToText } from './utils' import { htmlToText } from './utils'
m.withAttr = function(attrName, callback, context) { m.withAttr = function (attrName, callback, context) {
return function(e) { return function (e) {
callback.call(context || this, attrName in e.currentTarget ? e.currentTarget[attrName] : e.currentTarget.getAttribute(attrName)) callback.call(context || this, attrName in e.currentTarget ? e.currentTarget[attrName] : e.currentTarget.getAttribute(attrName))
} }
} }
function blobToDataURL (blob) { function blobToDataURL (blob) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let fr = new FileReader() const fr = new FileReader()
fr.onloadend = function (e) { resolve(fr.result) } fr.onloadend = function (e) { resolve(fr.result) }
fr.readAsDataURL(blob) fr.readAsDataURL(blob)
}) })
@ -24,7 +24,7 @@ function blobToDataURL (blob) {
function blobToArrayBuffer (blob) { function blobToArrayBuffer (blob) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let fr = new FileReader() const fr = new FileReader()
fr.onloadend = function (e) { resolve(fr.result) } fr.onloadend = function (e) { resolve(fr.result) }
fr.readAsArrayBuffer(blob) fr.readAsArrayBuffer(blob)
}) })
@ -37,23 +37,23 @@ try {
pageStoryId = document.location.pathname.match(/^\/story\/(\d*)/)[1] pageStoryId = document.location.pathname.match(/^\/story\/(\d*)/)[1]
} catch (e) {} } catch (e) {}
let logoUrl = chrome.extension.getURL('fimfic2epub-logo.png') const logoUrl = chrome.extension.getURL('fimfic2epub-logo.png')
let ffc let ffc
let stories = document.querySelectorAll('.story_container') const stories = document.querySelectorAll('.story_container')
stories.forEach((story) => { stories.forEach((story) => {
let id = story.dataset.story const id = story.dataset.story
function epubClick (e) { function epubClick (e) {
e.preventDefault() e.preventDefault()
openStory(id) openStory(id)
} }
let epubButtons = story.querySelectorAll('.drop-down ul li a[title="Download Story (.epub)"]') const epubButtons = story.querySelectorAll('.drop-down ul li a[title="Download Story (.epub)"]')
if (epubButtons.length === 0) return if (epubButtons.length === 0) return
for (let i = 0; i < epubButtons.length; i++) { for (let i = 0; i < epubButtons.length; i++) {
epubButtons[i].addEventListener('click', epubClick, false) epubButtons[i].addEventListener('click', epubClick, false)
} }
let logo = new Image() const logo = new Image()
logo.className = 'fimfic2epub-logo' logo.className = 'fimfic2epub-logo'
logo.title = 'Download EPUB with fimfic2epub' logo.title = 'Download EPUB with fimfic2epub'
logo.src = logoUrl logo.src = logoUrl
@ -61,17 +61,17 @@ stories.forEach((story) => {
logo.addEventListener('click', epubClick, false) logo.addEventListener('click', epubClick, false)
}) })
let cards = document.querySelectorAll('.story-card-container') const cards = document.querySelectorAll('.story-card-container')
cards.forEach((card) => { cards.forEach((card) => {
let id let id
let classes = card.className.split(' ') const classes = card.className.split(' ')
for (let i = 0; i < classes.length && !id; i++) { for (let i = 0; i < classes.length && !id; i++) {
let c = classes[i] const c = classes[i]
id = c.substring(21) id = c.substring(21)
} }
if (!id) return if (!id) return
let flip = card.querySelector('a.card-flip') const flip = card.querySelector('a.card-flip')
let epubButton = card.querySelector('a[title="Download .ePub"]') const epubButton = card.querySelector('a[title="Download .ePub"]')
if (!epubButton) return if (!epubButton) return
epubButton.addEventListener('click', function (e) { epubButton.addEventListener('click', function (e) {
e.preventDefault() e.preventDefault()
@ -84,7 +84,7 @@ const dialogContainer = document.createElement('div')
dialogContainer.id = 'epubDialogContainer' dialogContainer.id = 'epubDialogContainer'
document.body.appendChild(dialogContainer) document.body.appendChild(dialogContainer)
let checkbox = { const checkbox = {
view: function ({ attrs, children }) { view: function ({ attrs, children }) {
return m('label.toggleable-switch', [ return m('label.toggleable-switch', [
m('input', Object.assign({ m('input', Object.assign({
@ -113,10 +113,10 @@ function redraw (arg) {
} }
} }
let ffcProgress = prop(0) const ffcProgress = prop(0)
let ffcStatus = prop('') const ffcStatus = prop('')
let dialog = { const dialog = {
oninit () { oninit () {
const ctrl = this const ctrl = this
@ -173,7 +173,7 @@ let dialog = {
} }
this.setCoverFile = (e) => { this.setCoverFile = (e) => {
let el = e.dom || e.target const el = e.dom || e.target
if (el.target) { if (el.target) {
this.coverUrl('') this.coverUrl('')
} }
@ -182,7 +182,7 @@ let dialog = {
this.setSubjects = function () { this.setSubjects = function () {
// 'this' is the textarea // 'this' is the textarea
let set = new Set() const set = new Set()
ctrl.subjects(this.value.split('\n').map((s) => s.trim()).filter((s) => { ctrl.subjects(this.value.split('\n').map((s) => s.trim()).filter((s) => {
if (!s) return false if (!s) return false
if (set.has(s)) return false if (set.has(s)) return false
@ -200,16 +200,16 @@ let dialog = {
} }
this.ondown = (e) => { this.ondown = (e) => {
let rect = this.el().firstChild.getBoundingClientRect() const rect = this.el().firstChild.getBoundingClientRect()
let offset = { x: e.pageX - rect.left - document.documentElement.scrollLeft, y: e.pageY - rect.top - document.documentElement.scrollTop } const offset = { x: e.pageX - rect.left - document.documentElement.scrollLeft, y: e.pageY - rect.top - document.documentElement.scrollTop }
this.dragging(true) this.dragging(true)
let onmove = (e) => { const onmove = (e) => {
e.preventDefault() e.preventDefault()
if (this.dragging()) { if (this.dragging()) {
this.move(e.pageX - offset.x, e.pageY - offset.y) this.move(e.pageX - offset.x, e.pageY - offset.y)
} }
} }
let onup = () => { const onup = () => {
this.dragging(false) this.dragging(false)
window.removeEventListener('mousemove', onmove) window.removeEventListener('mousemove', onmove)
window.removeEventListener('mouseup', onup) window.removeEventListener('mouseup', onup)
@ -219,8 +219,8 @@ let dialog = {
} }
this.move = (xpos, ypos) => { this.move = (xpos, ypos) => {
let bc = document.querySelector('.body_container') const bc = document.querySelector('.body_container')
let rect = this.el().firstChild.getBoundingClientRect() const rect = this.el().firstChild.getBoundingClientRect()
this.xpos(Math.max(0, Math.min(xpos, bc.offsetWidth - rect.width))) this.xpos(Math.max(0, Math.min(xpos, bc.offsetWidth - rect.width)))
this.ypos(Math.max(0, Math.min(ypos, bc.offsetHeight - rect.height))) this.ypos(Math.max(0, Math.min(ypos, bc.offsetHeight - rect.height)))
this.el().style.left = this.xpos() + 'px' this.el().style.left = this.xpos() + 'px'
@ -228,7 +228,7 @@ let dialog = {
} }
this.center = () => { this.center = () => {
if (this.dragging()) return if (this.dragging()) return
let rect = this.el().firstChild.getBoundingClientRect() const rect = this.el().firstChild.getBoundingClientRect()
this.move( this.move(
Math.max(document.documentElement.scrollLeft, (window.innerWidth / 2) - (rect.width / 2) + document.documentElement.scrollLeft), Math.max(document.documentElement.scrollLeft, (window.innerWidth / 2) - (rect.width / 2) + document.documentElement.scrollLeft),
Math.max(document.documentElement.scrollTop, 100 + document.documentElement.scrollTop) Math.max(document.documentElement.scrollTop, 100 + document.documentElement.scrollTop)
@ -242,7 +242,7 @@ let dialog = {
}, },
view (vnode) { view (vnode) {
let ctrl = vnode.state const ctrl = vnode.state
return m('.drop-down-pop-up-container', { oncreate: ctrl.onOpen.bind(ctrl) }, m('.drop-down-pop-up', { style: { 'min-width': '720px' } }, [ return m('.drop-down-pop-up-container', { oncreate: ctrl.onOpen.bind(ctrl) }, m('.drop-down-pop-up', { style: { 'min-width': '720px' } }, [
m('h1', { onmousedown: ctrl.ondown }, m('i.fa.fa-book'), 'Export to EPUB (v' + FIMFIC2EPUB_VERSION + ')', m('a.close_button', { onclick: closeDialog })), m('h1', { onmousedown: ctrl.ondown }, m('i.fa.fa-book'), 'Export to EPUB (v' + FIMFIC2EPUB_VERSION + ')', m('a.close_button', { onclick: closeDialog })),
m('.drop-down-pop-up-content', [ m('.drop-down-pop-up-content', [
@ -295,7 +295,7 @@ let dialog = {
ffcProgress() >= 0 ? m('.rating_container', ffcProgress() >= 0 ? m('.rating_container',
m('.rating-bar', { style: { background: 'rgba(0, 0, 0, 0.2)', 'margin-right': '5px' } }, m('.like-bar', { style: { width: Math.max(0, ffcProgress()) * 100 + '%' } })), m('.rating-bar', { style: { background: 'rgba(0, 0, 0, 0.2)', 'margin-right': '5px' } }, m('.like-bar', { style: { width: Math.max(0, ffcProgress()) * 100 + '%' } })),
' ', ' ',
ffcProgress() >= 0 && ffcProgress() < 1 ? [ m('i.fa.fa-spin.fa-spinner'), m.trust('&nbsp;&nbsp;') ] : null, ffcProgress() >= 0 && ffcProgress() < 1 ? [m('i.fa.fa-spin.fa-spinner'), m.trust('&nbsp;&nbsp;')] : null,
ffcStatus() ffcStatus()
) : null, ) : null,
m('div', { style: 'clear: both' }) m('div', { style: 'clear: both' })

View file

@ -7,8 +7,8 @@ import fileType from 'file-type'
async function subsetFont (fontPath, glyphs, options = {}) { async function subsetFont (fontPath, glyphs, options = {}) {
let data let data
let fontdata = Buffer.from(fontPath, 'binary') const fontdata = Buffer.from(fontPath, 'binary')
let type = fileType(fontdata) const type = fileType(fontdata)
if (type && type.mime === 'font/ttf') { if (type && type.mime === 'font/ttf') {
data = fontdata.buffer data = fontdata.buffer
} else { } else {

View file

@ -18,7 +18,7 @@ function nth (d) {
export function prettyDate (d) { export function prettyDate (d) {
// format: 27th Oct 2011 // format: 27th Oct 2011
let months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'] const months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']
return d.getDate() + nth(d) + ' ' + months[d.getMonth()].substring(0, 3) + ' ' + d.getFullYear() return d.getDate() + nth(d) + ' ' + months[d.getMonth()].substring(0, 3) + ' ' + d.getFullYear()
} }
@ -28,7 +28,7 @@ function chapterBars (chapters, currentChapter = -1, highlightCurrent = false) {
if (chapters.length <= 1) return null if (chapters.length <= 1) return null
let windowSize = 50 let windowSize = 50
let wordCounts = [] let wordCounts = []
let highestWordCount = chapters.reduce((max, ch) => { const highestWordCount = chapters.reduce((max, ch) => {
wordCounts.push(ch.realWordCount) wordCounts.push(ch.realWordCount)
if (ch.realWordCount > max) return ch.realWordCount if (ch.realWordCount > max) return ch.realWordCount
return max return max
@ -42,9 +42,9 @@ function chapterBars (chapters, currentChapter = -1, highlightCurrent = false) {
currentChapter -= start currentChapter -= start
} }
wordCounts = wordCounts.map((c) => c / highestWordCount) wordCounts = wordCounts.map((c) => c / highestWordCount)
let barWidth = 9 const barWidth = 9
let barSpacing = 2 const barSpacing = 2
let rowSpacing = 9 const rowSpacing = 9
const barCount = Math.min(wordCounts.length, windowSize) const barCount = Math.min(wordCounts.length, windowSize)
const rows = Math.floor(wordCounts.length / barCount) + 1 const rows = Math.floor(wordCounts.length / barCount) + 1
const rowHeight = 30 + rowSpacing const rowHeight = 30 + rowSpacing
@ -68,9 +68,9 @@ function chapterBars (chapters, currentChapter = -1, highlightCurrent = false) {
} }
export function createChapter (ffc, ch, isNotesChapter) { export function createChapter (ffc, ch, isNotesChapter) {
let { content, notes, notesFirst, title, link, linkNotes, index, showHeadings, showDuration, showWordCount } = ch const { content, notes, notesFirst, title, link, linkNotes, index, showHeadings, showDuration, showWordCount } = ch
let sections = [ const sections = [
m.trust(content || ''), m.trust(content || ''),
notes ? m('div#author_notes', { className: notesFirst ? 'top' : 'bottom' }, [ notes ? m('div#author_notes', { className: notesFirst ? 'top' : 'bottom' }, [
m('p', m('b', 'Author\'s Note:')), m('p', m('b', 'Author\'s Note:')),
@ -124,7 +124,7 @@ export function createChapter (ffc, ch, isNotesChapter) {
function sortSpineItems (items) { function sortSpineItems (items) {
let count = items.length let count = items.length
for (let i = 0; i < count; i++) { for (let i = 0; i < count; i++) {
let item = items[i] const item = items[i]
if (!item) { if (!item) {
continue continue
} }
@ -140,9 +140,9 @@ function sortSpineItems (items) {
} }
export function createOpf (ffc) { export function createOpf (ffc) {
let remotes = [] const remotes = []
// let remoteCounter = 0 // let remoteCounter = 0
let remoteCache = new Set() const remoteCache = new Set()
ffc.remoteResources.forEach((r, url) => { ffc.remoteResources.forEach((r, url) => {
// remoteCounter++ // remoteCounter++
if (!ffc.options.includeExternal) { if (!ffc.options.includeExternal) {
@ -176,18 +176,18 @@ export function createOpf (ffc) {
remotes.push(m('item', { id: r.filename, href: r.dest, 'media-type': r.type })) remotes.push(m('item', { id: r.filename, href: r.dest, 'media-type': r.type }))
}) })
let manifestChapters = ffc.storyInfo.chapters.map((ch, num) => const manifestChapters = ffc.storyInfo.chapters.map((ch, num) =>
m('item', { id: 'chapter_' + zeroFill(3, num + 1), href: 'Text/chapter_' + zeroFill(3, num + 1) + '.xhtml', 'media-type': 'application/xhtml+xml', properties: ((ch.remote ? 'remote-resources' : '') + (ffc.options.addChapterBars ? ' svg' : '')).trim() || null }) m('item', { id: 'chapter_' + zeroFill(3, num + 1), href: 'Text/chapter_' + zeroFill(3, num + 1) + '.xhtml', 'media-type': 'application/xhtml+xml', properties: ((ch.remote ? 'remote-resources' : '') + (ffc.options.addChapterBars ? ' svg' : '')).trim() || null })
) )
let spineChapters = ffc.storyInfo.chapters.map((ch, num) => const spineChapters = ffc.storyInfo.chapters.map((ch, num) =>
m('itemref', { idref: 'chapter_' + zeroFill(3, num + 1) }) m('itemref', { idref: 'chapter_' + zeroFill(3, num + 1) })
) )
let manifestNotes = [] const manifestNotes = []
let spineNotes = [] const spineNotes = []
if (ffc.options.includeAuthorNotes && ffc.options.useAuthorNotesIndex && ffc.hasAuthorNotes) { if (ffc.options.includeAuthorNotes && ffc.options.useAuthorNotesIndex && ffc.hasAuthorNotes) {
spineNotes.push(m('itemref', { idref: 'notesnav' })) spineNotes.push(m('itemref', { idref: 'notesnav' }))
ffc.chaptersWithNotes.forEach((num) => { ffc.chaptersWithNotes.forEach((num) => {
let id = 'note_' + zeroFill(3, num + 1) const id = 'note_' + zeroFill(3, num + 1)
manifestNotes.push(m('item', { id: id, href: 'Text/' + id + '.xhtml', 'media-type': 'application/xhtml+xml' })) manifestNotes.push(m('item', { id: id, href: 'Text/' + id + '.xhtml', 'media-type': 'application/xhtml+xml' }))
spineNotes.push(m('itemref', { idref: id })) spineNotes.push(m('itemref', { idref: id }))
}) })
@ -219,8 +219,8 @@ export function createOpf (ffc) {
m('manifest', [ m('manifest', [
ffc.coverImage ? m('item', { id: 'cover', href: ffc.coverFilename, 'media-type': ffc.coverType, properties: 'cover-image' }) : null, ffc.coverImage ? m('item', { id: 'cover', href: ffc.coverFilename, 'media-type': ffc.coverType, properties: 'cover-image' }) : null,
m('item', { id: 'ncx', href: 'toc.ncx', 'media-type': 'application/x-dtbncx+xml' }), m('item', { id: 'ncx', href: 'toc.ncx', 'media-type': 'application/x-dtbncx+xml' }),
m('item', { id: 'nav', 'href': 'nav.xhtml', 'media-type': 'application/xhtml+xml', properties: 'nav' + (ffc.options.addChapterBars && ffc.storyInfo.chapters.length > 1 ? ' svg' : '') }), m('item', { id: 'nav', href: 'nav.xhtml', 'media-type': 'application/xhtml+xml', properties: 'nav' + (ffc.options.addChapterBars && ffc.storyInfo.chapters.length > 1 ? ' svg' : '') }),
ffc.options.includeAuthorNotes && ffc.options.useAuthorNotesIndex && ffc.hasAuthorNotes ? m('item', { id: 'notesnav', 'href': 'notesnav.xhtml', 'media-type': 'application/xhtml+xml' }) : null, ffc.options.includeAuthorNotes && ffc.options.useAuthorNotesIndex && ffc.hasAuthorNotes ? m('item', { id: 'notesnav', href: 'notesnav.xhtml', 'media-type': 'application/xhtml+xml' }) : null,
m('item', { id: 'style', href: 'Styles/style.css', 'media-type': 'text/css' }), m('item', { id: 'style', href: 'Styles/style.css', 'media-type': 'text/css' }),
m('item', { id: 'coverstyle', href: 'Styles/coverstyle.css', 'media-type': 'text/css' }), m('item', { id: 'coverstyle', href: 'Styles/coverstyle.css', 'media-type': 'text/css' }),
@ -255,7 +255,7 @@ export function createOpf (ffc) {
function navPoints (list) { function navPoints (list) {
let playOrder = 1 let playOrder = 1
let arr = [] const arr = []
for (let i = 0; i < list.length; i++) { for (let i = 0; i < list.length; i++) {
if (!list[i]) continue if (!list[i]) continue
arr.push(m('navPoint', { id: 'navPoint-' + (i + 1), playOrder: playOrder++ }, [ arr.push(m('navPoint', { id: 'navPoint-' + (i + 1), playOrder: playOrder++ }, [
@ -290,7 +290,7 @@ export function createNcx (ffc) {
} }
export function createNav (ffc) { export function createNav (ffc) {
let list = [ const list = [
m('li', m('a', { href: 'Text/cover.xhtml' }, 'Cover')), m('li', m('a', { href: 'Text/cover.xhtml' }, 'Cover')),
m('li', m('a', { href: 'Text/title.xhtml' }, 'Title Page')), m('li', m('a', { href: 'Text/title.xhtml' }, 'Title Page')),
ffc.storyInfo.chapters.length > 1 || (ffc.options.includeAuthorNotes && ffc.options.useAuthorNotesIndex && ffc.hasAuthorNotes) ? m('li', m('a', { href: 'nav.xhtml' }, 'Contents')) : null ffc.storyInfo.chapters.length > 1 || (ffc.options.includeAuthorNotes && ffc.options.useAuthorNotesIndex && ffc.hasAuthorNotes) ? m('li', m('a', { href: 'nav.xhtml' }, 'Contents')) : null
@ -299,7 +299,7 @@ export function createNav (ffc) {
m('a', { href: 'Text/chapter_' + zeroFill(3, num + 1) + '.xhtml' }, ch.title) m('a', { href: 'Text/chapter_' + zeroFill(3, num + 1) + '.xhtml' }, ch.title)
]) ])
)) ))
let prettyList = ffc.storyInfo.chapters.map((ch, num) => const prettyList = ffc.storyInfo.chapters.map((ch, num) =>
m('li.item', [ m('li.item', [
m('.floatbox', m('span.wordcount', ch.realWordCount.toLocaleString('en-GB'))), m('.floatbox', m('span.wordcount', ch.realWordCount.toLocaleString('en-GB'))),
m('a', { href: 'Text/chapter_' + zeroFill(3, num + 1) + '.xhtml' }, ch.title), m('a', { href: 'Text/chapter_' + zeroFill(3, num + 1) + '.xhtml' }, ch.title),
@ -334,8 +334,8 @@ export function createNav (ffc) {
} }
export function createNotesNav (ffc) { export function createNotesNav (ffc) {
let list = ffc.chaptersWithNotes.map((num) => { const list = ffc.chaptersWithNotes.map((num) => {
let ch = ffc.storyInfo.chapters[num] const ch = ffc.storyInfo.chapters[num]
return m('.item', m('a.leftalign', { href: 'Text/note_' + zeroFill(3, num + 1) + '.xhtml' }, ch.title)) return m('.item', m('a.leftalign', { href: 'Text/note_' + zeroFill(3, num + 1) + '.xhtml' }, ch.title))
}) })
@ -362,7 +362,7 @@ export function createNotesNav (ffc) {
export function createCoverPage (ffc) { export function createCoverPage (ffc) {
let body let body
let { width, height } = ffc.coverImageDimensions const { width, height } = ffc.coverImageDimensions
if (ffc.coverImage) { if (ffc.coverImage) {
body = m('svg#cover', { xmlns: NS.SVG, 'xmlns:xlink': NS.XLINK, version: '1.1', viewBox: '0 0 ' + width + ' ' + height }, body = m('svg#cover', { xmlns: NS.SVG, 'xmlns:xlink': NS.XLINK, version: '1.1', viewBox: '0 0 ' + width + ' ' + height },
@ -402,7 +402,7 @@ function infoBox (heading, data, title) {
function calcReadingTime (ffc, wordCount = 0) { function calcReadingTime (ffc, wordCount = 0) {
const wpm = ffc.options.wordsPerMinute const wpm = ffc.options.wordsPerMinute
let time = (wordCount || ffc.totalWordCount) / wpm const time = (wordCount || ffc.totalWordCount) / wpm
let value = 0 let value = 0
let unit = '' let unit = ''
if (time < 1) { if (time < 1) {

View file

@ -9,13 +9,13 @@ import { unicode } from './constants'
export function replaceAsync (str, re, callback) { export function replaceAsync (str, re, callback) {
// http://es5.github.io/#x15.5.4.11 // http://es5.github.io/#x15.5.4.11
str = String(str) str = String(str)
let parts = [] const parts = []
let i = 0 let i = 0
if (Object.prototype.toString.call(re) === '[object RegExp]') { if (Object.prototype.toString.call(re) === '[object RegExp]') {
if (re.global) { re.lastIndex = i } if (re.global) { re.lastIndex = i }
let m let m
while ((m = re.exec(str))) { while ((m = re.exec(str))) {
let args = m.concat([m.index, m.input]) const args = m.concat([m.index, m.input])
parts.push(str.slice(i, m.index), callback.apply(null, args)) parts.push(str.slice(i, m.index), callback.apply(null, args))
i = re.lastIndex i = re.lastIndex
if (!re.global) { break } // for non-global regexes only take the first match if (!re.global) { break } // for non-global regexes only take the first match
@ -45,22 +45,22 @@ export function webp2png (data) {
webpdecoder = new libwebp.WebPDecoder() webpdecoder = new libwebp.WebPDecoder()
} }
let frame = WebPRiffParser(data, 0).frames[0] const frame = WebPRiffParser(data, 0).frames[0]
let width = [0] const width = [0]
let height = [0] const height = [0]
let decodedData = webpdecoder.WebPDecodeRGBA( const decodedData = webpdecoder.WebPDecodeRGBA(
data, data,
frame['src_off'], frame['src_size'], frame.src_off, frame.src_size,
width, height width, height
) )
let png = new PNGPacker({}) const png = new PNGPacker({})
let buffers = [] const buffers = []
png.on('data', (chunk) => { png.on('data', (chunk) => {
buffers.push(chunk) buffers.push(chunk)
}) })
png.once('end', () => { png.once('end', () => {
let pngData = Buffer.concat(buffers) const pngData = Buffer.concat(buffers)
resolve(pngData) resolve(pngData)
}) })
png.pack(decodedData, width[0], height[0]) png.pack(decodedData, width[0], height[0])
@ -122,7 +122,7 @@ export async function readingEase (text, wakeupInterval = Infinity, progresscb)
let lastTime = Date.now() let lastTime = Date.now()
for (let i = 0; i < tokenSentences.length; i++) { for (let i = 0; i < tokenSentences.length; i++) {
let now = Date.now() const now = Date.now()
if (lastTime + wakeupInterval < now) { if (lastTime + wakeupInterval < now) {
lastTime = now lastTime = now
if (typeof progresscb === 'function') { if (typeof progresscb === 'function') {

View file

@ -2,7 +2,7 @@
import path from 'path' import path from 'path'
import nodeExternals from 'webpack-node-externals' import nodeExternals from 'webpack-node-externals'
let inProduction = process.env.NODE_ENV === 'production' || process.argv.indexOf('-p') !== -1 const inProduction = process.env.NODE_ENV === 'production' || process.argv.indexOf('-p') !== -1
const bundleExtensionConfig = { const bundleExtensionConfig = {
entry: { entry: {