Skip to content

Instantly share code, notes, and snippets.

@KCCat
Last active August 15, 2019 01:33
Show Gist options
  • Save KCCat/b288d5bae7339c709e4f4d5ab616d0d9 to your computer and use it in GitHub Desktop.
Save KCCat/b288d5bae7339c709e4f4d5ab616d0d9 to your computer and use it in GitHub Desktop.
转换轻国论坛帖子到epub. 用法: 0.切换到只看楼主模式 1.鼠标选取章节 2. 猴子扩展里的按钮开始运行 3. 完成后帖子标题转为下载链接
// ==UserScript==
// @name lightnovel2epub
// @description 快速输出半成品epub3.0
// @author KCC
// @version 0.99.25
// @namespace lightnovel2epub
// @match *://www.lightnovel.us/forum.php*
// @match *://www.lightnovel.cn/forum.php*
// @require https://github.com/Stuk/jszip/raw/master/dist/jszip.min.js
// @grant GM_registerMenuCommand
// @grant GM_setClipboard
// @grant GM_xmlhttpRequest
// ==/UserScript==
var Ngen = (function* () { var i = 0; while(true) {yield '_' + (i++).toString() + '.jpeg';}})();
var Pstarter = () => {
var rD,rP;
rP = new Promise((D,F) => {rD = D;});
return [rP,rD];
};
var GM_ajax = (b={}) => new Promise((D,F) => {
b.onload = (e) => D(e),
b.onerror = (e) => F(e);
GM_xmlhttpRequest(b);
});
var ajax = (b={}) => new Promise((D,F) => {
let xhr = new XMLHttpRequest();
xhr.onload = (e) => D(e),
xhr.onerror = (e) => F(e);
xhr.open(b.method, b.url);
xhr.responseType = b.responseType;
xhr.send();
});
var f = e => {
var d = document.createElement('div');
if(e.nodeName == '#text') {
let ne = document.createElement('p');
ne.textContent = e.textContent.trim();
d.append(ne);
if(ne.textContent.indexOf('作者')!=-1
&& ne.textContent.split(/\s/).join('').split(/[::]/).length==2
&& opf.split('{{creator}}').length > 1) {
opf=opf.split('{{creator}}').join(ne.textContent.split(/\s/).join('').split(/[::]/)[1]);
console.log('lightnovel2epub: create epub <p> <img>');
}
} else
if(e.nodeName == 'IGNORE_JS_OP') {
var ne = document.createElement('img');
let sfile = e.querySelector('strong')||e.querySelector('a[id]')
if (sfile) {
ne.setAttribute('srcepub', 'images/' + sfile.textContent.split(/\s/).join('') + Ngen.next().value);
ne.setAttribute('srclink', e.querySelector('a').getAttribute('href'));
} else {ne = document.createElement('p');}
d.append(ne);
} else
if(e.nodeName == 'IMG' && e.hasAttribute('file')) {
let ne = document.createElement('img');
if (IMGfix) {
let url = 'http' + e.getAttribute('file').split('https')[1];
e.src = url;
e.setAttribute('file', url);
}
ne.setAttribute('srcepub', 'images/' + e.getAttribute('file').split('?')[0].split('/').pop() + Ngen.next().value)
ne.setAttribute('srclink', e.getAttribute('file'));
d.append(ne);
} else if(e.className != 'locked')
{
d.append(...[...e.childNodes].map(f).reduce((a, e) => a.concat([...e.childNodes]),[]));
}
return d;
};
var img_1 = e => {
let sblob = URL.createObjectURL(e.response),
timg = document.createElement('img');
Pr = new Promise((D, F) => {
timg.addEventListener('load',() => D(timg))
,timg.addEventListener('error',() => F(timg));
});
timg.src = sblob;
//URL.revokeObjectURL(sblob);
return Pr;
};
var img_2 = (timg) => {
var Pr;
if (timg.naturalWidth > timg.naturalHeight) {
let tscanvas = document.createElement('canvas'),
tdcanvas = document.createElement('canvas');
tscanvas.width = timg.naturalWidth,
tscanvas.height = timg.naturalWidth,
tdcanvas.width = timg.naturalHeight,
tdcanvas.height = timg.naturalWidth;
tsC = tscanvas.getContext('2d'),
tdC = tdcanvas.getContext('2d'),
thh = ~~(timg.naturalHeight/2);
tsC.translate(thh, thh),
tsC.rotate(90 * Math.PI / 180),
tsC.drawImage(timg, -thh, -thh),
tdC.drawImage(tscanvas, 0, 0);
Pr = new Promise((D, F) => tdcanvas.toBlob(D, 'image/jpeg', 1));
} else {
let tscanvas = document.createElement('canvas');
tscanvas.width = timg.naturalWidth,
tscanvas.height = timg.naturalHeight;
tsC = tscanvas.getContext('2d'),
tsC.drawImage(timg, 0, 0),
Pr = new Promise((D, F) => tscanvas.toBlob(D, 'image/jpeg', 1));
}
return Pr;
};
console.log('lightnovel2epub: IMG url testing ...');
var IMGfix = 0;
var querySelectorIMG = document.querySelector('[id*=postmessage_] img[id],.pattl img[id]');
if (querySelectorIMG) {
GM_ajax({
method: "HEAD",
url: querySelectorIMG.getAttribute('file'),
})
.then(() => console.log('lightnovel2epub: IMG testing OK'))
.catch((r) => {
[...document.querySelectorAll('[id*=postmessage_] img[id],.pattl img[id]')]
.forEach(e => {
let url = 'http' + e.getAttribute('file').split('https')[1];
e.src = url;
e.setAttribute('file', url);
});
IMGfix = 1;
console.log('lightnovel2epub: IMG url fixed');
});
}
console.log('lightnovel2epub: zip loading');
var epub = new JSZip();
epub.file("mimetype", "application/epub+zip");
epub.file("META-INF/container.xml", '<?xml version="1.0" encoding="UTF-8"?>\n<container xmlns="urn:oasis:names:tc:opendocument:xmlns:container" version="1.0">\n<rootfiles>\n<rootfile full-path="OEBPS/content.opf" media-type="application/oebps-package+xml"/>\n</rootfiles>\n</container>');
epub.file("OEBPS/main.css", "img{\ndisplay: block;\nmargin: 0 auto;\n}");
var opf = '<?xml version="1.0" encoding="UTF-8"?>\n<package xmlns="http://www.idpf.org/2007/opf" version="3.0" unique-identifier="uid">\n<metadata xmlns:dc="http://purl.org/dc/elements/1.1/">\n<dc:identifier id="uid">{{uid}}</dc:identifier>\n<dc:title>{{title}}</dc:title>\n<dc:creator>{{creator}}</dc:creator>\n<dc:language>zh</dc:language>\n<meta property="dcterms:modified">{{time}}</meta>\n</metadata>\n<manifest>\n<item href="main.xhtml" id="main" media-type="application/xhtml+xml"/>\n<item href="nav.xhtml" id="nav" media-type="application/xhtml+xml" properties="nav"/>\n<item href="main.css" media-type="text/css" id="css"/>\n<item href="images/main_cover.jpeg" media-type="image/jpeg" id="cover" properties="cover-image"/>\n{{images}}</manifest>\n<spine>\n<itemref idref="main"/>\n<itemref idref="nav" linear="no"/>\n</spine>\n</package>';
var opf_img = '<item href="{{srcepub}}" media-type="image/jpeg" id="{{file}}" />\n';
var nav = '<?xml version="1.0" encoding="utf-8"?>\n<html xmlns="http://www.w3.org/1999/xhtml" xmlns:epub="http://www.idpf.org/2007/ops">\n<head>\n<meta charset="utf-8" />\n<title>Table of Contents</title>\n<link rel="stylesheet" type="text/css" href="main.css" />\n</head>\n<body>\n<nav epub:type="toc" id="toc">\n<h1 class="title">Table of Contents</h1>\n<ol>\n<li id="main"><a href="main.xhtml">{{title}}</a></li>\n{{nav}}\n<li id="nav"><a href="nav.xhtml">Table of Contents</a></li>\n</ol>\n</nav>\n</body>\n</html>';
var nav_p = '<li id="{{p}}"><a href="main.xhtml#{{p}}">{{h}}</a></li>\n';
var xhtml = '<?xml version="1.0" encoding="utf-8"?>\n<html xmlns="http://www.w3.org/1999/xhtml" xmlns:epub="http://www.idpf.org/2007/ops">\n<head>\n<meta charset="utf-8"/>\n<title>{{title}}</title>\n<link rel="stylesheet" type="text/css" href="main.css"/>\n</head>\n<body>\n{{main}}\n</body>\n</html>';
var xhtml_h ='<h3 id="{{p}}">{{h}}</h3>\n';
var [P_doc, doc_end] = Pstarter();
var [P_img_s, img_start] = Pstarter();
var [P_cover_s, cover_start] = Pstarter();
var [P_img_e, img_end] = Pstarter();
var [P_cover_e, cover_end] = Pstarter();
var final_list = [P_doc,P_img_e,P_cover_e];
var ss;
let Mf = (D,F) => {
console.log('lightnovel2epub: start! loading all page');
ss = window.getSelection().toString().split('\n').map(e => e.trim());
var pgl = [];
if (document.querySelector('.pg span')) {
let maxpg = Number(document.querySelector('.pg span').innerText.split(/[^\d]/).join(''));
let URL = document.documentURI.split(/&page=\d+&/).join('&');
for (let i=1; i <= maxpg; i++){
pgl.push({
method: "GET",
url: URL+'&page='+i,
responseType: "document",
});
}
} else {
let URL = document.documentURI.split(/&page=\d+&/).join('&');
pgl = [{
method: "GET",
url: URL,
responseType: "document",
}];
};
console.log('lightnovel2epub: document do same thin');
Promise.all(pgl.map((e) => ajax(e)))
.then((e) => e.map((e) => [...e.target.responseXML.querySelectorAll('[id*=postmessage_], .pattl')]))
.then((e) => e.reduce((a, e) => a.concat(e),[])
.filter((e) => !e.querySelector('.quote'))
.reduce((a, e) => a.concat([...e.childNodes]),[])
.filter((e) => e.nodeName != 'I')
.map(f)
.reduce((a, e) => a.concat([...e.childNodes]),[])
)
.then((e) => {img_start(e.filter(E=>E.nodeName === 'IMG').map(E=>E.cloneNode()));
console.log('lightnovel2epub: create epub img start');
return e;
})
.then((e) => e.map(e => {if(e.nodeName == 'IMG') e.removeAttribute('srclink'); return e.outerHTML;})
.join('\n')
.split(/(?:<p><\/p>\n)+/).join('<p></p>\n')
.split('srcepub').join('src')
)
.then((End) => {
ss.forEach((e,i) => {
if (e) {
End = End.replace(RegExp('^((?:.|\n)*)<p>'+ e.split(/\s/).join('').split('').join('\\s*?') +'</p>\n((?:.|\n)*?)$'), '$1'+ xhtml_h.split('{{h}}').join(e).split('{{p}}').join('main'+i).split('\n').join('') +'\n$2');
nav = nav.split('{{nav}}').join(nav_p.split('{{h}}').join(e).split('{{p}}').join('main'+i)+'{{nav}}');
}
});
End = End.replace(/^(<img.*?)>$/mg, '$1 />');
return End;
})
.then((End) => {
var tslt = document.querySelector('#thread_subject').innerText;
xhtml = xhtml
.split('{{main}}').join(End)
.split('{{title}}').join(tslt);
nav = nav
.split('{{title}}').join(tslt);
opf = opf
.split('{{time}}').join(new Date().toJSON())
.split('{{uid}}').join('UID'+new Date().getTime())
.split('{{title}}').join(tslt);
epub.file("OEBPS/main.xhtml", xhtml);
epub.file("OEBPS/nav.xhtml", nav.split('{{nav}}').join(''));
GM_setClipboard(End);
})
.then(() => doc_end());
};
P_img_s.then((e) => {
Promise.all(
e.map((e1, i) => GM_ajax(
{
method: "GET",
url: e1.getAttribute('srclink'),
responseType: "blob",
})
.then(e => img_1(e).then(e => [e, e1]))
.catch(e => {
console.log('lightnovel2epub: img load error');
return 'error img load';
})
)
)
.then(a => a.filter(E => E !== 'error'))
.then(a => {
var cover_ed = 0;
var tmp_end = a.filter(([e, E]) => {
if (Math.max(e.naturalWidth, e.naturalHeight) >= 600) {
if (!cover_ed) {
cover_start(e);
cover_ed = 1;
}
return true;
} else {
console.log('lightnovel2epub: img too small');
return false;
}
});
if (!tmp_end.length) {
console.log('lightnovel2epub: no img');
throw 'error no img';
} else return tmp_end;
})
.then(a => a.map(([e, E]) => [img_2(e), E]))
.then(a => a.map(([b, e]) => {
var tmp_srcepub = e.getAttribute('srcepub');
epub.file("OEBPS/"+tmp_srcepub, b);
let temp_img = opf_img
.split('{{srcepub}}').join(tmp_srcepub)
.split('{{file}}').join(tmp_srcepub);
opf = opf.split('{{images}}').join(temp_img+'{{images}}');
}))
.catch(cover_end)
})
.then(() => img_end());
P_cover_s.then((timg) => new Promise((D, F) => {
console.log('lightnovel2epub: create epub cover img');
if (timg.naturalWidth > timg.naturalHeight) {
// bakacrop.js start
var luma = (id, od) => {
for (let i=0; i < id.length; i+=4) {
od[i+1] = 0.21*id[i]+ 0.72*id[i+1]+ 0.07*id[i+2]
,od[i+3] = 255;
}
}
var sobel_edge = (p1, p2, p3
,p4, p6
,p7, p8, p9) => {
let Gy = p1+ 2*p2+ p3
+
-p7+ -2*p8+ -p9,
Gx = p1 + -p3+
2*p4 + -2*p6+
p7 + -p9;
return (Math.abs(Gx) + Math.abs(Gy))/6;
}
var sobel = (id,o) => {
var w4 = o.width*4
,isN = Number.isInteger;
for (var l=0; l < o.height; l++) {
for (var x=0; x < o.width; x++) {
let p = l * w4 + x*4 +1;
let p1 = isN(id[p-w4-4]) ? id[p-w4-4] : 255
,p2 = isN(id[p-w4 ]) ? id[p-w4 ] : 255
,p3 = isN(id[p-w4+4]) ? id[p-w4+4] : 255
,p4 = isN(id[p -4]) ? id[p -4] : 255
,p6 = isN(id[p +4]) ? id[p +4] : 255
,p7 = isN(id[p+w4-4]) ? id[p+w4-4] : 255
,p8 = isN(id[p+w4 ]) ? id[p+w4 ] : 255
,p9 = isN(id[p+w4+4]) ? id[p+w4+4] : 255;
o.data[p] = sobel_edge(p1, p2, p3
,p4, p6
,p7, p8, p9);
}
}
}
var tscanvas = document.createElement('canvas')
,ys = Math.max(~~(Math.max(timg.naturalWidth, timg.naturalHeight)/1080),2);
tscanvas.width = ~~(timg.naturalWidth /ys)
tscanvas.height = ~~(timg.naturalHeight /ys);
var tsC = tscanvas.getContext('2d');
tsC.drawImage(timg, 0, 0, tscanvas.width, tscanvas.height);
var i = tsC.getImageData(0, 0, tscanvas.width, tscanvas.height)
,o = new ImageData(tscanvas.width, tscanvas.height);
var id = i.data
,od = o.data;
luma(id, od);
sobel(o.data.slice(), o);
tsC.putImageData(o, 0, 0);
var t = tsC.getImageData(0, 0, o.width, o.height)
,cw = ~~(o.height /1448*1072)
,s = [[], 0, []];
for (var x=0; x < t.width; x++) {
var _ls = 0;
for (var l=0; l < t.height; l++) {
let p = (x + t.width * l)*4 +1;
_ls += t.data[p];
}
s[1] += _ls
,s[0].unshift(_ls);
if (s[0].length === cw) {
s[2].push(s[1])
,s[1] -= s[0].pop();
}
}
var rx = s[2].indexOf(Math.max(...s[2])) * ys;
let tdcanvas = document.createElement('canvas');
tdcanvas.width = ~~(timg.naturalHeight/1448*1072),
tdcanvas.height = timg.naturalHeight,
tdC = tdcanvas.getContext('2d');
tdC.drawImage(timg, rx, 0, tdcanvas.width, tdcanvas.height, 0, 0, tdcanvas.width, tdcanvas.height);
// bakacrop.js end
tdcanvas.toBlob(D, 'image/jpeg', 1);
} else {
let tscanvas = document.createElement('canvas');
tscanvas.width = timg.naturalWidth,
tscanvas.height = timg.naturalHeight;
tsC = tscanvas.getContext('2d'),
tsC.drawImage(timg, 0, 0),
tscanvas.toBlob(D, 'image/jpeg', 1);
}
})
)
.then((b) => {
epub.file("OEBPS/images/main_cover.jpeg", b);
console.log('lightnovel2epub: create epub cover img end');
cover_end();
})
.catch(e=>{
console.log('lightnovel2epub: create epub cover img error');
});
Promise.all(final_list).then(v => {
epub.file("OEBPS/content.opf", opf.split('{{images}}').join(''));
console.log('lightnovel2epub: ziping');
return epub.generateAsync({
type: "blob",
mimeType: "application/epub+zip",
compression: "DEFLATE",
compressionOptions: {level: 9}
});
})
.then(u => {
console.log('lightnovel2epub: Done!');
console.debug(u);
let fname = document.querySelector('#thread_subject').innerText+'.epub';
var fhref = URL.createObjectURL(u);
document.querySelector('#thread_subject').outerHTML = '<a id="thread_subject" href="'+ fhref +'" download="'+ fname +'">'+ fname +'</a>';
location.href += '#thread_subject';
window.addEventListener("close", function(event) {
URL.revokeObjectURL(fhref);
}, false);
});
GM_registerMenuCommand('lightnovel2epub: Run', Mf);
console.log('lightnovel2epub: JSZip.version='+JSZip.version);
//console.debug(GM_ajax);
//console.debug(final_list);
//P_img_s.then((e) => console.debug(e));
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment