it worksrm test.html

This commit is contained in:
daniel-j 2016-06-20 23:04:08 +02:00
parent 39433e6daf
commit 2a3c6174b2
9 changed files with 412 additions and 88 deletions

7
.babelrc Normal file
View file

@ -0,0 +1,7 @@
{
"presets": [
"node6"
],
"plugins": ["transform-strict-mode"]
}

25
extension/eventPage.js Normal file
View file

@ -0,0 +1,25 @@
function fetch(url, cb, type) {
if (url.indexOf('//') === 0) {
url = 'http:'+url
}
let x = new XMLHttpRequest()
x.open('get', url, true)
if (type) {
x.responseType = type
}
x.onload = function () {
console.log(x.getResponseHeader('content-type'))
cb(URL.createObjectURL(x.response), x.getResponseHeader('content-type'))
}
x.onerror = function () {
console.error('error')
cb(null)
}
x.send()
}
chrome.extension.onMessage.addListener(function(request, sender, sendResponse) {
fetch(request, sendResponse, 'blob')
return true
})

23
extension/manifest.json Normal file
View file

@ -0,0 +1,23 @@
{
"manifest_version": 2,
"name": "fimfic2epub",
"description": "",
"version": "1.0",
"background": {
"scripts": ["eventPage.js"],
"persistent": false
},
"content_scripts": [
{
"matches": ["https://www.fimfiction.net/story/*"],
"js": ["tidy.js", "fimfic2epub.js"]
}
],
"permissions": [
"http://*/*",
"https://*/*"
]
}

31
extension/tidy.js Normal file

File diff suppressed because one or more lines are too long

118
gulpfile.babel.js Normal file
View file

@ -0,0 +1,118 @@
'use strict'
// gulp and utilities
import gulp from 'gulp'
import gutil from 'gulp-util'
import del from 'del'
import mergeStream from 'merge-stream'
import Sequence from 'run-sequence'
import watch from 'gulp-watch'
import lazypipe from 'lazypipe'
// script
import eslint from 'gulp-eslint'
import webpack from 'webpack'
import webpackConfig from './webpack.config.babel.js'
const sequence = Sequence.use(gulp)
let inProduction = process.env.NODE_ENV === 'production' || process.argv.indexOf('-p') !== -1
let eslintOpts = {
envs: ['browser', 'node'],
rules: {
'strict': 0,
'semi': [1, 'never'],
'quotes': [1, 'single'],
'space-infix-ops': [0, {'int32Hint': true}],
'no-empty': 0
}
}
let watchOpts = {
readDelay: 500,
verbose: true
}
if (inProduction) {
webpackConfig.plugins.push(new webpack.optimize.DedupePlugin())
webpackConfig.plugins.push(new webpack.optimize.OccurenceOrderPlugin(false))
webpackConfig.plugins.push(new webpack.optimize.UglifyJsPlugin({
compress: {
warnings: false,
screw_ie8: true
},
comments: false,
mangle: {
screw_ie8: true
},
screw_ie8: true,
sourceMap: false
}))
}
let wpCompiler = webpack(Object.assign({}, webpackConfig, {
cache: {},
devtool: inProduction? null:'inline-source-map',
debug: !inProduction
}))
function webpackTask(callback) {
// run webpack
wpCompiler.run(function(err, stats) {
if(err) throw new gutil.PluginError('webpack', err)
gutil.log('[webpack]', stats.toString({
colors: true,
hash: false,
version: false,
chunks: false,
chunkModules: false
}))
callback()
})
}
let lintESPipe = lazypipe()
.pipe(eslint, eslintOpts)
.pipe(eslint.format)
// Cleanup tasks
gulp.task('clean', () => del('build'))
gulp.task('clean:script', () => {
return del('build/script')
})
// Main tasks
gulp.task('webpack', webpackTask)
gulp.task('script', ['webpack'])
gulp.task('watch:script', () => {
return watch(['src/**/*.js'], watchOpts, function () {
return sequence('script')
})
})
gulp.task('lint', () => {
return gulp.src(['src/**/*.js']).pipe(lintESPipe())
})
gulp.task('watch:lint', () => {
return watch(['src/**/*.js'], watchOpts, function (file) {
gulp.src(file.path).pipe(lintESPipe())
})
})
// Default task
gulp.task('default', (done) => {
sequence('clean', ['script', 'lint'], done)
})
// Watch task
gulp.task('watch', (done) => {
sequence('default', ['watch:lint', 'watch:script'], done)
})

View file

@ -6,10 +6,28 @@
"author": "djazz",
"dependencies": {
"escape-string-regexp": "^1.0.5",
"file-saver": "^1.3.2",
"html-entities": "^1.2.0",
"jszip": "^3.0.0",
"mithril": "^0.2.5",
"pretty-data": "^0.40.0",
"tidy-html5": "^0.1.1",
"xhr2": "^0.1.3",
"xhr2": "^0.1.3"
},
"devDependencies": {
"babel-core": "^6.9.1",
"babel-loader": "^6.2.4",
"babel-plugin-transform-strict-mode": "^6.8.0",
"babel-preset-es2015": "^6.9.0",
"babel-preset-node6": "^11.0.0",
"del": "^2.2.0",
"gulp": "^3.9.1",
"gulp-eslint": "^2.0.0",
"gulp-util": "^3.0.7",
"gulp-watch": "^4.3.8",
"lazypipe": "^1.0.1",
"merge-stream": "^1.0.0",
"run-sequence": "^1.2.1",
"webpack": "^1.13.1"
}
}

View file

@ -1,17 +1,16 @@
#!/usr/bin/env node
'use strict'
const fs = require('fs')
import m from 'mithril'
import render from './mithril-node-render'
import { pd as pretty } from 'pretty-data'
import escapeStringRegexp from 'escape-string-regexp'
import { XmlEntities } from 'html-entities'
import { saveAs } from 'file-saver'
const m = require('mithril')
const render = require('./mithril-node-render')
const pretty = require('pretty-data').pd
const tidy = require("tidy-html5").tidy_html5
const escapeStringRegexp = require('escape-string-regexp')
const entities = new XmlEntities()
const tidy = tidy_html5
const entities = new require('html-entities').XmlEntities
const XMLHttpRequest = require('xhr2')
//const XMLHttpRequest = require('xhr2')
let beautifyOptions = {
indent_size: 2,
@ -43,17 +42,35 @@ let mimeMap = {
'image/gif': 'Images/*.gif'
}
const STORY_ID = 180690 // bbcode test tags
//const STORY_ID = 180690 // bbcode test tags
//const STORY_ID = 931 // pink eyes
//const STORY_ID = 119190 // fallout equestria
const STORY_ID = document.location.pathname.match(/^\/story\/(\d*)/)[1]
let apiUrl = 'http://www.fimfiction.net/api/story.php?story=' + STORY_ID
let apiUrl = 'https://www.fimfiction.net/api/story.php?story=' + STORY_ID
let storyInfo
let remoteResources = new Map()
let chapterContent = {}
let epubButton = document.querySelector('.story_container ul.chapters li.bottom a[title="Download Story (.epub)"]')
let isDownloading = false
let cachedBlob = null
if (epubButton) {
epubButton.addEventListener('click', function (e) {
e.preventDefault()
if (isDownloading) {
return
}
if (cachedBlob) {
saveStory()
return
}
downloadStory()
}, false)
}
function fetch(url, cb, type) {
if (url.indexOf('//') === 0) {
url = 'http:'+url
@ -79,7 +96,7 @@ function fetchChapters(cb) {
function recursive() {
let ch = chapters[currentChapter]
console.log('Fetching chapter '+ch.id+' '+ch.title)
fetch(ch.link, function (html) {
fetch(ch.link.replace('http', 'https'), function (html) {
html = parseChapter(ch, html)
chapterContent[ch.id] = html
currentChapter++
@ -96,6 +113,7 @@ function fetchChapters(cb) {
function fetchRemote(cb) {
let iter = remoteResources.entries()
let counter = 0
function recursive() {
let r = iter.next().value
if (!r) {
@ -105,85 +123,117 @@ function fetchRemote(cb) {
let url = r[0]
r = r[1]
console.log('Fetching remote file '+r.filename, url)
fetch(url, function (data, type) {
r.type = type
let dest = mimeMap[type]
if (dest) {
r.dest = dest.replace('*', r.filename)
zip.file(r.dest, data)
chrome.runtime.sendMessage(url, function (objUrl) {
if (objUrl) {
fetch(objUrl, function (data, type) {
r.dest = null
r.type = type
let dest = mimeMap[type]
if (dest) {
r.dest = dest.replace('*', r.filename)
zip.file(r.dest, data)
}
URL.revokeObjectURL(objUrl)
counter++
recursive()
}, 'arraybuffer')
} else {
r.dest = null
counter++
recursive()
}
counter++
recursive()
}, 'arraybuffer')
})
}
recursive()
}
function downloadStory() {
isDownloading = true
console.log('Fetching story...')
fetch(apiUrl, function (raw) {
console.log('Fetching story...')
fetch(apiUrl, function (raw) {
let data
try {
data = JSON.parse(raw)
} catch (e) {
console.log('Unable to fetch story json')
return
}
storyInfo = data.story
storyInfo.uuid = 'urn:fimfiction:'+storyInfo.id
storyInfo.publishDate = '1970-01-01' // TODO!
console.log(storyInfo)
remoteResources.set(storyInfo.full_image, {filename: 'cover'})
zip.file('toc.ncx', createNcx())
zip.file('nav.xhtml', createNav())
fetchChapters(function () {
fetchRemote(function () {
remoteResources.forEach((r, url) => {
if (r.chapter && r.originalUrl) {
chapterContent[r.chapter] = chapterContent[r.chapter].replace(
new RegExp(escapeStringRegexp(r.originalUrl), 'g'),
r.dest
)
}
})
for (let id in chapterContent) {
let html = chapterContent[id]
let filename = 'chapter_'+id+'.xhtml'
zip.file(filename, html)
}
zip.file('cover.xhtml', createCoverPage())
zip.file('content.opf', createOpf())
zip
.generateNodeStream({
type: 'nodebuffer',
streamFiles: true,
mimeType: 'application/epub+zip',
compression: 'DEFLATE',
compressionOptions: {level: 9}
})
.pipe(fs.createWriteStream('out.epub'))
.on('finish', function () {
// JSZip generates a readable stream with a "end" event,
// but is piped here in a writable stream which emits a "finish" event.
console.log("out.epub written.");
})
})
let data
try {
data = JSON.parse(raw)
} catch (e) {
console.log('Unable to fetch story json')
return
}
storyInfo = data.story
storyInfo.uuid = 'urn:fimfiction:'+storyInfo.id
storyInfo.publishDate = '1970-01-01' // TODO!
console.log(storyInfo)
remoteResources.set(storyInfo.full_image, {filename: 'cover'})
let coverImage = new Image()
coverImage.src = storyInfo.full_image
coverImage.addEventListener('load', function () {
zip.file('toc.ncx', createNcx())
zip.file('nav.xhtml', createNav())
fetchChapters(function () {
fetchRemote(function () {
remoteResources.forEach((r, url) => {
if (r.chapter && r.originalUrl && r.dest) {
chapterContent[r.chapter] = chapterContent[r.chapter].replace(
new RegExp(escapeStringRegexp(r.originalUrl), 'g'),
r.dest
)
} else {
r.remote = true
}
})
for (let id in chapterContent) {
let html = chapterContent[id]
let filename = 'chapter_'+id+'.xhtml'
zip.file(filename, html)
}
zip.file('cover.xhtml', createCoverPage(coverImage.width, coverImage.height))
zip.file('content.opf', createOpf())
/*zip
.generateNodeStream({
type: 'nodebuffer',
streamFiles: true,
mimeType: 'application/epub+zip',
compression: 'DEFLATE',
compressionOptions: {level: 9}
})
.pipe(fs.createWriteStream('out.epub'))
.on('finish', function () {
// JSZip generates a readable stream with a "end" event,
// but is piped here in a writable stream which emits a "finish" event.
console.log("out.epub written.");
})*/
zip
.generateAsync({
type: 'blob',
mimeType: 'application/epub+zip',
compression: 'DEFLATE',
compressionOptions: {level: 9}
})
.then((blob) => {
cachedBlob = blob
saveStory()
isDownloading = false
})
})
})
}, false)
})
})
}
function saveStory() {
saveAs(cachedBlob, storyInfo.title+' by '+storyInfo.author.name+'.epub')
}
//const parse5 = require('parse5')
//const xmlserializer = require('xmlserializer')
@ -288,7 +338,10 @@ function subjects(s) {
function createOpf() {
let remotes = []
remoteResources.forEach((r) => {
remoteResources.forEach((r, url) => {
if (!r.dest) {
return
}
let attrs = {id: r.filename, href: r.dest, 'media-type': r.type}
if (r.filename === 'cover') {
attrs.properties = 'cover-image'
@ -403,19 +456,19 @@ function createNav() {
return navDocument
}
function createCoverPage() {
function createCoverPage(w, h) {
let coverPage = `<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE html>
`+pretty.xml(render(
m('html', {xmlns: NS.XHTML, 'xmlns:epub': NS.OPS}, [
m('head', [
m('meta', {name: 'viewport', content: 'width='+100+', height='+100}),
m('meta', {name: 'viewport', content: 'width='+w+', height='+h}),
m('title', 'Cover'),
m('link', {rel: 'stylesheet', type: 'text/css', href: 'coverstyle.css'})
]),
m('body', {'epub:type': 'cover'}, [
m('svg', {xmlns: NS.SVG, 'xmlns:xlink': NS.XLINK, version: '1.1', viewBox: '0 0 100 100', id: 'cover'},
m('image', {width: 100, height: 100, 'xlink:href': 'Images/cover.jpg'})
m('svg', {xmlns: NS.SVG, 'xmlns:xlink': NS.XLINK, version: '1.1', viewBox: '0 0 '+w+' '+h, id: 'cover'},
m('image', {width: w, height: h, 'xlink:href': 'Images/cover.jpg'})
)
])
])

49
webpack.config.babel.js Normal file
View file

@ -0,0 +1,49 @@
'use strict'
import webpack from 'webpack'
let inProduction = process.env.NODE_ENV === 'production' || process.argv.indexOf('-p') !== -1
export default {
entry: {
fimfic2epub: ['./src/main']
},
output: {
path: __dirname + '/',
filename: './extension/[name].js',
chunkFilename: './build/[id].js'
},
module: {
loaders: [
{
test: /\.js$/, loader: 'babel', exclude: /node_modules/, query: {
sourceMaps: inProduction,
//presets: ['es2015'],
//plugins: ['transform-strict-mode']
}
}
],
noParse: [
/[\/\\]node_modules[\/\\]tidy-html5[\/\\]tidy\.js$/
]
},
resolve: {
extensions: ['', '.js', '.json'],
root: [__dirname+'/src']
},
plugins: [
new webpack.ProvidePlugin({
// Detect and inject
//tidy: 'tidy-html5'
}),
new webpack.DefinePlugin({
})
],
devtool: 'inline-source-map',
debug: true
}