diff options
-rw-r--r-- | README.rst | 5 | ||||
-rw-r--r-- | generator/atomgen.js | 46 | ||||
-rwxr-xr-x | generator/main.js | 35 | ||||
-rw-r--r-- | generator/package.json | 2 | ||||
-rw-r--r-- | generator/postrenderer.js | 9 | ||||
-rw-r--r-- | generator/scanner.js | 10 | ||||
-rw-r--r-- | generator/util.js | 11 | ||||
-rw-r--r-- | templates/list_template | 2 | ||||
-rw-r--r-- | templates/post_template | 2 |
9 files changed, 97 insertions, 25 deletions
@@ -10,10 +10,11 @@ List of features of the SBS: - Automatic table of contents and footnotes generation - Generates static webpages, ready to be served with httpd - Flexible templates - - Automatic image compression (TODO, using imagemagick) + - Automatic image compression + - Atom feed generation Dependencies: - - node.js >8.x + - node.js >20.x - jsdom - aes-js - scrypt-js diff --git a/generator/atomgen.js b/generator/atomgen.js new file mode 100644 index 0000000..ede9771 --- /dev/null +++ b/generator/atomgen.js @@ -0,0 +1,46 @@ +//Copyright Chris Xiong 2024 +//License: Expat (MIT) +module.exports = { + gen_atom:function(list, config) { _gen_atom(list, config); } +}; +const jsdom=require('jsdom'); +const path=require('path'); +const fs=require('fs'); +const htmlescape=require('./util').htmlescape; + +function _gen_atom(list, config) +{ + const sorted_list = list.toSorted((a, b) => (new Date(b.date)).getTime() - (new Date(a.date)).getTime()); + let xmlbuf = `<?xml version="1.0" encoding="utf-8"?>`; + xmlbuf += `<feed xmlns="http://www.w3.org/2005/Atom">`; + xmlbuf += `<generator uri="https://cgit.chrisoft.org/sbs.git" version="${config.version_string}">SSBS</generator>`; + xmlbuf += `<updated>${new Date().toISOString()}</updated>`; + xmlbuf += `<link href="${config.atom_url}" rel="self" type="application/atom+xml" title="Atom"/>`; + xmlbuf += `<link href="${config.published_url}" rel="alternate" type="text/html"/>`; + xmlbuf += `<id>${config.atom_url}</id>`; + xmlbuf += `<title>${config.atom_title}</title>`; + xmlbuf += `<subtitle>${config.atom_subtitle}</subtitle>`; + xmlbuf += `<icon>${config.atom_icon}</icon>`; + xmlbuf += `<logo>${config.atom_icon}</logo>`; + for (let i = 0; i < sorted_list.length && i < config.atom_feed_nposts; ++i) + { + const p = sorted_list[i]; + const postpath = path.join(path.join(config.dest_dir,'post'), `${p.file}.html`); + const contfull = fs.readFileSync(postpath, 'utf8'); + const doc = new jsdom.JSDOM(contfull).window.document; + const content = doc.querySelector('article').innerHTML; + const footnotes = doc.getElementById('notediv').outerHTML; + const link = `https://chrisoft.org/blog/post/${p.file}.html`; + xmlbuf += `<entry>`; + xmlbuf += `<title>${htmlescape(p.title)}</title>`; + xmlbuf += `<link href="${link}" title="${htmlescape(p.title)}"/>`; + xmlbuf += `<published>${new Date(p.date).toISOString()}</published>`; + xmlbuf += `<updated>${new Date(p.mdate).toISOString()}</updated>`; + xmlbuf += `<id>${link}</id>`; + xmlbuf += `<author><name>${config.atom_author}</name></author>` + xmlbuf += `<content type="html" xml:base="${link}"><![CDATA[${content}<br><hr>${footnotes}]]></content>`; + xmlbuf += `</entry>`; + } + xmlbuf += `</feed>`; + fs.writeFileSync(path.join(config.dest_dir, "atom.xml"), xmlbuf ,'utf8'); +} diff --git a/generator/main.js b/generator/main.js index ce6c8ff..3697dbd 100755 --- a/generator/main.js +++ b/generator/main.js @@ -1,22 +1,33 @@ #!/usr/bin/node //Copyright Chris Xiong 2018 //License: Expat (MIT) -const content_dir='../content'; -const template_dir='../templates'; -const dest_dir='../generated'; -const posts_per_listpage=5; +const config = { + content_dir: '../content', + template_dir: '../templates', + dest_dir: '../generated', + posts_per_listpage: 5, + // These are only used by the Atom feed generator + atom_feed_nposts: 10, + version_string: '2.6', + atom_url: 'https://chrisoft.org/blog/atom.xml', + published_url: 'https://chrisoft.org/blog/', + atom_title: 'Specluncam Ursae', + atom_subtitle: 'Chris Xiong\'s blog posts', + atom_author: 'Chris Xiong', + atom_icon: 'https://chrisoft.org/cx.png' +}; const fs=require('fs'); const path=require('path'); const scanner=require('./scanner'); -let list=scanner.scan(content_dir,dest_dir); +let list=scanner.scan(config.content_dir,config.dest_dir); const tags=scanner.build_list_index(); const taglist=Object.keys(tags).sort(); const force=process.argv.indexOf('--force')!=-1; const postrenderer=require('./postrenderer'); -postrenderer.set_template(path.join(template_dir,'post_template')); +postrenderer.set_template(path.join(config.template_dir,'post_template')); function ensure_dir(p) { @@ -32,13 +43,13 @@ function ensure_dir(p) if(!fs.statSync(p).isDirectory())throw 'shit'; } -const post_dir=path.join(dest_dir,'post'); +const post_dir=path.join(config.dest_dir,'post'); ensure_dir(post_dir); for(let j=0;j<list.length;++j){ const i=list[j]; if(i.needsupdate||force) postrenderer.render( - path.join(content_dir,`${i.file}.txt`), + path.join(config.content_dir,`${i.file}.txt`), path.join(post_dir,`${i.file}.html`), j?list[j-1].file:undefined, j<list.length-1?list[j+1].file:undefined @@ -47,11 +58,13 @@ if(i.needsupdate||force) } const listrenderer=require('./listrenderer'); -listrenderer.set_template(path.join(template_dir,'list_template')); +listrenderer.set_template(path.join(config.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; +const gen_atom = require('./atomgen').gen_atom; +gen_atom(list, config); +const list_dir=path.join(config.dest_dir,'list'); +const ppp=config.posts_per_listpage; ensure_dir(list_dir); let pc=Math.floor(list.length/ppp)+(list.length%ppp!=0); for(let i=0;i<pc;++i) diff --git a/generator/package.json b/generator/package.json index 84473e2..882433d 100644 --- a/generator/package.json +++ b/generator/package.json @@ -5,7 +5,7 @@ "child_process": "^1.0.2", "fs": "^0.0.1-security", "image-size": "^1.1.1", - "jsdom": "^24.0.0", + "jsdom": "^24.1.0", "path": "^0.12.7", "scrypt-js": "^3.0.1" } diff --git a/generator/postrenderer.js b/generator/postrenderer.js index cc29b08..64cbb99 100644 --- a/generator/postrenderer.js +++ b/generator/postrenderer.js @@ -13,6 +13,7 @@ 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/'; @@ -148,14 +149,6 @@ function _style_to_header(doc) for (let s of styles) head.appendChild(s); } -function htmlescape(s) -{ - return s.replace(/</g, "<") - .replace(/>/g, ">") - .replace(/&/g, "&") - .replace(/"/g, """) - .replace(/'/g, "'"); -} async function _render(inf,outf,np,pp) { const postcont=fs.readFileSync(inf,'utf8'); diff --git a/generator/scanner.js b/generator/scanner.js index c8c1069..a1630d1 100644 --- a/generator/scanner.js +++ b/generator/scanner.js @@ -1,5 +1,15 @@ //Copyright Chris Xiong 2018 //License: Expat (MIT) +/* Returns all scanned posts in an array in the following format: + * { + * file: String = file name of post (excluding the .txt extension) + * title: String = title extracted from the post + * date: String = post date extracted from the post (expected to be an ISO8601 full-date string, with optional post-status suffix) + * tags: String = comma-separated list of tags + * mdate: Number = source last modification time in msec since epoch + * needsupdate: boolean = true if post needs re-rendering + * } + */ module.exports={ scan:function(s,d){return _scan(s,d);}, build_list_index:function(){return _build_list_index();} diff --git a/generator/util.js b/generator/util.js index e9b5c78..85e7ab3 100644 --- a/generator/util.js +++ b/generator/util.js @@ -1,7 +1,8 @@ //Copyright Chris Xiong 2019 //License: Expat (MIT) module.exports={ - mtime_cmp:function(a,b){return _mtime_cmp(a,b);} + mtime_cmp: function(a, b){ return _mtime_cmp(a, b); }, + htmlescape: function(s){ return _htmlescape(s); } }; const fs=require('fs'); @@ -17,3 +18,11 @@ function _mtime_cmp(a,b) }catch(e){return false}; return mtimeb>=mtimea; } +function _htmlescape(s) +{ + return s.replace(/</g, "<") + .replace(/>/g, ">") + .replace(/&/g, "&") + .replace(/"/g, """) + .replace(/'/g, "'"); +} diff --git a/templates/list_template b/templates/list_template index 453a951..b589da0 100644 --- a/templates/list_template +++ b/templates/list_template @@ -62,7 +62,7 @@ function ol() <span style="text-align:right;display:table-cell;"><a id="nexpage">>></a></span> </div> <div style="text-align:center;" class="TText"> - Proudly powered by SSBS <reduced style="font-size:70%;">(the static stupid blogging system)</reduced> 2.5 + Proudly powered by SSBS <reduced style="font-size:70%;">(the static stupid blogging system)</reduced> 2.6 <br> Content licensed under CC BY-SA 4.0. </div> diff --git a/templates/post_template b/templates/post_template index caa87b5..ad20598 100644 --- a/templates/post_template +++ b/templates/post_template @@ -68,7 +68,7 @@ function ol() <div id="footer" style=""> <div id="pagesw" class="TText" style="width:100%;height:0.5em;"></div> <div style="text-align:center;" class="TText"> - Proudly powered by SSBS <reduced style="font-size:70%;">(the static stupid blogging system)</reduced> 2.5 + Proudly powered by SSBS <reduced style="font-size:70%;">(the static stupid blogging system)</reduced> 2.6 <br> Content licensed under CC BY-SA 4.0. <span id="purgep" style="display:none;font-size:70%;">This page has passphrase(s) stored. Click <a href="javascript:_purgep()">here</a> to purge.</span> </div> |