Upgrade mithril, bugfixes, UTF8 character support

This commit is contained in:
djazz 2017-10-18 15:02:07 +02:00
parent 019bbf9c9a
commit d3dbcc150b
8 changed files with 249 additions and 341 deletions

View file

@ -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) {

View file

@ -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

View file

@ -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) {

View file

@ -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(/&nbsp;/g, '&#160;')
html = html.replace(/&emsp;/g, '&#8195;')
html = twemoji.parse(html, {ext: '.svg', folder: 'svg'})
// replace HTML entities with decimal entities
html = html.replace(/&nbsp;/g, '&#160;')
html = html.replace(/&emsp;/g, '&#8195;')
// 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) {

View file

@ -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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
if (replaceDoubleQuote) {
return s.replace(/"/g, '&quot;')
}
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

View file

@ -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

View file

@ -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
View 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('')
})
}