@lexho111/plainblog 0.6.3 → 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 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="button edit">edit</div><div id="deleteButton${this.id}" class="button delete">delete</div></div>`
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 params = new URLSearchParams(req.headers.cookie.replace(/; /g, "&"));
165
- return this.sessions.has(params.get("session"));
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
- /** post a blog article */
352
- async postArticle(req, res) {
353
- const body = await new Promise((resolve, reject) => {
354
- let data = "";
355
- req.on("data", (chunk) => (data += chunk.toString()));
356
- req.on("end", () => resolve(data));
357
- req.on("error", reject);
358
- });
359
-
360
- //req.on("end", async () => {
361
- const params = new URLSearchParams(body);
362
- const title = params.get("title");
363
- const content = params.get("content");
364
-
365
- if (title && content) {
366
- const newArticleData = { title, content };
367
- try {
368
- // Save the new article to the database via the ApiServer
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
- // POST new article
424
- if (req.method === "POST" && req.url === "/") {
425
- if (!this.#isAuthenticated(req)) {
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 publicDir = path.join(process.cwd(), "public");
468
- const parsedUrl = new URL(
469
- req.url,
470
- `http://${req.headers.host || "localhost"}`,
471
- );
472
- const filePath = path.join(publicDir, parsedUrl.pathname);
473
-
474
- if (filePath.startsWith(publicDir)) {
475
- const stats = await fs.promises.stat(filePath);
476
- if (stats.isFile()) {
477
- const ext = path.extname(filePath).toLowerCase();
478
- const mimeTypes = {
479
- ".html": "text/html",
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
- // ENOENT (file not found) is expected, leading to a 404.
497
- // Only log other, unexpected errors.
498
- if (err.code !== "ENOENT") {
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" && pathname === "/api/articles") {
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 id = url.searchParams.get("id");
660
- if (id) {
661
- this.#articles.remove(parseInt(id));
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.remove(parseInt(id));
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: "deleted", id }));
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
- const publicDir = path.join(process.cwd(), "public");
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
- const publicDir = path.join(process.cwd(), "public");
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
  }
Binary file
package/blog_test_load.db CHANGED
Binary file
package/index.js CHANGED
@@ -1,32 +1,5 @@
1
1
  import Blog from "./Blog.js";
2
2
  import Article from "./Article.js";
3
- import FileAdapter from "./model/FileAdapter.js";
4
- import SqliteAdapter from "./model/SqliteAdapter.js";
5
- import PostgresAdapter from "./model/PostgresAdapter.js";
6
- import DataModel from "./model/DataModel.js";
7
- import ArrayList from "./model/datastructures/ArrayList.js";
8
- import FileList from "./model/datastructures/FileList.js";
9
- import { BinarySearchTree } from "./model/datastructures/BinarySearchTree.js";
10
- import {
11
- generateTitle,
12
- generateContent,
13
- generateDateList,
14
- } from "./utilities.js";
15
- import { appendArticle } from "./model/FileModel.js";
16
3
 
17
- export {
18
- Blog,
19
- Article,
20
- FileAdapter,
21
- SqliteAdapter,
22
- PostgresAdapter,
23
- DataModel,
24
- ArrayList,
25
- FileList,
26
- BinarySearchTree,
27
- generateTitle,
28
- generateContent,
29
- generateDateList,
30
- appendArticle,
31
- };
4
+ export { Blog, Article };
32
5
  export default Blog;
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;
@@ -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 = this.getAllArticles().filter(
44
- (a) =>
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lexho111/plainblog",
3
- "version": "0.6.3",
3
+ "version": "0.6.5",
4
4
  "description": "A tool for creating and serving a minimalist, single-page blog.",
5
5
  "main": "index.js",
6
6
  "type": "module",
Binary file