@lexho111/plainblog 0.5.27 → 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 (65) hide show
  1. package/Article.js +73 -4
  2. package/Blog.js +348 -149
  3. package/Formatter.js +5 -12
  4. package/README.md +5 -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 +11 -2
  59. package/postinstall.js +89 -0
  60. package/public/fetchData.js +0 -0
  61. package/public/scripts.min.js +2 -1
  62. package/public/styles.min.css +2 -2
  63. package/src/fetchData.js +150 -30
  64. package/src/styles.css +47 -0
  65. package/utilities.js +142 -0
package/Blog.js CHANGED
@@ -2,16 +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" };
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");
13
27
 
14
28
  export default class Blog {
29
+ #makeDataModel() {
30
+ return new BinarySearchTreeHashMap();
31
+ }
32
+
33
+ setDataModel(datamodel) {
34
+ this.#articles = datamodel;
35
+ }
36
+
15
37
  constructor() {
16
38
  this.database = {
17
39
  type: "file",
@@ -21,7 +43,7 @@ export default class Blog {
21
43
  dbname: "articles.txt", // x
22
44
  };
23
45
  this.#title = "";
24
- this.#articles = [];
46
+ this.#articles = new DataModel(this.#makeDataModel());
25
47
  this.#server = null;
26
48
  this.#password = "admin";
27
49
  this.#styles = "body { font-family: Arial; }";
@@ -47,7 +69,7 @@ export default class Blog {
47
69
  const json = {
48
70
  version: this.#version,
49
71
  title: this.#title,
50
- articles: this.#articles,
72
+ articles: this.#articles.getAllArticles(),
51
73
  server: serverInfo,
52
74
  compiledStyles: this.compiledStyles,
53
75
  sessions: this.sessions,
@@ -57,7 +79,7 @@ export default class Blog {
57
79
  reloadStylesOnGET: this.reloadStylesOnGET,
58
80
  };
59
81
 
60
- return JSON.parse(JSON.stringify(json));
82
+ return JSON.parse(JSON.stringify(json)); // make json read-only
61
83
  }
62
84
 
63
85
  // Private fields
@@ -71,7 +93,7 @@ export default class Blog {
71
93
  #articles = [];
72
94
  #styles = "";
73
95
  #stylesHash = "";
74
- //#scriptsHash = "";
96
+ #scriptsHash = "";
75
97
  #stylesheetPath = "";
76
98
  compilestyle = false;
77
99
  #initPromise = null;
@@ -111,7 +133,10 @@ export default class Blog {
111
133
  */
112
134
  setDatabaseAdapter(adapter) {
113
135
  if (!this.#databaseModel) {
114
- 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
+ }
115
140
  }
116
141
  this.#databaseModel.setDatabaseAdapter(adapter);
117
142
  }
@@ -136,7 +161,7 @@ export default class Blog {
136
161
  }
137
162
 
138
163
  addArticle(article) {
139
- this.#articles.push(article);
164
+ this.#articles.insert(article);
140
165
  }
141
166
 
142
167
  #isAuthenticated(req) {
@@ -146,6 +171,7 @@ export default class Blog {
146
171
  }
147
172
 
148
173
  async #handleLogin(req, res) {
174
+ debug("handle login");
149
175
  const body = await new Promise((resolve, reject) => {
150
176
  let data = "";
151
177
  req.on("data", (chunk) => (data += chunk.toString()));
@@ -166,17 +192,18 @@ export default class Blog {
166
192
  res.writeHead(401, { "Content-Type": "text/html" });
167
193
  res.end(`${header("My Blog")}
168
194
  <body>
169
- <h1>Unauthorized</h1><p>Please enter the password.<form method="POST">
195
+ <h1>Unauthorized</h1><div class="box"><p>Please enter the password.<form method="POST">
170
196
  <input type="password" name="password" placeholder="Password" />
171
- <button style="margin: 2px;">Login</button></form>
197
+ <button style="margin: 2px;">Login</button></form></div>
172
198
  </body></html>`);
173
199
  }
174
200
  }
175
201
 
176
202
  #handleLogout(req, res) {
203
+ debug("handle logout");
177
204
  if (req.headers.cookie) {
178
205
  const params = new URLSearchParams(
179
- req.headers.cookie.replace(/; /g, "&")
206
+ req.headers.cookie.replace(/; /g, "&"),
180
207
  );
181
208
  const sessionId = params.get("session");
182
209
  if (this.sessions.has(sessionId)) {
@@ -194,105 +221,134 @@ export default class Blog {
194
221
  async init() {
195
222
  if (this.#initPromise) return this.#initPromise;
196
223
  this.#initPromise = (async () => {
197
- //this.loadStyles();
198
- //this.loadScripts();
199
- // if there is a stylesheet path provided, process it
200
- if (this.#stylesheetPath != null && this.compilestyle) {
201
- // read file from stylesheet path, compare checksums and write to public/styles.min.css
202
- await this.#processStylesheets(this.#stylesheetPath);
203
- }
204
- if (!this.#stylesheetPath) {
205
- // this.#styles
206
- // src/styles.css
207
- // compile and merge hardcoded styles in "this.#styles" with "src/styles.css" and write to file "styles.min.css"
208
- // 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"...'
209
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
+ }
291
+
292
+ // Process Scripts
210
293
  const __filename = fileURLToPath(import.meta.url);
211
294
  const __dirname = path.dirname(__filename);
212
- const srcStylePath = path.join(__dirname, "src", "styles.css");
213
- const publicStylePath = path.join(__dirname, "public", "styles.min.css");
214
-
215
- let publicHash = null;
216
- let srcStyles = "";
217
-
218
- await Promise.all([
219
- fs.promises
220
- .readFile(publicStylePath, "utf8")
221
- .then((publicCSS) => {
222
- const match = publicCSS.match(
223
- /\/\* source-hash: ([a-f0-9]{64}) \*\//
224
- );
225
- if (match) publicHash = match[1];
226
- })
227
- .catch((err) => console.error(err)), // public/styles.min.css doesn't exist, will be created.
228
- fs.promises
229
- .readFile(srcStylePath, "utf8")
230
- .then((content) => {
231
- srcStyles = content;
232
- })
233
- .catch((err) => console.error(err)), // ignore if src/styles.css doesn't exist
234
- ]);
235
-
236
- const combinedStyles = this.#styles + srcStyles;
237
- const srcHash = crypto
238
- .createHash("sha256")
239
- .update(combinedStyles)
240
- .digest("hex");
295
+ const srcScriptPath = path.join(__dirname, "src", "fetchData.js");
296
+ await this.#processScripts(srcScriptPath);
241
297
 
242
- if (srcHash !== publicHash && this.compilestyle) {
243
- console.log("Styles have changed. Recompiling...");
244
- const finalStyles = await mergeStyles(this.#styles, srcStyles);
245
- try {
246
- //await fs.promises.mkdir(path.dirname(publicStylePath), { recursive: true });
247
- await fs.promises.writeFile(
248
- publicStylePath,
249
- 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
+ ),
250
341
  );
251
- } catch (err) {
252
- console.error("Failed to write styles to public folder:", err);
253
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);
254
351
  }
255
- }
256
- if (this.#isExternalAPI) {
257
- console.log("external API");
258
- await this.#loadFromAPI();
259
- } else {
260
- console.log(`database: ${this.database.type}`);
261
- if (!this.#databaseModel) {
262
- this.#databaseModel = new DatabaseModel(this.database);
263
- }
264
- console.log(`connected to database`);
265
- await this.#databaseModel.initialize();
266
- const [dbTitle, dbArticles] = await Promise.all([
267
- this.#databaseModel.getBlogTitle(),
268
- this.#databaseModel.findAll(),
269
- ]);
270
-
271
- if (dbArticles.length == 0) {
272
- dbArticles.push(
273
- new Article(
274
- "Sample Entry #1",
275
- "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.",
276
- new Date()
277
- )
278
- );
279
- dbArticles.push(
280
- new Article(
281
- "Sample Entry #2",
282
- "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.",
283
- new Date()
284
- )
285
- );
286
- }
287
- if (this.reloadStylesOnGET)
288
- console.log("reload scripts and styles on GET-Request");
289
- let title = "";
290
- if (this.#title != null && this.#title.length > 0)
291
- title = this.#title; // use blog title if set
292
- else title = dbTitle; // use title from the database
293
- const responseData = { title: title, articles: dbArticles };
294
- this.#applyBlogData(responseData);
295
- }
296
352
  })();
297
353
  return this.#initPromise;
298
354
  }
@@ -322,12 +378,10 @@ export default class Blog {
322
378
  promises.push(postData(this.#apiUrl, newArticleData));
323
379
  await Promise.all(promises);
324
380
  // Add the article to the local list for immediate display
325
- this.#articles.unshift(new Article(title, content, new Date()));
381
+ this.#articles.insert(Article.createNew(title, content));
326
382
  // remove sample entries
327
- this.#articles = this.#articles.filter(
328
- (art) =>
329
- art.title !== "Sample Entry #1" && art.title !== "Sample Entry #2"
330
- );
383
+ this.#articles.remove(1); // "Sample Entry #1"
384
+ this.#articles.remove(2); // "Sample Entry #2"
331
385
  } catch (error) {
332
386
  console.error("Failed to post new article to API:", error);
333
387
  }
@@ -343,6 +397,7 @@ export default class Blog {
343
397
  await this.init();
344
398
 
345
399
  const server = http.createServer(async (req, res) => {
400
+ //debug("query %s", req.url);
346
401
  // API routes
347
402
  if (req.url.startsWith("/api")) {
348
403
  await this.#jsonAPI(req, res);
@@ -355,9 +410,9 @@ export default class Blog {
355
410
  res.writeHead(200, { "Content-Type": "text/html" });
356
411
  res.end(`${header("My Blog")}
357
412
  <body>
358
- <h1>Login</h1><form method="POST">
413
+ <h1>Login</h1><div class="box"><form method="POST">
359
414
  <input type="password" name="password" placeholder="Password" />
360
- <button style="margin: 2px;">Login</button></form>
415
+ <button style="margin: 2px;">Login</button></form></div>
361
416
  </body></html>`);
362
417
  return;
363
418
  } else if (req.method === "POST") {
@@ -387,6 +442,10 @@ export default class Blog {
387
442
  if (this.#stylesheetPath != null && this.compilestyle) {
388
443
  await this.#processStylesheets(this.#stylesheetPath);
389
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);
390
449
  }
391
450
 
392
451
  let loggedin = false;
@@ -410,12 +469,10 @@ export default class Blog {
410
469
  } else {
411
470
  // Try to serve static files from public folder
412
471
  try {
413
- const __filename = fileURLToPath(import.meta.url);
414
- const __dirname = path.dirname(__filename);
415
- const publicDir = path.join(__dirname, "public");
472
+ const publicDir = path.join(process.cwd(), "public");
416
473
  const parsedUrl = new URL(
417
474
  req.url,
418
- `http://${req.headers.host || "localhost"}`
475
+ `http://${req.headers.host || "localhost"}`,
419
476
  );
420
477
  const filePath = path.join(publicDir, parsedUrl.pathname);
421
478
 
@@ -441,8 +498,11 @@ export default class Blog {
441
498
  }
442
499
  }
443
500
  } catch (err) {
444
- console.error(err);
445
- // 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
+ }
446
506
  }
447
507
 
448
508
  // Error 404
@@ -456,7 +516,8 @@ export default class Blog {
456
516
  return new Promise((resolve, reject) => {
457
517
  const errorHandler = (err) => reject(err);
458
518
  this.#server.once("error", errorHandler);
459
- 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
460
521
  this.#server.removeListener("error", errorHandler);
461
522
  console.log(`server running at http://localhost:${port}/`);
462
523
  resolve(); // Resolve the promise when the server is listening
@@ -482,18 +543,42 @@ export default class Blog {
482
543
 
483
544
  /** Populates the blog's title and articles from a data object. */
484
545
  #applyBlogData(data) {
485
- 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
+ }
486
552
  this.#title = data.title;
487
553
  // Assuming data contains a title and an array of articles with title and content
488
- if (data.articles && Array.isArray(data.articles)) {
489
- for (const articleData of data.articles) {
490
- const article = new Article(
491
- articleData.title,
492
- articleData.content,
493
- articleData.createdAt
494
- );
495
- article.id = articleData.id; // TODO x
496
- 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
+ }
497
582
  }
498
583
  }
499
584
  }
@@ -519,22 +604,31 @@ export default class Blog {
519
604
  };
520
605
  res.end(JSON.stringify(data));
521
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
+ }
522
615
  // GET all blog data
523
616
  if (pathname === "/api/articles") {
524
617
  // Use 'offset' param as startId (filter) to get items starting at ID
525
- const pStartID = parseInt(url.searchParams.get("startID"));
526
- const startID = !isNaN(pStartID) ? pStartID : null;
527
- const pEndID = parseInt(url.searchParams.get("endID"));
528
- const endID = !isNaN(pEndID) ? pEndID : null;
529
- 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
+
530
629
  // controller
531
630
  res.writeHead(200, { "Content-Type": "application/json" });
532
- const dbArticles = await this.#databaseModel.findAll(
533
- limit,
534
- 0,
535
- startID,
536
- endID
537
- );
631
+ const dbArticles = await this.#articles.findAll(start, end, limit);
538
632
  const responseData = {
539
633
  title: this.title, // Keep the title from the original constant
540
634
  articles: dbArticles,
@@ -561,6 +655,40 @@ export default class Blog {
561
655
  // extern
562
656
  res.writeHead(201, { "Content-Type": "application/json" });
563
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 }));
564
692
  }
565
693
  }
566
694
 
@@ -574,7 +702,7 @@ export default class Blog {
574
702
  print() {
575
703
  const data = {
576
704
  title: this.title,
577
- articles: this.#articles,
705
+ articles: this.#articles.getAllArticles(),
578
706
  };
579
707
  const markdown = formatMarkdown(data);
580
708
  console.log(markdown);
@@ -582,9 +710,13 @@ export default class Blog {
582
710
 
583
711
  /** render this blog content to valid html */
584
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);
585
717
  const data = {
586
718
  title: this.title,
587
- articles: this.#articles,
719
+ articles: articles_after,
588
720
  loggedin,
589
721
  login: "",
590
722
  };
@@ -592,6 +724,12 @@ export default class Blog {
592
724
  if (loggedin) data.login = `<a href="/logout">logout</a>`;
593
725
  else data.login = `<a href="/login">login</a>`;
594
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());
595
733
  const html = formatHTML(data);
596
734
  if (validate(html)) return html;
597
735
  throw new Error("Error. Invalid HTML!");
@@ -612,7 +750,7 @@ export default class Blog {
612
750
  (f) =>
613
751
  typeof f === "string" &&
614
752
  (f.endsWith(".scss") || f.endsWith(".css")) &&
615
- !f.endsWith(".min.css")
753
+ !f.endsWith(".min.css"),
616
754
  );
617
755
  //const scriptFiles = files.filter((f) => f.endsWith(".js") && !f.endsWith(".min.js"));
618
756
 
@@ -624,7 +762,7 @@ export default class Blog {
624
762
  const content = await fs.promises.readFile(f, "utf-8");
625
763
  if (content == "") throw new Error("Invalid Filepath or empty file!");
626
764
  return { path: f, content };
627
- })
765
+ }),
628
766
  );
629
767
 
630
768
  // compute hash
@@ -633,9 +771,9 @@ export default class Blog {
633
771
  .update(
634
772
  fileData
635
773
  .map((f) =>
636
- crypto.createHash("sha256").update(f.content).digest("hex")
774
+ crypto.createHash("sha256").update(f.content).digest("hex"),
637
775
  )
638
- .join("")
776
+ .join(""),
639
777
  )
640
778
  .digest("hex");
641
779
 
@@ -648,17 +786,78 @@ export default class Blog {
648
786
  this.compiledStyles = await compileStyles(fileData);
649
787
 
650
788
  // generate a file
651
- const __filename = fileURLToPath(import.meta.url);
652
- const __dirname = path.dirname(__filename);
653
- const publicDir = path.join(__dirname, "public");
789
+ const publicDir = path.join(process.cwd(), "public");
654
790
 
791
+ await fs.promises.mkdir(publicDir, { recursive: true });
655
792
  await fs.promises.writeFile(
656
793
  path.join(publicDir, "styles.min.css"),
657
- this.compiledStyles + `\n/* source-hash: ${currentHash} */`
794
+ this.compiledStyles + `\n/* source-hash: ${currentHash} */`,
658
795
  );
659
796
  } else {
660
797
  console.log("styles are up-to-date");
661
798
  }
662
799
  }
663
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
+ }
664
863
  }