@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.
Files changed (64) hide show
  1. package/Article.js +73 -4
  2. package/Blog.js +341 -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 +1043 -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 +2635 -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 +307 -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 +346 -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 +2063 -0
  45. package/index.js +25 -1
  46. package/model/DataModel.js +79 -0
  47. package/model/DatabaseModel.js +20 -8
  48. package/model/FileAdapter.js +43 -4
  49. package/model/FileModel.js +47 -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 +276 -0
  55. package/model/datastructures/BinarySearchTreeHashMap.js +89 -0
  56. package/model/datastructures/BinarySearchTreeTest.js +16 -0
  57. package/model/datastructures/FileList.js +53 -0
  58. package/package.json +10 -2
  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,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 pkg from "./package.json" with { type: "json" };
13
- import process from 'node:process';
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
- //#scriptsHash = "";
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
- this.#databaseModel = new DatabaseModel(this.database);
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.push(article);
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
- //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"...'
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 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");
295
+ const srcScriptPath = path.join(__dirname, "src", "fetchData.js");
296
+ await this.#processScripts(srcScriptPath);
242
297
 
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} */`
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.unshift(new Article(title, content, new Date()));
381
+ this.#articles.insert(Article.createNew(title, content));
327
382
  // remove sample entries
328
- this.#articles = this.#articles.filter(
329
- (art) =>
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
- console.error(err);
444
- // Continue to 404
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", () => { // <-- for docker 0.0.0.0, localhost 127.0.0.1
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
- this.#articles = []; // Clear existing articles before loading new ones
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
- 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);
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 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;
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.#databaseModel.findAll(
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: this.#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
  }