aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.rst5
-rw-r--r--generator/atomgen.js46
-rwxr-xr-xgenerator/main.js35
-rw-r--r--generator/package.json2
-rw-r--r--generator/postrenderer.js9
-rw-r--r--generator/scanner.js10
-rw-r--r--generator/util.js11
-rw-r--r--templates/list_template2
-rw-r--r--templates/post_template2
9 files changed, 97 insertions, 25 deletions
diff --git a/README.rst b/README.rst
index feee8fc..670a191 100644
--- a/README.rst
+++ b/README.rst
@@ -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, "&lt;")
- .replace(/>/g, "&gt;")
- .replace(/&/g, "&amp;")
- .replace(/"/g, "&quot;")
- .replace(/'/g, "&#39;");
-}
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, "&lt;")
+ .replace(/>/g, "&gt;")
+ .replace(/&/g, "&amp;")
+ .replace(/"/g, "&quot;")
+ .replace(/'/g, "&#39;");
+}
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">&gt;&gt;</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>