@bwog/bwog 0.0.0-stage → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +15 -0
- package/COPYING +21 -0
- package/README.md +191 -3
- package/app.js +20 -0
- package/bwog.config.js +5 -0
- package/bwog.service +12 -0
- package/cli.js +23 -0
- package/cloudflare/Database.js +105 -0
- package/cloudflare/worker.js +104 -0
- package/config.js +13 -0
- package/controller.js +58 -0
- package/databases/DurableObjectDatabase.js +30 -0
- package/databases/JsonDatabase.js +38 -0
- package/databases/SqliteDatabase.js +99 -0
- package/generator.js +460 -0
- package/images/logo.png +0 -0
- package/images/model.blend +0 -0
- package/nginx.config +61 -0
- package/package.json +48 -6
- package/routes/auth.js +94 -0
- package/routes/editor.js +34 -0
- package/scripts/compile_templates.js +21 -0
- package/scripts/generate_site.js +58 -0
- package/scripts/json_to_sqlite.js +20 -0
- package/stores/NodeFsStore.js +14 -0
- package/stores/R2Store.js +11 -0
- package/templates.js +244 -0
- package/views/atom_feed.eta +23 -0
- package/views/basic_html.eta +13 -0
- package/views/default.eta +168 -0
- package/views/sitemap.eta +9 -0
- package/wrangler.jsonc +72 -0
package/routes/auth.js
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import express from 'express';
|
|
2
|
+
import passport from 'passport';
|
|
3
|
+
import GoogleStrategy from 'passport-google-oauth20';
|
|
4
|
+
|
|
5
|
+
import Controller from '../controller.js';
|
|
6
|
+
import NodeFsStore from "../stores/NodeFsStore.js";
|
|
7
|
+
import JsonDatabase from "../databases/JsonDatabase.js";
|
|
8
|
+
import SqliteDatabase from "../databases/SqliteDatabase.js";
|
|
9
|
+
|
|
10
|
+
passport.use(new GoogleStrategy({
|
|
11
|
+
clientID: process.env['GOOGLE_OAUTH_CLIENT_ID'],
|
|
12
|
+
clientSecret: process.env['GOOGLE_OAUTH_CLIENT_SECRET'],
|
|
13
|
+
callbackURL: `https://${process.env['SITE_DOMAIN']}/oauth2/redirect/google`,
|
|
14
|
+
scope: ['profile', 'email'],
|
|
15
|
+
state: true,
|
|
16
|
+
store: true,
|
|
17
|
+
},
|
|
18
|
+
(accessToken, refreshToken, profile, cb) => {
|
|
19
|
+
if (process.env['GOOGLE_OAUTH_AUTHORIZED_USER_ID'] === profile.id) {
|
|
20
|
+
cb(null, {id: profile.id, name: profile.displayName, email: profile.emails[0].value});
|
|
21
|
+
} else {
|
|
22
|
+
return cb(null, false);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
));
|
|
26
|
+
|
|
27
|
+
passport.serializeUser((user, cb) => {
|
|
28
|
+
process.nextTick(() => {
|
|
29
|
+
cb(null, {id: user.id, name: user.name, email: user.email});
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
passport.deserializeUser((user, cb) => {
|
|
34
|
+
process.nextTick(() => {
|
|
35
|
+
return cb(null, user);
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const router = express.Router();
|
|
40
|
+
|
|
41
|
+
router.get('/login', (req, res, next) => {
|
|
42
|
+
passport.authenticate('google', {
|
|
43
|
+
state: {pendingPost: req.session.pendingPost},
|
|
44
|
+
})(req, res, next);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
router.get('/oauth2/redirect/google',
|
|
48
|
+
passport.authenticate('google', {
|
|
49
|
+
failureRedirect: '/loginFailure'
|
|
50
|
+
}), async (req, res) => {
|
|
51
|
+
console.log(`user logged in: ${req.user.name} <${req.user.email}>`);
|
|
52
|
+
res.cookie('isAuthed', true);
|
|
53
|
+
|
|
54
|
+
if (req.authInfo.state.pendingPost) {
|
|
55
|
+
const store = new NodeFsStore(process.env['PUBLIC_PATH']);
|
|
56
|
+
let database;
|
|
57
|
+
switch (process.env['DATABASE_TYPE']) {
|
|
58
|
+
case 'JSON':
|
|
59
|
+
database = new JsonDatabase(process.env['DATABASE_FILE']);
|
|
60
|
+
break;
|
|
61
|
+
case 'SQLITE':
|
|
62
|
+
database = new SqliteDatabase(process.env['DATABASE_FILE']);
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
await database.connect();
|
|
66
|
+
const controller = new Controller(store, database);
|
|
67
|
+
await controller.createPost(req.authInfo.state.pendingPost, req.user);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
res.redirect('/');
|
|
71
|
+
}
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
router.get('/loginFailure', (req, res) => {
|
|
75
|
+
console.log(`user failed to log in`);
|
|
76
|
+
res.clearCookie('isAuthed');
|
|
77
|
+
res.redirect('/');
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
router.get('/logout', (req, res, next) => {
|
|
81
|
+
const userName = req.user?.name;
|
|
82
|
+
const userEmail = req.user?.email;
|
|
83
|
+
res.clearCookie('isAuthed');
|
|
84
|
+
req.logout(error => {
|
|
85
|
+
if (error) {
|
|
86
|
+
console.log(`user failed to log out: ${userName} <${userEmail}>`);
|
|
87
|
+
return next(error);
|
|
88
|
+
}
|
|
89
|
+
console.log(`user logged out: ${userName} <${userEmail}>`);
|
|
90
|
+
res.redirect('/');
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
export default router;
|
package/routes/editor.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import express from 'express';
|
|
2
|
+
|
|
3
|
+
import Controller from '../controller.js';
|
|
4
|
+
import NodeFsStore from '../stores/NodeFsStore.js';
|
|
5
|
+
import JsonDatabase from '../databases/JsonDatabase.js';
|
|
6
|
+
import SqliteDatabase from '../databases/SqliteDatabase.js';
|
|
7
|
+
|
|
8
|
+
const router = express.Router();
|
|
9
|
+
|
|
10
|
+
router.post('/post', async (req, res) => {
|
|
11
|
+
if (!req.isAuthenticated()) {
|
|
12
|
+
req.session.pendingPost = req.body;
|
|
13
|
+
res.redirect('/login');
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const store = new NodeFsStore(process.env['PUBLIC_PATH']);
|
|
18
|
+
let database;
|
|
19
|
+
switch (process.env['DATABASE_TYPE']) {
|
|
20
|
+
case 'JSON':
|
|
21
|
+
database = new JsonDatabase(process.env['DATABASE_FILE']);
|
|
22
|
+
break;
|
|
23
|
+
case 'SQLITE':
|
|
24
|
+
database = new SqliteDatabase(process.env['DATABASE_FILE']);
|
|
25
|
+
break;
|
|
26
|
+
}
|
|
27
|
+
await database.connect();
|
|
28
|
+
|
|
29
|
+
const controller = new Controller(store, database);
|
|
30
|
+
await controller.createPost(req.body, req.user);
|
|
31
|
+
res.redirect('/');
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
export default router;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { readdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { Eta } from 'eta';
|
|
4
|
+
|
|
5
|
+
let templates = 'export default {';
|
|
6
|
+
|
|
7
|
+
const eta = new Eta();
|
|
8
|
+
|
|
9
|
+
const templatePaths = readdirSync('./views', { withFileTypes: true })
|
|
10
|
+
.filter(entry => entry.isFile() && path.extname(entry.name).toLowerCase() === '.eta')
|
|
11
|
+
.map(entry => entry.name);
|
|
12
|
+
|
|
13
|
+
templatePaths.forEach(templatePath => {
|
|
14
|
+
const templateFunction = eta.compile(readFileSync(path.join('./views', templatePath), 'utf8'));
|
|
15
|
+
templates += `${templatePath.split('.')[0]}: ${templateFunction.toString()},`;
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
templates += '};';
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
writeFileSync('templates.js', templates, 'utf8');
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { program, Option } from 'commander';
|
|
4
|
+
import { pathToFileURL } from 'url';
|
|
5
|
+
|
|
6
|
+
import Generator from '../generator.js';
|
|
7
|
+
import NodeFsStore from '../stores/NodeFsStore.js';
|
|
8
|
+
import JsonDatabase from '../databases/JsonDatabase.js';
|
|
9
|
+
import SqliteDatabase from '../databases/SqliteDatabase.js';
|
|
10
|
+
|
|
11
|
+
program
|
|
12
|
+
.description('Generate blog')
|
|
13
|
+
.requiredOption('-u, --url <string>', 'site URL, e.g. https://example.com')
|
|
14
|
+
.addOption(new Option('-t, --input_type <string>', 'database type').choices(['JSON', 'SQLITE']).makeOptionMandatory())
|
|
15
|
+
.requiredOption('-i, --input <string>', 'database file, e.g. ./blog.json')
|
|
16
|
+
.requiredOption('-o, --output <string>', 'output directory')
|
|
17
|
+
.requiredOption('-n, --name <string>', 'author display name')
|
|
18
|
+
.requiredOption('-e, --email <string>', 'author email address')
|
|
19
|
+
.option('-l, --enable_ap', 'If ActivityPub files should be generated', false)
|
|
20
|
+
.option('-a, --ap_handle <string>', 'ActivityPub handle, e.g. author@example.com')
|
|
21
|
+
.option('-m, --ap_username <string>', 'ActivityPub username, e.g. author')
|
|
22
|
+
.option('-k, --ap_public_key <string>', 'ActivityPub public key path, e.g. ./public.pem')
|
|
23
|
+
program.parse();
|
|
24
|
+
const options = program.opts();
|
|
25
|
+
const siteUrl = options.url;
|
|
26
|
+
const inputType = options.input_type;
|
|
27
|
+
const input = pathToFileURL(path.resolve(options.input)).href;
|
|
28
|
+
const outputDir = path.resolve(options.output);
|
|
29
|
+
const authorName = options.name;
|
|
30
|
+
const authorEmail = options.email;
|
|
31
|
+
const enableAP = !!options.enable_ap;
|
|
32
|
+
const apHandle = options.ap_handle;
|
|
33
|
+
const apUsername = options.ap_username;
|
|
34
|
+
const apPublicKey = fs.readFileSync(options.ap_public_key, 'utf8').replaceAll('\r\n', '\n').trimEnd();
|
|
35
|
+
if (enableAP && !(apHandle && apUsername && apPublicKey)) {
|
|
36
|
+
program.error("error: '--ap_*' options are required when '--enable_ap=true'");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (enableAP) {
|
|
40
|
+
fs.mkdirSync(path.resolve(outputDir, 'ap/notes'), {recursive: true});
|
|
41
|
+
fs.mkdirSync(path.resolve(outputDir, 'ap/creates'), {recursive: true});
|
|
42
|
+
fs.mkdirSync(path.resolve(outputDir, 'ap/outbox_pages'), {recursive: true});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
let database;
|
|
46
|
+
switch (inputType) {
|
|
47
|
+
case 'JSON':
|
|
48
|
+
database = new JsonDatabase(input);
|
|
49
|
+
break;
|
|
50
|
+
case 'SQLITE':
|
|
51
|
+
database = new SqliteDatabase(input);
|
|
52
|
+
break;
|
|
53
|
+
}
|
|
54
|
+
await database.connect();
|
|
55
|
+
|
|
56
|
+
const store = new NodeFsStore(outputDir);
|
|
57
|
+
const generator = new Generator(store, database);
|
|
58
|
+
await generator.generate(siteUrl, authorName, authorEmail, enableAP, apHandle, apUsername, apPublicKey);
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import JsonDatabase from '../databases/JsonDatabase.js';
|
|
2
|
+
import SqliteDatabase from '../databases/SqliteDatabase.js';
|
|
3
|
+
import { program } from 'commander';
|
|
4
|
+
import {pathToFileURL} from "url";
|
|
5
|
+
import path from "path";
|
|
6
|
+
|
|
7
|
+
program.description('Generate blog');
|
|
8
|
+
program.requiredOption('-j, --json <string>', 'json file, e.g. ./posts.json');
|
|
9
|
+
program.requiredOption('-s, --sqlite <string>', 'sqlite file, e.g. ./posts.sqlite');
|
|
10
|
+
program.parse();
|
|
11
|
+
const options = program.opts();
|
|
12
|
+
const jsonFile = pathToFileURL(path.resolve(options.json)).href;
|
|
13
|
+
const sqliteFile = pathToFileURL(path.resolve(options.sqlite)).href;
|
|
14
|
+
|
|
15
|
+
const jsonDatabase = new JsonDatabase(jsonFile);
|
|
16
|
+
await jsonDatabase.connect();
|
|
17
|
+
|
|
18
|
+
const sqliteDatabase = new SqliteDatabase(sqliteFile);
|
|
19
|
+
await sqliteDatabase.connect();
|
|
20
|
+
await sqliteDatabase.makeFromJsonDatabase(jsonDatabase);
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
class NodeFsStore {
|
|
5
|
+
constructor(outputDir) {
|
|
6
|
+
this.outputDir = outputDir;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
async write(file, data, mimeType) {
|
|
10
|
+
fs.writeFileSync(path.resolve(this.outputDir, `${file}${mimeType === 'text/html' ? '.html' : ''}`), data);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export default NodeFsStore;
|
package/templates.js
ADDED
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
export default {atom_feed: function anonymous(it,options
|
|
2
|
+
) {
|
|
3
|
+
|
|
4
|
+
let include = (__eta_t, __eta_d) => this.render(__eta_t, {...it, ...(__eta_d ?? {})}, options);
|
|
5
|
+
let includeAsync = (__eta_t, __eta_d) => this.renderAsync(__eta_t, {...it, ...(__eta_d ?? {})}, options);
|
|
6
|
+
|
|
7
|
+
let __eta = {res: "", e: this.config.escapeFunction, f: this.config.filterFunction, blocks: {}};
|
|
8
|
+
|
|
9
|
+
function layout(path, data) {
|
|
10
|
+
__eta.layout = path;
|
|
11
|
+
__eta.layoutData = data;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function output(s){__eta.res+=s;}
|
|
15
|
+
function capture(fn){const s=__eta.res;__eta.res='';try{fn();return __eta.res}finally{__eta.res=s;}}
|
|
16
|
+
async function captureAsync(fn){const s=__eta.res;__eta.res='';try{await fn();return __eta.res}finally{__eta.res=s;}}
|
|
17
|
+
function block(name,fn){if(__eta.layout){if(fn){__eta.blocks[name]=capture(fn);}return '';}const b=it.__blocks||{};if(name in b){return b[name];}return fn?capture(fn):'';}
|
|
18
|
+
async function blockAsync(name,fn){if(__eta.layout){if(fn){__eta.blocks[name]=await captureAsync(fn);}return '';}const b=it.__blocks||{};if(name in b){return b[name];}return fn?await captureAsync(fn):'';}
|
|
19
|
+
|
|
20
|
+
__eta.res+='<?xml version="1.0" encoding="UTF-8"?>\n<feed xmlns="http://www.w3.org/2005/Atom">\n <title>music blog</title>\n <id>';
|
|
21
|
+
__eta.res+=__eta.e(it.siteUrl);
|
|
22
|
+
__eta.res+='</id>\n <icon>';
|
|
23
|
+
__eta.res+=__eta.e(it.siteUrl);
|
|
24
|
+
__eta.res+='/favicon.png</icon>\n <link rel="alternate" type="text/html" href="';
|
|
25
|
+
__eta.res+=__eta.e(it.siteUrl);
|
|
26
|
+
__eta.res+='" />\n <link rel="self" href="';
|
|
27
|
+
__eta.res+=__eta.e(it.siteUrl);
|
|
28
|
+
__eta.res+='/feed.atom" />\n <updated>';
|
|
29
|
+
__eta.res+=__eta.e(it.posts[0].createdAt);
|
|
30
|
+
__eta.res+='</updated>\n <author>\n <name>';
|
|
31
|
+
__eta.res+=__eta.e(it.authorName);
|
|
32
|
+
__eta.res+='</name>\n <email>';
|
|
33
|
+
__eta.res+=__eta.e(it.authorEmail);
|
|
34
|
+
__eta.res+='</email>\n </author>\n ';
|
|
35
|
+
it.posts.forEach(post => {
|
|
36
|
+
__eta.res+=' <entry>\n <title>';
|
|
37
|
+
__eta.res+=__eta.e(post.title);
|
|
38
|
+
__eta.res+='</title>\n <link rel="alternate" type="text/html" href="';
|
|
39
|
+
__eta.res+=__eta.e(post.archivePage);
|
|
40
|
+
__eta.res+='"/>\n <id>urn:uuid:';
|
|
41
|
+
__eta.res+=__eta.e(post.uuid);
|
|
42
|
+
__eta.res+='</id>\n <published>';
|
|
43
|
+
__eta.res+=__eta.e(post.createdAt);
|
|
44
|
+
__eta.res+='</published>\n <updated>';
|
|
45
|
+
__eta.res+=__eta.e(post.createdAt);
|
|
46
|
+
__eta.res+='</updated>\n <content type="html">';
|
|
47
|
+
__eta.res+=__eta.e(post.html);
|
|
48
|
+
__eta.res+='</content>\n </entry>\n ';
|
|
49
|
+
})
|
|
50
|
+
__eta.res+='</feed>\n';
|
|
51
|
+
|
|
52
|
+
if (__eta.layout) {
|
|
53
|
+
__eta.res = include (__eta.layout, {...it, body: __eta.res, ...__eta.layoutData, __blocks: __eta.blocks});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return __eta.res;
|
|
57
|
+
|
|
58
|
+
},basic_html: function anonymous(it,options
|
|
59
|
+
) {
|
|
60
|
+
|
|
61
|
+
let include = (__eta_t, __eta_d) => this.render(__eta_t, {...it, ...(__eta_d ?? {})}, options);
|
|
62
|
+
let includeAsync = (__eta_t, __eta_d) => this.renderAsync(__eta_t, {...it, ...(__eta_d ?? {})}, options);
|
|
63
|
+
|
|
64
|
+
let __eta = {res: "", e: this.config.escapeFunction, f: this.config.filterFunction, blocks: {}};
|
|
65
|
+
|
|
66
|
+
function layout(path, data) {
|
|
67
|
+
__eta.layout = path;
|
|
68
|
+
__eta.layoutData = data;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function output(s){__eta.res+=s;}
|
|
72
|
+
function capture(fn){const s=__eta.res;__eta.res='';try{fn();return __eta.res}finally{__eta.res=s;}}
|
|
73
|
+
async function captureAsync(fn){const s=__eta.res;__eta.res='';try{await fn();return __eta.res}finally{__eta.res=s;}}
|
|
74
|
+
function block(name,fn){if(__eta.layout){if(fn){__eta.blocks[name]=capture(fn);}return '';}const b=it.__blocks||{};if(name in b){return b[name];}return fn?capture(fn):'';}
|
|
75
|
+
async function blockAsync(name,fn){if(__eta.layout){if(fn){__eta.blocks[name]=await captureAsync(fn);}return '';}const b=it.__blocks||{};if(name in b){return b[name];}return fn?await captureAsync(fn):'';}
|
|
76
|
+
|
|
77
|
+
__eta.res+='<p>';
|
|
78
|
+
__eta.res+=it.post.text.replace(/(https:\/\/\S*)/g, '<a href="$1">$1</a>').replaceAll('\n', '<br />');
|
|
79
|
+
__eta.res+='</p>\n';
|
|
80
|
+
if (it.post.type === 'song' || it.post.type === 'album') {
|
|
81
|
+
__eta.res+=' <p><i>';
|
|
82
|
+
__eta.res+=__eta.e(`${it.post.artist} - ${it.post.title}`);
|
|
83
|
+
__eta.res+='</i></p>\n';
|
|
84
|
+
} else if (it.post.type === 'playlist') {
|
|
85
|
+
__eta.res+=' <p>\n ';
|
|
86
|
+
it.post.tracks.forEach(function(song, i) {
|
|
87
|
+
__eta.res+=' ';
|
|
88
|
+
if (i !== 0) {
|
|
89
|
+
__eta.res+=' <br />\n ';
|
|
90
|
+
}
|
|
91
|
+
__eta.res+=' <i>';
|
|
92
|
+
__eta.res+=__eta.e(`${song.artist} - ${song.title}`);
|
|
93
|
+
__eta.res+='</i>\n ';
|
|
94
|
+
})
|
|
95
|
+
__eta.res+=' </p>\n';
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (__eta.layout) {
|
|
99
|
+
__eta.res = include (__eta.layout, {...it, body: __eta.res, ...__eta.layoutData, __blocks: __eta.blocks});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return __eta.res;
|
|
103
|
+
|
|
104
|
+
},default: function anonymous(it,options
|
|
105
|
+
) {
|
|
106
|
+
|
|
107
|
+
let include = (__eta_t, __eta_d) => this.render(__eta_t, {...it, ...(__eta_d ?? {})}, options);
|
|
108
|
+
let includeAsync = (__eta_t, __eta_d) => this.renderAsync(__eta_t, {...it, ...(__eta_d ?? {})}, options);
|
|
109
|
+
|
|
110
|
+
let __eta = {res: "", e: this.config.escapeFunction, f: this.config.filterFunction, blocks: {}};
|
|
111
|
+
|
|
112
|
+
function layout(path, data) {
|
|
113
|
+
__eta.layout = path;
|
|
114
|
+
__eta.layoutData = data;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function output(s){__eta.res+=s;}
|
|
118
|
+
function capture(fn){const s=__eta.res;__eta.res='';try{fn();return __eta.res}finally{__eta.res=s;}}
|
|
119
|
+
async function captureAsync(fn){const s=__eta.res;__eta.res='';try{await fn();return __eta.res}finally{__eta.res=s;}}
|
|
120
|
+
function block(name,fn){if(__eta.layout){if(fn){__eta.blocks[name]=capture(fn);}return '';}const b=it.__blocks||{};if(name in b){return b[name];}return fn?capture(fn):'';}
|
|
121
|
+
async function blockAsync(name,fn){if(__eta.layout){if(fn){__eta.blocks[name]=await captureAsync(fn);}return '';}const b=it.__blocks||{};if(name in b){return b[name];}return fn?await captureAsync(fn):'';}
|
|
122
|
+
|
|
123
|
+
__eta.res+='<!DOCTYPE html>\n<html lang="en">\n<head>\n <meta charset="UTF-8">\n <meta name="viewport" content="width=device-width, initial-scale=1.0">\n ';
|
|
124
|
+
if (it.title === null) {
|
|
125
|
+
__eta.res+=' <meta name="description" content="A microblog of music I\'m currently interested in.">\n ';
|
|
126
|
+
} else {
|
|
127
|
+
__eta.res+=' <meta name="description" content="An archive of posts from ';
|
|
128
|
+
__eta.res+=__eta.e(`${it.title}`);
|
|
129
|
+
__eta.res+='.">\n ';
|
|
130
|
+
}
|
|
131
|
+
__eta.res+=' <link rel="icon" type="image/svg+xml" sizes="any" href="data:image/svg+xml;charset=utf-8,<svg viewBox=\'0 0 100 100\' xmlns=\'http://www.w3.org/2000/svg\'><rect style=\'fill: %23ee009a\' width=\'100\' height=\'100\' /><path style=\'fill: white\' d=\'m 46.896145,88.64921 C 40.374404,74.369955 49.107189,69.176421 63.300981,75.966678 a 1.0075789,1.0075789 128.88245 0 0 1.239574,-1.537184 C 52.98292,61.336397 44.490904,53.053429 54.4857,40.9555 62.7098,31.0007 63.8784,28.384 56.693,22.0911 L 44.70917,11.59588 a 0.99988834,0.99988834 139.8069 0 0 -1.50978,1.275551 C 51.301904,26.208399 50.713597,27.307903 44.5515,34.8343 32.9606,48.9916 34.6,51.1771 49.6209,64.3152 32.226741,63.721624 28.851587,76.714805 45.431949,89.900492 a 0.99029164,0.99029164 139.48324 0 0 1.464196,-1.251282 z\' /></svg>">\n ';
|
|
132
|
+
if (it.canonical !== null) {
|
|
133
|
+
__eta.res+=' <link rel="canonical" href="';
|
|
134
|
+
__eta.res+=__eta.e(it.canonical);
|
|
135
|
+
__eta.res+='">\n ';
|
|
136
|
+
}
|
|
137
|
+
__eta.res+=' <link rel="alternate" type="application/atom+xml" href="feed.atom">\n <title>music blog';
|
|
138
|
+
__eta.res+=__eta.e(it.title === null ? '' : ` (${it.title.toLowerCase()})`);
|
|
139
|
+
__eta.res+='</title>\n <style>\n body {\n font-family: sans-serif;\n width: 100%;\n max-width: 35em;\n margin: 0 auto;\n padding: 1em;\n box-sizing: border-box;\n }\n\n body > .title {\n font-style: oblique;\n display: block;\n width: 70%;\n }\n\n body > .title > svg {\n width: 100%;\n }\n\n #editor {\n display: none;\n }\n\n form.editor {\n margin-top: 2em;\n }\n\n form.editor textarea {\n width: 100%;\n box-sizing: border-box;\n resize: vertical;\n }\n\n form.editor input[type="submit"] {\n float: right;\n }\n\n a {\n color: #099;\n }\n\n ol.posts {\n list-style-type: none;\n padding-left: 0;\n margin: 0;\n clear: both;\n }\n\n ol.posts > li .timestamp {\n margin-left: -1em;\n font-size: 0.7em;\n }\n\n ol.posts > li .text a {\n line-break: anywhere;\n }\n\n ol.posts > li .song, ol.posts > li .album {\n font-style: italic;\n }\n\n ol.posts > li > .playlist {\n list-style-type: none;\n padding-left: 0;\n margin: 1em 0;\n }\n\n .pagination {\n text-align: right;\n }\n\n .pagination > a:not(:first-child) {\n margin-left: 1em;\n }\n\n .pagination.top {\n margin: 0;\n float: right;\n }\n </style>\n <script>\n window.addEventListener(\'load\', () => {\n document.querySelectorAll(\'ol.posts > li > time\').forEach(element => {\n const datetime = element.getAttribute(\'datetime\');\n const date = (new Date(Date.parse(datetime))).toLocaleDateString();\n const time = (new Date(Date.parse(datetime))).toLocaleTimeString([], {timeStyle: \'short\'});\n element.innerHTML = `${date} (${time})`;\n });\n\n const isAuthed = document.cookie.split(\'; \').find((row) => row.startsWith(\'isAuthed=\'))?.split("=")[1] === \'true\';\n if (isAuthed) {\n document.getElementById(\'editor\').style.display = \'block\';\n }\n });\n </script>\n</head>\n<body>\n <a class="title" href="/">\n <svg viewBox="0 0 100 30">\n <path style="fill: #ee009a;" d="m 6,0\n L 100,0\n L 94,30\n L 0,30 z" />\n <text role="heading" aria-level="1" style="fill: white; font-size: 16px; font-style: oblique" x="5" y="25">music</text>\n </svg>\n </a>\n <p class="pagination top">\n ';
|
|
140
|
+
if (it.prevPage !== null) {
|
|
141
|
+
__eta.res+=' <a href="';
|
|
142
|
+
__eta.res+=__eta.e(it.prevPage);
|
|
143
|
+
__eta.res+='">←prev</a>\n ';
|
|
144
|
+
}
|
|
145
|
+
__eta.res+=' ';
|
|
146
|
+
if (it.nextPage !== null) {
|
|
147
|
+
__eta.res+=' <a href="';
|
|
148
|
+
__eta.res+=__eta.e(it.nextPage);
|
|
149
|
+
__eta.res+='">next→</a>\n ';
|
|
150
|
+
}
|
|
151
|
+
__eta.res+=' </p>\n <form id="editor" class="editor" action="./post" method="POST">\n <textarea name="text" rows="3"></textarea>\n <textarea name="tracks" rows="1" placeholder="Artist - Title"></textarea>\n <input type="submit" value="post">\n </form>\n <ol class="posts">\n ';
|
|
152
|
+
it.posts.forEach(function(post) {
|
|
153
|
+
__eta.res+=' <li id="';
|
|
154
|
+
__eta.res+=__eta.e(post.id);
|
|
155
|
+
__eta.res+='">\n <time class="timestamp" datetime="';
|
|
156
|
+
__eta.res+=__eta.e(post.createdAt);
|
|
157
|
+
__eta.res+='">\n ';
|
|
158
|
+
__eta.res+=__eta.e((new Date(Date.parse(post.createdAt))).toLocaleDateString());
|
|
159
|
+
__eta.res+=' (';
|
|
160
|
+
__eta.res+=__eta.e((new Date(Date.parse(post.createdAt))).toLocaleTimeString([], {timeStyle: 'short'}));
|
|
161
|
+
__eta.res+=')\n </time>\n <p class="text">';
|
|
162
|
+
__eta.res+=post.text.replace(/(https:\/\/\S*)/g, '<a href="$1">$1</a>').replaceAll('\n', '<br />');
|
|
163
|
+
__eta.res+='</p>\n ';
|
|
164
|
+
if (post.type === 'song') {
|
|
165
|
+
__eta.res+=' <p class="song"><span class="artist">';
|
|
166
|
+
__eta.res+=__eta.e(post.artist);
|
|
167
|
+
__eta.res+='</span> - <span class="title">';
|
|
168
|
+
__eta.res+=__eta.e(post.title);
|
|
169
|
+
__eta.res+='</span></p>\n ';
|
|
170
|
+
} else if (post.type === 'album') {
|
|
171
|
+
__eta.res+=' <p class="album"><span class="artist">';
|
|
172
|
+
__eta.res+=__eta.e(post.artist);
|
|
173
|
+
__eta.res+='</span> - <span class="title">';
|
|
174
|
+
__eta.res+=__eta.e(post.title);
|
|
175
|
+
__eta.res+='</span></p>\n ';
|
|
176
|
+
} else if (post.type === 'playlist') {
|
|
177
|
+
__eta.res+=' <ol class="playlist">\n ';
|
|
178
|
+
post.tracks.forEach(function(song) {
|
|
179
|
+
__eta.res+=' <li class="song"><span class="artist">';
|
|
180
|
+
__eta.res+=__eta.e(song.artist);
|
|
181
|
+
__eta.res+='</span> - <span class="title">';
|
|
182
|
+
__eta.res+=__eta.e(song.title);
|
|
183
|
+
__eta.res+='</span></li>\n ';
|
|
184
|
+
})
|
|
185
|
+
__eta.res+=' </ol>\n ';
|
|
186
|
+
}
|
|
187
|
+
__eta.res+=' </li>\n ';
|
|
188
|
+
})
|
|
189
|
+
__eta.res+=' </ol>\n <p class="pagination">\n ';
|
|
190
|
+
if (it.prevPage !== null) {
|
|
191
|
+
__eta.res+=' <a href="';
|
|
192
|
+
__eta.res+=__eta.e(it.prevPage);
|
|
193
|
+
__eta.res+='">←prev</a>\n ';
|
|
194
|
+
}
|
|
195
|
+
__eta.res+=' ';
|
|
196
|
+
if (it.nextPage !== null) {
|
|
197
|
+
__eta.res+=' <a href="';
|
|
198
|
+
__eta.res+=__eta.e(it.nextPage);
|
|
199
|
+
__eta.res+='">next→</a>\n ';
|
|
200
|
+
}
|
|
201
|
+
__eta.res+=' </p>\n</body>\n</html>\n';
|
|
202
|
+
|
|
203
|
+
if (__eta.layout) {
|
|
204
|
+
__eta.res = include (__eta.layout, {...it, body: __eta.res, ...__eta.layoutData, __blocks: __eta.blocks});
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return __eta.res;
|
|
208
|
+
|
|
209
|
+
},sitemap: function anonymous(it,options
|
|
210
|
+
) {
|
|
211
|
+
|
|
212
|
+
let include = (__eta_t, __eta_d) => this.render(__eta_t, {...it, ...(__eta_d ?? {})}, options);
|
|
213
|
+
let includeAsync = (__eta_t, __eta_d) => this.renderAsync(__eta_t, {...it, ...(__eta_d ?? {})}, options);
|
|
214
|
+
|
|
215
|
+
let __eta = {res: "", e: this.config.escapeFunction, f: this.config.filterFunction, blocks: {}};
|
|
216
|
+
|
|
217
|
+
function layout(path, data) {
|
|
218
|
+
__eta.layout = path;
|
|
219
|
+
__eta.layoutData = data;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function output(s){__eta.res+=s;}
|
|
223
|
+
function capture(fn){const s=__eta.res;__eta.res='';try{fn();return __eta.res}finally{__eta.res=s;}}
|
|
224
|
+
async function captureAsync(fn){const s=__eta.res;__eta.res='';try{await fn();return __eta.res}finally{__eta.res=s;}}
|
|
225
|
+
function block(name,fn){if(__eta.layout){if(fn){__eta.blocks[name]=capture(fn);}return '';}const b=it.__blocks||{};if(name in b){return b[name];}return fn?capture(fn):'';}
|
|
226
|
+
async function blockAsync(name,fn){if(__eta.layout){if(fn){__eta.blocks[name]=await captureAsync(fn);}return '';}const b=it.__blocks||{};if(name in b){return b[name];}return fn?await captureAsync(fn):'';}
|
|
227
|
+
|
|
228
|
+
__eta.res+='<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n ';
|
|
229
|
+
it.pages.forEach(function(page) {
|
|
230
|
+
__eta.res+=' <url>\n <loc>';
|
|
231
|
+
__eta.res+=__eta.e(page.url);
|
|
232
|
+
__eta.res+='</loc>\n <lastmod>';
|
|
233
|
+
__eta.res+=__eta.e(page.updatedAt);
|
|
234
|
+
__eta.res+='</lastmod>\n </url>\n ';
|
|
235
|
+
})
|
|
236
|
+
__eta.res+='</urlset>\n';
|
|
237
|
+
|
|
238
|
+
if (__eta.layout) {
|
|
239
|
+
__eta.res = include (__eta.layout, {...it, body: __eta.res, ...__eta.layoutData, __blocks: __eta.blocks});
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return __eta.res;
|
|
243
|
+
|
|
244
|
+
},};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<feed xmlns="http://www.w3.org/2005/Atom">
|
|
3
|
+
<title>music blog</title>
|
|
4
|
+
<id><%= it.siteUrl %></id>
|
|
5
|
+
<icon><%= it.siteUrl %>/favicon.png</icon>
|
|
6
|
+
<link rel="alternate" type="text/html" href="<%= it.siteUrl %>" />
|
|
7
|
+
<link rel="self" href="<%= it.siteUrl %>/feed.atom" />
|
|
8
|
+
<updated><%= it.posts[0].createdAt %></updated>
|
|
9
|
+
<author>
|
|
10
|
+
<name><%= it.authorName %></name>
|
|
11
|
+
<email><%= it.authorEmail %></email>
|
|
12
|
+
</author>
|
|
13
|
+
<% it.posts.forEach(post => { %>
|
|
14
|
+
<entry>
|
|
15
|
+
<title><%= post.title %></title>
|
|
16
|
+
<link rel="alternate" type="text/html" href="<%= post.archivePage %>"/>
|
|
17
|
+
<id>urn:uuid:<%= post.uuid %></id>
|
|
18
|
+
<published><%= post.createdAt %></published>
|
|
19
|
+
<updated><%= post.createdAt %></updated>
|
|
20
|
+
<content type="html"><%= post.html %></content>
|
|
21
|
+
</entry>
|
|
22
|
+
<% }) %>
|
|
23
|
+
</feed>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<p><%~ it.post.text.replace(/(https:\/\/\S*)/g, '<a href="$1">$1</a>').replaceAll('\n', '<br />') %></p>
|
|
2
|
+
<% if (it.post.type === 'song' || it.post.type === 'album') { %>
|
|
3
|
+
<p><i><%= `${it.post.artist} - ${it.post.title}` %></i></p>
|
|
4
|
+
<% } else if (it.post.type === 'playlist') { %>
|
|
5
|
+
<p>
|
|
6
|
+
<% it.post.tracks.forEach(function(song, i) { %>
|
|
7
|
+
<% if (i !== 0) { %>
|
|
8
|
+
<br />
|
|
9
|
+
<% } %>
|
|
10
|
+
<i><%= `${song.artist} - ${song.title}` %></i>
|
|
11
|
+
<% }) %>
|
|
12
|
+
</p>
|
|
13
|
+
<% } %>
|