@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.
Files changed (64) hide show
  1. package/Article.js +73 -4
  2. package/Blog.js +342 -140
  3. package/Formatter.js +2 -11
  4. package/README.md +1 -1
  5. package/{blog → blog_test_empty.db} +0 -0
  6. package/blog_test_load.db +0 -0
  7. package/build-scripts.js +54 -0
  8. package/coverage/clover.xml +1051 -0
  9. package/coverage/coverage-final.json +20 -0
  10. package/coverage/lcov-report/base.css +224 -0
  11. package/coverage/lcov-report/block-navigation.js +87 -0
  12. package/coverage/lcov-report/favicon.png +0 -0
  13. package/coverage/lcov-report/index.html +161 -0
  14. package/coverage/lcov-report/package/Article.js.html +406 -0
  15. package/coverage/lcov-report/package/Blog.js.html +2674 -0
  16. package/coverage/lcov-report/package/Formatter.js.html +379 -0
  17. package/coverage/lcov-report/package/build-scripts.js.html +247 -0
  18. package/coverage/lcov-report/package/build-styles.js.html +367 -0
  19. package/coverage/lcov-report/package/index.html +191 -0
  20. package/coverage/lcov-report/package/model/APIModel.js.html +190 -0
  21. package/coverage/lcov-report/package/model/ArrayList.js.html +382 -0
  22. package/coverage/lcov-report/package/model/ArrayListHashMap.js.html +379 -0
  23. package/coverage/lcov-report/package/model/BinarySearchTree.js.html +856 -0
  24. package/coverage/lcov-report/package/model/BinarySearchTreeHashMap.js.html +346 -0
  25. package/coverage/lcov-report/package/model/DataModel.js.html +322 -0
  26. package/coverage/lcov-report/package/model/DatabaseModel.js.html +232 -0
  27. package/coverage/lcov-report/package/model/FileAdapter.js.html +394 -0
  28. package/coverage/lcov-report/package/model/FileList.js.html +244 -0
  29. package/coverage/lcov-report/package/model/FileModel.js.html +358 -0
  30. package/coverage/lcov-report/package/model/SequelizeAdapter.js.html +538 -0
  31. package/coverage/lcov-report/package/model/SqliteAdapter.js.html +247 -0
  32. package/coverage/lcov-report/package/model/datastructures/ArrayList.js.html +439 -0
  33. package/coverage/lcov-report/package/model/datastructures/ArrayListHashMap.js.html +196 -0
  34. package/coverage/lcov-report/package/model/datastructures/BinarySearchTree.js.html +913 -0
  35. package/coverage/lcov-report/package/model/datastructures/BinarySearchTreeHashMap.js.html +352 -0
  36. package/coverage/lcov-report/package/model/datastructures/FileList.js.html +244 -0
  37. package/coverage/lcov-report/package/model/datastructures/index.html +176 -0
  38. package/coverage/lcov-report/package/model/index.html +206 -0
  39. package/coverage/lcov-report/package/utilities.js.html +511 -0
  40. package/coverage/lcov-report/prettify.css +1 -0
  41. package/coverage/lcov-report/prettify.js +2 -0
  42. package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
  43. package/coverage/lcov-report/sorter.js +210 -0
  44. package/coverage/lcov.info +2078 -0
  45. package/index.js +25 -1
  46. package/model/DataModel.js +80 -0
  47. package/model/DatabaseModel.js +21 -8
  48. package/model/FileAdapter.js +44 -4
  49. package/model/FileModel.js +48 -9
  50. package/model/SequelizeAdapter.js +11 -3
  51. package/model/datastructures/ArrayList.js +118 -0
  52. package/model/datastructures/ArrayListHashMap.js +37 -0
  53. package/model/datastructures/ArrayListHashMap.js.bk +90 -0
  54. package/model/datastructures/BinarySearchTree.js +277 -0
  55. package/model/datastructures/BinarySearchTreeHashMap.js +90 -0
  56. package/model/datastructures/BinarySearchTreeTest.js +16 -0
  57. package/model/datastructures/FileList.js +53 -0
  58. package/package.json +9 -3
  59. package/public/fetchData.js +0 -0
  60. package/public/scripts.min.js +2 -1
  61. package/public/styles.min.css +2 -1
  62. package/src/fetchData.js +150 -30
  63. package/src/styles.css +29 -0
  64. 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 pkg from "./package.json" with { type: "json" };
13
- import process from 'node:process';
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
- //#scriptsHash = "";
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
- this.#databaseModel = new DatabaseModel(this.database);
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.push(article);
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
- //this.loadStyles();
199
- //this.loadScripts();
200
- // if there is a stylesheet path provided, process it
201
- if (this.#stylesheetPath != null && this.compilestyle) {
202
- // read file from stylesheet path, compare checksums and write to public/styles.min.css
203
- await this.#processStylesheets(this.#stylesheetPath);
204
- }
205
- if (!this.#stylesheetPath) {
206
- // this.#styles
207
- // src/styles.css
208
- // compile and merge hardcoded styles in "this.#styles" with "src/styles.css" and write to file "styles.min.css"
209
- // which will be imported by webbrowser via '<link rel="stylesheet" href="styles.min.css"...'
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 srcStylePath = path.join(__dirname, "src", "styles.css");
214
- const publicStylePath = path.join(process.cwd(), "public", "styles.min.css");
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 (srcHash !== publicHash && this.compilestyle) {
244
- console.log("Styles have changed. Recompiling...");
245
- const finalStyles = await mergeStyles(this.#styles, srcStyles);
246
- try {
247
- await fs.promises.mkdir(path.dirname(publicStylePath), { recursive: true });
248
- await fs.promises.writeFile(
249
- publicStylePath,
250
- finalStyles + `\n/* source-hash: ${srcHash} */`
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.unshift(new Article(title, content, new Date()));
382
+ this.#articles.insert(Article.createNew(title, content));
327
383
  // remove sample entries
328
- this.#articles = this.#articles.filter(
329
- (art) =>
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
- console.error(err);
444
- // Continue to 404
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", () => { // <-- for docker 0.0.0.0, localhost 127.0.0.1
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
- this.#articles = []; // Clear existing articles before loading new ones
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
- for (const articleData of data.articles) {
489
- const article = new Article(
490
- articleData.title,
491
- articleData.content,
492
- articleData.createdAt
493
- );
494
- article.id = articleData.id; // TODO x
495
- this.addArticle(article);
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 pStartID = parseInt(url.searchParams.get("startID"));
525
- const startID = !isNaN(pStartID) ? pStartID : null;
526
- const pEndID = parseInt(url.searchParams.get("endID"));
527
- const endID = !isNaN(pEndID) ? pEndID : null;
528
- const limit = parseInt(url.searchParams.get("limit")) || 10;
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.#databaseModel.findAll(
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: this.#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
  }