import JSZip from 'jszip' import escapeStringRegexp from 'escape-string-regexp' import zeroFill from 'zero-fill' import { decode } from 'html-entities' import sanitize from 'sanitize-filename' import { URL } from 'url' import isNode from 'detect-node' import FileType from 'file-type' import isSvg from 'is-svg' import sizeOf from 'image-size' import EventEmitter from 'events' import { buf as crc32 } from 'crc-32' import { cleanMarkup } from './cleanMarkup' import fetch from './fetch' import fetchRemote from './fetchRemote' import * as template from './templates' import { styleCss, coverstyleCss, titlestyleCss, iconsCss, navstyleCss, paragraphsCss } from './styles' import * as utils from './utils' import kepubify from './kepubify' import subsetFont from './subsetFont' import { containerXml } from './constants' const fontAwesomeCodes = require('../build/font-awesome-codes.json') const trimWhitespace = /^\s*()+|()+\s*$/ig class FimFic2Epub extends EventEmitter { static getStoryId (id) { if (isNaN(id)) { const url = new URL(id) if (url.hostname === 'www.fimfiction.net' || url.hostname === 'fimfiction.net') { const m = url.pathname.match(/^\/story\/(\d+)/) if (m) { id = m[1] } } } return id } static getFilename (storyInfo) { return sanitize(storyInfo.author.name + ' - ' + storyInfo.title + '.epub') } static fetchStoryInfo (storyId, raw = false) { return new Promise((resolve, reject) => { storyId = FimFic2Epub.getStoryId(storyId) const url = '/api/story.php?story=' + storyId fetch(url).then((content) => { let data try { data = JSON.parse(content) } catch (e) {} if (!data) { reject(new Error('Unable to fetch story info')) return } if (data.error) { reject(new Error(data.error + ' (id: ' + storyId + ')')) return } const story = data.story if (raw) { resolve(story) return } // 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 }) // Add version number story.FIMFIC2EPUB_VERSION = FIMFIC2EPUB_VERSION resolve(story) }) }) } constructor (storyId, options = {}) { super() this.storyId = FimFic2Epub.getStoryId(storyId) this.defaultOptions = { typogrify: false, addCommentsLink: true, includeAuthorNotes: true, useAuthorNotesIndex: false, showChapterHeadings: true, showChapterWordCount: true, showChapterDuration: true, includeExternal: true, paragraphStyle: 'spaced', kepubify: false, joinSubjects: false, calculateReadingEase: true, readingEaseWakeupInterval: isNode ? 50 : 200, // lower for node, to not slow down thread wordsPerMinute: 200, // 0 to disable addChapterBars: true } this.options = Object.assign(this.defaultOptions, options) // promise cache this.pcache = { metadata: null, chapters: null, remoteResources: null, coverImage: null, fetchAll: null } this.storyInfo = null this.description = '' this.subjects = [] this.chapters = [] this.chaptersHtml = [] this.notesHtml = [] this.hasAuthorNotes = false this.chaptersWithNotes = [] this.pages = {} this.remoteResourcesCached = false this.remoteResources = new Map() this.usedIcons = new Set() this.iconsFont = null this.coverUrl = '' this.coverImage = null this.coverFilename = '' this.coverType = '' this.coverImageDimensions = { width: 0, height: 0 } this.readingEase = null this.hasRemoteResources = { titlePage: false } this.cachedFile = null this.tags = [] this.zip = null } fetchAll () { if (this.pcache.fetchAll) { return this.pcache.fetchAll } this.progress(0, 0, '') this.pcache.fetchAll = this.fetchMetadata() .then(this.fetchChapters.bind(this)) .then(this.fetchCoverImage.bind(this)) .then(this.buildChapters.bind(this)) .then(this.buildPages.bind(this)) .then(this.findIcons.bind(this)) .then(this.fetchRemoteFiles.bind(this)) .then(() => { this.progress(0, 0.95) this.pcache.fetchAll = null }) return this.pcache.fetchAll } fetchMetadata () { if (this.pcache.metadata) { return this.pcache.metadata } if (this.storyInfo) { return Promise.resolve() } this.storyInfo = null this.description = '' this.subjects = [] this.progress(0, 0, 'Fetching metadata...') this.pcache.metadata = FimFic2Epub.fetchStoryInfo(this.storyId) .then((storyInfo) => { this.storyInfo = storyInfo this.storyInfo.uuid = 'urn:fimfiction:' + this.storyInfo.id 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 }) return this.pcache.metadata } 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 this.hasAuthorNotes = false this.chaptersWithNotes.length = 0 this.progress(0, 0, 'Fetching chapters...') const chapterCount = this.storyInfo.chapters.length const url = '/story/download/' + this.storyInfo.id + '/html' this.pcache.chapters = fetch(url).then((html) => { let p = Promise.resolve() const matchChapter = /
[\s\S]*?<\/header>([\s\S]*?)<\/article>/g for (let ma, i = 0; (ma = matchChapter.exec(html)); i++) { const ch = this.storyInfo.chapters[i] let chapterContent = ma[1] chapterContent = chapterContent.replace(/