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