@lexho111/plainblog 0.5.28 → 0.6.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/Article.js +73 -4
- package/Blog.js +342 -140
- package/Formatter.js +2 -11
- package/README.md +1 -1
- package/{blog → blog_test_empty.db} +0 -0
- package/blog_test_load.db +0 -0
- package/build-scripts.js +54 -0
- package/coverage/clover.xml +1051 -0
- package/coverage/coverage-final.json +20 -0
- package/coverage/lcov-report/base.css +224 -0
- package/coverage/lcov-report/block-navigation.js +87 -0
- package/coverage/lcov-report/favicon.png +0 -0
- package/coverage/lcov-report/index.html +161 -0
- package/coverage/lcov-report/package/Article.js.html +406 -0
- package/coverage/lcov-report/package/Blog.js.html +2674 -0
- package/coverage/lcov-report/package/Formatter.js.html +379 -0
- package/coverage/lcov-report/package/build-scripts.js.html +247 -0
- package/coverage/lcov-report/package/build-styles.js.html +367 -0
- package/coverage/lcov-report/package/index.html +191 -0
- package/coverage/lcov-report/package/model/APIModel.js.html +190 -0
- package/coverage/lcov-report/package/model/ArrayList.js.html +382 -0
- package/coverage/lcov-report/package/model/ArrayListHashMap.js.html +379 -0
- package/coverage/lcov-report/package/model/BinarySearchTree.js.html +856 -0
- package/coverage/lcov-report/package/model/BinarySearchTreeHashMap.js.html +346 -0
- package/coverage/lcov-report/package/model/DataModel.js.html +322 -0
- package/coverage/lcov-report/package/model/DatabaseModel.js.html +232 -0
- package/coverage/lcov-report/package/model/FileAdapter.js.html +394 -0
- package/coverage/lcov-report/package/model/FileList.js.html +244 -0
- package/coverage/lcov-report/package/model/FileModel.js.html +358 -0
- package/coverage/lcov-report/package/model/SequelizeAdapter.js.html +538 -0
- package/coverage/lcov-report/package/model/SqliteAdapter.js.html +247 -0
- package/coverage/lcov-report/package/model/datastructures/ArrayList.js.html +439 -0
- package/coverage/lcov-report/package/model/datastructures/ArrayListHashMap.js.html +196 -0
- package/coverage/lcov-report/package/model/datastructures/BinarySearchTree.js.html +913 -0
- package/coverage/lcov-report/package/model/datastructures/BinarySearchTreeHashMap.js.html +352 -0
- package/coverage/lcov-report/package/model/datastructures/FileList.js.html +244 -0
- package/coverage/lcov-report/package/model/datastructures/index.html +176 -0
- package/coverage/lcov-report/package/model/index.html +206 -0
- package/coverage/lcov-report/package/utilities.js.html +511 -0
- package/coverage/lcov-report/prettify.css +1 -0
- package/coverage/lcov-report/prettify.js +2 -0
- package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
- package/coverage/lcov-report/sorter.js +210 -0
- package/coverage/lcov.info +2078 -0
- package/index.js +25 -1
- package/model/DataModel.js +80 -0
- package/model/DatabaseModel.js +21 -8
- package/model/FileAdapter.js +44 -4
- package/model/FileModel.js +48 -9
- package/model/SequelizeAdapter.js +11 -3
- package/model/datastructures/ArrayList.js +118 -0
- package/model/datastructures/ArrayListHashMap.js +37 -0
- package/model/datastructures/ArrayListHashMap.js.bk +90 -0
- package/model/datastructures/BinarySearchTree.js +277 -0
- package/model/datastructures/BinarySearchTreeHashMap.js +90 -0
- package/model/datastructures/BinarySearchTreeTest.js +16 -0
- package/model/datastructures/FileList.js +53 -0
- package/package.json +9 -3
- package/public/fetchData.js +0 -0
- package/public/scripts.min.js +2 -1
- package/public/styles.min.css +2 -1
- package/src/fetchData.js +150 -30
- package/src/styles.css +29 -0
- package/utilities.js +142 -0
package/Blog.js
CHANGED
|
@@ -2,17 +2,39 @@ import http from "http";
|
|
|
2
2
|
import crypto from "node:crypto";
|
|
3
3
|
import fs from "fs";
|
|
4
4
|
import { URLSearchParams } from "url";
|
|
5
|
+
import process from "node:process";
|
|
6
|
+
import path from "path";
|
|
7
|
+
import { fileURLToPath } from "url";
|
|
8
|
+
import pkg from "./package.json" with { type: "json" };
|
|
9
|
+
//import createDebug from "debug";
|
|
10
|
+
import { debuglog as createDebug } from "node:util";
|
|
5
11
|
import Article from "./Article.js";
|
|
6
12
|
import DatabaseModel from "./model/DatabaseModel.js";
|
|
7
13
|
import { fetchData, postData } from "./model/APIModel.js";
|
|
8
14
|
import { formatHTML, header, formatMarkdown, validate } from "./Formatter.js"; // import pkg from "./package.json" with { type: "json" };
|
|
9
|
-
import path from "path";
|
|
10
|
-
import { fileURLToPath } from "url";
|
|
11
15
|
import { compileStyles, mergeStyles } from "./build-styles.js";
|
|
12
|
-
import
|
|
13
|
-
import
|
|
16
|
+
import { compileScripts } from "./build-scripts.js";
|
|
17
|
+
import FileAdapter from "./model/FileAdapter.js";
|
|
18
|
+
import { table, log } from "./utilities.js";
|
|
19
|
+
|
|
20
|
+
import DataModel from "./model/DataModel.js";
|
|
21
|
+
import ArrayList from "./model/datastructures/ArrayList.js";
|
|
22
|
+
import ArrayListHashMap from "./model/datastructures/ArrayListHashMap.js";
|
|
23
|
+
import { BinarySearchTreeHashMap } from "./model/datastructures/BinarySearchTreeHashMap.js";
|
|
24
|
+
import FileList from "./model/datastructures/FileList.js";
|
|
25
|
+
|
|
26
|
+
// Initialize the debugger with a specific namespace
|
|
27
|
+
const debug = createDebug("plainblog:Blog");
|
|
14
28
|
|
|
15
29
|
export default class Blog {
|
|
30
|
+
#makeDataModel() {
|
|
31
|
+
return new BinarySearchTreeHashMap();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
setDataModel(datamodel) {
|
|
35
|
+
this.#articles = datamodel;
|
|
36
|
+
}
|
|
37
|
+
|
|
16
38
|
constructor() {
|
|
17
39
|
this.database = {
|
|
18
40
|
type: "file",
|
|
@@ -22,7 +44,7 @@ export default class Blog {
|
|
|
22
44
|
dbname: "articles.txt", // x
|
|
23
45
|
};
|
|
24
46
|
this.#title = "";
|
|
25
|
-
this.#articles =
|
|
47
|
+
this.#articles = new DataModel(this.#makeDataModel());
|
|
26
48
|
this.#server = null;
|
|
27
49
|
this.#password = "admin";
|
|
28
50
|
this.#styles = "body { font-family: Arial; }";
|
|
@@ -48,7 +70,7 @@ export default class Blog {
|
|
|
48
70
|
const json = {
|
|
49
71
|
version: this.#version,
|
|
50
72
|
title: this.#title,
|
|
51
|
-
articles: this.#articles,
|
|
73
|
+
articles: this.#articles.getAllArticles(),
|
|
52
74
|
server: serverInfo,
|
|
53
75
|
compiledStyles: this.compiledStyles,
|
|
54
76
|
sessions: this.sessions,
|
|
@@ -58,7 +80,7 @@ export default class Blog {
|
|
|
58
80
|
reloadStylesOnGET: this.reloadStylesOnGET,
|
|
59
81
|
};
|
|
60
82
|
|
|
61
|
-
return JSON.parse(JSON.stringify(json));
|
|
83
|
+
return JSON.parse(JSON.stringify(json)); // make json read-only
|
|
62
84
|
}
|
|
63
85
|
|
|
64
86
|
// Private fields
|
|
@@ -72,7 +94,7 @@ export default class Blog {
|
|
|
72
94
|
#articles = [];
|
|
73
95
|
#styles = "";
|
|
74
96
|
#stylesHash = "";
|
|
75
|
-
|
|
97
|
+
#scriptsHash = "";
|
|
76
98
|
#stylesheetPath = "";
|
|
77
99
|
compilestyle = false;
|
|
78
100
|
#initPromise = null;
|
|
@@ -112,7 +134,10 @@ export default class Blog {
|
|
|
112
134
|
*/
|
|
113
135
|
setDatabaseAdapter(adapter) {
|
|
114
136
|
if (!this.#databaseModel) {
|
|
115
|
-
|
|
137
|
+
if (this.database.type === "file") {
|
|
138
|
+
const adapter = new FileAdapter(this.database);
|
|
139
|
+
this.#databaseModel = new DatabaseModel(adapter);
|
|
140
|
+
}
|
|
116
141
|
}
|
|
117
142
|
this.#databaseModel.setDatabaseAdapter(adapter);
|
|
118
143
|
}
|
|
@@ -137,7 +162,7 @@ export default class Blog {
|
|
|
137
162
|
}
|
|
138
163
|
|
|
139
164
|
addArticle(article) {
|
|
140
|
-
this.#articles.
|
|
165
|
+
this.#articles.insert(article);
|
|
141
166
|
}
|
|
142
167
|
|
|
143
168
|
#isAuthenticated(req) {
|
|
@@ -147,6 +172,7 @@ export default class Blog {
|
|
|
147
172
|
}
|
|
148
173
|
|
|
149
174
|
async #handleLogin(req, res) {
|
|
175
|
+
debug("handle login");
|
|
150
176
|
const body = await new Promise((resolve, reject) => {
|
|
151
177
|
let data = "";
|
|
152
178
|
req.on("data", (chunk) => (data += chunk.toString()));
|
|
@@ -175,9 +201,10 @@ export default class Blog {
|
|
|
175
201
|
}
|
|
176
202
|
|
|
177
203
|
#handleLogout(req, res) {
|
|
204
|
+
debug("handle logout");
|
|
178
205
|
if (req.headers.cookie) {
|
|
179
206
|
const params = new URLSearchParams(
|
|
180
|
-
req.headers.cookie.replace(/; /g, "&")
|
|
207
|
+
req.headers.cookie.replace(/; /g, "&"),
|
|
181
208
|
);
|
|
182
209
|
const sessionId = params.get("session");
|
|
183
210
|
if (this.sessions.has(sessionId)) {
|
|
@@ -195,105 +222,134 @@ export default class Blog {
|
|
|
195
222
|
async init() {
|
|
196
223
|
if (this.#initPromise) return this.#initPromise;
|
|
197
224
|
this.#initPromise = (async () => {
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
225
|
+
//this.loadStyles();
|
|
226
|
+
//this.loadScripts();
|
|
227
|
+
// if there is a stylesheet path provided, process it
|
|
228
|
+
if (this.#stylesheetPath != null && this.compilestyle) {
|
|
229
|
+
// read file from stylesheet path, compare checksums and write to public/styles.min.css
|
|
230
|
+
await this.#processStylesheets(this.#stylesheetPath);
|
|
231
|
+
}
|
|
232
|
+
if (!this.#stylesheetPath) {
|
|
233
|
+
// this.#styles
|
|
234
|
+
// src/styles.css
|
|
235
|
+
// compile and merge hardcoded styles in "this.#styles" with "src/styles.css" and write to file "styles.min.css"
|
|
236
|
+
// which will be imported by webbrowser via '<link rel="stylesheet" href="styles.min.css"...'
|
|
237
|
+
|
|
238
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
239
|
+
const __dirname = path.dirname(__filename);
|
|
240
|
+
const srcStylePath = path.join(__dirname, "src", "styles.css");
|
|
241
|
+
const publicStylePath = path.join(
|
|
242
|
+
process.cwd(),
|
|
243
|
+
"public",
|
|
244
|
+
"styles.min.css",
|
|
245
|
+
);
|
|
246
|
+
|
|
247
|
+
let publicHash = null;
|
|
248
|
+
let srcStyles = "";
|
|
249
|
+
|
|
250
|
+
await Promise.all([
|
|
251
|
+
fs.promises
|
|
252
|
+
.readFile(publicStylePath, "utf8")
|
|
253
|
+
.then((publicCSS) => {
|
|
254
|
+
const match = publicCSS.match(
|
|
255
|
+
/\/\* source-hash: ([a-f0-9]{64}) \*\//,
|
|
256
|
+
);
|
|
257
|
+
if (match) publicHash = match[1];
|
|
258
|
+
})
|
|
259
|
+
.catch((err) => console.error(err)), // public/styles.min.css doesn't exist, will be created.
|
|
260
|
+
fs.promises
|
|
261
|
+
.readFile(srcStylePath, "utf8")
|
|
262
|
+
.then((content) => {
|
|
263
|
+
srcStyles = content;
|
|
264
|
+
})
|
|
265
|
+
.catch((err) => {
|
|
266
|
+
if (err.code !== "ENOENT") console.error(err);
|
|
267
|
+
}), // ignore if src/styles.css doesn't exist
|
|
268
|
+
]);
|
|
269
|
+
|
|
270
|
+
const combinedStyles = this.#styles + srcStyles;
|
|
271
|
+
const srcHash = crypto
|
|
272
|
+
.createHash("sha256")
|
|
273
|
+
.update(combinedStyles)
|
|
274
|
+
.digest("hex");
|
|
275
|
+
|
|
276
|
+
if (srcHash !== publicHash && this.compilestyle) {
|
|
277
|
+
console.log("Styles have changed. Recompiling...");
|
|
278
|
+
const finalStyles = await mergeStyles(this.#styles, srcStyles);
|
|
279
|
+
try {
|
|
280
|
+
await fs.promises.mkdir(path.dirname(publicStylePath), {
|
|
281
|
+
recursive: true,
|
|
282
|
+
});
|
|
283
|
+
await fs.promises.writeFile(
|
|
284
|
+
publicStylePath,
|
|
285
|
+
finalStyles + `\n/* source-hash: ${srcHash} */`,
|
|
286
|
+
);
|
|
287
|
+
} catch (err) {
|
|
288
|
+
console.error("Failed to write styles to public folder:", err);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
210
292
|
|
|
293
|
+
// Process Scripts
|
|
211
294
|
const __filename = fileURLToPath(import.meta.url);
|
|
212
295
|
const __dirname = path.dirname(__filename);
|
|
213
|
-
const
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
let publicHash = null;
|
|
217
|
-
let srcStyles = "";
|
|
218
|
-
|
|
219
|
-
await Promise.all([
|
|
220
|
-
fs.promises
|
|
221
|
-
.readFile(publicStylePath, "utf8")
|
|
222
|
-
.then((publicCSS) => {
|
|
223
|
-
const match = publicCSS.match(
|
|
224
|
-
/\/\* source-hash: ([a-f0-9]{64}) \*\//
|
|
225
|
-
);
|
|
226
|
-
if (match) publicHash = match[1];
|
|
227
|
-
})
|
|
228
|
-
.catch((err) => console.error(err)), // public/styles.min.css doesn't exist, will be created.
|
|
229
|
-
fs.promises
|
|
230
|
-
.readFile(srcStylePath, "utf8")
|
|
231
|
-
.then((content) => {
|
|
232
|
-
srcStyles = content;
|
|
233
|
-
})
|
|
234
|
-
.catch((err) => console.error(err)), // ignore if src/styles.css doesn't exist
|
|
235
|
-
]);
|
|
236
|
-
|
|
237
|
-
const combinedStyles = this.#styles + srcStyles;
|
|
238
|
-
const srcHash = crypto
|
|
239
|
-
.createHash("sha256")
|
|
240
|
-
.update(combinedStyles)
|
|
241
|
-
.digest("hex");
|
|
296
|
+
const srcScriptPath = path.join(__dirname, "src", "fetchData.js");
|
|
297
|
+
await this.#processScripts(srcScriptPath);
|
|
242
298
|
|
|
243
|
-
if (
|
|
244
|
-
console.log("
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
299
|
+
if (this.#isExternalAPI) {
|
|
300
|
+
console.log("external API");
|
|
301
|
+
await this.#loadFromAPI();
|
|
302
|
+
} else {
|
|
303
|
+
console.log(`database: ${this.database.type}`);
|
|
304
|
+
if (!this.#databaseModel) {
|
|
305
|
+
if (this.database.type === "file") {
|
|
306
|
+
const adapter = new FileAdapter(this.database);
|
|
307
|
+
this.#databaseModel = new DatabaseModel(adapter);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
console.log(`connected to database`);
|
|
311
|
+
await this.#databaseModel.initialize();
|
|
312
|
+
this.#articles.setDatabase(this.#databaseModel);
|
|
313
|
+
const [dbTitle, dbArticles] = await Promise.all([
|
|
314
|
+
this.#databaseModel.getBlogTitle(),
|
|
315
|
+
this.#databaseModel.findAll(),
|
|
316
|
+
]);
|
|
317
|
+
|
|
318
|
+
debug("dbArticles.size(): %d", dbArticles.length);
|
|
319
|
+
//log(filename, "dbArticles.size(): " + dbArticles.length);
|
|
320
|
+
//log(filename, "dbArticles.size(): " + dbArticles.length, "Blog.js");
|
|
321
|
+
debug("all articles in Blog after loading from db");
|
|
322
|
+
|
|
323
|
+
// Displays a beautiful table in the console
|
|
324
|
+
//table(dbArticles)
|
|
325
|
+
|
|
326
|
+
if (dbArticles.length == 0) {
|
|
327
|
+
dbArticles.push(
|
|
328
|
+
new Article(
|
|
329
|
+
1,
|
|
330
|
+
"Sample Entry #1",
|
|
331
|
+
"Prow scuttle parrel provost Sail ho shrouds spirits boom mizzenmast yardarm. Pinnace holystone mizzenmast quarter crow's nest nipperkin grog yardarm hempen halter furl. Swab barque interloper chantey doubloon starboard grog black jack gangway rutters.",
|
|
332
|
+
new Date(),
|
|
333
|
+
),
|
|
334
|
+
);
|
|
335
|
+
dbArticles.push(
|
|
336
|
+
new Article(
|
|
337
|
+
2,
|
|
338
|
+
"Sample Entry #2",
|
|
339
|
+
"Deadlights jack lad schooner scallywag dance the hempen jig carouser broadside cable strike colors. Bring a spring upon her cable holystone blow the man down spanker Shiver me timbers to go on account lookout wherry doubloon chase. Belay yo-ho-ho keelhaul squiffy black spot yardarm spyglass sheet transom heave to.",
|
|
340
|
+
new Date(),
|
|
341
|
+
),
|
|
251
342
|
);
|
|
252
|
-
} catch (err) {
|
|
253
|
-
console.error("Failed to write styles to public folder:", err);
|
|
254
343
|
}
|
|
344
|
+
if (this.reloadStylesOnGET)
|
|
345
|
+
console.log("reload scripts and styles on GET-Request");
|
|
346
|
+
let title = "";
|
|
347
|
+
if (this.#title != null && this.#title.length > 0)
|
|
348
|
+
title = this.#title; // use blog title if set
|
|
349
|
+
else title = dbTitle; // use title from the database
|
|
350
|
+
const responseData = { title: title, articles: dbArticles };
|
|
351
|
+
this.#applyBlogData(responseData);
|
|
255
352
|
}
|
|
256
|
-
}
|
|
257
|
-
if (this.#isExternalAPI) {
|
|
258
|
-
console.log("external API");
|
|
259
|
-
await this.#loadFromAPI();
|
|
260
|
-
} else {
|
|
261
|
-
console.log(`database: ${this.database.type}`);
|
|
262
|
-
if (!this.#databaseModel) {
|
|
263
|
-
this.#databaseModel = new DatabaseModel(this.database);
|
|
264
|
-
}
|
|
265
|
-
console.log(`connected to database`);
|
|
266
|
-
await this.#databaseModel.initialize();
|
|
267
|
-
const [dbTitle, dbArticles] = await Promise.all([
|
|
268
|
-
this.#databaseModel.getBlogTitle(),
|
|
269
|
-
this.#databaseModel.findAll(),
|
|
270
|
-
]);
|
|
271
|
-
|
|
272
|
-
if (dbArticles.length == 0) {
|
|
273
|
-
dbArticles.push(
|
|
274
|
-
new Article(
|
|
275
|
-
"Sample Entry #1",
|
|
276
|
-
"Prow scuttle parrel provost Sail ho shrouds spirits boom mizzenmast yardarm. Pinnace holystone mizzenmast quarter crow's nest nipperkin grog yardarm hempen halter furl. Swab barque interloper chantey doubloon starboard grog black jack gangway rutters.",
|
|
277
|
-
new Date()
|
|
278
|
-
)
|
|
279
|
-
);
|
|
280
|
-
dbArticles.push(
|
|
281
|
-
new Article(
|
|
282
|
-
"Sample Entry #2",
|
|
283
|
-
"Deadlights jack lad schooner scallywag dance the hempen jig carouser broadside cable strike colors. Bring a spring upon her cable holystone blow the man down spanker Shiver me timbers to go on account lookout wherry doubloon chase. Belay yo-ho-ho keelhaul squiffy black spot yardarm spyglass sheet transom heave to.",
|
|
284
|
-
new Date()
|
|
285
|
-
)
|
|
286
|
-
);
|
|
287
|
-
}
|
|
288
|
-
if (this.reloadStylesOnGET)
|
|
289
|
-
console.log("reload scripts and styles on GET-Request");
|
|
290
|
-
let title = "";
|
|
291
|
-
if (this.#title != null && this.#title.length > 0)
|
|
292
|
-
title = this.#title; // use blog title if set
|
|
293
|
-
else title = dbTitle; // use title from the database
|
|
294
|
-
const responseData = { title: title, articles: dbArticles };
|
|
295
|
-
this.#applyBlogData(responseData);
|
|
296
|
-
}
|
|
297
353
|
})();
|
|
298
354
|
return this.#initPromise;
|
|
299
355
|
}
|
|
@@ -323,12 +379,10 @@ export default class Blog {
|
|
|
323
379
|
promises.push(postData(this.#apiUrl, newArticleData));
|
|
324
380
|
await Promise.all(promises);
|
|
325
381
|
// Add the article to the local list for immediate display
|
|
326
|
-
this.#articles.
|
|
382
|
+
this.#articles.insert(Article.createNew(title, content));
|
|
327
383
|
// remove sample entries
|
|
328
|
-
this.#articles
|
|
329
|
-
|
|
330
|
-
art.title !== "Sample Entry #1" && art.title !== "Sample Entry #2"
|
|
331
|
-
);
|
|
384
|
+
this.#articles.remove(1); // "Sample Entry #1"
|
|
385
|
+
this.#articles.remove(2); // "Sample Entry #2"
|
|
332
386
|
} catch (error) {
|
|
333
387
|
console.error("Failed to post new article to API:", error);
|
|
334
388
|
}
|
|
@@ -344,6 +398,7 @@ export default class Blog {
|
|
|
344
398
|
await this.init();
|
|
345
399
|
|
|
346
400
|
const server = http.createServer(async (req, res) => {
|
|
401
|
+
//debug("query %s", req.url);
|
|
347
402
|
// API routes
|
|
348
403
|
if (req.url.startsWith("/api")) {
|
|
349
404
|
await this.#jsonAPI(req, res);
|
|
@@ -388,6 +443,10 @@ export default class Blog {
|
|
|
388
443
|
if (this.#stylesheetPath != null && this.compilestyle) {
|
|
389
444
|
await this.#processStylesheets(this.#stylesheetPath);
|
|
390
445
|
}
|
|
446
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
447
|
+
const __dirname = path.dirname(__filename);
|
|
448
|
+
const srcScriptPath = path.join(__dirname, "src", "fetchData.js");
|
|
449
|
+
await this.#processScripts(srcScriptPath);
|
|
391
450
|
}
|
|
392
451
|
|
|
393
452
|
let loggedin = false;
|
|
@@ -414,7 +473,7 @@ export default class Blog {
|
|
|
414
473
|
const publicDir = path.join(process.cwd(), "public");
|
|
415
474
|
const parsedUrl = new URL(
|
|
416
475
|
req.url,
|
|
417
|
-
`http://${req.headers.host || "localhost"}
|
|
476
|
+
`http://${req.headers.host || "localhost"}`,
|
|
418
477
|
);
|
|
419
478
|
const filePath = path.join(publicDir, parsedUrl.pathname);
|
|
420
479
|
|
|
@@ -440,8 +499,11 @@ export default class Blog {
|
|
|
440
499
|
}
|
|
441
500
|
}
|
|
442
501
|
} catch (err) {
|
|
443
|
-
|
|
444
|
-
//
|
|
502
|
+
// ENOENT (file not found) is expected, leading to a 404.
|
|
503
|
+
// Only log other, unexpected errors.
|
|
504
|
+
if (err.code !== "ENOENT") {
|
|
505
|
+
console.error(err);
|
|
506
|
+
}
|
|
445
507
|
}
|
|
446
508
|
|
|
447
509
|
// Error 404
|
|
@@ -455,7 +517,8 @@ export default class Blog {
|
|
|
455
517
|
return new Promise((resolve, reject) => {
|
|
456
518
|
const errorHandler = (err) => reject(err);
|
|
457
519
|
this.#server.once("error", errorHandler);
|
|
458
|
-
this.#server.listen(port, "0.0.0.0", () => {
|
|
520
|
+
this.#server.listen(port, "0.0.0.0", () => {
|
|
521
|
+
// <-- for docker 0.0.0.0, localhost 127.0.0.1
|
|
459
522
|
this.#server.removeListener("error", errorHandler);
|
|
460
523
|
console.log(`server running at http://localhost:${port}/`);
|
|
461
524
|
resolve(); // Resolve the promise when the server is listening
|
|
@@ -481,18 +544,42 @@ export default class Blog {
|
|
|
481
544
|
|
|
482
545
|
/** Populates the blog's title and articles from a data object. */
|
|
483
546
|
#applyBlogData(data) {
|
|
484
|
-
|
|
547
|
+
debug("applyBlogData");
|
|
548
|
+
if (this.#articles.storage.clear) {
|
|
549
|
+
this.#articles.storage.clear();
|
|
550
|
+
} else {
|
|
551
|
+
//this.#articles.setDataModel(this.#makeDataModel()); // Fallback if clear() isn't implemented
|
|
552
|
+
}
|
|
485
553
|
this.#title = data.title;
|
|
486
554
|
// Assuming data contains a title and an array of articles with title and content
|
|
487
|
-
if (data.articles && Array.isArray(data.articles)) {
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
555
|
+
if (this.#articles && data.articles && Array.isArray(data.articles)) {
|
|
556
|
+
if (this.#articles.getStorageName().includes("BinarySearchTree")) {
|
|
557
|
+
debug("using special insert method for BST");
|
|
558
|
+
const insertBalanced = (start, end) => {
|
|
559
|
+
if (start > end) return;
|
|
560
|
+
const mid = Math.floor((start + end) / 2);
|
|
561
|
+
const articleData = data.articles[mid];
|
|
562
|
+
const article = new Article(
|
|
563
|
+
articleData.id,
|
|
564
|
+
articleData.title,
|
|
565
|
+
articleData.content,
|
|
566
|
+
articleData.createdAt,
|
|
567
|
+
);
|
|
568
|
+
this.addArticle(article);
|
|
569
|
+
insertBalanced(start, mid - 1);
|
|
570
|
+
insertBalanced(mid + 1, end);
|
|
571
|
+
};
|
|
572
|
+
insertBalanced(0, data.articles.length - 1);
|
|
573
|
+
} else {
|
|
574
|
+
for (const articleData of data.articles) {
|
|
575
|
+
const article = new Article(
|
|
576
|
+
articleData.id,
|
|
577
|
+
articleData.title,
|
|
578
|
+
articleData.content,
|
|
579
|
+
articleData.createdAt,
|
|
580
|
+
);
|
|
581
|
+
this.addArticle(article);
|
|
582
|
+
}
|
|
496
583
|
}
|
|
497
584
|
}
|
|
498
585
|
}
|
|
@@ -518,22 +605,31 @@ export default class Blog {
|
|
|
518
605
|
};
|
|
519
606
|
res.end(JSON.stringify(data));
|
|
520
607
|
}
|
|
608
|
+
// Search
|
|
609
|
+
if (url.searchParams.has("q")) {
|
|
610
|
+
const query = url.searchParams.get("q");
|
|
611
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
612
|
+
const results = this.#articles.search(query);
|
|
613
|
+
res.end(JSON.stringify({ articles: results }));
|
|
614
|
+
return;
|
|
615
|
+
}
|
|
521
616
|
// GET all blog data
|
|
522
617
|
if (pathname === "/api/articles") {
|
|
523
618
|
// Use 'offset' param as startId (filter) to get items starting at ID
|
|
524
|
-
const
|
|
525
|
-
const
|
|
526
|
-
|
|
527
|
-
const
|
|
528
|
-
const
|
|
619
|
+
const pLimit = parseInt(url.searchParams.get("limit"));
|
|
620
|
+
const limit = !isNaN(pLimit) ? pLimit : null;
|
|
621
|
+
|
|
622
|
+
const start = url.searchParams.get("startdate");
|
|
623
|
+
const end = url.searchParams.get("enddate");
|
|
624
|
+
//const parsedStart = parseDateParam(qStartdate);
|
|
625
|
+
//const parsedEnd = parseDateParam(qEnddate, true);
|
|
626
|
+
|
|
627
|
+
//const effectiveStart = parsedStart !== null ? parsedStart : startID;
|
|
628
|
+
//const effectiveEnd = parsedEnd !== null ? parsedEnd : endID;
|
|
629
|
+
|
|
529
630
|
// controller
|
|
530
631
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
531
|
-
const dbArticles = await this.#
|
|
532
|
-
limit,
|
|
533
|
-
0,
|
|
534
|
-
startID,
|
|
535
|
-
endID
|
|
536
|
-
);
|
|
632
|
+
const dbArticles = await this.#articles.findAll(start, end, limit);
|
|
537
633
|
const responseData = {
|
|
538
634
|
title: this.title, // Keep the title from the original constant
|
|
539
635
|
articles: dbArticles,
|
|
@@ -560,6 +656,40 @@ export default class Blog {
|
|
|
560
656
|
// extern
|
|
561
657
|
res.writeHead(201, { "Content-Type": "application/json" });
|
|
562
658
|
res.end(JSON.stringify(newArticle));
|
|
659
|
+
} else if (req.method === "DELETE" && pathname === "/api/articles") {
|
|
660
|
+
if (!this.#isAuthenticated(req)) {
|
|
661
|
+
res.writeHead(403, { "Content-Type": "application/json" });
|
|
662
|
+
res.end(JSON.stringify({ error: "Forbidden" }));
|
|
663
|
+
return;
|
|
664
|
+
}
|
|
665
|
+
const id = url.searchParams.get("id");
|
|
666
|
+
if (id) {
|
|
667
|
+
this.#articles.remove(parseInt(id));
|
|
668
|
+
if (this.#databaseModel) {
|
|
669
|
+
await this.#databaseModel.remove(parseInt(id));
|
|
670
|
+
}
|
|
671
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
672
|
+
res.end(JSON.stringify({ status: "deleted", id }));
|
|
673
|
+
}
|
|
674
|
+
} else if (req.method === "PUT" && pathname === "/api/articles") {
|
|
675
|
+
if (!this.#isAuthenticated(req)) {
|
|
676
|
+
res.writeHead(403, { "Content-Type": "application/json" });
|
|
677
|
+
res.end(JSON.stringify({ error: "Forbidden" }));
|
|
678
|
+
return;
|
|
679
|
+
}
|
|
680
|
+
const id = url.searchParams.get("id");
|
|
681
|
+
const body = await new Promise((resolve) => {
|
|
682
|
+
let data = "";
|
|
683
|
+
req.on("data", (chunk) => (data += chunk));
|
|
684
|
+
req.on("end", () => resolve(data));
|
|
685
|
+
});
|
|
686
|
+
const { title, content } = JSON.parse(body);
|
|
687
|
+
this.#articles.update(parseInt(id), title, content);
|
|
688
|
+
if (this.#databaseModel) {
|
|
689
|
+
await this.#databaseModel.update(parseInt(id), { title, content });
|
|
690
|
+
}
|
|
691
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
692
|
+
res.end(JSON.stringify({ status: "updated", id }));
|
|
563
693
|
}
|
|
564
694
|
}
|
|
565
695
|
|
|
@@ -573,7 +703,7 @@ export default class Blog {
|
|
|
573
703
|
print() {
|
|
574
704
|
const data = {
|
|
575
705
|
title: this.title,
|
|
576
|
-
articles: this.#articles,
|
|
706
|
+
articles: this.#articles.getAllArticles(),
|
|
577
707
|
};
|
|
578
708
|
const markdown = formatMarkdown(data);
|
|
579
709
|
console.log(markdown);
|
|
@@ -581,9 +711,13 @@ export default class Blog {
|
|
|
581
711
|
|
|
582
712
|
/** render this blog content to valid html */
|
|
583
713
|
async toHTML(loggedin) {
|
|
714
|
+
const articles = this.#articles.getAllArticles();
|
|
715
|
+
const articles_after = articles.slice(0, 50);
|
|
716
|
+
// prettier-ignore
|
|
717
|
+
debug("slice articles from %d to %d", articles.length, articles_after.length);
|
|
584
718
|
const data = {
|
|
585
719
|
title: this.title,
|
|
586
|
-
articles:
|
|
720
|
+
articles: articles_after,
|
|
587
721
|
loggedin,
|
|
588
722
|
login: "",
|
|
589
723
|
};
|
|
@@ -591,6 +725,12 @@ export default class Blog {
|
|
|
591
725
|
if (loggedin) data.login = `<a href="/logout">logout</a>`;
|
|
592
726
|
else data.login = `<a href="/login">login</a>`;
|
|
593
727
|
|
|
728
|
+
//debug("typeof data: %o", typeof data);
|
|
729
|
+
//debug("typeof data.articles: %o", typeof data.articles);
|
|
730
|
+
//debug("typeof data.articles: %O", data.articles);
|
|
731
|
+
const article = data.articles[0];
|
|
732
|
+
debug("first article: ");
|
|
733
|
+
debug("%d %s %s", article.id, article.title, article.getContentVeryShort());
|
|
594
734
|
const html = formatHTML(data);
|
|
595
735
|
if (validate(html)) return html;
|
|
596
736
|
throw new Error("Error. Invalid HTML!");
|
|
@@ -611,7 +751,7 @@ export default class Blog {
|
|
|
611
751
|
(f) =>
|
|
612
752
|
typeof f === "string" &&
|
|
613
753
|
(f.endsWith(".scss") || f.endsWith(".css")) &&
|
|
614
|
-
!f.endsWith(".min.css")
|
|
754
|
+
!f.endsWith(".min.css"),
|
|
615
755
|
);
|
|
616
756
|
//const scriptFiles = files.filter((f) => f.endsWith(".js") && !f.endsWith(".min.js"));
|
|
617
757
|
|
|
@@ -623,7 +763,7 @@ export default class Blog {
|
|
|
623
763
|
const content = await fs.promises.readFile(f, "utf-8");
|
|
624
764
|
if (content == "") throw new Error("Invalid Filepath or empty file!");
|
|
625
765
|
return { path: f, content };
|
|
626
|
-
})
|
|
766
|
+
}),
|
|
627
767
|
);
|
|
628
768
|
|
|
629
769
|
// compute hash
|
|
@@ -632,9 +772,9 @@ export default class Blog {
|
|
|
632
772
|
.update(
|
|
633
773
|
fileData
|
|
634
774
|
.map((f) =>
|
|
635
|
-
crypto.createHash("sha256").update(f.content).digest("hex")
|
|
775
|
+
crypto.createHash("sha256").update(f.content).digest("hex"),
|
|
636
776
|
)
|
|
637
|
-
.join("")
|
|
777
|
+
.join(""),
|
|
638
778
|
)
|
|
639
779
|
.digest("hex");
|
|
640
780
|
|
|
@@ -652,11 +792,73 @@ export default class Blog {
|
|
|
652
792
|
await fs.promises.mkdir(publicDir, { recursive: true });
|
|
653
793
|
await fs.promises.writeFile(
|
|
654
794
|
path.join(publicDir, "styles.min.css"),
|
|
655
|
-
this.compiledStyles + `\n/* source-hash: ${currentHash}
|
|
795
|
+
this.compiledStyles + `\n/* source-hash: ${currentHash} */`,
|
|
656
796
|
);
|
|
657
797
|
} else {
|
|
658
798
|
console.log("styles are up-to-date");
|
|
659
799
|
}
|
|
660
800
|
}
|
|
661
801
|
}
|
|
802
|
+
|
|
803
|
+
/**
|
|
804
|
+
* read files, compare checksums, compile and write to public/scripts.min.js
|
|
805
|
+
* @param {string|string[]} files - File path(s) to process.
|
|
806
|
+
*/
|
|
807
|
+
async #processScripts(files) {
|
|
808
|
+
// Normalize input to array
|
|
809
|
+
const fileList = Array.isArray(files) ? files : [files];
|
|
810
|
+
const scriptFiles = fileList.filter(
|
|
811
|
+
(f) =>
|
|
812
|
+
typeof f === "string" && f.endsWith(".js") && !f.endsWith(".min.js"),
|
|
813
|
+
);
|
|
814
|
+
|
|
815
|
+
if (scriptFiles.length > 0) {
|
|
816
|
+
const fileData = await Promise.all(
|
|
817
|
+
scriptFiles.map(async (f) => {
|
|
818
|
+
const content = await fs.promises.readFile(f, "utf-8");
|
|
819
|
+
if (content == "") throw new Error("Invalid Filepath or empty file!");
|
|
820
|
+
return { path: f, content };
|
|
821
|
+
}),
|
|
822
|
+
);
|
|
823
|
+
|
|
824
|
+
const currentHash = crypto
|
|
825
|
+
.createHash("sha256")
|
|
826
|
+
.update(
|
|
827
|
+
fileData
|
|
828
|
+
.map((f) =>
|
|
829
|
+
crypto.createHash("sha256").update(f.content).digest("hex"),
|
|
830
|
+
)
|
|
831
|
+
.join(""),
|
|
832
|
+
)
|
|
833
|
+
.digest("hex");
|
|
834
|
+
|
|
835
|
+
if (!this.#scriptsHash) {
|
|
836
|
+
try {
|
|
837
|
+
const publicDir = path.join(process.cwd(), "public");
|
|
838
|
+
const existing = await fs.promises.readFile(
|
|
839
|
+
path.join(publicDir, "scripts.min.js"),
|
|
840
|
+
"utf-8",
|
|
841
|
+
);
|
|
842
|
+
const match = existing.match(/\/\* source-hash: ([a-f0-9]{64}) \*\//);
|
|
843
|
+
if (match) this.#scriptsHash = match[1];
|
|
844
|
+
} catch (err) {
|
|
845
|
+
// ignore
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
if (currentHash !== this.#scriptsHash) {
|
|
850
|
+
console.log("Script assets have changed. Recompiling...");
|
|
851
|
+
this.#scriptsHash = currentHash;
|
|
852
|
+
|
|
853
|
+
const compiledScripts = await compileScripts(fileData);
|
|
854
|
+
|
|
855
|
+
const publicDir = path.join(process.cwd(), "public");
|
|
856
|
+
await fs.promises.mkdir(publicDir, { recursive: true });
|
|
857
|
+
await fs.promises.writeFile(
|
|
858
|
+
path.join(publicDir, "scripts.min.js"),
|
|
859
|
+
compiledScripts + `\n/* source-hash: ${currentHash} */`,
|
|
860
|
+
);
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
}
|
|
662
864
|
}
|