mirror of
https://github.com/daniel-j/fimfic2epub.git
synced 2024-05-21 21:03:33 +12:00
Upgrade mithril, bugfixes, UTF8 character support
This commit is contained in:
parent
019bbf9c9a
commit
d3dbcc150b
|
@ -1,5 +1,12 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
// Fix for mithril
|
||||
const noop = () => {}
|
||||
global.window = {
|
||||
document: { createDocumentFragment: noop },
|
||||
history: { pushState: noop }
|
||||
}
|
||||
|
||||
const FimFic2Epub = require('../fimfic2epub')
|
||||
const fs = require('fs')
|
||||
|
||||
|
@ -31,7 +38,7 @@ ffc.fetchAll()
|
|||
} else {
|
||||
stream = fs.createWriteStream(filename)
|
||||
}
|
||||
ffc.streamFile()
|
||||
ffc.streamFile(null)
|
||||
.pipe(stream)
|
||||
.on('finish', () => {
|
||||
if (!outputStdout) {
|
||||
|
|
|
@ -29,6 +29,7 @@ let watchOpts = {
|
|||
|
||||
webpackConfig.forEach((c) => {
|
||||
if (inProduction) {
|
||||
c.plugins.push(new webpack.optimize.ModuleConcatenationPlugin())
|
||||
c.plugins.push(new webpack.LoaderOptionsPlugin({
|
||||
minimize: true,
|
||||
debug: false
|
||||
|
|
|
@ -26,7 +26,6 @@ const entities = new XmlEntities()
|
|||
const trimWhitespace = /^\s*(<br\s*\/?\s*>)+|(<br\s*\/?\s*>)+\s*$/ig
|
||||
|
||||
class FimFic2Epub extends Emitter {
|
||||
|
||||
static getStoryId (id) {
|
||||
if (isNaN(id)) {
|
||||
let url = URL.parse(id, false, true)
|
||||
|
@ -370,10 +369,10 @@ class FimFic2Epub extends Emitter {
|
|||
this.zip.file('OEBPS/' + this.coverFilename, this.coverImage)
|
||||
}
|
||||
this.zip.file('OEBPS/Text/cover.xhtml', template.createCoverPage(this))
|
||||
this.zip.file('OEBPS/Styles/coverstyle.css', coverstyleCss)
|
||||
this.zip.file('OEBPS/Styles/coverstyle.css', Buffer.from(coverstyleCss, 'utf8'))
|
||||
|
||||
this.zip.file('OEBPS/Text/title.xhtml', template.createTitlePage(this))
|
||||
this.zip.file('OEBPS/Styles/titlestyle.css', titlestyleCss)
|
||||
this.zip.file('OEBPS/Styles/titlestyle.css', Buffer.from(titlestyleCss, 'utf8'))
|
||||
|
||||
this.zip.file('OEBPS/Text/nav.xhtml', template.createNav(this, 0))
|
||||
this.zip.file('OEBPS/toc.ncx', template.createNcx(this))
|
||||
|
@ -381,7 +380,7 @@ class FimFic2Epub extends Emitter {
|
|||
for (let i = 0; i < this.chapters.length; i++) {
|
||||
let filename = 'OEBPS/Text/chapter_' + zeroFill(3, i + 1) + '.xhtml'
|
||||
let html = this.chaptersHtml[i]
|
||||
this.zip.file(filename, html)
|
||||
this.zip.file(filename, Buffer.from(html, 'utf8'))
|
||||
}
|
||||
|
||||
if (this.options.includeAuthorNotes && this.options.useAuthorNotesIndex && this.hasAuthorNotes) {
|
||||
|
@ -391,11 +390,11 @@ class FimFic2Epub extends Emitter {
|
|||
if (!this.chapters[i].notes) continue
|
||||
let filename = 'OEBPS/Text/note_' + zeroFill(3, i + 1) + '.xhtml'
|
||||
let html = this.notesHtml[i]
|
||||
this.zip.file(filename, html)
|
||||
this.zip.file(filename, Buffer.from(html, 'utf8'))
|
||||
}
|
||||
}
|
||||
|
||||
this.zip.file('OEBPS/Styles/style.css', styleCss + '\n\n' + (paragraphsCss[this.options.paragraphStyle] || ''))
|
||||
this.zip.file('OEBPS/Styles/style.css', Buffer.from(styleCss + '\n\n' + (paragraphsCss[this.options.paragraphStyle] || ''), 'utf8'))
|
||||
|
||||
this.remoteResources.forEach((r) => {
|
||||
if (r.dest) {
|
||||
|
@ -412,7 +411,9 @@ class FimFic2Epub extends Emitter {
|
|||
if (this.cachedFile) {
|
||||
return Promise.resolve(this.cachedFile)
|
||||
}
|
||||
this.progress(0, 0.95, 'Compressing...')
|
||||
this.progress(0, 0, 'Compressing...')
|
||||
|
||||
let lastPercent = -1
|
||||
|
||||
return this.zip
|
||||
.generateAsync({
|
||||
|
@ -420,6 +421,12 @@ class FimFic2Epub extends Emitter {
|
|||
mimeType: 'application/epub+zip',
|
||||
compression: 'DEFLATE',
|
||||
compressionOptions: {level: 9}
|
||||
}, (metadata) => { // onUpdate
|
||||
let currentPercent = Math.round(metadata.percent / 10) * 10
|
||||
if (lastPercent !== currentPercent) {
|
||||
lastPercent = currentPercent
|
||||
this.progress(0, currentPercent / 100, 'Compressing...')
|
||||
}
|
||||
})
|
||||
.then((file) => {
|
||||
this.progress(0, 1, 'Complete!')
|
||||
|
@ -429,7 +436,7 @@ class FimFic2Epub extends Emitter {
|
|||
}
|
||||
|
||||
// example usage: .pipe(fs.createWriteStream(filename))
|
||||
streamFile () {
|
||||
streamFile (onUpdate) {
|
||||
if (!this.zip) {
|
||||
return null
|
||||
}
|
||||
|
@ -440,7 +447,7 @@ class FimFic2Epub extends Emitter {
|
|||
mimeType: 'application/epub+zip',
|
||||
compression: 'DEFLATE',
|
||||
compressionOptions: {level: 9}
|
||||
})
|
||||
}, onUpdate)
|
||||
}
|
||||
|
||||
setTitle (title) {
|
||||
|
|
|
@ -2,129 +2,121 @@
|
|||
import m from 'mithril'
|
||||
import { XmlEntities } from 'html-entities'
|
||||
import twemoji from 'twemoji'
|
||||
import render from './lib/mithril-node-render'
|
||||
import render from 'mithril-node-render'
|
||||
|
||||
import fetch from './fetch'
|
||||
import { youtubeKey } from './constants'
|
||||
import { replaceAsync } from './utils'
|
||||
|
||||
const entities = new XmlEntities()
|
||||
|
||||
export function cleanMarkup (html) {
|
||||
export async function cleanMarkup (html) {
|
||||
if (!html) {
|
||||
return Promise.resolve('')
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
html = twemoji.parse(html, {ext: '.svg', folder: 'svg'})
|
||||
html = html.replace(/(<img class="emoji" draggable="false" alt=".*?" src=".*?")>/g, '$1/>')
|
||||
// replace HTML entities with decimal entities
|
||||
html = html.replace(/ /g, ' ')
|
||||
html = html.replace(/ /g, ' ')
|
||||
html = twemoji.parse(html, {ext: '.svg', folder: 'svg'})
|
||||
// replace HTML entities with decimal entities
|
||||
html = html.replace(/ /g, ' ')
|
||||
html = html.replace(/ /g, ' ')
|
||||
|
||||
// fix some tags
|
||||
html = html.replace(/<u>/g, '<span style="text-decoration: underline">')
|
||||
html = html.replace(/<\/u>/g, '</span>')
|
||||
html = html.replace(/<s>/g, '<span style="text-decoration: line-through">')
|
||||
html = html.replace(/<\/s>/g, '</span>')
|
||||
// fix some tags
|
||||
html = html.replace(/<u>/g, '<span style="text-decoration: underline">')
|
||||
html = html.replace(/<\/u>/g, '</span>')
|
||||
html = html.replace(/<s>/g, '<span style="text-decoration: line-through">')
|
||||
html = html.replace(/<\/s>/g, '</span>')
|
||||
|
||||
html = html.replace(/<p>\s*/g, '<p>')
|
||||
html = html.replace(/\s*<\/p>/g, '</p>')
|
||||
html = html.replace(/<p>\s*/g, '<p>')
|
||||
html = html.replace(/\s*<\/p>/g, '</p>')
|
||||
|
||||
// html = fixParagraphIndent(html)
|
||||
// html = fixParagraphIndent(html)
|
||||
|
||||
html = fixDoubleSpacing(html)
|
||||
html = fixDoubleSpacing(html)
|
||||
|
||||
// fix floating blockquote tags
|
||||
html = html.replace('<blockquote style="margin: 10px 0px; box-sizing:border-box; -moz-box-sizing:border-box;margin-right:25px; padding: 15px;background-color: #F7F7F7;border: 1px solid #AAA;width: 50%;float:left;box-shadow: 5px 5px 0px #EEE;">', '<blockquote class="left_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">')
|
||||
// fix floating blockquote tags
|
||||
html = html.replace('<blockquote style="margin: 10px 0px; box-sizing:border-box; -moz-box-sizing:border-box;margin-right:25px; padding: 15px;background-color: #F7F7F7;border: 1px solid #AAA;width: 50%;float:left;box-shadow: 5px 5px 0px #EEE;">', '<blockquote class="left_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">')
|
||||
|
||||
let imageEmbed = /<img data-src="(.*?)" class="user_image" src="(.*?)" data-lightbox\/>/g
|
||||
html = html.replace(imageEmbed, (match, originalUrl, cdnUrl) => {
|
||||
return render(m('img', {src: entities.decode(cdnUrl), alt: 'Image'}))
|
||||
})
|
||||
|
||||
// Fix links pointing to pages on fimfiction
|
||||
// Example: <a href="/user/djazz" rel="nofollow">djazz</a>
|
||||
let matchLink = /(<a .?href=")(.+?)(".+?>)/g
|
||||
html = html.replace(matchLink, (match, head, url, tail) => {
|
||||
if (url.substring(0, 1) !== '#' && url.substring(0, 2) !== '//' && url.substring(0, 4) !== 'http') {
|
||||
if (url.substring(0, 1) === '/') {
|
||||
url = 'http://www.fimfiction.net' + entities.decode(url)
|
||||
} else {
|
||||
// do something else
|
||||
}
|
||||
}
|
||||
|
||||
return head + url + tail
|
||||
})
|
||||
|
||||
let cache = new Map()
|
||||
let completeCount = 0
|
||||
|
||||
let matchYouTube = /<p><a class="embed" href="https:\/\/www\.youtube\.com\/watch\?v=(.*?)">.*?<\/a><\/p>/g
|
||||
for (let ma; (ma = matchYouTube.exec(html));) {
|
||||
let youtubeId = ma[1]
|
||||
cache.set(youtubeId, null)
|
||||
}
|
||||
|
||||
let matchSoundCloud = /<p><a class="embed" href="(https:\/\/soundcloud\.com\/.*?)">.*?<\/a><\/p>/g
|
||||
html = html.replace(matchSoundCloud, (match, url) => {
|
||||
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, ' '))
|
||||
]))
|
||||
})
|
||||
|
||||
if (cache.size === 0) {
|
||||
continueParsing()
|
||||
} else {
|
||||
getYoutubeInfo([...cache.keys()])
|
||||
}
|
||||
|
||||
function getYoutubeInfo (ids) {
|
||||
fetch('https://www.googleapis.com/youtube/v3/videos?id=' + ids + '&part=snippet&maxResults=50&key=' + youtubeKey).then((raw) => {
|
||||
let data = []
|
||||
try {
|
||||
data = JSON.parse(raw).items
|
||||
} catch (e) { }
|
||||
data.forEach((video) => {
|
||||
cache.set(video.id, video.snippet)
|
||||
completeCount++
|
||||
})
|
||||
if (completeCount === cache.size || data.length === 0) {
|
||||
html = html.replace(matchYouTube, replaceYouTube)
|
||||
continueParsing()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function replaceYouTube (match, id) {
|
||||
let youtubeId = id
|
||||
let thumbnail = 'http://img.youtube.com/vi/' + youtubeId + '/hqdefault.jpg'
|
||||
let youtubeUrl = 'https://youtube.com/watch?v=' + youtubeId
|
||||
let title = 'Youtube Video'
|
||||
let caption = ''
|
||||
let data = cache.get(youtubeId)
|
||||
if (data) {
|
||||
thumbnail = (data.thumbnails.standard || data.thumbnails.high || data.thumbnails.medium || data.thumbnails.default).url
|
||||
title = data.title
|
||||
caption = data.title + ' on YouTube'
|
||||
} else {
|
||||
return ''
|
||||
}
|
||||
return render(m('figure.youtube', [
|
||||
m('a', {href: youtubeUrl, rel: 'nofollow'},
|
||||
m('img', {src: thumbnail, alt: title})
|
||||
),
|
||||
m('figcaption', m('a', {href: youtubeUrl, rel: 'nofollow'}, caption))
|
||||
]))
|
||||
}
|
||||
|
||||
function continueParsing () {
|
||||
// html = tidy(html, tidyOptions).trim()
|
||||
|
||||
resolve(html)
|
||||
}
|
||||
let imageEmbed = /<img data-src="(.*?)" class="user_image" src="(.*?)" data-lightbox\/>/g
|
||||
html = await replaceAsync(html, imageEmbed, (match, originalUrl, cdnUrl) => {
|
||||
return render(m('img', {src: entities.decode(cdnUrl), alt: 'Image'}), {strict: true})
|
||||
})
|
||||
|
||||
// Fix links pointing to pages on fimfiction
|
||||
// Example: <a href="/user/djazz" rel="nofollow">djazz</a>
|
||||
let matchLink = /(<a .?href=")(.+?)(".+?>)/g
|
||||
html = html.replace(matchLink, (match, head, url, tail) => {
|
||||
if (url.substring(0, 1) !== '#' && url.substring(0, 2) !== '//' && url.substring(0, 4) !== 'http') {
|
||||
if (url.substring(0, 1) === '/') {
|
||||
url = 'http://www.fimfiction.net' + entities.decode(url)
|
||||
} else {
|
||||
// do something else
|
||||
}
|
||||
}
|
||||
|
||||
return head + url + tail
|
||||
})
|
||||
|
||||
let cache = new Map()
|
||||
let completeCount = 0
|
||||
|
||||
let matchYouTube = /<p><a class="embed" href="https:\/\/www\.youtube\.com\/watch\?v=(.*?)">.*?<\/a><\/p>/g
|
||||
for (let ma; (ma = matchYouTube.exec(html));) {
|
||||
let youtubeId = ma[1]
|
||||
cache.set(youtubeId, null)
|
||||
}
|
||||
|
||||
let matchSoundCloud = /<p><a class="embed" href="(https:\/\/soundcloud\.com\/.*?)">.*?<\/a><\/p>/g
|
||||
html = await replaceAsync(html, matchSoundCloud, (match, url) => {
|
||||
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, ' '))
|
||||
]), {strict: true})
|
||||
})
|
||||
|
||||
if (cache.size === 0) {
|
||||
return html
|
||||
} else {
|
||||
return getYoutubeInfo([...cache.keys()])
|
||||
}
|
||||
|
||||
async function getYoutubeInfo (ids) {
|
||||
return fetch('https://www.googleapis.com/youtube/v3/videos?id=' + ids + '&part=snippet&maxResults=50&key=' + youtubeKey).then(async (raw) => {
|
||||
let data = []
|
||||
try {
|
||||
data = JSON.parse(raw).items
|
||||
} catch (e) { }
|
||||
data.forEach((video) => {
|
||||
cache.set(video.id, video.snippet)
|
||||
completeCount++
|
||||
})
|
||||
if (completeCount === cache.size || data.length === 0) {
|
||||
html = await replaceAsync(html, matchYouTube, replaceYouTube)
|
||||
return html
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function replaceYouTube (match, id) {
|
||||
let youtubeId = id
|
||||
let thumbnail = 'http://img.youtube.com/vi/' + youtubeId + '/hqdefault.jpg'
|
||||
let youtubeUrl = 'https://youtube.com/watch?v=' + youtubeId
|
||||
let title = 'Youtube Video'
|
||||
let caption = ''
|
||||
let data = cache.get(youtubeId)
|
||||
if (data) {
|
||||
thumbnail = (data.thumbnails.standard || data.thumbnails.high || data.thumbnails.medium || data.thumbnails.default).url
|
||||
title = data.title
|
||||
caption = data.title + ' on YouTube'
|
||||
} else {
|
||||
return Promise.resolve('')
|
||||
}
|
||||
return render(m('figure.youtube', [
|
||||
m('a', {href: youtubeUrl, rel: 'nofollow'},
|
||||
m('img', {src: thumbnail, alt: title})
|
||||
),
|
||||
m('figcaption', m('a', {href: youtubeUrl, rel: 'nofollow'}, caption))
|
||||
]), {strict: true})
|
||||
}
|
||||
}
|
||||
|
||||
export function fixDoubleSpacing (html) {
|
||||
|
|
|
@ -1,132 +0,0 @@
|
|||
'use strict'
|
||||
|
||||
function isArray (thing) {
|
||||
return Object.prototype.toString.call(thing) === '[object Array]'
|
||||
}
|
||||
|
||||
function camelToDash (str) {
|
||||
return str.replace(/\W+/g, '-')
|
||||
.replace(/([a-z\d])([A-Z])/g, '$1-$2')
|
||||
}
|
||||
|
||||
function removeEmpties (n) {
|
||||
return n !== ''
|
||||
}
|
||||
|
||||
// shameless stolen from https://github.com/punkave/sanitize-html
|
||||
function escapeHtml (s, replaceDoubleQuote) {
|
||||
if (s === 'undefined') {
|
||||
s = ''
|
||||
}
|
||||
if (typeof (s) !== 'string') {
|
||||
s = s + ''
|
||||
}
|
||||
s = s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
||||
if (replaceDoubleQuote) {
|
||||
return s.replace(/"/g, '"')
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
function createAttrString (view, escapeAttributeValue) {
|
||||
var attrs = view.attrs
|
||||
|
||||
if (!attrs || !Object.keys(attrs).length) {
|
||||
return ''
|
||||
}
|
||||
|
||||
return Object.keys(attrs).map(function (name) {
|
||||
var value = attrs[name]
|
||||
if (typeof value === 'undefined' || value === null || typeof value === 'function') {
|
||||
return
|
||||
}
|
||||
if (typeof value === 'boolean') {
|
||||
return value ? ' ' + name : ''
|
||||
}
|
||||
if (name === 'style') {
|
||||
if (!value) {
|
||||
return
|
||||
}
|
||||
var styles = attrs.style
|
||||
if (typeof styles === 'object') {
|
||||
styles = Object.keys(styles).map(function (property) {
|
||||
return styles[property] !== '' ? [camelToDash(property).toLowerCase(), styles[property]].join(':') : ''
|
||||
}).filter(removeEmpties).join(';')
|
||||
}
|
||||
return styles !== '' ? ' style="' + escapeAttributeValue(styles, true) + '"' : ''
|
||||
}
|
||||
|
||||
// Handle SVG <use> tags specially
|
||||
if (name === 'href' && view.tag === 'use') {
|
||||
return ' xlink:href="' + escapeAttributeValue(value, true) + '"'
|
||||
}
|
||||
|
||||
return ' ' + (name === 'className' ? 'class' : name) + '="' + escapeAttributeValue(value, true) + '"'
|
||||
}).join('')
|
||||
}
|
||||
|
||||
function createChildrenContent (view) {
|
||||
if (isArray(view.children) && !view.children.length) {
|
||||
return ''
|
||||
}
|
||||
|
||||
return render(view.children)
|
||||
}
|
||||
|
||||
function render (view, options) {
|
||||
options = options || {}
|
||||
|
||||
var defaultOptions = {
|
||||
escapeAttributeValue: escapeHtml,
|
||||
escapeString: escapeHtml
|
||||
}
|
||||
|
||||
Object.keys(defaultOptions).forEach(function (key) {
|
||||
if (!options.hasOwnProperty(key)) options[key] = defaultOptions[key]
|
||||
})
|
||||
|
||||
var type = typeof view
|
||||
|
||||
if (type === 'string') {
|
||||
return options.escapeString(view)
|
||||
}
|
||||
|
||||
if (type === 'number' || type === 'boolean') {
|
||||
return view
|
||||
}
|
||||
|
||||
if (!view) {
|
||||
return ''
|
||||
}
|
||||
|
||||
if (isArray(view)) {
|
||||
return view.map(function (view) { return render(view, options) }).join('')
|
||||
}
|
||||
|
||||
// compontent
|
||||
if (view.view) {
|
||||
var scope = view.controller ? new view.controller() : {}
|
||||
var result = render(view.view(scope), options)
|
||||
if (scope.onunload) {
|
||||
scope.onunload()
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
if (view.$trusted) {
|
||||
return '' + view
|
||||
}
|
||||
var children = createChildrenContent(view)
|
||||
if (!children) {
|
||||
return '<' + view.tag + createAttrString(view, options.escapeAttributeValue) + '/>'
|
||||
}
|
||||
return [
|
||||
'<', view.tag, createAttrString(view, options.escapeAttributeValue), '>',
|
||||
children,
|
||||
'</', view.tag, '>'
|
||||
].join('')
|
||||
}
|
||||
|
||||
render.escapeHtml = escapeHtml
|
||||
|
||||
module.exports = render
|
106
src/main.js
106
src/main.js
|
@ -3,6 +3,7 @@
|
|||
|
||||
import FimFic2Epub from './FimFic2Epub'
|
||||
import m from 'mithril'
|
||||
import prop from 'mithril/stream'
|
||||
import { saveAs } from 'file-saver'
|
||||
import autosize from 'autosize'
|
||||
|
||||
|
@ -79,13 +80,13 @@ dialogContainer.id = 'epubDialogContainer'
|
|||
document.body.appendChild(dialogContainer)
|
||||
|
||||
let checkbox = {
|
||||
view: function (ctrl, args, text) {
|
||||
view: function ({attrs, children}) {
|
||||
return m('label.toggleable-switch', [
|
||||
m('input', Object.assign({
|
||||
type: 'checkbox'
|
||||
}, args)),
|
||||
}, attrs)),
|
||||
m('a'),
|
||||
text ? m('span', text) : null
|
||||
children ? m('span', children) : null
|
||||
])
|
||||
}
|
||||
}
|
||||
|
@ -107,58 +108,56 @@ function redraw (arg) {
|
|||
}
|
||||
}
|
||||
|
||||
let ffcProgress = m.prop(0)
|
||||
let ffcStatus = m.prop('')
|
||||
let ffcProgress = prop(0)
|
||||
let ffcStatus = prop('')
|
||||
|
||||
let dialog = {
|
||||
controller (args) {
|
||||
oninit () {
|
||||
const ctrl = this
|
||||
|
||||
ffcProgress(0)
|
||||
|
||||
this.isLoading = m.prop(true)
|
||||
this.dragging = m.prop(false)
|
||||
this.xpos = m.prop(0)
|
||||
this.ypos = m.prop(0)
|
||||
this.el = m.prop(null)
|
||||
this.coverFile = m.prop(null)
|
||||
this.coverUrl = m.prop('')
|
||||
this.checkboxCoverUrl = m.prop(false)
|
||||
this.isLoading = prop(true)
|
||||
this.dragging = prop(false)
|
||||
this.xpos = prop(0)
|
||||
this.ypos = prop(0)
|
||||
this.el = prop(null)
|
||||
this.coverFile = prop(null)
|
||||
this.coverUrl = prop('')
|
||||
this.checkboxCoverUrl = prop(false)
|
||||
|
||||
this.title = m.prop('')
|
||||
this.author = m.prop('')
|
||||
this.description = m.prop('')
|
||||
this.subjects = m.prop([])
|
||||
this.addCommentsLink = m.prop(ffc.options.addCommentsLink)
|
||||
this.includeAuthorNotes = m.prop(ffc.options.includeAuthorNotes)
|
||||
this.useAuthorNotesIndex = m.prop(ffc.options.useAuthorNotesIndex)
|
||||
this.addChapterHeadings = m.prop(ffc.options.addChapterHeadings)
|
||||
this.includeExternal = m.prop(ffc.options.includeExternal)
|
||||
this.joinSubjects = m.prop(ffc.options.joinSubjects)
|
||||
this.paragraphStyle = m.prop(ffc.options.paragraphStyle)
|
||||
this.title = prop('')
|
||||
this.author = prop('')
|
||||
this.description = prop('')
|
||||
this.subjects = prop([])
|
||||
this.addCommentsLink = prop(ffc.options.addCommentsLink)
|
||||
this.includeAuthorNotes = prop(ffc.options.includeAuthorNotes)
|
||||
this.useAuthorNotesIndex = prop(ffc.options.useAuthorNotesIndex)
|
||||
this.addChapterHeadings = prop(ffc.options.addChapterHeadings)
|
||||
this.includeExternal = prop(ffc.options.includeExternal)
|
||||
this.joinSubjects = prop(ffc.options.joinSubjects)
|
||||
this.paragraphStyle = prop(ffc.options.paragraphStyle)
|
||||
|
||||
this.onOpen = function (el, isInitialized) {
|
||||
if (!isInitialized) {
|
||||
this.el(el)
|
||||
this.onOpen = function (vnode) {
|
||||
this.el(vnode.dom)
|
||||
this.center()
|
||||
this.isLoading(true)
|
||||
ffc.fetchMetadata().then(() => {
|
||||
this.isLoading(false)
|
||||
ffcProgress(-1)
|
||||
this.title(ffc.storyInfo.title)
|
||||
this.author(ffc.storyInfo.author.name)
|
||||
this.description(ffc.storyInfo.short_description)
|
||||
this.subjects(ffc.subjects.slice(0))
|
||||
redraw(true)
|
||||
this.center()
|
||||
this.isLoading(true)
|
||||
ffc.fetchMetadata().then(() => {
|
||||
this.isLoading(false)
|
||||
ffc.fetchChapters().then(() => {
|
||||
ffcProgress(-1)
|
||||
this.title(ffc.storyInfo.title)
|
||||
this.author(ffc.storyInfo.author.name)
|
||||
this.description(ffc.storyInfo.short_description)
|
||||
this.subjects(ffc.subjects.slice(0))
|
||||
redraw(true)
|
||||
this.center()
|
||||
ffc.fetchChapters().then(() => {
|
||||
ffcProgress(-1)
|
||||
redraw()
|
||||
})
|
||||
}).catch((err) => {
|
||||
throw err
|
||||
redraw()
|
||||
})
|
||||
}
|
||||
}).catch((err) => {
|
||||
throw err
|
||||
})
|
||||
}
|
||||
|
||||
this.setCoverFile = (e) => {
|
||||
|
@ -186,7 +185,7 @@ let dialog = {
|
|||
|
||||
this.ondown = (e) => {
|
||||
let rect = this.el().firstChild.getBoundingClientRect()
|
||||
let offset = {x: e.pageX - rect.left - document.body.scrollLeft, y: e.pageY - rect.top - document.body.scrollTop}
|
||||
let offset = {x: e.pageX - rect.left - document.documentElement.scrollLeft, y: e.pageY - rect.top - document.documentElement.scrollTop}
|
||||
this.dragging(true)
|
||||
let onmove = (e) => {
|
||||
e.preventDefault()
|
||||
|
@ -215,8 +214,8 @@ let dialog = {
|
|||
if (this.dragging()) return
|
||||
let rect = this.el().firstChild.getBoundingClientRect()
|
||||
this.move(
|
||||
Math.max(document.body.scrollLeft, (window.innerWidth / 2) - (rect.width / 2) + document.body.scrollLeft),
|
||||
Math.max(document.body.scrollTop, (window.innerHeight / 2) - (rect.height / 2) + document.body.scrollTop)
|
||||
Math.max(document.documentElement.scrollLeft, (window.innerWidth / 2) - (rect.width / 2) + document.documentElement.scrollLeft),
|
||||
Math.max(document.documentElement.scrollTop, (window.innerHeight / 2) - (rect.height / 2) + document.documentElement.scrollTop)
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -261,8 +260,9 @@ let dialog = {
|
|||
}
|
||||
},
|
||||
|
||||
view (ctrl, args, extras) {
|
||||
return m('.drop-down-pop-up-container', {config: ctrl.onOpen.bind(ctrl)}, m('.drop-down-pop-up', {style: {'min-width': '700px'}}, [
|
||||
view (vnode) {
|
||||
let ctrl = vnode.state
|
||||
return m('.drop-down-pop-up-container', {oncreate: ctrl.onOpen.bind(ctrl)}, m('.drop-down-pop-up', {style: {'min-width': '700px'}}, [
|
||||
m('h1', {onmousedown: ctrl.ondown}, m('i.fa.fa-book'), 'Export to EPUB', m('a.close_button', {onclick: closeDialog})),
|
||||
m('.drop-down-pop-up-content', [
|
||||
ctrl.isLoading() ? m('div', {style: 'text-align:center;'}, m('i.fa.fa-spin.fa-spinner', {style: 'font-size:50px; margin:20px; color:#777;'})) : m('table.properties', [
|
||||
|
@ -293,9 +293,9 @@ let dialog = {
|
|||
)),
|
||||
|
||||
m('tr', m('td.section_header', {colspan: 3}, m('b', 'Metadata customization'))),
|
||||
m('tr', m('td.label', {style: 'vertical-align: top;'}, 'Description'), m('td', {colspan: 2}, m('textarea', {config: autosize, onchange: ctrl.setDescription}, ctrl.description()))),
|
||||
m('tr', m('td.label', {style: 'vertical-align: top;'}, 'Description'), m('td', {colspan: 2}, m('textarea', {oncreate: ({dom}) => autosize(dom), onchange: ctrl.setDescription}, ctrl.description()))),
|
||||
m('tr', m('td.label', {style: 'vertical-align: top;'}, 'Categories'), m('td', {colspan: 2},
|
||||
m('textarea', {rows: 2, config: autosize, onchange: ctrl.setSubjects}, ctrl.subjects().join('\n')),
|
||||
m('textarea', {rows: 2, oncreate: ({dom}) => autosize(dom), onchange: ctrl.setSubjects}, ctrl.subjects().join('\n')),
|
||||
m(checkbox, {checked: ctrl.joinSubjects(), onchange: m.withAttr('checked', ctrl.joinSubjects)}, 'Join categories and separate with commas (for iBooks only)')
|
||||
))
|
||||
]),
|
||||
|
@ -315,12 +315,12 @@ let dialog = {
|
|||
}
|
||||
|
||||
let dialogOpen = false
|
||||
function openDialog (args, extras) {
|
||||
function openDialog () {
|
||||
if (dialogOpen) {
|
||||
return
|
||||
}
|
||||
dialogOpen = true
|
||||
m.mount(dialogContainer, m(dialog, args, extras))
|
||||
m.mount(dialogContainer, dialog)
|
||||
}
|
||||
function closeDialog () {
|
||||
dialogOpen = false
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
|
||||
import m from 'mithril'
|
||||
import render from './lib/mithril-node-render'
|
||||
import render from 'mithril-node-render'
|
||||
import { pd as pretty } from 'pretty-data'
|
||||
import zeroFill from 'zero-fill'
|
||||
|
||||
|
@ -23,22 +23,22 @@ function prettyDate (d) {
|
|||
}
|
||||
|
||||
export function createChapter (ch) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let {content, notes, notesFirst, title, link, linkNotes} = ch
|
||||
let {content, notes, notesFirst, title, link, linkNotes} = ch
|
||||
|
||||
let sections = [
|
||||
m.trust(content || ''),
|
||||
notes ? m('div#author_notes', {className: notesFirst ? 'top' : 'bottom'}, [
|
||||
m('p', m('b', 'Author\'s Note:')),
|
||||
m.trust(notes)]) : null
|
||||
]
|
||||
let sections = [
|
||||
m.trust(content || ''),
|
||||
notes ? m('div#author_notes', {className: notesFirst ? 'top' : 'bottom'}, [
|
||||
m('p', m('b', 'Author\'s Note:')),
|
||||
m.trust(notes)]) : null
|
||||
]
|
||||
|
||||
// if author notes are a the beginning of the chapter
|
||||
if (notes && notesFirst) {
|
||||
sections.reverse()
|
||||
}
|
||||
// if author notes are a the beginning of the chapter
|
||||
if (notes && notesFirst) {
|
||||
sections.reverse()
|
||||
}
|
||||
|
||||
let chapterPage = '<?xml version="1.0" encoding="utf-8"?>\n<!DOCTYPE html>\n' + pretty.xml(render(
|
||||
return Promise.all([
|
||||
render(
|
||||
m('html', {xmlns: NS.XHTML, 'xmlns:epub': NS.OPS}, [
|
||||
m('head', [
|
||||
m('meta', {charset: 'utf-8'}),
|
||||
|
@ -57,11 +57,12 @@ export function createChapter (ch) {
|
|||
) : null
|
||||
])
|
||||
])
|
||||
))
|
||||
|
||||
chapterPage = chapterPage.replace('%%HTML_CONTENT%%', '\n' + render(sections) + '\n')
|
||||
|
||||
resolve(chapterPage)
|
||||
, {strict: true}),
|
||||
render(sections)
|
||||
]).then(([chapterPage, sectionsData]) => {
|
||||
chapterPage = '<?xml version="1.0" encoding="utf-8"?>\n<!DOCTYPE html>\n' + pretty.xml(chapterPage)
|
||||
chapterPage = chapterPage.replace('%%HTML_CONTENT%%', '\n' + sectionsData + '\n')
|
||||
return chapterPage
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -139,7 +140,7 @@ export function createOpf (ffc) {
|
|||
subjects = [subjects.join(', ')]
|
||||
}
|
||||
|
||||
let contentOpf = '<?xml version="1.0" encoding="utf-8"?>\n' + pretty.xml(render(
|
||||
return render(
|
||||
m('package', {xmlns: NS.OPF, version: '3.0', 'unique-identifier': 'BookId'}, [
|
||||
m('metadata', {'xmlns:dc': NS.DC, 'xmlns:opf': NS.OPF}, [
|
||||
m('dc:identifier#BookId', ffc.storyInfo.uuid),
|
||||
|
@ -185,9 +186,10 @@ export function createOpf (ffc) {
|
|||
m('reference', {type: 'toc', title: 'Contents', href: 'Text/nav.xhtml'})
|
||||
])
|
||||
])
|
||||
))
|
||||
// console.log(contentOpf)
|
||||
return contentOpf
|
||||
, {strict: true}).then((contentOpf) => {
|
||||
contentOpf = '<?xml version="1.0" encoding="utf-8"?>\n' + pretty.xml(contentOpf)
|
||||
return Buffer.from(contentOpf, 'utf8')
|
||||
})
|
||||
}
|
||||
|
||||
function navPoints (list) {
|
||||
|
@ -202,7 +204,7 @@ function navPoints (list) {
|
|||
return arr
|
||||
}
|
||||
export function createNcx (ffc) {
|
||||
let tocNcx = '<?xml version="1.0" encoding="utf-8" ?>\n' + pretty.xml(render(
|
||||
return render(
|
||||
m('ncx', {version: '2005-1', xmlns: NS.DAISY}, [
|
||||
m('head', [
|
||||
m('meta', {content: ffc.storyInfo.uuid, name: 'dtb:uid'}),
|
||||
|
@ -217,9 +219,10 @@ export function createNcx (ffc) {
|
|||
[ch.title, 'Text/chapter_' + zeroFill(3, num + 1) + '.xhtml']
|
||||
), ffc.options.includeAuthorNotes && ffc.options.useAuthorNotesIndex && ffc.hasAuthorNotes ? [['Author\'s Notes', 'Text/notesnav.xhtml']] : null)))
|
||||
])
|
||||
))
|
||||
// console.log(tocNcx)
|
||||
return tocNcx
|
||||
, {strict: true}).then((tocNcx) => {
|
||||
tocNcx = '<?xml version="1.0" encoding="utf-8" ?>\n' + pretty.xml(tocNcx)
|
||||
return Buffer.from(tocNcx, 'utf8')
|
||||
})
|
||||
}
|
||||
|
||||
export function createNav (ffc, mode = 0) {
|
||||
|
@ -248,7 +251,7 @@ export function createNav (ffc, mode = 0) {
|
|||
break
|
||||
}
|
||||
|
||||
let navDocument = '<?xml version="1.0" encoding="utf-8"?>\n<!DOCTYPE html>\n' + pretty.xml(render(
|
||||
return render(
|
||||
m('html', {xmlns: NS.XHTML, 'xmlns:epub': NS.OPS}, [
|
||||
m('head', [
|
||||
m('meta', {charset: 'utf-8'}),
|
||||
|
@ -262,9 +265,10 @@ export function createNav (ffc, mode = 0) {
|
|||
])
|
||||
])
|
||||
])
|
||||
))
|
||||
// console.log(navDocument)
|
||||
return navDocument
|
||||
, {strict: true}).then((navDocument) => {
|
||||
navDocument = '<?xml version="1.0" encoding="utf-8"?>\n<!DOCTYPE html>\n' + pretty.xml(navDocument)
|
||||
return Buffer.from(navDocument, 'utf8')
|
||||
})
|
||||
}
|
||||
|
||||
export function createCoverPage (ffc) {
|
||||
|
@ -283,7 +287,7 @@ export function createCoverPage (ffc) {
|
|||
]
|
||||
}
|
||||
|
||||
let coverPage = '<?xml version="1.0" encoding="utf-8"?>\n<!DOCTYPE html>\n' + pretty.xml(render(
|
||||
return render(
|
||||
m('html', {xmlns: NS.XHTML, 'xmlns:epub': NS.OPS}, [
|
||||
m('head', [
|
||||
ffc.coverImage ? m('meta', {name: 'viewport', content: 'width=' + width + ', height=' + height}) : null,
|
||||
|
@ -292,9 +296,10 @@ export function createCoverPage (ffc) {
|
|||
]),
|
||||
m('body', {'epub:type': 'cover'}, body)
|
||||
])
|
||||
))
|
||||
// console.log(coverPage)
|
||||
return coverPage
|
||||
, {strict: true}).then((coverPage) => {
|
||||
coverPage = '<?xml version="1.0" encoding="utf-8"?>\n<!DOCTYPE html>\n' + pretty.xml(coverPage)
|
||||
return Buffer.from(coverPage, 'utf8')
|
||||
})
|
||||
}
|
||||
|
||||
function infoBox (heading, data) {
|
||||
|
@ -315,7 +320,7 @@ function calcWordCount (chapters) {
|
|||
}
|
||||
|
||||
export function createTitlePage (ffc) {
|
||||
let titlePage = '<?xml version="1.0" encoding="utf-8"?>\n<!DOCTYPE html>\n' + pretty.xml(render(
|
||||
return render(
|
||||
m('html', {xmlns: NS.XHTML, 'xmlns:epub': NS.OPS}, [
|
||||
m('head', [
|
||||
m('meta', {charset: 'utf-8'}),
|
||||
|
@ -357,8 +362,9 @@ export function createTitlePage (ffc) {
|
|||
])
|
||||
])
|
||||
])
|
||||
))
|
||||
titlePage = titlePage.replace('%%HTML_CONTENT%%', '\n' + ffc.storyInfo.description + '\n')
|
||||
// console.log(titlePage)
|
||||
return titlePage
|
||||
, {strict: true}).then((titlePage) => {
|
||||
titlePage = '<?xml version="1.0" encoding="utf-8"?>\n<!DOCTYPE html>\n' + pretty.xml(titlePage)
|
||||
titlePage = titlePage.replace('%%HTML_CONTENT%%', '\n' + ffc.storyInfo.description + '\n')
|
||||
return Buffer.from(titlePage, 'utf8')
|
||||
})
|
||||
}
|
||||
|
|
27
src/utils.js
Normal file
27
src/utils.js
Normal file
|
@ -0,0 +1,27 @@
|
|||
|
||||
export function replaceAsync (str, re, callback) {
|
||||
// http://es5.github.io/#x15.5.4.11
|
||||
str = String(str)
|
||||
let parts = []
|
||||
let i = 0
|
||||
if (Object.prototype.toString.call(re) === '[object RegExp]') {
|
||||
if (re.global) { re.lastIndex = i }
|
||||
let m
|
||||
while ((m = re.exec(str))) {
|
||||
let args = m.concat([m.index, m.input])
|
||||
parts.push(str.slice(i, m.index), callback.apply(null, args))
|
||||
i = re.lastIndex
|
||||
if (!re.global) { break } // for non-global regexes only take the first match
|
||||
if (m[0].length === 0) { re.lastIndex++ }
|
||||
}
|
||||
} else {
|
||||
re = String(re)
|
||||
i = str.indexOf(re)
|
||||
parts.push(str.slice(0, i), callback(re, i, str))
|
||||
i += re.length
|
||||
}
|
||||
parts.push(str.slice(i))
|
||||
return Promise.all(parts).then(function (strings) {
|
||||
return strings.join('')
|
||||
})
|
||||
}
|
Loading…
Reference in a new issue