@lexho111/plainblog 0.6.4 → 0.6.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Article.js +1 -1
- package/Blog.js +187 -126
- package/blog_test_empty.db +0 -0
- package/blog_test_load.db +0 -0
- package/jest.config.js +15 -0
- package/model/DataModel.js +6 -2
- package/model/datastructures/BinarySearchTree.js +27 -0
- package/model/datastructures/BinarySearchTreeHashMap.js +14 -5
- package/package.json +1 -1
- package/public/favicon.ico +0 -0
- package/public/index.html +17 -0
- package/public/main-EXNUAFJ6.js +6 -0
- package/public/scripts.min.js +2 -2
- package/public/styles-OPUTW5UJ.css +1 -0
- package/public/styles.min.css +2 -2
- package/src/fetchData.js +3 -3
- package/src/styles.css +60 -6
package/Article.js
CHANGED
|
@@ -52,7 +52,7 @@ export default class Article {
|
|
|
52
52
|
if (this.content == null) return "";
|
|
53
53
|
const moreButton = this.content.length > 400 ? `<a href="#">more</a>` : "";
|
|
54
54
|
const buttons = loggedin
|
|
55
|
-
? `<div class="buttons"><div id="editButton${this.id}" class="
|
|
55
|
+
? `<div class="buttons"><div id="editButton${this.id}" class="btn edit">edit</div><div id="deleteButton${this.id}" class="btn delete">delete</div></div>`
|
|
56
56
|
: "";
|
|
57
57
|
const script = loggedin
|
|
58
58
|
? `<script>
|
package/Blog.js
CHANGED
|
@@ -16,6 +16,7 @@ import FileAdapter from "./model/FileAdapter.js";
|
|
|
16
16
|
import DataModel from "./model/DataModel.js";
|
|
17
17
|
import { BinarySearchTreeHashMap } from "./model/datastructures/BinarySearchTreeHashMap.js";
|
|
18
18
|
import createDebug from "./debug-loader.js";
|
|
19
|
+
import { readFile } from "node:fs/promises";
|
|
19
20
|
|
|
20
21
|
// Initialize the debugger with a specific namespace
|
|
21
22
|
const debug = createDebug("plainblog:Blog");
|
|
@@ -92,6 +93,7 @@ export default class Blog {
|
|
|
92
93
|
#stylesheetPath = "";
|
|
93
94
|
compilestyle = false;
|
|
94
95
|
#initPromise = null;
|
|
96
|
+
#publicDir = path.join(process.cwd(), "public");
|
|
95
97
|
|
|
96
98
|
setTitle(title) {
|
|
97
99
|
this.#title = title;
|
|
@@ -161,8 +163,8 @@ export default class Blog {
|
|
|
161
163
|
|
|
162
164
|
#isAuthenticated(req) {
|
|
163
165
|
if (!req.headers.cookie) return false;
|
|
164
|
-
const
|
|
165
|
-
return this.sessions.has(
|
|
166
|
+
const match = req.headers.cookie.match(/(?:^|;\s*)session=([^;]*)/);
|
|
167
|
+
return match ? this.sessions.has(match[1]) : false;
|
|
166
168
|
}
|
|
167
169
|
|
|
168
170
|
async #handleLogin(req, res) {
|
|
@@ -184,6 +186,8 @@ export default class Blog {
|
|
|
184
186
|
});
|
|
185
187
|
res.end();
|
|
186
188
|
} else {
|
|
189
|
+
debug("login failed");
|
|
190
|
+
debug("password did not match %s", params.get("password"));
|
|
187
191
|
res.writeHead(401, { "Content-Type": "text/html" });
|
|
188
192
|
res.end(`${header("My Blog")}
|
|
189
193
|
<body>
|
|
@@ -232,11 +236,7 @@ export default class Blog {
|
|
|
232
236
|
const __filename = fileURLToPath(import.meta.url);
|
|
233
237
|
const __dirname = path.dirname(__filename);
|
|
234
238
|
const srcStylePath = path.join(__dirname, "src", "styles.css");
|
|
235
|
-
const publicStylePath = path.join(
|
|
236
|
-
process.cwd(),
|
|
237
|
-
"public",
|
|
238
|
-
"styles.min.css",
|
|
239
|
-
);
|
|
239
|
+
const publicStylePath = path.join(this.#publicDir, "styles.min.css");
|
|
240
240
|
|
|
241
241
|
let publicHash = null;
|
|
242
242
|
let srcStyles = "";
|
|
@@ -348,43 +348,25 @@ export default class Blog {
|
|
|
348
348
|
return this.#initPromise;
|
|
349
349
|
}
|
|
350
350
|
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
const promises = [];
|
|
370
|
-
if (this.#databaseModel)
|
|
371
|
-
promises.push(this.#databaseModel.save(newArticleData));
|
|
372
|
-
if (this.#isExternalAPI)
|
|
373
|
-
promises.push(postData(this.#apiUrl, newArticleData));
|
|
374
|
-
await Promise.all(promises);
|
|
375
|
-
// Add the article to the local list for immediate display
|
|
376
|
-
this.#articles.insert(Article.createNew(title, content));
|
|
377
|
-
// remove sample entries
|
|
378
|
-
this.#articles.remove(1); // "Sample Entry #1"
|
|
379
|
-
this.#articles.remove(2); // "Sample Entry #2"
|
|
380
|
-
} catch (error) {
|
|
381
|
-
console.error("Failed to post new article to API:", error);
|
|
382
|
-
}
|
|
351
|
+
async postArticle(newArticle) {
|
|
352
|
+
try {
|
|
353
|
+
// Save the new article to the database via the ApiServer
|
|
354
|
+
const promises = [];
|
|
355
|
+
//if (this.#databaseModel)
|
|
356
|
+
// promises.push(this.#databaseModel.save(newArticle));
|
|
357
|
+
if (this.#isExternalAPI)
|
|
358
|
+
promises.push(postData(this.#apiUrl, newArticle));
|
|
359
|
+
await Promise.all(promises);
|
|
360
|
+
const title = newArticle.title;
|
|
361
|
+
const content = newArticle.content;
|
|
362
|
+
// Add the article to the local list for immediate display
|
|
363
|
+
this.#articles.insert(Article.createNew(title, content));
|
|
364
|
+
// remove sample entries
|
|
365
|
+
this.#articles.remove(1); // "Sample Entry #1"
|
|
366
|
+
this.#articles.remove(2); // "Sample Entry #2"
|
|
367
|
+
} catch (error) {
|
|
368
|
+
console.error("Failed to post new article to API:", error);
|
|
383
369
|
}
|
|
384
|
-
|
|
385
|
-
res.writeHead(303, { Location: "/" });
|
|
386
|
-
res.end();
|
|
387
|
-
//});
|
|
388
370
|
}
|
|
389
371
|
|
|
390
372
|
/** start a http server with default port 8080 */
|
|
@@ -393,7 +375,18 @@ export default class Blog {
|
|
|
393
375
|
|
|
394
376
|
const server = http.createServer(async (req, res) => {
|
|
395
377
|
//debug("query %s", req.url);
|
|
378
|
+
|
|
396
379
|
// API routes
|
|
380
|
+
// workaround for angular frontend
|
|
381
|
+
if (req.url === "/api/login" && req.method === "POST") {
|
|
382
|
+
await this.#handleLogin(req, res);
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
if (req.url === "/api/logout") {
|
|
386
|
+
this.#handleLogout(req, res);
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
// ---------------------------------------------
|
|
397
390
|
if (req.url.startsWith("/api")) {
|
|
398
391
|
await this.#jsonAPI(req, res);
|
|
399
392
|
return;
|
|
@@ -420,18 +413,9 @@ export default class Blog {
|
|
|
420
413
|
this.#handleLogout(req, res);
|
|
421
414
|
return;
|
|
422
415
|
}
|
|
423
|
-
//
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
res.writeHead(403, { "Content-Type": "text/plain" });
|
|
427
|
-
res.end("Forbidden");
|
|
428
|
-
return;
|
|
429
|
-
}
|
|
430
|
-
await this.postArticle(req, res);
|
|
431
|
-
// GET artciles
|
|
432
|
-
} else if (req.method === "GET" && req.url === "/") {
|
|
433
|
-
// load articles
|
|
434
|
-
|
|
416
|
+
// load articles
|
|
417
|
+
// GET artciles
|
|
418
|
+
if (req.method === "GET" && req.url === "/") {
|
|
435
419
|
// reload styles and scripts on (every) request
|
|
436
420
|
if (this.reloadStylesOnGET) {
|
|
437
421
|
if (this.#stylesheetPath != null && this.compilestyle) {
|
|
@@ -452,6 +436,7 @@ export default class Blog {
|
|
|
452
436
|
loggedin = true;
|
|
453
437
|
}
|
|
454
438
|
|
|
439
|
+
// use built in view engine
|
|
455
440
|
try {
|
|
456
441
|
const html = await this.toHTML(loggedin); // render this blog to HTML
|
|
457
442
|
res.writeHead(200, { "Content-Type": "text/html; charset=UTF-8" });
|
|
@@ -461,48 +446,72 @@ export default class Blog {
|
|
|
461
446
|
res.writeHead(500, { "Content-Type": "text/plain" });
|
|
462
447
|
res.end("Internal Server Error");
|
|
463
448
|
}
|
|
449
|
+
|
|
450
|
+
// use angular frontend
|
|
451
|
+
/*const filePath = path.join(this.#publicDir, "index.html");
|
|
452
|
+
|
|
453
|
+
debug("%s", filePath);
|
|
454
|
+
try {
|
|
455
|
+
const data = await readFile(filePath);
|
|
456
|
+
// Manual MIME type detection (simplified)
|
|
457
|
+
const ext = path.extname(filePath);
|
|
458
|
+
const mimeTypes = {
|
|
459
|
+
".html": "text/html",
|
|
460
|
+
".css": "text/css",
|
|
461
|
+
".js": "text/javascript",
|
|
462
|
+
};
|
|
463
|
+
|
|
464
|
+
res.writeHead(200, {
|
|
465
|
+
"Content-Type": mimeTypes[ext] || "application/octet-stream",
|
|
466
|
+
});
|
|
467
|
+
res.end(data);
|
|
468
|
+
} catch (err) {
|
|
469
|
+
if (err) {
|
|
470
|
+
res.writeHead(404);
|
|
471
|
+
return res.end("Index-File Not Found");
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
try {
|
|
476
|
+
//const html = await this.toHTML(loggedin); // render this blog to HTML
|
|
477
|
+
//res.writeHead(200, { "Content-Type": "text/html; charset=UTF-8" });
|
|
478
|
+
//res.end(html);
|
|
479
|
+
} catch (err) {
|
|
480
|
+
console.error(err);
|
|
481
|
+
res.writeHead(500, { "Content-Type": "text/plain" });
|
|
482
|
+
res.end("Internal Server Error");
|
|
483
|
+
}*/
|
|
464
484
|
} else {
|
|
465
485
|
// Try to serve static files from public folder
|
|
486
|
+
// Normalize path to prevent directory traversal attacks
|
|
487
|
+
const safePath = path.normalize(req.url).replace(/^(\.\.[\/\\])+/, "");
|
|
488
|
+
const filePath = path.join(
|
|
489
|
+
this.#publicDir,
|
|
490
|
+
safePath === "/" ? "index.html" : safePath,
|
|
491
|
+
);
|
|
492
|
+
|
|
493
|
+
debug("%s", filePath);
|
|
466
494
|
try {
|
|
467
|
-
const
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
".js": "text/javascript",
|
|
481
|
-
".css": "text/css",
|
|
482
|
-
".json": "application/json",
|
|
483
|
-
".png": "image/png",
|
|
484
|
-
".jpg": "image/jpeg",
|
|
485
|
-
".gif": "image/gif",
|
|
486
|
-
".svg": "image/svg+xml",
|
|
487
|
-
".ico": "image/x-icon",
|
|
488
|
-
};
|
|
489
|
-
const contentType = mimeTypes[ext] || "application/octet-stream";
|
|
490
|
-
res.writeHead(200, { "Content-Type": contentType });
|
|
491
|
-
fs.createReadStream(filePath).pipe(res);
|
|
492
|
-
return;
|
|
493
|
-
}
|
|
494
|
-
}
|
|
495
|
+
const data = await readFile(filePath);
|
|
496
|
+
// Manual MIME type detection (simplified)
|
|
497
|
+
const ext = path.extname(filePath);
|
|
498
|
+
const mimeTypes = {
|
|
499
|
+
".html": "text/html",
|
|
500
|
+
".css": "text/css",
|
|
501
|
+
".js": "text/javascript",
|
|
502
|
+
};
|
|
503
|
+
|
|
504
|
+
res.writeHead(200, {
|
|
505
|
+
"Content-Type": mimeTypes[ext] || "application/octet-stream",
|
|
506
|
+
});
|
|
507
|
+
res.end(data);
|
|
495
508
|
} catch (err) {
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
console.error(err);
|
|
509
|
+
if (err) {
|
|
510
|
+
res.writeHead(404);
|
|
511
|
+
return res.end("File Not Found");
|
|
500
512
|
}
|
|
501
513
|
}
|
|
502
|
-
|
|
503
|
-
// Error 404
|
|
504
|
-
res.writeHead(404, { "Content-Type": "application/json" });
|
|
505
|
-
res.end(JSON.stringify({ message: "Not Found" }));
|
|
514
|
+
return;
|
|
506
515
|
}
|
|
507
516
|
});
|
|
508
517
|
|
|
@@ -588,11 +597,27 @@ export default class Blog {
|
|
|
588
597
|
// controller
|
|
589
598
|
/** everything that happens in /api */
|
|
590
599
|
async #jsonAPI(req, res) {
|
|
600
|
+
const origin = req.headers.origin;
|
|
601
|
+
if (origin) {
|
|
602
|
+
res.setHeader("Access-Control-Allow-Origin", origin);
|
|
603
|
+
res.setHeader("Access-Control-Allow-Credentials", "true");
|
|
604
|
+
res.setHeader(
|
|
605
|
+
"Access-Control-Allow-Methods",
|
|
606
|
+
"GET, POST, PUT, DELETE, OPTIONS",
|
|
607
|
+
);
|
|
608
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
|
|
609
|
+
}
|
|
610
|
+
if (req.method === "OPTIONS") {
|
|
611
|
+
res.writeHead(204);
|
|
612
|
+
res.end();
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
591
615
|
const url = new URL(req.url, `http://${req.headers.host || "localhost"}`);
|
|
592
616
|
const pathname = url.pathname;
|
|
593
617
|
|
|
594
618
|
if (req.method === "GET") {
|
|
595
619
|
if (pathname === "/api" || pathname === "/api/") {
|
|
620
|
+
debug("GET /api");
|
|
596
621
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
597
622
|
const data = {
|
|
598
623
|
title: this.title,
|
|
@@ -601,20 +626,46 @@ export default class Blog {
|
|
|
601
626
|
}
|
|
602
627
|
// Search
|
|
603
628
|
if (url.searchParams.has("q")) {
|
|
629
|
+
debug("GET search article by query");
|
|
604
630
|
const query = url.searchParams.get("q");
|
|
631
|
+
const pLimit = parseInt(url.searchParams.get("limit"));
|
|
632
|
+
const limit = !isNaN(pLimit) ? pLimit : null;
|
|
633
|
+
debug("GET search article by query %s with limit %s", query, limit);
|
|
605
634
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
606
|
-
const results = this.#articles.search(query);
|
|
635
|
+
const results = this.#articles.search(query, limit);
|
|
607
636
|
res.end(JSON.stringify({ articles: results }));
|
|
608
637
|
return;
|
|
609
638
|
}
|
|
639
|
+
// GET article by ID
|
|
640
|
+
// /api/articles/1
|
|
641
|
+
const match = pathname.match(/^\/api\/articles\/(\d+)$/);
|
|
642
|
+
if (match) {
|
|
643
|
+
debug("GET article by id");
|
|
644
|
+
const id = parseInt(match[1]);
|
|
645
|
+
debug("GET article by id %d", id);
|
|
646
|
+
//console.log(this.#articles.getAllArticles());
|
|
647
|
+
const article = this.#articles.get(id);
|
|
648
|
+
if (article) {
|
|
649
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
650
|
+
res.end(JSON.stringify(article));
|
|
651
|
+
} else {
|
|
652
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
653
|
+
res.end(JSON.stringify({ error: "Not Found" }));
|
|
654
|
+
}
|
|
655
|
+
return;
|
|
656
|
+
}
|
|
610
657
|
// GET all blog data
|
|
611
658
|
if (pathname === "/api/articles") {
|
|
659
|
+
debug("GET all articles");
|
|
612
660
|
// Use 'offset' param as startId (filter) to get items starting at ID
|
|
613
661
|
const pLimit = parseInt(url.searchParams.get("limit"));
|
|
614
662
|
const limit = !isNaN(pLimit) ? pLimit : null;
|
|
615
663
|
|
|
616
664
|
const start = url.searchParams.get("startdate");
|
|
617
665
|
const end = url.searchParams.get("enddate");
|
|
666
|
+
|
|
667
|
+
debug("startdate: %d, enddate: %d, limit: %d", start, end, limit);
|
|
668
|
+
|
|
618
669
|
//const parsedStart = parseDateParam(qStartdate);
|
|
619
670
|
//const parsedEnd = parseDateParam(qEnddate, true);
|
|
620
671
|
|
|
@@ -633,7 +684,9 @@ export default class Blog {
|
|
|
633
684
|
|
|
634
685
|
// POST a new article
|
|
635
686
|
} else if (req.method === "POST" && pathname === "/api/articles") {
|
|
687
|
+
debug("POST an article");
|
|
636
688
|
if (!this.#isAuthenticated(req)) {
|
|
689
|
+
debug("not authenticated");
|
|
637
690
|
res.writeHead(403, { "Content-Type": "application/json" });
|
|
638
691
|
res.end(JSON.stringify({ error: "Forbidden" }));
|
|
639
692
|
return;
|
|
@@ -645,45 +698,57 @@ export default class Blog {
|
|
|
645
698
|
req.on("error", reject);
|
|
646
699
|
});
|
|
647
700
|
const newArticle = JSON.parse(body);
|
|
701
|
+
debug("new article: %s", newArticle.title);
|
|
648
702
|
// local
|
|
649
703
|
await this.#databaseModel.save(newArticle); // --> to api server
|
|
704
|
+
this.postArticle(newArticle);
|
|
650
705
|
// extern
|
|
651
706
|
res.writeHead(201, { "Content-Type": "application/json" });
|
|
652
707
|
res.end(JSON.stringify(newArticle));
|
|
653
|
-
} else if (req.method === "DELETE"
|
|
708
|
+
} else if (req.method === "DELETE") {
|
|
709
|
+
debug("DELETE an article");
|
|
710
|
+
const match = pathname.match(/^\/api\/articles\/(\d+)$/);
|
|
711
|
+
if (pathname === "/api/articles" || match) {
|
|
712
|
+
if (!this.#isAuthenticated(req)) {
|
|
713
|
+
res.writeHead(403, { "Content-Type": "application/json" });
|
|
714
|
+
res.end(JSON.stringify({ error: "Forbidden" }));
|
|
715
|
+
return;
|
|
716
|
+
}
|
|
717
|
+
const id = match ? match[1] : url.searchParams.get("id");
|
|
718
|
+
debug("delete an article by id $d", id);
|
|
719
|
+
if (id) {
|
|
720
|
+
this.#articles.remove(parseInt(id));
|
|
721
|
+
if (this.#databaseModel) {
|
|
722
|
+
await this.#databaseModel.remove(parseInt(id));
|
|
723
|
+
}
|
|
724
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
725
|
+
res.end(JSON.stringify({ status: "deleted", id }));
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
} else if (req.method === "PUT") {
|
|
729
|
+
debug("PUT an article");
|
|
654
730
|
if (!this.#isAuthenticated(req)) {
|
|
655
731
|
res.writeHead(403, { "Content-Type": "application/json" });
|
|
656
732
|
res.end(JSON.stringify({ error: "Forbidden" }));
|
|
657
733
|
return;
|
|
658
734
|
}
|
|
659
|
-
const
|
|
660
|
-
if (
|
|
661
|
-
|
|
735
|
+
const match = pathname.match(/^\/api\/articles\/(\d+)$/);
|
|
736
|
+
if (pathname === "/api/articles" || match) {
|
|
737
|
+
const id = match ? match[1] : url.searchParams.get("id");
|
|
738
|
+
debug("PUT article id: %d", id);
|
|
739
|
+
const body = await new Promise((resolve) => {
|
|
740
|
+
let data = "";
|
|
741
|
+
req.on("data", (chunk) => (data += chunk));
|
|
742
|
+
req.on("end", () => resolve(data));
|
|
743
|
+
});
|
|
744
|
+
const { title, content } = JSON.parse(body);
|
|
745
|
+
this.#articles.update(parseInt(id), title, content);
|
|
662
746
|
if (this.#databaseModel) {
|
|
663
|
-
await this.#databaseModel.
|
|
747
|
+
await this.#databaseModel.update(parseInt(id), { title, content });
|
|
664
748
|
}
|
|
665
749
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
666
|
-
res.end(JSON.stringify({ status: "
|
|
750
|
+
res.end(JSON.stringify({ status: "updated", id }));
|
|
667
751
|
}
|
|
668
|
-
} else if (req.method === "PUT" && pathname === "/api/articles") {
|
|
669
|
-
if (!this.#isAuthenticated(req)) {
|
|
670
|
-
res.writeHead(403, { "Content-Type": "application/json" });
|
|
671
|
-
res.end(JSON.stringify({ error: "Forbidden" }));
|
|
672
|
-
return;
|
|
673
|
-
}
|
|
674
|
-
const id = url.searchParams.get("id");
|
|
675
|
-
const body = await new Promise((resolve) => {
|
|
676
|
-
let data = "";
|
|
677
|
-
req.on("data", (chunk) => (data += chunk));
|
|
678
|
-
req.on("end", () => resolve(data));
|
|
679
|
-
});
|
|
680
|
-
const { title, content } = JSON.parse(body);
|
|
681
|
-
this.#articles.update(parseInt(id), title, content);
|
|
682
|
-
if (this.#databaseModel) {
|
|
683
|
-
await this.#databaseModel.update(parseInt(id), { title, content });
|
|
684
|
-
}
|
|
685
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
686
|
-
res.end(JSON.stringify({ status: "updated", id }));
|
|
687
752
|
}
|
|
688
753
|
}
|
|
689
754
|
|
|
@@ -781,11 +846,9 @@ export default class Blog {
|
|
|
781
846
|
this.compiledStyles = await compileStyles(fileData);
|
|
782
847
|
|
|
783
848
|
// generate a file
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
await fs.promises.mkdir(publicDir, { recursive: true });
|
|
849
|
+
await fs.promises.mkdir(this.#publicDir, { recursive: true });
|
|
787
850
|
await fs.promises.writeFile(
|
|
788
|
-
path.join(publicDir, "styles.min.css"),
|
|
851
|
+
path.join(this.#publicDir, "styles.min.css"),
|
|
789
852
|
this.compiledStyles + `\n/* source-hash: ${currentHash} */`,
|
|
790
853
|
);
|
|
791
854
|
} else {
|
|
@@ -828,9 +891,8 @@ export default class Blog {
|
|
|
828
891
|
|
|
829
892
|
if (!this.#scriptsHash) {
|
|
830
893
|
try {
|
|
831
|
-
const publicDir = path.join(process.cwd(), "public");
|
|
832
894
|
const existing = await fs.promises.readFile(
|
|
833
|
-
path.join(publicDir, "scripts.min.js"),
|
|
895
|
+
path.join(this.#publicDir, "scripts.min.js"),
|
|
834
896
|
"utf-8",
|
|
835
897
|
);
|
|
836
898
|
const match = existing.match(/\/\* source-hash: ([a-f0-9]{64}) \*\//);
|
|
@@ -846,10 +908,9 @@ export default class Blog {
|
|
|
846
908
|
|
|
847
909
|
const compiledScripts = await compileScripts(fileData);
|
|
848
910
|
|
|
849
|
-
|
|
850
|
-
await fs.promises.mkdir(publicDir, { recursive: true });
|
|
911
|
+
await fs.promises.mkdir(this.#publicDir, { recursive: true });
|
|
851
912
|
await fs.promises.writeFile(
|
|
852
|
-
path.join(publicDir, "scripts.min.js"),
|
|
913
|
+
path.join(this.#publicDir, "scripts.min.js"),
|
|
853
914
|
compiledScripts + `\n/* source-hash: ${currentHash} */`,
|
|
854
915
|
);
|
|
855
916
|
}
|
package/blog_test_empty.db
CHANGED
|
Binary file
|
package/blog_test_load.db
CHANGED
|
Binary file
|
package/jest.config.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** @type {import('jest').Config} */
|
|
2
|
+
const config = {
|
|
3
|
+
// Use testPathIgnorePatterns to exclude folders from discovery
|
|
4
|
+
testPathIgnorePatterns: [
|
|
5
|
+
"/node_modules/", // Always recommended
|
|
6
|
+
"<rootDir>/angular-frontend/", // Replace with your folder name
|
|
7
|
+
],
|
|
8
|
+
|
|
9
|
+
// If you also want to exclude the folder from coverage reports
|
|
10
|
+
coveragePathIgnorePatterns: ["<rootDir>/angular-frontend/"],
|
|
11
|
+
|
|
12
|
+
transform: {}, // Optional: Add transformers if using TS or Babel
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export default config;
|
package/model/DataModel.js
CHANGED
|
@@ -34,10 +34,10 @@ export default class DataModel {
|
|
|
34
34
|
}
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
search(query) {
|
|
37
|
+
search(query, limit) {
|
|
38
38
|
debug("search");
|
|
39
39
|
if (this.storage.search) {
|
|
40
|
-
return this.storage.search(query);
|
|
40
|
+
return this.storage.search(query, limit);
|
|
41
41
|
}
|
|
42
42
|
}
|
|
43
43
|
|
|
@@ -46,6 +46,10 @@ export default class DataModel {
|
|
|
46
46
|
this.storage.remove(article);
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
get(id) {
|
|
50
|
+
return this.storage.get(id);
|
|
51
|
+
}
|
|
52
|
+
|
|
49
53
|
getAllArticles() {
|
|
50
54
|
debug("get all articles");
|
|
51
55
|
const order = "newest";
|
|
@@ -122,6 +122,33 @@ export class BinarySearchTree {
|
|
|
122
122
|
}
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
+
*traverse(order = "newest") {
|
|
126
|
+
const stack = [];
|
|
127
|
+
let current = this.root;
|
|
128
|
+
|
|
129
|
+
if (order === "newest") {
|
|
130
|
+
while (current || stack.length > 0) {
|
|
131
|
+
while (current) {
|
|
132
|
+
stack.push(current);
|
|
133
|
+
current = current.right;
|
|
134
|
+
}
|
|
135
|
+
current = stack.pop();
|
|
136
|
+
yield current.article;
|
|
137
|
+
current = current.left;
|
|
138
|
+
}
|
|
139
|
+
} else {
|
|
140
|
+
while (current || stack.length > 0) {
|
|
141
|
+
while (current) {
|
|
142
|
+
stack.push(current);
|
|
143
|
+
current = current.left;
|
|
144
|
+
}
|
|
145
|
+
current = stack.pop();
|
|
146
|
+
yield current.article;
|
|
147
|
+
current = current.right;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
125
152
|
getRange(startdate, enddate, limit) {
|
|
126
153
|
const results = [];
|
|
127
154
|
// Optimization: Convert to timestamps once to avoid Date object creation/coercion in recursion
|
|
@@ -37,14 +37,19 @@ export class BinarySearchTreeHashMap extends BinarySearchTree {
|
|
|
37
37
|
return false;
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
search(query) {
|
|
40
|
+
search(query, limit) {
|
|
41
41
|
debug("search %s ", query);
|
|
42
42
|
const lower = query.toLowerCase();
|
|
43
|
-
const results =
|
|
44
|
-
|
|
43
|
+
const results = [];
|
|
44
|
+
for (const a of this.traverse()) {
|
|
45
|
+
if (
|
|
45
46
|
a.title.toLowerCase().includes(lower) ||
|
|
46
|
-
a.content.toLowerCase().includes(lower)
|
|
47
|
-
|
|
47
|
+
a.content.toLowerCase().includes(lower)
|
|
48
|
+
) {
|
|
49
|
+
results.push(a);
|
|
50
|
+
if (limit != null && results.length >= limit) break;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
48
53
|
debug("%d results found", results.length);
|
|
49
54
|
return results;
|
|
50
55
|
}
|
|
@@ -59,6 +64,10 @@ export class BinarySearchTreeHashMap extends BinarySearchTree {
|
|
|
59
64
|
return super.getAllArticles(order);
|
|
60
65
|
}
|
|
61
66
|
|
|
67
|
+
get(id) {
|
|
68
|
+
return this.ids.get(id);
|
|
69
|
+
}
|
|
70
|
+
|
|
62
71
|
getRange(startdate, enddate, limit) {
|
|
63
72
|
return super.getRange(startdate, enddate, limit);
|
|
64
73
|
}
|
package/package.json
CHANGED
|
Binary file
|