From 23325bdab3108c42f0157625f995b532a13096c7 Mon Sep 17 00:00:00 2001
From: Chris Xiong <chirs241097@gmail.com>
Date: Wed, 28 Feb 2024 23:20:32 -0500
Subject: Um... Version bump anybody?

 - Corrected treatment of "WIP" and "UNLISTED" posts.
 - Add wrapper for make4ht.
 - Improved html5 compliance (still not fully compliant tho).
 - Node 22 compatibility.
---
 generator/listrenderer.js   |   8 ++--
 generator/main.js           |   3 +-
 generator/make4htwrapper.js | 105 ++++++++++++++++++++++++++++++++++++++++++++
 generator/package.json      |  12 +++++
 generator/postrenderer.js   |  76 +++++++++++++++++++++++---------
 generator/scanner.js        |   5 ++-
 6 files changed, 183 insertions(+), 26 deletions(-)
 create mode 100755 generator/make4htwrapper.js
 create mode 100644 generator/package.json

diff --git a/generator/listrenderer.js b/generator/listrenderer.js
index ce360af..e1fd797 100644
--- a/generator/listrenderer.js
+++ b/generator/listrenderer.js
@@ -6,7 +6,7 @@ module.exports={
 };
 const fs=require('fs');
 const jsdom=require('jsdom');
-const path=require('path'); 
+const path=require('path');
 let template_file='';
 let tmplcont='';
 
@@ -20,7 +20,7 @@ async function _render(outf,pginfo,posts)
 {
 	const tr=new jsdom.JSDOM(tmplcont);
 	const trd=tr.window.document;
-	
+
 	const tags_list=trd.getElementById('tagslist');
 	const posts_list=trd.getElementById('postslist');
 	const atag_template=trd.getElementById('active_taglist_item_template');
@@ -64,7 +64,7 @@ async function _render(outf,pginfo,posts)
 	atag_template.parentNode.removeChild(atag_template);
 	tag_template.parentNode.removeChild(tag_template);
 	pitm_template.parentNode.removeChild(pitm_template);
-	
-	fs.writeFileSync(outf,trd.documentElement.outerHTML,'utf8');
+
+	fs.writeFileSync(outf,"<!DOCTYPE html>"+trd.documentElement.outerHTML,'utf8');
 	return outf;
 }
diff --git a/generator/main.js b/generator/main.js
index e25d5f2..ce6c8ff 100755
--- a/generator/main.js
+++ b/generator/main.js
@@ -10,7 +10,7 @@ const fs=require('fs');
 const path=require('path');
 
 const scanner=require('./scanner');
-const list=scanner.scan(content_dir,dest_dir);
+let list=scanner.scan(content_dir,dest_dir);
 const tags=scanner.build_list_index();
 const taglist=Object.keys(tags).sort();
 const force=process.argv.indexOf('--force')!=-1;
@@ -49,6 +49,7 @@ if(i.needsupdate||force)
 const listrenderer=require('./listrenderer');
 listrenderer.set_template(path.join(template_dir,'list_template'));
 
+list = list.filter((t) => (t.date.indexOf('UNLISTED') == -1));
 const list_dir=path.join(dest_dir,'list');
 const ppp=posts_per_listpage;
 ensure_dir(list_dir);
diff --git a/generator/make4htwrapper.js b/generator/make4htwrapper.js
new file mode 100755
index 0000000..b8e0c3a
--- /dev/null
+++ b/generator/make4htwrapper.js
@@ -0,0 +1,105 @@
+#!/usr/bin/node
+//Copyright Chris Xiong 2024
+//License: Expat (MIT)
+
+const content_dir='../content';
+const image_dir='../content/img';
+
+const process = require('node:process');
+const path = require('node:path');
+const util = require('node:util');
+const fs = require('node:fs');
+const jsdom = require('jsdom');
+const spawn = require('child_process').spawn;
+const image_size = require('image-size');
+
+function promote_header(doc)
+{
+	for (let i = 2; i <= 6; ++i)
+	{
+		const list = doc.querySelectorAll(`h${i}`);
+		for (let j = list.length - 1; j >= 0; --j)
+		{
+			const e = list[j];
+			const newe = doc.createElement(`h${i-1}`);
+			newe.innerHTML = e.innerHTML;
+			e.parentNode.replaceChild(newe, e);
+		}
+	}
+}
+
+function image_unsquash(doc, wd)
+{
+	for (let im of doc.querySelectorAll('img'))
+	{
+		const d = image_size(path.join(wd, im.src));
+		if (d.width > d.height)
+			im.removeAttribute('width');
+		else if (d.width < d.height)
+			im.removeAttribute('height');
+	}
+}
+
+function image_relocate(doc, wd, imd)
+{
+	for (let im of doc.querySelectorAll('img'))
+	{
+		fs.copyFileSync(path.join(wd, im.src), path.join(imd, im.src));
+		im.src = `//filestorage.chrisoft.org/blog/img/${im.src}`;
+	}
+}
+
+function css_embed(doc, cssc)
+{
+	const style = doc.createElement('style');
+	style.textContent = cssc.replaceAll('font-family', 'not-font-family');
+	const body = doc.querySelector('body');
+	body.insertBefore(style, body.childNodes[0]);
+}
+
+async function main()
+{
+	process.stdin.setEncoding('utf8');
+
+	const input = await new Promise((resolv, rej) => {
+		let d = '';
+		process.stdin.on('readable', () => { const r = process.stdin.read(); if (r) d += r; });
+		process.stdin.on('end',() => { resolv(d.trim()); });
+	});
+
+	const wd = path.resolve(path.join(content_dir, path.dirname(input)));
+	const fn = path.basename(input);
+	const fnb = path.basename(input, '.tex');
+
+	const make4htp = spawn('make4ht', ['-f', 'html5', fn, '"fn-in"'], {
+		cwd: wd,
+		shell: true,
+		stdio: ['ignore', 'pipe', 'pipe']
+	});
+	make4htp.stdout.setEncoding('utf8');
+	make4htp.stderr.setEncoding('utf8');
+	make4htp.stdout.on('data', (d) => { console.error(d); });
+	make4htp.stderr.on('data', (d) => { console.error(d); });
+	try {
+		await new Promise((resolv, rej) => {
+			make4htp.on('close', (r) => r ? rej(r) : resolv())
+		});
+	} catch (e) {
+		console.error(`make4ht failed with exitcode ${e}`);
+		return;
+	}
+
+	const htmlc = fs.readFileSync(path.join(wd, `${fnb}.html`), 'utf8');
+	const cssc = fs.readFileSync(path.join(wd, `${fnb}.css`), 'utf8');
+	const domrt = new jsdom.JSDOM(htmlc);
+
+	promote_header(domrt.window.document);
+	image_unsquash(domrt.window.document, wd);
+	image_relocate(domrt.window.document, wd, path.resolve(image_dir));
+	css_embed(domrt.window.document, cssc);
+
+	process.stdout.setEncoding('utf8');
+	process.stdout.write(domrt.window.document.querySelector('body').innerHTML);
+}
+
+main()
diff --git a/generator/package.json b/generator/package.json
new file mode 100644
index 0000000..84473e2
--- /dev/null
+++ b/generator/package.json
@@ -0,0 +1,12 @@
+{
+  "dependencies": {
+    "aes-js": "^3.1.2",
+    "btoa": "^1.2.1",
+    "child_process": "^1.0.2",
+    "fs": "^0.0.1-security",
+    "image-size": "^1.1.1",
+    "jsdom": "^24.0.0",
+    "path": "^0.12.7",
+    "scrypt-js": "^3.0.1"
+  }
+}
diff --git a/generator/postrenderer.js b/generator/postrenderer.js
index f3cf46b..80991a0 100644
--- a/generator/postrenderer.js
+++ b/generator/postrenderer.js
@@ -22,10 +22,20 @@ 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}">${el.innerHTML}</a>`;
+	e.innerHTML=`<a class="toctarg" href="#tocanch${tocid}">${texts(el)}</a>`;
 	le.appendChild(e);
 	el.id='tocanch'+(tocid++);
 	el.classList.add('tvis');
@@ -86,10 +96,10 @@ function gen_thumb(doc,el)
 		if(w<=0)w=h;
 		else if(h<=0)h=w;
 	}
-	const dstfn=`ssbsthumb_${p>0?'s'+p.toFixed(2):w+'x'+h}_${srcfn}`
+	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(!util.mtime_cmp(srcf,dstf))
+	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}`);});
@@ -110,11 +120,8 @@ async function encrypt(doc)
 		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('hellwhymustiaddsalttothiscrap');
-		const enckey=await new Promise(
-			(resolv,rej)=>
-			{scrypt(keyu8,saltu8,1024,16,2,32,(e,p,k)=>{e?rej(e):k?resolv(k):undefined;});}
-		);
+		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]);
@@ -133,6 +140,14 @@ 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');
@@ -142,6 +157,12 @@ async function _render(inf,outf,np,pp)
 	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(' #')}`;
@@ -149,23 +170,28 @@ async function _render(inf,outf,np,pp)
 	{
 		const ppargs=preproc.split(' ');
 		const ppcmd=ppargs.splice(0,1);
-		const pp=spawn(ppcmd[0],ppargs,{maxBuffer:1048576});
+		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)=>
+		const ppp = new Promise(
+			(resolv, rej)=>
 			{
-				pp.on('close',(r)=>{if(r)rej(r);resolv(out);});
+				pp.on('error', (e) => rej(e));
+				pp.on('close',(r) => { if(r) rej(r); resolv(out); });
 			}
 		);
 		pp.stdin.end();
-		trd.getElementById('article').innerHTML=await ppp;
+		try {
+			trd.getElementById('article').innerHTML =  await ppp;
+		} catch (e) {
+			console.log(`failed to render ${inf} : ${e}`);
+		}
 	}
 	else
 	trd.getElementById('article').innerHTML=contsplit.join('\n');
-	
+
 	//async images
 	trd.getElementById('article').querySelectorAll('img').forEach(
 		(i)=>{if(!i.getAttribute('decoding'))i.setAttribute('decoding','async');}
@@ -186,7 +212,7 @@ async function _render(inf,outf,np,pp)
 	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);
 
@@ -198,11 +224,21 @@ async function _render(inf,outf,np,pp)
 		l.innerHTML=`<a href="/blog/list/${tgs[i]}/">${tgs[i]}</a>`;
 		trd.getElementById('tagslist').appendChild(l);
 	}
-	
+
+	_style_to_header(trd);
+
 	//neighboring posts
-	if(pp)trd.getElementById('prevp').href=`${pp}.html`;
-	if(np)trd.getElementById('nextp').href=`${np}.html`;
-		
-	fs.writeFileSync(outf,trd.documentElement.outerHTML,'utf8');
+	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;
 }
diff --git a/generator/scanner.js b/generator/scanner.js
index 52744a4..c8c1069 100644
--- a/generator/scanner.js
+++ b/generator/scanner.js
@@ -34,18 +34,21 @@ function _scan(s,dst)
 				mdate:fs.statSync(path.join(s,i)).mtimeMs,
 				needsupdate:!util.mtime_cmp(path.join(s,i),path.join(pdst,i.substring(0,i.length-4)+'.html'))
 			});
-			
+
 		}
 	return list;
 }
 function _build_list_index()
 {
 	for(let i of list)
+	{
+		if (i.date.indexOf('UNLISTED') != -1) continue;
 		for(let tg of i.tags.split(','))
 		{
 			if(!tags[tg])tags[tg]={posts:[],needsupdate:false};
 			tags[tg].posts.push(i);
 			tags[tg].needsupdate|=i.needsupdate;
 		}
+	}
 	return tags;
 }
-- 
cgit v1.2.3