@lexho111/plainblog 0.4.3 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,2 @@
1
+ {
2
+ }
package/Blog.js CHANGED
@@ -1,19 +1,16 @@
1
1
  import http from "http";
2
- import crypto from "crypto";
2
+ import crypto from "node:crypto";
3
3
  import fs from "fs";
4
4
  import { URLSearchParams } from "url";
5
- import { MapTransform } from "./streams.js";
6
5
  import Article from "./Article.js";
7
- import DatabaseModel from "./model/DatabaseModel.js";
6
+ import DatabaseModel from "./model/DatabaseModel2.js";
8
7
  import { fetchData, postData } from "./model/APIModel.js";
9
- import { save as saveToFile, load as loadFromFile } from "./model/fileModel.js";
10
8
  import { formatHTML, header, formatMarkdown, validate } from "./Formatter.js";
11
9
  import pkg from "./package.json" with { type: "json" };
12
10
  import path from "path";
13
11
  import { fileURLToPath } from "url";
14
12
  import { exec } from "child_process";
15
13
  import { promisify } from "util";
16
- import { compileStyles, mergeStyles } from "./build-styles.js";
17
14
 
18
15
  const execPromise = promisify(exec);
19
16
 
@@ -31,11 +28,11 @@ export default class Blog {
31
28
  this.reloadStylesOnGET = false;
32
29
 
33
30
  this.database = {
34
- type: "sqlite",
35
- username: "user1",
36
- password: "password1",
31
+ type: "file",
32
+ username: "user",
33
+ password: "password",
37
34
  host: "localhost",
38
- dbname: "blog",
35
+ dbname: "blog.json",
39
36
  };
40
37
  this.sessions = new Set();
41
38
 
@@ -68,6 +65,10 @@ export default class Blog {
68
65
 
69
66
  set title(t) {
70
67
  this.#title = t;
68
+ this.#databaseModel = new DatabaseModel(this.database);
69
+ console.log(`connected to database`);
70
+ if(t != this.#title && t.length == 0)
71
+ this.#databaseModel.updateBlogTitle(t);
71
72
  }
72
73
 
73
74
  get title() {
@@ -180,7 +181,8 @@ export default class Blog {
180
181
 
181
182
  if (srcHash !== publicHash) {
182
183
  console.log("Styles have changed. Recompiling...");
183
- const finalStyles = await mergeStyles(this.styles, srcStyles);
184
+ //const finalStyles = await mergeStyles(this.styles, srcStyles);
185
+ const finalStyles = this.styles + " " + srcStyles;
184
186
  try {
185
187
  await fs.promises.mkdir(path.dirname(publicStylePath), { recursive: true });
186
188
  await fs.promises.writeFile(publicStylePath, finalStyles + `\n/* source-hash: ${srcHash} */`);
@@ -289,7 +291,6 @@ export default class Blog {
289
291
  res.end("Forbidden");
290
292
  return;
291
293
  }
292
- await this.#databaseModel.updateBlogTitle(this.title);
293
294
  await this.postArticle(req, res);
294
295
  // GET artciles
295
296
  } else if (req.method === "GET" && req.url === "/") {
@@ -387,7 +388,7 @@ export default class Blog {
387
388
  /** Populates the blog's title and articles from a data object. */
388
389
  #applyBlogData(data) {
389
390
  this.articles = []; // Clear existing articles before loading new ones
390
- this.title = data.title;
391
+ this.#title = data.title;
391
392
  // Assuming the API returns an array of objects with title and content
392
393
  if (data.articles && Array.isArray(data.articles)) {
393
394
  for (const articleData of data.articles) {
@@ -398,23 +399,6 @@ export default class Blog {
398
399
  }
399
400
  }
400
401
 
401
- async save(filename = this.filename) {
402
- if (this.#databaseModel === undefined) this.init(); // init blog if it didn't already happen
403
- //await this.#apiServer.initialize();
404
- if (this.#isExternalAPI) await this.loadFromAPI();
405
- const blogData = { title: this.title, articles: this.articles };
406
- saveToFile(filename, blogData);
407
- }
408
-
409
- async load(filename) {
410
- loadFromFile(filename, (title, articles) => {
411
- this.title = title;
412
- this.articles = articles.map(
413
- (article) => new Article(article.title, article.content)
414
- );
415
- });
416
- }
417
-
418
402
  async loadFromAPI() {
419
403
  const data = await fetchData(this.#apiUrl);
420
404
  if (data) {
@@ -556,7 +540,8 @@ export default class Blog {
556
540
  this.#stylesHash = currentHash;
557
541
 
558
542
  // Compile styles using the standalone script from build-styles.js
559
- this.compiledStyles = await compileStyles(fileData);
543
+ //this.compiledStyles = await compileStyles(fileData);
544
+ this.compiledStyles = fileData; // TODO workaround
560
545
 
561
546
  // generate a file
562
547
  const __filename = fileURLToPath(import.meta.url);
package/Formatter.js CHANGED
@@ -33,7 +33,10 @@ export function formatHTML(data) {
33
33
  <nav>
34
34
  ${data.login}
35
35
  </nav>
36
- <h1>${data.title}</h1>
36
+ <div id="header">
37
+ <h1>${data.title}</h1>
38
+ <!--<img src="headerphoto.jpg"/>-->
39
+ </div>
37
40
  <div id="wrapper">
38
41
  ${form}
39
42
  <section id="articles" class="grid">
package/README.md CHANGED
@@ -25,24 +25,6 @@ Now you can open your blog in your webbrowser on `http://localhost:8080`. Login
25
25
 
26
26
  ## More Features
27
27
 
28
- **SQLite** is the default database. But you can use **PostgreSQL** instead.
29
-
30
- ### run api server with postgres database
31
-
32
- ```
33
- import Blog from "@lexho111/plainblog";
34
-
35
- const blog = new Blog();
36
- blog.database.type = "postgres";
37
- blog.database.username = "user";
38
- blog.database.password = "password";
39
- blog.database.host = "localhost";
40
- blog.setStyle("body { font-family: Arial, sans-serif; } h1 { color: #333; }");
41
- await blog.init(); // load data from database
42
-
43
- blog.startServer(8080);
44
- ```
45
-
46
28
  ### set an API to fetch data from an external database
47
29
 
48
30
  ```
@@ -56,36 +38,18 @@ await blog.init(); // load data from database
56
38
  blog.startServer(8080);
57
39
  ```
58
40
 
59
- ### provide custom style sheets and scripts
41
+ ### provide custom style sheets
60
42
 
61
43
  ```
62
44
  const blog = new Blog();
63
45
  blog.title = "My Blog";
64
46
  blog.style = "body { font-family: Arial, sans-serif; } h1 { color: #333; }";
65
47
  blog.password = "mypassword";
66
- blog.stylesheetPath = "path/to/my/styles.scss";
48
+ blog.stylesheetPath = "path/to/my/styles.css";
67
49
 
68
50
  blog.startServer(8080);
69
51
  ```
70
52
 
71
- save data to file
72
-
73
- ```
74
- import Blog from "@lexho111/plainblog";
75
- import { Article } from "@lexho111/plainblog";
76
-
77
- const blog = new Blog();
78
- blog.setStyle("body { font-family: Arial, sans-serif; } h1 { color: #333; }");
79
-
80
- const article = new Article("hello", "hello world!");
81
- blog.addArticle(article);
82
-
83
- blog.save("myblog.json");
84
-
85
- // load data from 'myblog.json'
86
- await blog.load("myblog.json");
87
- ```
88
-
89
53
  print your blog articles in markdown
90
54
 
91
55
  ```
package/blog.json CHANGED
@@ -3,7 +3,93 @@
3
3
  "articles": [
4
4
  {
5
5
  "title": "hello",
6
- "content": "hello world!"
6
+ "content": "hML`JfDS]ARPVAfsGdogseNVYPXTpZaJhKJkPoMTWA\\q[CkNsXYrUGyGHWTDZpByubZMf_Y\\bMELh[]afM^XMAFqvxuyWat^hqSBMIifvbTdHNOQP_rbwwlWnGvVbPi]jlTIixyAaaU_]ecJYTjr]FqLq`UY\\XoYqhmuY`rvWH]EZO`tBHZSan`mAuhnwpXgdRJtjtH[",
7
+ "createdAt": "2026-01-07T12:49:43.098Z"
8
+ },
9
+ {
10
+ "title": "hello",
11
+ "content": "HgcFKFWDMl]cxiCVVrgFddJaOQCSPF^KYkROxkLTmxSc^ZssZh`ENyOXZn_DtOQe^V_vbu^KRonZ]bGcqsP]FYMmY\\YLjWknhiUksXwIUsvtpfXqViiKJ`xcmScZRXfAdHshvFlEHvPXWOfZRRIDIOxAafPoMD[ZgVlFkl\\Uw]RfubZWQ]`IHIUOKea_RTO`KiZisSA[",
12
+ "createdAt": "2026-01-07T12:49:43.444Z"
13
+ },
14
+ {
15
+ "title": "Test Title from Jest",
16
+ "content": "This is the content of the test article.",
17
+ "createdAt": "2026-01-07T13:10:50.239Z"
18
+ },
19
+ {
20
+ "title": "hello",
21
+ "content": "_\\jAuGsYByS]q\\QsnlqT\\]]TEDDalywtlmwmBZRlL[vtFdGqB\\[dlVx]A^[IV`KjhWU`DW`oorGwUlBU_IMCYoymIFi[`FpXrJfUmLZTRhoisxhXIcWqmk\\SPMV^]dvCuL_Pu^EAcMvBfadWFbUuSxFkjh\\KQyLnNWWsP`agQQkmU\\TAaOvYZAO\\FGtstSbaCJPXRQTO",
22
+ "createdAt": "2026-01-07T13:10:57.249Z"
23
+ },
24
+ {
25
+ "title": "hello",
26
+ "content": "HRkfwNRL_r\\DF]tyVBM[AMIjfMoBbohiqUf_T^]LwiJo[duXB`mQiRmmTkbMSJxvvHccfbXsdmKMbuYmxo[JGkwfCpqXSbhKiZID[vouGhbnLZ[wjkFFgm]fOaZcxoWduI`TNvin]\\dXoGW]YtueqSYPRLmGJswvUnyPHhfLUtf`]QNfM]hcvqqZasrQZVcXYjAnJxRP",
27
+ "createdAt": "2026-01-07T13:10:57.357Z"
28
+ },
29
+ {
30
+ "title": "hello",
31
+ "content": "UI\\KBodT[CgUg]L^WJ`Tuafq`rS]n\\T_eMqmBQqJVQisfABj^msS]u\\OEoJ]D^CtwE`UY[UbOICHcYPjNqNQ[LlwlJGPOoppSrkaEmDh_tm]CrrPGbBSPxgQQIZ^efcmITQQaAgT_vx[GUepg]LDagmTmihHbOIqHAa_NmRsX_EjxeE]EKhnHwFm]vFUpJTPFiDNU^BE",
32
+ "createdAt": "2026-01-07T13:17:47.559Z"
33
+ },
34
+ {
35
+ "title": "hello",
36
+ "content": "aMDnSSZDIZjbfrMlOcjMB\\CPup[GfoqHkLWAi]V_Ctkt[ekYRqIaufps[]TOMcw\\ojbVpPNv`lTTH\\[jbOcJXU[_tBSCsZW\\akiWuRf^mZGGrFxAq^oAdZjHwmJBjt^_GUXnHdXfjSrifiqXdAAdo]EM`aBFpJvyVLnLOfqrqoPrAIhRwoPHRy^lRCLyaS^VXChowHCn",
37
+ "createdAt": "2026-01-07T13:17:47.681Z"
38
+ },
39
+ {
40
+ "title": "Test Title from Jest",
41
+ "content": "This is the content of the test article.",
42
+ "createdAt": "2026-01-07T13:18:50.764Z"
43
+ },
44
+ {
45
+ "title": "hello",
46
+ "content": "[vL^GSDHJjBq^qdTMyyMm`SGhZhvgXsyTp[kYNkiBDavKevpIkSlWQr`bJaWyH]Qa`LQMKisdHeRvTKm`dviPLkLcJCtWbGPaDnNhJtEcEQM^MmP[RYERk\\uYeXruvPmJMjdvsOxA`btQtTmtBlOvrHLANJhcYRTAXmwOjLhp_SgkhWsB_pcIUQbd^X`RIWAdqUBPVmk",
47
+ "createdAt": "2026-01-07T13:18:52.387Z"
48
+ },
49
+ {
50
+ "title": "hello",
51
+ "content": "jRaJYEUFnMoEcjrWFREUuIQEoCtLoEuJSxJapEGpdPvq[GyrOqdSLY`VDlqSMTh\\BgybfwxfQQ_QqKkh\\YcEHYpA^W[ttGyGjA[TdZHn]]yoPWnFyFHJfAUkxkSM[RkQVqxAg_AGoJahaoOsNnoSGx^uM\\smWlWVDGvRcWVW\\rI_[lKnZEifvdPwkCcBRLXKGw[aNedN",
52
+ "createdAt": "2026-01-07T13:18:52.971Z"
53
+ },
54
+ {
55
+ "title": "Test Title from Jest",
56
+ "content": "This is the content of the test article.",
57
+ "createdAt": "2026-01-07T13:22:54.227Z"
58
+ },
59
+ {
60
+ "title": "Test Title from Jest",
61
+ "content": "This is the content of the test article.",
62
+ "createdAt": "2026-01-07T13:23:08.140Z"
63
+ },
64
+ {
65
+ "title": "hello",
66
+ "content": "M_RFGy[auVjFYypv^FdPqesIlcBkg]uoeIwMNLM]TOcDWKw[CFpsPpQWxSAXAupUdVsaiGJgLgIeuJEwBMXk`VG\\OoXIxCERYs`hqGRYGTnP]ZIrOSoJk[MFAxfVjJNMyZycryoRWr\\bhCXlurxgKPFSsTYbhlqZmrqaiJyvXpFsbiLQx^LBcZi`VqZSbjc`TNF_\\Eex",
67
+ "createdAt": "2026-01-07T13:26:02.437Z"
68
+ },
69
+ {
70
+ "title": "hello",
71
+ "content": "\\uRyVjar]vDZKHqRqJbEJpZbdKCWmQOGToNFQAqWdJQxSwmKTAGXywMDF`D\\nBA_eJxmFV^XtTuSGNJZTFjYvYibQIlgZdV[YXZpujmuWBgKNNBjrGZLVC[QlRZOwBdY^QPom\\b^WIIkeawsGBaafELGmfmTaIikRhCnCV_SYKEwogtqdfhkMkJslllumPyeGKqe\\cca",
72
+ "createdAt": "2026-01-07T13:26:02.816Z"
73
+ },
74
+ {
75
+ "title": "Test Title from Jest",
76
+ "content": "This is the content of the test article.",
77
+ "createdAt": "2026-01-07T13:26:07.866Z"
78
+ },
79
+ {
80
+ "title": "hello",
81
+ "content": "UecpUr`PFkpfKJ^V^q[T_lTr\\uP\\WFwcFtZgrdoao[japNpDycoAgWyELuIZtQroKxXSYdI]LrDTQSGqegWjgArWk[[eYXpy_OchWDywkus`GLcdwwr[qHoLINIdUoB^C]JcLfacOEJtMZOeWjRNbGcv\\XCfSAXUKIVqkPwJbaRTjlpGBxCGXxnchUhRoePYoLE]fexb",
82
+ "createdAt": "2026-01-07T13:45:46.304Z"
83
+ },
84
+ {
85
+ "title": "hello",
86
+ "content": "ef[SK^DuSJrKOlieeJHPLLoGgS[ZO\\gk_dWggGZchFewEnwspxJlhCWQ\\aTEKlDj\\a\\xRj\\uJANSkNLwrnPoOxJ[h]CrLppVtoeCEWPXRpCcoNFU`YA\\RPK\\dk\\Jp^fbdmC`Hpuf[ZYExseoPRbAYAaIolQ_mkJKRfrKsthmsBtCZdeQiBhY\\XAodKo_rOeQvtfNtDQB",
87
+ "createdAt": "2026-01-07T13:45:46.469Z"
88
+ },
89
+ {
90
+ "title": "Test Title from Jest",
91
+ "content": "This is the content of the test article.",
92
+ "createdAt": "2026-01-07T13:45:47.311Z"
7
93
  }
8
94
  ]
9
95
  }
@@ -0,0 +1,86 @@
1
+ import { save as saveToFile, load as loadFromFile } from "./fileModel.js";
2
+
3
+ export default class DatabaseModel {
4
+ filename = "blog.json";
5
+
6
+ //new DatabaseModel(this.database);
7
+ constructor(options) {}
8
+
9
+ async initialize() {}
10
+
11
+ async getBlogTitle() {
12
+ let blogTitle = "";
13
+ try {
14
+ await loadFromFile(this.filename, (title) => {
15
+ blogTitle = title;
16
+ });
17
+ } catch (err) {
18
+ console.error(err);
19
+ }
20
+ return blogTitle;
21
+ }
22
+
23
+ //findAll();
24
+ //save(newArticleData);
25
+ async save(newArticle) {
26
+ if (!newArticle.createdAt) {
27
+ newArticle.createdAt = new Date().toISOString();
28
+ }
29
+
30
+ let blogTitle = "";
31
+ let articles = [];
32
+ try {
33
+ await loadFromFile(this.filename, (t, a) => {
34
+ blogTitle = t;
35
+ articles = a || [];
36
+ });
37
+ } catch (err) {
38
+ console.error(err);
39
+ }
40
+
41
+ articles.push(newArticle);
42
+ saveToFile(this.filename, { title: blogTitle, articles });
43
+ }
44
+
45
+ //updateBlogTitle(this.title);
46
+ async updateBlogTitle(newTitle) {
47
+ let articles = [];
48
+ try {
49
+ await loadFromFile(this.filename, (t, a) => {
50
+ //blogTitle = t;
51
+ articles = a || [];
52
+ });
53
+ } catch (err) {
54
+ console.error(err);
55
+ }
56
+ saveToFile(this.filename, { title: newTitle, articles });
57
+ }
58
+
59
+ //findAll(limit, 0, startID, endID);
60
+ async findAll(
61
+ limit = 4,
62
+ offset = 0,
63
+ startId = null,
64
+ endId = null,
65
+ order = "DESC"
66
+ ) {
67
+ let dbArticles = [];
68
+ try {
69
+ await loadFromFile(this.filename, (title, articles) => {
70
+ if (Array.isArray(articles)) {
71
+ // Sort by createdAt
72
+ articles.sort((a, b) => {
73
+ const dateA = new Date(a.createdAt || 0);
74
+ const dateB = new Date(b.createdAt || 0);
75
+ return order === "DESC" ? dateB - dateA : dateA - dateB;
76
+ });
77
+ // Apply pagination
78
+ dbArticles = articles.slice(offset, offset + limit);
79
+ }
80
+ });
81
+ } catch (err) {
82
+ console.error(err);
83
+ }
84
+ return dbArticles;
85
+ }
86
+ }
@@ -17,9 +17,19 @@ export async function save(filename, data) {
17
17
 
18
18
  /** load blog content from file */
19
19
  export async function load(filename, f) {
20
- const data = await fs.readFile(filename, "utf8");
21
- const jsonData = JSON.parse(data);
22
- const title = jsonData.title;
23
- const articles = jsonData.articles;
24
- f(title, articles);
20
+ try {
21
+ const data = await fs.readFile(filename, "utf8");
22
+ const jsonData = JSON.parse(data);
23
+ const title = jsonData.title;
24
+ const articles = jsonData.articles;
25
+ f(title, articles);
26
+ } catch (err) {
27
+ if (err.code === "ENOENT") {
28
+ const defaultData = { title: "Blog", articles: [] };
29
+ await save(filename, defaultData);
30
+ f(defaultData.title, defaultData.articles);
31
+ } else {
32
+ throw err;
33
+ }
34
+ }
25
35
  }
package/package.json CHANGED
@@ -1,10 +1,11 @@
1
1
  {
2
2
  "name": "@lexho111/plainblog",
3
- "version": "0.4.3",
3
+ "version": "0.5.1",
4
4
  "description": "A tool for creating and serving a minimalist, single-page blog.",
5
5
  "main": "index.js",
6
6
  "type": "module",
7
7
  "scripts": {
8
+ "dev": "node index.js",
8
9
  "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js",
9
10
  "lint": "eslint ."
10
11
  },
@@ -17,14 +18,14 @@
17
18
  "license": "ISC",
18
19
  "dependencies": {
19
20
  "autoprefixer": "^10.4.23",
20
- "cssnano": "^7.1.2",
21
- "node-fetch": "^3.3.2",
22
- "pg": "^8.16.3",
23
- "pg-hstore": "^2.3.4",
21
+ "child_process": "^1.0.2",
22
+ "fs": "^0.0.1-security",
23
+ "http": "^0.0.1-security",
24
+ "path": "^0.12.7",
24
25
  "postcss": "^8.4.35",
25
26
  "sass": "^1.97.1",
26
- "sequelize": "^6.37.7",
27
- "sqlite3": "^5.1.7"
27
+ "url": "^0.11.4",
28
+ "util": "^0.12.5"
28
29
  },
29
30
  "devDependencies": {
30
31
  "dom-parser": "^1.1.5",
@@ -1,2 +1,68 @@
1
- body{font-family:Arial;font-family:Arial,sans-serif}h1{color:#333}.grid{border:0 solid #000;display:grid;gap:.25rem;grid-template-columns:1fr}.grid article{border:0 solid #ccc;border-radius:4px;min-width:0;overflow-wrap:break-word;padding:.25rem}.grid article h2{color:#353535;margin-bottom:5px}.grid article .datetime{color:#757575;margin:0}.grid article p{margin-bottom:0;margin-top:10px}article a,article a:visited,h1{color:#696969}nav a{color:#3b40c1;font-size:20px;text-decoration:underline}nav a:visited{color:#3b40c1;text-decoration-color:#3b40c1}@media screen and (max-width:1000px){*{font-size:4vw}#wrapper{box-sizing:border-box;max-width:100%;padding:0 10px;width:100%}}
2
- /* source-hash: 374cb67a628dde5695d1104c5f9037d737d17d6b6b9a791be81779d7dd91beac */
1
+ body { font-family: Arial; } .grid {
2
+ border: 0 solid #000;
3
+ display: grid;
4
+ gap: 0.25rem;
5
+ grid-template-columns: 1fr;
6
+ }
7
+ .grid article {
8
+ border: 0 solid #ccc;
9
+ border-radius: 4px;
10
+ min-width: 0;
11
+ overflow-wrap: break-word;
12
+ padding: 0.25rem;
13
+ }
14
+ .grid article h2 {
15
+ color: rgb(53, 53, 53);
16
+ margin-bottom: 5px;
17
+ }
18
+
19
+ .grid article .datetime {
20
+ margin: 0;
21
+ color: rgb(117, 117, 117);
22
+ }
23
+
24
+ .grid article p {
25
+ margin-top: 10px;
26
+ margin-bottom: 0;
27
+ }
28
+
29
+ article a {
30
+ color: rgb(105, 105, 105);
31
+ }
32
+
33
+ article a:visited {
34
+ color: rgb(105, 105, 105);
35
+ }
36
+
37
+ h1 {
38
+ color: #696969;
39
+ }
40
+ nav a {
41
+ color: #3b40c1;
42
+ font-size: 20px;
43
+ text-decoration: underline;
44
+ }
45
+ nav a:visited {
46
+ color: #3b40c1;
47
+ text-decoration-color: #3b40c1;
48
+ }
49
+
50
+ #wrapper {
51
+ max-width: 500px;
52
+ width: 100%;
53
+ }
54
+
55
+ /* Mobile Layout (screens smaller than 1000px) */
56
+ @media screen and (max-width: 1000px) {
57
+ * {
58
+ font-size: 4vw;
59
+ }
60
+ #wrapper {
61
+ max-width: 100%;
62
+ width: 100%;
63
+ padding: 0 10px; /* Prevents text from touching the edges */
64
+ box-sizing: border-box;
65
+ }
66
+ }
67
+
68
+ /* source-hash: bcb6644ec5b5c6f9685c9ad6c14ee551a6f908b8a2c372d3294e2d2e80d17fb7 */
package/src/styles.css CHANGED
@@ -46,6 +46,12 @@ nav a:visited {
46
46
  color: #3b40c1;
47
47
  text-decoration-color: #3b40c1;
48
48
  }
49
+
50
+ #wrapper {
51
+ max-width: 500px;
52
+ width: 100%;
53
+ }
54
+
49
55
  /* Mobile Layout (screens smaller than 1000px) */
50
56
  @media screen and (max-width: 1000px) {
51
57
  * {
package/test/blog.test.js CHANGED
@@ -58,13 +58,13 @@ describe("test blog", () => {
58
58
  const styles = [
59
59
  {
60
60
  style: "body { font-family: Courier; }",
61
- expected: "font-family:Courier",
61
+ expected: "font-family: Courier",
62
62
  },
63
63
  {
64
- style: "body{background-color:black;color:white;}",
65
- expected: "background-color:#000;color:#fff",
64
+ style: "body{ background-color:black; color:white; }",
65
+ expected: "background-color:black; color:white",
66
66
  },
67
- { style: "body{font-size: 1.2em;}", expected: "font-size:1.2em" },
67
+ { style: "body{ font-size: 1.2em; }", expected: "font-size: 1.2em" },
68
68
  ];
69
69
  for (const style of styles) {
70
70
  const cssPath = path.join(publicDir, "styles.min.css");
@@ -2,6 +2,7 @@ import Blog from "../Blog.js";
2
2
  import Article from "../Article.js";
3
3
  import { fetchData, postData } from "../model/APIModel.js";
4
4
  import { server } from "./simpleServer.js";
5
+ import { load as loadFromFile } from "../model/fileModel.js";
5
6
 
6
7
  function generateRandomContent(length) {
7
8
  let str = "";
@@ -26,18 +27,19 @@ describe("File Model test", () => {
26
27
  const article = new Article("hello", content);
27
28
  blog.addArticle(article);
28
29
 
29
- await blog.load("blog.json");
30
-
31
- expect(await blog.toHTML()).toContain(content);
32
- expect(blog).toBeDefined();
30
+ await loadFromFile("blog.json", async (title) => {
31
+ expect(await blog.toHTML()).toContain(content);
32
+ expect(blog).toBeDefined();
33
+ });
33
34
  });
34
35
  });
35
36
 
36
37
  describe("API Model test", () => {
37
- beforeAll(() => {
38
+ beforeAll((done) => {
38
39
  const port = 8081;
39
40
  server.listen(port, () => {
40
41
  console.log(`Simple server running at http://localhost:${port}/`);
42
+ done();
41
43
  });
42
44
  });
43
45
  afterAll(() => {
@@ -104,7 +104,8 @@ describe("server test", () => {
104
104
 
105
105
  expect(response.status).toBe(201);
106
106
  const responseData = await response.json();
107
- expect(responseData).toEqual(newArticle);
107
+ expect(responseData.title).toEqual(newArticle.title);
108
+ expect(responseData.content).toEqual(newArticle.content);
108
109
 
109
110
  expect(await blog.toHTML()).toContain(newArticle.content); // does blog contain my new article?
110
111
  });
@@ -77,7 +77,7 @@ describe("Blog Stylesheet Test", () => {
77
77
  expect(publicCSS).toContain("body");
78
78
  expect(publicCSS).toContain("nav a");
79
79
  expect(publicCSS).toContain(".datetime");
80
- expect(publicCSS).toContain("font-style:normal");
80
+ expect(publicCSS).toContain("font-style: normal");
81
81
  expect(publicCSS).toContain("color:");
82
82
  });
83
83
 
@@ -99,7 +99,7 @@ describe("Blog Stylesheet Test", () => {
99
99
  expect(publicCSS).toContain("body");
100
100
  expect(publicCSS).toContain("nav a");
101
101
  expect(publicCSS).toContain(".datetime");
102
- expect(publicCSS).toContain("font-style:normal");
102
+ expect(publicCSS).toContain("font-style: normal");
103
103
  expect(publicCSS).toContain("color:");
104
104
  });
105
105
  });
package/build-styles.js DELETED
@@ -1,71 +0,0 @@
1
- import path from "path";
2
- import { pathToFileURL } from "url";
3
- import * as sass from "sass";
4
- import postcss from "postcss";
5
- import autoprefixer from "autoprefixer";
6
- import cssnano from "cssnano";
7
-
8
- // array of files or a single file
9
- export async function compileStyles(fileData) {
10
- try {
11
- let combinedCss = "";
12
-
13
- if (fileData) {
14
- const scssFiles = fileData.filter(
15
- (f) =>
16
- f.path.endsWith(".scss") && !path.basename(f.path).startsWith("_")
17
- );
18
-
19
- for (const file of scssFiles) {
20
- try {
21
- const result = sass.compileString(file.content.toString(), {
22
- style: "expanded",
23
- url: pathToFileURL(file.path),
24
- });
25
- combinedCss += result.css + "\n";
26
- } catch (err) {
27
- console.error(
28
- `Error compiling ${path.basename(file.path)}:`,
29
- err.message
30
- );
31
- }
32
- }
33
-
34
- const cssFiles = fileData.filter((f) => f.path.endsWith(".css"));
35
- for (const file of cssFiles) {
36
- combinedCss += file.content + "\n";
37
- }
38
- }
39
-
40
- // 2. PostCSS (Autoprefixer + CSSNano)
41
- if (combinedCss) {
42
- const plugins = [autoprefixer(), cssnano()];
43
- const result = await postcss(plugins).process(combinedCss, {
44
- from: undefined,
45
- });
46
- return result.css;
47
- }
48
- return "";
49
- } catch (error) {
50
- console.error("Build failed:", error);
51
- return "";
52
- }
53
- }
54
-
55
- export async function mergeStyles(...cssContents) {
56
- try {
57
- const combinedCss = cssContents.join("\n");
58
-
59
- if (combinedCss) {
60
- const plugins = [autoprefixer(), cssnano()];
61
- const result = await postcss(plugins).process(combinedCss, {
62
- from: undefined,
63
- });
64
- return result.css;
65
- }
66
- return "";
67
- } catch (error) {
68
- console.error("Merge failed:", error);
69
- return "";
70
- }
71
- }
@@ -1,139 +0,0 @@
1
- import { Sequelize, DataTypes, Op } from "sequelize";
2
-
3
- export default class DatabaseModel {
4
- #username;
5
- #password;
6
- #host;
7
- #dbport = 5432;
8
- #dbname = "blog";
9
-
10
- #sequelize;
11
- #Article;
12
- #BlogInfo;
13
-
14
- constructor(options) {
15
- const databasetype = options.type; // the database type defines
16
- if (databasetype === "sqlite") {
17
- // Use the full path for the database file from the options.
18
- if (options.dbname) this.#dbname = options.dbname;
19
- this.#sequelize = new Sequelize({
20
- dialect: "sqlite",
21
- storage: this.#dbname + ".db",
22
- logging: false,
23
- });
24
- } else if (databasetype === "postgres") {
25
- this.#username = options.username;
26
- this.#password = options.password;
27
- this.#host = options.host;
28
- if (options.dbname) this.#dbname = options.dbname;
29
- if (!this.#username || !this.#password || !this.#host) {
30
- throw new Error(
31
- "PostgreSQL credentials not set. Please provide 'username', 'password', and 'host' in the options."
32
- );
33
- }
34
- console.log(
35
- `postgres://${this.#username}:${this.#password}@${this.#host}:${
36
- this.#dbport
37
- }/${this.#dbname}`
38
- );
39
-
40
- this.#sequelize = new Sequelize(
41
- `postgres://${this.#username}:${this.#password}@${this.#host}:${
42
- this.#dbport
43
- }/${this.#dbname}`,
44
- { logging: false }
45
- );
46
- } else {
47
- throw new Error(`Error! ${databasetype} is an unknown database type.`);
48
- }
49
-
50
- this.#Article = this.#sequelize.define(
51
- "Article",
52
- {
53
- title: DataTypes.STRING,
54
- content: DataTypes.TEXT,
55
- createdAt: {
56
- type: DataTypes.DATE,
57
- defaultValue: DataTypes.NOW,
58
- },
59
- updatedAt: {
60
- type: DataTypes.DATE,
61
- defaultValue: DataTypes.NOW,
62
- },
63
- },
64
- {
65
- timestamps: true,
66
- }
67
- );
68
-
69
- this.#BlogInfo = this.#sequelize.define(
70
- "BlogInfo",
71
- {
72
- title: DataTypes.STRING,
73
- },
74
- {
75
- timestamps: false,
76
- }
77
- );
78
- }
79
-
80
- async initialize() {
81
- // This creates the tables if they don't exist.
82
- await this.#sequelize.sync({ alter: true });
83
- console.log("database tables synced and ready.");
84
-
85
- // Check for and create the initial blog title right after syncing.
86
- const blogInfoCount = await this.#BlogInfo.count();
87
- if (blogInfoCount === 0) {
88
- await this.#BlogInfo.create({ title: "My Default Blog Title" });
89
- console.log("initialized blog title in database.");
90
- }
91
- }
92
-
93
- // model
94
- async findAll(
95
- limit = 4,
96
- offset = 0,
97
- startId = null,
98
- endId = null,
99
- order = "DESC"
100
- ) {
101
- const where = {};
102
- if (startId !== null && endId !== null) {
103
- where.id = {
104
- [Op.between]: [Math.min(startId, endId), Math.max(startId, endId)],
105
- };
106
- } else if (startId !== null) {
107
- where.id = { [order === "DESC" ? Op.lte : Op.gte]: startId };
108
- }
109
- const options = {
110
- where,
111
- order: [
112
- ["createdAt", order],
113
- ["id", order],
114
- ],
115
- limit,
116
- offset,
117
- };
118
- const articles = await this.#Article.findAll(options);
119
- return articles.map((article) => article.get({ plain: true }));
120
- }
121
-
122
- async save(newArticle) {
123
- await this.#Article.create(newArticle);
124
- console.log("Added new article:", newArticle);
125
- }
126
-
127
- async getBlogTitle() {
128
- // Find the first (and only) entry in the BlogInfo table.
129
- const blogInfo = await this.#BlogInfo.findOne();
130
-
131
- return blogInfo.title;
132
- }
133
-
134
- async updateBlogTitle(newTitle) {
135
- // Find the first (and only) entry and update its title.
136
- // Using where: {} will always find the first row.
137
- await this.#BlogInfo.update({ title: newTitle }, { where: {} });
138
- }
139
- }
package/streams.js DELETED
@@ -1,17 +0,0 @@
1
- import { Transform } from "stream";
2
-
3
- export class MapTransform extends Transform {
4
- constructor(fn) {
5
- super({
6
- objectMode: true,
7
- transform: (chunk, encoding, callback) => {
8
- try {
9
- // Call the mapping function with the chunk
10
- callback(null, fn(chunk));
11
- } catch (err) {
12
- callback(err);
13
- }
14
- },
15
- });
16
- }
17
- }
Binary file
Binary file
Binary file
Binary file
Binary file
package/test_server_db.db DELETED
Binary file
package/test_styles_1.db DELETED
Binary file
package/test_styles_2.db DELETED
Binary file
package/test_styles_3.db DELETED
Binary file
package/test_styles_4.db DELETED
Binary file