//Copyright Chris Xiong 2018
//License: Expat (MIT)
module.exports={
render:function(inf,outf,np,pp){return _render(inf,outf,np,pp);},
set_template:function(tmplf){return _set_template(tmplf);}
};
const fs=require('fs');
const jsdom=require('jsdom');
const path=require('path');
const aes=require('aes-js');
const scrypt=require('scrypt-js');
const btoa=require('btoa');
const sha256=require('./sha256').sha256;
const spawn=require('child_process').spawn;
const util=require('./util');
const htmlescape = util.htmlescape;
const THUMB_IMAGE_URL='//filestorage.chrisoft.org/blog/img/';
const THUMB_LOCAL_DIR='../content/img/';
let template_file='';
let tmplcont='';
let tocid;
let headerlist;
function texts(n)
{
if (typeof n.wholeText == 'string')
return n.wholeText;
let ret = '';
for (let c of n.childNodes)
ret += texts(c);
return ret;
}
function _dfs(doc,el,le,p)
{
var e=doc.createElement('li');
e.innerHTML=`<a class="toctarg" href="#tocanch${tocid}">${texts(el)}</a>`;
le.appendChild(e);
el.id='tocanch'+(tocid++);
el.classList.add('tvis');
var che=null,i;
for(i=p+1;i<headerlist.length;)
{
if(headerlist[i].tagName<=el.tagName)break;
if(headerlist[i].classList.contains('notoc'))continue;
if(che===null)
{
var te=doc.createElement('li');
che=doc.createElement('ul');
che.classList.add('tocnode');
te.appendChild(che);
le.appendChild(te);
}
i=_dfs(doc,headerlist[i],che,i);
}
return i;
}
function footnoter(doc)
{
var footnotes=doc.body.getElementsByTagName('footnote');
let starting=doc.getElementById('notediv').children.length;
for(var i=0;i<footnotes.length;++i)
{
var s=footnotes[i].innerHTML;
footnotes[i].innerHTML='';
var a=doc.createElement('a');
a.setAttribute('id',`n${(starting+i+1)}`);
a.setAttribute('href',`#note${(starting+i+1)}`);
a.setAttribute('class','note');
a.innerHTML=`[${(starting+i+1)}]`;
footnotes[i].parentNode.insertBefore(a,footnotes[i]);
var span=doc.createElement('span');
span.setAttribute('class','TText');
span.innerHTML=`<a id="note${(starting+i+1)}" href="#n${(starting+i+1)}">[${(starting+i+1)}]</a>: ${s}<br>`;
doc.getElementById('notediv').appendChild(span);
}
starting+=footnotes.length;
while(footnotes.length)footnotes[0].remove();
}
function gen_thumb(doc,el)
{
const a=doc.createElement('a');
const im=doc.createElement('img');
const srcfn=el.innerHTML;
let w=el.hasAttribute('width')?Number(el.getAttribute('width')):-1;
let h=el.hasAttribute('height')?Number(el.getAttribute('height')):-1;
let p=el.hasAttribute('scale')?Number(el.getAttribute('scale')):-1;
if(el.hasAttribute('iw'))
im.style.width=el.getAttribute('iw');
if(el.hasAttribute('ih'))
im.style.height=el.getAttribute('ih');
if(p>0)w=h=-1;
if(w>0||h>0)
{
if(w<=0)w=h;
else if(h<=0)h=w;
}
const dstfn=(p>0||w>0||h>0)?`ssbsthumb_${p>0?'s'+p.toFixed(2):w+'x'+h}_${srcfn}`:srcfn;
const srcf=path.join(THUMB_LOCAL_DIR,srcfn);
const dstf=path.join(THUMB_LOCAL_DIR,dstfn);
if(dstfn!=srcfn&&!util.mtime_cmp(srcf,dstf))
{
const m=spawn('magick',[srcf,'-resize',p>0?(p*100).toFixed()+'%':w+'x'+h,dstf]);
m.stdout.on('data',(d)=>{console.log(`IMout:${d}`);});
m.stderr.on('data',(d)=>{console.log(`IMerr:${d}`);});
}
a.setAttribute('href',THUMB_IMAGE_URL+srcfn);
im.src=THUMB_IMAGE_URL+dstfn;
a.appendChild(im);
el.parentNode.replaceChild(a,el);
}
async function encrypt(doc)
{
let enid=0;
for(let i of doc.querySelectorAll('encrypted'))
{
const key=i.getAttribute('key');
i.removeAttribute('key');
const contu8=aes.utils.utf8.toBytes(i.innerHTML);
const conths=sha256.hash(aes.utils.hex.fromBytes(contu8),{msgFormat:'hex-bytes'});
const keyu8=aes.utils.utf8.toBytes(key);
const saltu8=aes.utils.utf8.toBytes('mmmdeliciousthermalpasteletmeconsumeallofit');
const enckey=scrypt.syncScrypt(keyu8, saltu8, 1024, 16, 2, 32);
const ctr=new aes.ModeOfOperation.ctr(enckey);
const enccont=ctr.encrypt(contu8);
let encconts='';for(let i=0;i<enccont.length;++i)encconts+=String.fromCharCode(enccont[i]);
encconts=btoa(encconts);
i.setAttribute('hash',conths);
i.setAttribute('encont',encconts);
i.setAttribute('enid',enid);
i.innerHTML=
`This section is encrypted. Click <a href="javascript:void(0)" onclick="decryptui(${enid})">here</a>
to decrypt. <noscript>Note that you need have JavaScript enabled for decryption.</noscript>`;
++enid;
}
}
function _set_template(tmplf)
{
template_file=tmplf;
tmplcont=fs.readFileSync(template_file,'utf8');
}
function _style_to_header(doc)
{
const head = doc.querySelector('head');
const body = doc.querySelector('body');
const styles = body.querySelectorAll('style');
for (let s of styles)
head.appendChild(s);
}
async function _render(inf,outf,np,pp)
{
const postcont=fs.readFileSync(inf,'utf8');
const tr=new jsdom.JSDOM(tmplcont);
const trd=tr.window.document;
const cont=fs.readFileSync(inf,'utf8');
const contsplit=cont.split('\n');
const meta=contsplit.splice(0,4);
const preproc=meta[3].trim();
let unlisted = false;
if (meta[1].indexOf('UNLISTED') != -1)
{
unlisted = true;
meta[1] = meta[1].substr(0, meta[1].indexOf('UNLISTED'));
}
trd.getElementById('title').innerHTML=
trd.getElementById('titleh').innerHTML=meta[0].trim();
trd.getElementById('datetags').innerHTML=`${meta[1].trim()}<br>#${meta[2].split(',').join(' #')}`;
if(preproc.length)
{
const ppargs=preproc.split(' ');
const ppcmd=ppargs.splice(0,1);
const pp=spawn(ppcmd[0],ppargs,{maxBuffer:1048576, stdio:['pipe', 'pipe', 'inherit']});
let out='';
pp.stdout.setEncoding('utf8');
pp.stdout.on('data',(d)=>{out+=d;});
pp.stdin.write(contsplit.join('\n'),'utf8');
const ppp = new Promise(
(resolv, rej)=>
{
pp.on('error', (e) => rej(e));
pp.on('close',(r) => { if(r) rej(r); resolv(out); });
}
);
pp.stdin.end();
try {
trd.getElementById('article').innerHTML = await ppp;
} catch (e) {
console.log(`failed to render ${inf} : ${e}`);
}
}
else
trd.getElementById('article').innerHTML=contsplit.join('\n');
//title and meta description
const titlee = trd.querySelector('title');
titlee.innerHTML = titlee.innerHTML.replace(
/%t/, htmlescape(meta[0].trim())
);
const metade = trd.querySelector('meta[name="description"]');
metade.setAttribute('content', metade.getAttribute('content').replace(/%t/, meta[0].trim()));
//async images
trd.getElementById('article').querySelectorAll('img').forEach(
(i)=>{if(!i.getAttribute('decoding'))i.setAttribute('decoding','async');}
);
//thumbnails generation
trd.getElementById('article').querySelectorAll('thumb').forEach((el)=>{gen_thumb(trd,el);});
//Encryption
await encrypt(trd);
//TOC
tocid=0;
headerlist=[];
trd.getElementById('article').querySelectorAll('h2,h3,h4,h5,h6').forEach(
(i)=>{if(!i.classList.contains('notoc'))headerlist.push(i);}
);
const tocroot=trd.getElementById('tocroot');
for(let i=0;i<headerlist.length;)i=_dfs(trd,headerlist[i],tocroot,i);
if(!tocroot.children.length)trd.getElementById('tocouter').style.display='none';
//Footnotes
footnoter(trd);
//Tag list
const tgs=meta[2].split(',');
for(let i=0;i<tgs.length;++i)
{
let l=trd.createElement('li');
l.innerHTML=`<a href="/blog/list/${tgs[i]}/">${tgs[i]}</a>`;
trd.getElementById('tagslist').appendChild(l);
}
_style_to_header(trd);
//neighboring posts
if (unlisted)
{
trd.getElementById('prevp').parentNode.remove();
trd.getElementById('nextp').parentNode.remove();
}
else
{
if(pp)trd.getElementById('prevp').href=`${pp}.html`;
if(np)trd.getElementById('nextp').href=`${np}.html`;
}
fs.writeFileSync(outf,"<!DOCTYPE html>"+trd.documentElement.outerHTML,'utf8');
return outf;
}