@lexho111/plainblog 0.4.3 → 0.5.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.
- package/.vscode/settings.json +2 -0
- package/Blog.js +14 -29
- package/Formatter.js +4 -1
- package/README.md +2 -38
- package/blog.json +87 -1
- package/build-styles.js +3 -3
- package/model/DatabaseModel2.js +86 -0
- package/model/fileModel.js +15 -5
- package/package.json +9 -7
- package/public/styles.min.css +68 -2
- package/src/styles.css +6 -0
- package/test/blog.test.js +4 -4
- package/test/model.test.js +7 -5
- package/test/server.test.js +2 -1
- package/test/styles.test.js +2 -2
- package/model/DatabaseModel.js +0 -139
- package/streams.js +0 -17
- package/test_1767619969536.db +0 -0
- package/test_1767620053052.db +0 -0
- package/test_1767621184498.db +0 -0
- package/test_1767623166288.db +0 -0
- package/test_1767623750051.db +0 -0
- package/test_server_db.db +0 -0
- package/test_styles_1.db +0 -0
- package/test_styles_2.db +0 -0
- package/test_styles_3.db +0 -0
- package/test_styles_4.db +0 -0
package/Blog.js
CHANGED
|
@@ -2,18 +2,15 @@ import http from "http";
|
|
|
2
2
|
import crypto from "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/
|
|
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: "
|
|
35
|
-
username: "
|
|
36
|
-
password: "
|
|
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
|
|
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
|
-
|
|
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
|
|
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.
|
|
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": "
|
|
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
|
}
|
package/build-styles.js
CHANGED
|
@@ -3,7 +3,7 @@ import { pathToFileURL } from "url";
|
|
|
3
3
|
import * as sass from "sass";
|
|
4
4
|
import postcss from "postcss";
|
|
5
5
|
import autoprefixer from "autoprefixer";
|
|
6
|
-
import cssnano from "cssnano";
|
|
6
|
+
//import cssnano from "cssnano";
|
|
7
7
|
|
|
8
8
|
// array of files or a single file
|
|
9
9
|
export async function compileStyles(fileData) {
|
|
@@ -39,7 +39,7 @@ export async function compileStyles(fileData) {
|
|
|
39
39
|
|
|
40
40
|
// 2. PostCSS (Autoprefixer + CSSNano)
|
|
41
41
|
if (combinedCss) {
|
|
42
|
-
const plugins = [autoprefixer()
|
|
42
|
+
const plugins = [autoprefixer()];
|
|
43
43
|
const result = await postcss(plugins).process(combinedCss, {
|
|
44
44
|
from: undefined,
|
|
45
45
|
});
|
|
@@ -57,7 +57,7 @@ export async function mergeStyles(...cssContents) {
|
|
|
57
57
|
const combinedCss = cssContents.join("\n");
|
|
58
58
|
|
|
59
59
|
if (combinedCss) {
|
|
60
|
-
const plugins = [autoprefixer()
|
|
60
|
+
const plugins = [autoprefixer()];
|
|
61
61
|
const result = await postcss(plugins).process(combinedCss, {
|
|
62
62
|
from: undefined,
|
|
63
63
|
});
|
|
@@ -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
|
+
}
|
package/model/fileModel.js
CHANGED
|
@@ -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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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.
|
|
3
|
+
"version": "0.5.0",
|
|
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,15 @@
|
|
|
17
18
|
"license": "ISC",
|
|
18
19
|
"dependencies": {
|
|
19
20
|
"autoprefixer": "^10.4.23",
|
|
20
|
-
"
|
|
21
|
-
"
|
|
22
|
-
"
|
|
23
|
-
"
|
|
21
|
+
"child_process": "^1.0.2",
|
|
22
|
+
"crypto": "^1.0.1",
|
|
23
|
+
"fs": "^0.0.1-security",
|
|
24
|
+
"http": "^0.0.1-security",
|
|
25
|
+
"path": "^0.12.7",
|
|
24
26
|
"postcss": "^8.4.35",
|
|
25
27
|
"sass": "^1.97.1",
|
|
26
|
-
"
|
|
27
|
-
"
|
|
28
|
+
"url": "^0.11.4",
|
|
29
|
+
"util": "^0.12.5"
|
|
28
30
|
},
|
|
29
31
|
"devDependencies": {
|
|
30
32
|
"dom-parser": "^1.1.5",
|
package/public/styles.min.css
CHANGED
|
@@ -1,2 +1,68 @@
|
|
|
1
|
-
body{font-family:Arial;
|
|
2
|
-
|
|
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
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
|
|
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");
|
package/test/model.test.js
CHANGED
|
@@ -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
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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(() => {
|
package/test/server.test.js
CHANGED
|
@@ -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
|
});
|
package/test/styles.test.js
CHANGED
|
@@ -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/model/DatabaseModel.js
DELETED
|
@@ -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
|
-
}
|
package/test_1767619969536.db
DELETED
|
Binary file
|
package/test_1767620053052.db
DELETED
|
Binary file
|
package/test_1767621184498.db
DELETED
|
Binary file
|
package/test_1767623166288.db
DELETED
|
Binary file
|
package/test_1767623750051.db
DELETED
|
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
|