@lexho111/plainblog 0.5.8 → 0.5.10
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/Blog.js +3 -4
- package/README.md +1 -1
- package/articles.txt +11 -0
- package/blog.db +0 -0
- package/bloginfo.json +3 -0
- package/build-styles.js +59 -0
- package/model/FileAdapter.js +23 -39
- package/model/FileModel.js +36 -22
- package/model/PostgresAdapter.js +30 -14
- package/model/SqliteAdapter.js +15 -6
- package/package.json +7 -2
- package/public/styles.min.css +2 -68
- package/test/blog.test.js +3 -3
- package/test/model.test.js +138 -62
- package/test/styles.test.js +19 -3
package/Blog.js
CHANGED
|
@@ -11,6 +11,7 @@ import path from "path";
|
|
|
11
11
|
import { fileURLToPath } from "url";
|
|
12
12
|
import { exec } from "child_process";
|
|
13
13
|
import { promisify } from "util";
|
|
14
|
+
import { compileStyles, mergeStyles } from "./build-styles.js";
|
|
14
15
|
|
|
15
16
|
const execPromise = promisify(exec);
|
|
16
17
|
|
|
@@ -189,8 +190,7 @@ export default class Blog {
|
|
|
189
190
|
|
|
190
191
|
if (srcHash !== publicHash) {
|
|
191
192
|
console.log("Styles have changed. Recompiling...");
|
|
192
|
-
|
|
193
|
-
const finalStyles = this.styles + " " + srcStyles;
|
|
193
|
+
const finalStyles = await mergeStyles(this.styles, srcStyles);
|
|
194
194
|
try {
|
|
195
195
|
await fs.promises.mkdir(path.dirname(publicStylePath), { recursive: true });
|
|
196
196
|
await fs.promises.writeFile(publicStylePath, finalStyles + `\n/* source-hash: ${srcHash} */`);
|
|
@@ -552,8 +552,7 @@ export default class Blog {
|
|
|
552
552
|
this.#stylesHash = currentHash;
|
|
553
553
|
|
|
554
554
|
// Compile styles using the standalone script from build-styles.js
|
|
555
|
-
|
|
556
|
-
this.compiledStyles = fileData.map((f) => f.content).join("\n");
|
|
555
|
+
this.compiledStyles = await compileStyles(fileData);
|
|
557
556
|
|
|
558
557
|
// generate a file
|
|
559
558
|
const __filename = fileURLToPath(import.meta.url);
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Plainblog
|
|
2
2
|
|
|
3
|
-
Plainblog is a simple blog generator to help you to set up and to maintain a minimalistic **single-page blog**. You can add new articles directly in the browser. Your data will be stored by default in
|
|
3
|
+
Plainblog is a simple blog generator to help you to set up and to maintain a minimalistic **single-page blog**. You can add new articles directly in the browser. Your data will be stored by default in two file called _bloginfo.json_ and _articles.txt_.
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
package/articles.txt
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{"title":"Test Title from Jest","content":"This is the content of the test article.","createdAt":"2026-01-08T13:19:39.939Z"}
|
|
2
|
+
{"title":"Test Title from Jest","content":"This is the content of the test article.","createdAt":"2026-01-08T13:34:38.866Z"}
|
|
3
|
+
{"title":"Test Title from Jest","content":"This is the content of the test article.","createdAt":"2026-01-08T13:43:32.343Z"}
|
|
4
|
+
{"title":"Test Title from Jest","content":"This is the content of the test article.","createdAt":"2026-01-08T18:48:23.123Z"}
|
|
5
|
+
{"title":"Test Title from Jest","content":"This is the content of the test article.","createdAt":"2026-01-08T18:50:06.993Z"}
|
|
6
|
+
{"title":"Test Title from Jest","content":"This is the content of the test article.","createdAt":"2026-01-08T18:56:28.369Z"}
|
|
7
|
+
{"title":"Test Title from Jest","content":"This is the content of the test article.","createdAt":"2026-01-08T18:57:53.780Z"}
|
|
8
|
+
{"title":"Test Title from Jest","content":"This is the content of the test article.","createdAt":"2026-01-08T18:58:54.261Z"}
|
|
9
|
+
{"title":"Test Title from Jest","content":"This is the content of the test article.","createdAt":"2026-01-08T19:01:02.613Z"}
|
|
10
|
+
{"title":"Test Title from Jest","content":"This is the content of the test article.","createdAt":"2026-01-08T19:01:30.473Z"}
|
|
11
|
+
{"title":"Test Title from Jest","content":"This is the content of the test article.","createdAt":"2026-01-08T19:03:33.773Z"}
|
package/blog.db
CHANGED
|
Binary file
|
package/bloginfo.json
ADDED
package/build-styles.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import { pathToFileURL } from "url";
|
|
3
|
+
import postcss from "postcss";
|
|
4
|
+
import autoprefixer from "autoprefixer";
|
|
5
|
+
import cssnano from "cssnano";
|
|
6
|
+
|
|
7
|
+
// array of files or a single file
|
|
8
|
+
export async function compileStyles(fileData) {
|
|
9
|
+
try {
|
|
10
|
+
let combinedCss = "";
|
|
11
|
+
|
|
12
|
+
if (fileData) {
|
|
13
|
+
const scssFiles = fileData.filter(
|
|
14
|
+
(f) =>
|
|
15
|
+
f.path.endsWith(".scss") && !path.basename(f.path).startsWith("_")
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
for (const file of scssFiles) {
|
|
19
|
+
console.error("sass files are not supported.");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const cssFiles = fileData.filter((f) => f.path.endsWith(".css"));
|
|
23
|
+
for (const file of cssFiles) {
|
|
24
|
+
combinedCss += file.content + "\n";
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// 2. PostCSS (Autoprefixer + CSSNano)
|
|
29
|
+
if (combinedCss) {
|
|
30
|
+
const plugins = [autoprefixer(), cssnano()];
|
|
31
|
+
const result = await postcss(plugins).process(combinedCss, {
|
|
32
|
+
from: undefined,
|
|
33
|
+
});
|
|
34
|
+
return result.css;
|
|
35
|
+
}
|
|
36
|
+
return "";
|
|
37
|
+
} catch (error) {
|
|
38
|
+
console.error("Build failed:", error);
|
|
39
|
+
return "";
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function mergeStyles(...cssContents) {
|
|
44
|
+
try {
|
|
45
|
+
const combinedCss = cssContents.join("\n");
|
|
46
|
+
|
|
47
|
+
if (combinedCss) {
|
|
48
|
+
const plugins = [autoprefixer(), cssnano()];
|
|
49
|
+
const result = await postcss(plugins).process(combinedCss, {
|
|
50
|
+
from: undefined,
|
|
51
|
+
});
|
|
52
|
+
return result.css;
|
|
53
|
+
}
|
|
54
|
+
return "";
|
|
55
|
+
} catch (error) {
|
|
56
|
+
console.error("Merge failed:", error);
|
|
57
|
+
return "";
|
|
58
|
+
}
|
|
59
|
+
}
|
package/model/FileAdapter.js
CHANGED
|
@@ -1,13 +1,21 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
saveInfo,
|
|
3
|
+
loadInfo,
|
|
4
|
+
appendArticle,
|
|
5
|
+
loadArticles,
|
|
6
|
+
initFiles,
|
|
7
|
+
} from "./FileModel.js";
|
|
2
8
|
|
|
3
9
|
export default class FileAdapter {
|
|
4
10
|
dbtype = "file";
|
|
5
11
|
constructor(options) {
|
|
6
|
-
this.
|
|
12
|
+
this.infoFile = "bloginfo.json";
|
|
13
|
+
this.articlesFile = "articles.txt";
|
|
7
14
|
}
|
|
8
15
|
|
|
9
16
|
async initialize() {
|
|
10
17
|
console.log("file adapter init");
|
|
18
|
+
await initFiles(this.infoFile, this.articlesFile);
|
|
11
19
|
}
|
|
12
20
|
|
|
13
21
|
test() {
|
|
@@ -15,42 +23,19 @@ export default class FileAdapter {
|
|
|
15
23
|
}
|
|
16
24
|
|
|
17
25
|
async getBlogTitle() {
|
|
18
|
-
const
|
|
19
|
-
return
|
|
20
|
-
res(blogTitle);
|
|
21
|
-
});
|
|
26
|
+
const info = await loadInfo(this.infoFile);
|
|
27
|
+
return info.title || "Blog";
|
|
22
28
|
}
|
|
23
29
|
|
|
24
30
|
async save(newArticle) {
|
|
25
31
|
if (!newArticle.createdAt) {
|
|
26
32
|
newArticle.createdAt = new Date().toISOString();
|
|
27
33
|
}
|
|
28
|
-
|
|
29
|
-
let blogTitle = "";
|
|
30
|
-
let articles = [];
|
|
31
|
-
try {
|
|
32
|
-
await loadFromFile(this.filename, (t, a) => {
|
|
33
|
-
blogTitle = t;
|
|
34
|
-
articles = a || [];
|
|
35
|
-
});
|
|
36
|
-
} catch (err) {
|
|
37
|
-
console.error(err);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
articles.push(newArticle);
|
|
41
|
-
saveToFile(this.filename, { title: blogTitle, articles });
|
|
34
|
+
await appendArticle(this.articlesFile, newArticle);
|
|
42
35
|
}
|
|
43
36
|
|
|
44
37
|
async updateBlogTitle(newTitle) {
|
|
45
|
-
|
|
46
|
-
try {
|
|
47
|
-
await loadFromFile(this.filename, (t, a) => {
|
|
48
|
-
articles = a || [];
|
|
49
|
-
});
|
|
50
|
-
} catch (err) {
|
|
51
|
-
console.error(err);
|
|
52
|
-
}
|
|
53
|
-
saveToFile(this.filename, { title: newTitle, articles });
|
|
38
|
+
await saveInfo(this.infoFile, { title: newTitle });
|
|
54
39
|
}
|
|
55
40
|
|
|
56
41
|
async findAll(
|
|
@@ -62,16 +47,15 @@ export default class FileAdapter {
|
|
|
62
47
|
) {
|
|
63
48
|
let dbArticles = [];
|
|
64
49
|
try {
|
|
65
|
-
await
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
});
|
|
50
|
+
const articles = await loadArticles(this.articlesFile);
|
|
51
|
+
if (Array.isArray(articles)) {
|
|
52
|
+
articles.sort((a, b) => {
|
|
53
|
+
const dateA = new Date(a.createdAt || 0);
|
|
54
|
+
const dateB = new Date(b.createdAt || 0);
|
|
55
|
+
return order === "DESC" ? dateB - dateA : dateA - dateB;
|
|
56
|
+
});
|
|
57
|
+
dbArticles = articles.slice(offset, offset + limit);
|
|
58
|
+
}
|
|
75
59
|
} catch (err) {
|
|
76
60
|
console.error(err);
|
|
77
61
|
}
|
package/model/FileModel.js
CHANGED
|
@@ -1,35 +1,49 @@
|
|
|
1
1
|
import { promises as fs } from "fs";
|
|
2
2
|
|
|
3
|
-
/** save blog
|
|
4
|
-
export async function
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
return;
|
|
8
|
-
}
|
|
3
|
+
/** save blog info (title, etc) to a standard JSON file */
|
|
4
|
+
export async function saveInfo(filename, data) {
|
|
5
|
+
await fs.writeFile(filename, JSON.stringify(data, null, 2));
|
|
6
|
+
}
|
|
9
7
|
|
|
8
|
+
/** load blog info */
|
|
9
|
+
export async function loadInfo(filename) {
|
|
10
10
|
try {
|
|
11
|
-
await fs.
|
|
12
|
-
|
|
11
|
+
const data = await fs.readFile(filename, "utf8");
|
|
12
|
+
return JSON.parse(data);
|
|
13
13
|
} catch (err) {
|
|
14
|
-
|
|
14
|
+
return { title: "Blog" };
|
|
15
15
|
}
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
/**
|
|
19
|
-
export async function
|
|
18
|
+
/** append an article as a new line to the file */
|
|
19
|
+
export async function appendArticle(filename, article) {
|
|
20
|
+
await fs.appendFile(filename, JSON.stringify(article) + "\n");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** load all articles by reading line by line */
|
|
24
|
+
export async function loadArticles(filename) {
|
|
20
25
|
try {
|
|
21
26
|
const data = await fs.readFile(filename, "utf8");
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
27
|
+
return data
|
|
28
|
+
.split("\n")
|
|
29
|
+
.filter((line) => line.trim() !== "")
|
|
30
|
+
.map((line) => JSON.parse(line));
|
|
31
|
+
} catch (err) {
|
|
32
|
+
return [];
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** ensure files exist */
|
|
37
|
+
export async function initFiles(infoFilename, articlesFilename) {
|
|
38
|
+
try {
|
|
39
|
+
await fs.access(infoFilename);
|
|
40
|
+
} catch (err) {
|
|
41
|
+
await saveInfo(infoFilename, { title: "Blog" });
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
await fs.access(articlesFilename);
|
|
26
46
|
} catch (err) {
|
|
27
|
-
|
|
28
|
-
const defaultData = { title: "Blog", articles: [] };
|
|
29
|
-
await save(filename, defaultData);
|
|
30
|
-
f(defaultData.title, defaultData.articles);
|
|
31
|
-
} else {
|
|
32
|
-
throw err;
|
|
33
|
-
}
|
|
47
|
+
await fs.writeFile(articlesFilename, "");
|
|
34
48
|
}
|
|
35
49
|
}
|
package/model/PostgresAdapter.js
CHANGED
|
@@ -12,6 +12,7 @@ export default class PostgresAdapter extends SequelizeAdapter {
|
|
|
12
12
|
this.host = options.host;
|
|
13
13
|
if (options.dbname) this.dbname = options.dbname;
|
|
14
14
|
if (options.dbport) this.dbport = options.dbport;
|
|
15
|
+
else this.dbport = 5432;
|
|
15
16
|
if (!this.username || !this.password || !this.host) {
|
|
16
17
|
throw new Error(
|
|
17
18
|
"PostgreSQL credentials not set. Please provide 'username', 'password', and 'host' in the options."
|
|
@@ -21,24 +22,39 @@ export default class PostgresAdapter extends SequelizeAdapter {
|
|
|
21
22
|
|
|
22
23
|
async initialize() {
|
|
23
24
|
console.log("initialize database");
|
|
25
|
+
const maxRetries = 10;
|
|
26
|
+
const retryDelay = 3000;
|
|
24
27
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
28
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
29
|
+
try {
|
|
30
|
+
console.log(
|
|
31
|
+
`postgres://${this.username}:${this.password}@${this.host}:${this.dbport}/${this.dbname}`
|
|
32
|
+
);
|
|
29
33
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
34
|
+
this.sequelize = new Sequelize(
|
|
35
|
+
`postgres://${this.username}:${this.password}@${this.host}:${this.dbport}/${this.dbname}`,
|
|
36
|
+
{ logging: false }
|
|
37
|
+
);
|
|
38
|
+
await this.sequelize.authenticate();
|
|
39
|
+
await this.initializeModels();
|
|
40
|
+
console.log("Database connection established successfully.");
|
|
41
|
+
return;
|
|
42
|
+
} catch (err) {
|
|
43
|
+
if (err.message.includes("Please install")) {
|
|
44
|
+
throw new Error(
|
|
45
|
+
"PostgreSQL driver is not installed. Please install it to use PostgresAdapter: npm install pg pg-hstore"
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
console.error(
|
|
49
|
+
`Database connection attempt ${attempt} failed: ${err.message}`
|
|
38
50
|
);
|
|
51
|
+
if (attempt === maxRetries) {
|
|
52
|
+
console.error("Max retries reached. Exiting.");
|
|
53
|
+
throw err;
|
|
54
|
+
}
|
|
55
|
+
console.log(`Retrying in ${retryDelay / 1000} seconds...`);
|
|
56
|
+
await new Promise((resolve) => setTimeout(resolve, retryDelay));
|
|
39
57
|
}
|
|
40
|
-
throw err;
|
|
41
58
|
}
|
|
42
|
-
await this.initializeModels();
|
|
43
59
|
}
|
|
44
60
|
}
|
package/model/SqliteAdapter.js
CHANGED
|
@@ -8,14 +8,23 @@ export default class SqliteAdapter extends SequelizeAdapter {
|
|
|
8
8
|
|
|
9
9
|
// Use the full path for the database file from the options.
|
|
10
10
|
if (options.dbname) this.dbname = options.dbname;
|
|
11
|
-
this.sequelize = new Sequelize({
|
|
12
|
-
dialect: "sqlite",
|
|
13
|
-
storage: this.dbname + ".db",
|
|
14
|
-
logging: false,
|
|
15
|
-
});
|
|
16
11
|
}
|
|
17
12
|
|
|
18
13
|
async initialize() {
|
|
19
|
-
|
|
14
|
+
try {
|
|
15
|
+
this.sequelize = new Sequelize({
|
|
16
|
+
dialect: "sqlite",
|
|
17
|
+
storage: this.dbname + ".db",
|
|
18
|
+
logging: false,
|
|
19
|
+
});
|
|
20
|
+
await this.initializeModels();
|
|
21
|
+
} catch (err) {
|
|
22
|
+
if (err.message.includes("Please install")) {
|
|
23
|
+
throw new Error(
|
|
24
|
+
"SQLite driver is not installed. Please install it: npm install sqlite3 --save-dev"
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
throw err;
|
|
28
|
+
}
|
|
20
29
|
}
|
|
21
30
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lexho111/plainblog",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.10",
|
|
4
4
|
"description": "A tool for creating and serving a minimalist, single-page blog.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -17,11 +17,15 @@
|
|
|
17
17
|
"author": "lexho111",
|
|
18
18
|
"license": "ISC",
|
|
19
19
|
"dependencies": {
|
|
20
|
+
"autoprefixer": "^10.4.23",
|
|
20
21
|
"child_process": "^1.0.2",
|
|
22
|
+
"cssnano": "^7.1.2",
|
|
21
23
|
"fs": "^0.0.1-security",
|
|
22
24
|
"http": "^0.0.1-security",
|
|
23
25
|
"node-fetch": "^3.3.2",
|
|
24
26
|
"path": "^0.12.7",
|
|
27
|
+
"postcss": "^8.5.6",
|
|
28
|
+
"postcss-preset-env": "^10.6.0",
|
|
25
29
|
"sass": "^1.97.1",
|
|
26
30
|
"url": "^0.11.4",
|
|
27
31
|
"util": "^0.12.5"
|
|
@@ -30,7 +34,8 @@
|
|
|
30
34
|
"dom-parser": "^1.1.5",
|
|
31
35
|
"eslint": "^9.8.0",
|
|
32
36
|
"eslint-plugin-jest": "^28.6.0",
|
|
33
|
-
"jest": "^29.7.0"
|
|
37
|
+
"jest": "^29.7.0",
|
|
38
|
+
"sqlite3": "^5.1.7"
|
|
34
39
|
},
|
|
35
40
|
"optionalDependencies": {
|
|
36
41
|
"pg": "^8.16.3",
|
package/public/styles.min.css
CHANGED
|
@@ -1,68 +1,2 @@
|
|
|
1
|
-
body
|
|
2
|
-
|
|
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 */
|
|
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}#wrapper{max-width:500px;width:100%}@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: a07f631befba4b6bc703f8709f5ef455faafeff4e5f00b62f835576eea7fb529 */
|
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:
|
|
61
|
+
expected: "font-family:Courier",
|
|
62
62
|
},
|
|
63
63
|
{
|
|
64
64
|
style: "body{ background-color:black; color:white; }",
|
|
65
|
-
expected: "background-color
|
|
65
|
+
expected: "background-color:#000;color:#fff;",
|
|
66
66
|
},
|
|
67
|
-
{ style: "body{ font-size: 1.2em; }", expected: "font-size:
|
|
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,7 +2,18 @@ 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
|
|
5
|
+
import fs from "fs";
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
saveInfo,
|
|
9
|
+
loadInfo,
|
|
10
|
+
appendArticle,
|
|
11
|
+
loadArticles,
|
|
12
|
+
initFiles,
|
|
13
|
+
} from "../model/FileModel.js";
|
|
14
|
+
|
|
15
|
+
import SqliteAdapter from "../model/SqliteAdapter.js";
|
|
16
|
+
import FileAdapter from "../model/FileAdapter.js";
|
|
6
17
|
|
|
7
18
|
function generateRandomContent(length) {
|
|
8
19
|
let str = "";
|
|
@@ -15,25 +26,6 @@ function generateRandomContent(length) {
|
|
|
15
26
|
return str;
|
|
16
27
|
}
|
|
17
28
|
|
|
18
|
-
describe("File Model test", () => {
|
|
19
|
-
it("should load blog data from blog.json", async () => {
|
|
20
|
-
const blog = new Blog();
|
|
21
|
-
blog.setStyle(
|
|
22
|
-
"body { font-family: Arial, sans-serif; } h1 { color: #333; }"
|
|
23
|
-
);
|
|
24
|
-
|
|
25
|
-
const content = generateRandomContent(200);
|
|
26
|
-
|
|
27
|
-
const article = new Article("hello", content);
|
|
28
|
-
blog.addArticle(article);
|
|
29
|
-
|
|
30
|
-
await loadFromFile("blog.json", async (title) => {
|
|
31
|
-
expect(await blog.toHTML()).toContain(content);
|
|
32
|
-
expect(blog).toBeDefined();
|
|
33
|
-
});
|
|
34
|
-
});
|
|
35
|
-
});
|
|
36
|
-
|
|
37
29
|
describe("API Model test", () => {
|
|
38
30
|
beforeAll((done) => {
|
|
39
31
|
const port = 8081;
|
|
@@ -98,56 +90,140 @@ describe("API Model test", () => {
|
|
|
98
90
|
});
|
|
99
91
|
|
|
100
92
|
describe("Database Model test", () => {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
93
|
+
describe("Sqlite Database Adapter test", () => {
|
|
94
|
+
it("should be empty", async () => {
|
|
95
|
+
const blog = new Blog();
|
|
96
|
+
const sqliteAdapter = new SqliteAdapter({
|
|
97
|
+
dbname: "blog",
|
|
98
|
+
});
|
|
99
|
+
blog.setDatabaseAdapter(sqliteAdapter);
|
|
100
|
+
await blog.init();
|
|
101
|
+
|
|
102
|
+
expect(await blog.toHTML()).not.toContain("<article>");
|
|
103
|
+
expect(blog).toBeDefined();
|
|
104
|
+
});
|
|
111
105
|
|
|
112
|
-
|
|
106
|
+
it("should load blog data from sqlite database", async () => {
|
|
107
|
+
const blog = new Blog();
|
|
108
|
+
const sqliteAdapter = new SqliteAdapter({
|
|
109
|
+
dbname: "blog",
|
|
110
|
+
});
|
|
111
|
+
blog.setDatabaseAdapter(sqliteAdapter);
|
|
112
|
+
await blog.init();
|
|
113
|
+
|
|
114
|
+
const content = generateRandomContent(200);
|
|
115
|
+
|
|
116
|
+
await new Promise((resolve) => {
|
|
117
|
+
const req = {
|
|
118
|
+
on: (event, cb) => {
|
|
119
|
+
if (event === "data") {
|
|
120
|
+
setTimeout(() => cb(`title=hello&content=${content}`), 0);
|
|
121
|
+
}
|
|
122
|
+
if (event === "end") {
|
|
123
|
+
setTimeout(() => cb(), 20);
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
off: () => {},
|
|
127
|
+
};
|
|
128
|
+
const res = {
|
|
129
|
+
writeHead: () => {},
|
|
130
|
+
end: resolve,
|
|
131
|
+
};
|
|
132
|
+
blog.postArticle(req, res);
|
|
133
|
+
});
|
|
113
134
|
|
|
114
|
-
|
|
115
|
-
|
|
135
|
+
expect(await blog.toHTML()).toContain(content);
|
|
136
|
+
expect(blog).toBeDefined();
|
|
137
|
+
});
|
|
116
138
|
});
|
|
139
|
+
describe("Postgres Adapter test", () => {});
|
|
140
|
+
describe("File Adapter test", () => {
|
|
141
|
+
it("should update blog title", async () => {
|
|
142
|
+
const title_org = "Test Blog Title";
|
|
143
|
+
const fileAdapter = new FileAdapter();
|
|
144
|
+
await fileAdapter.initialize();
|
|
145
|
+
await fileAdapter.updateBlogTitle(title_org);
|
|
146
|
+
const title = await fileAdapter.getBlogTitle();
|
|
147
|
+
console.log(title);
|
|
148
|
+
expect(title).toBe(title_org);
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
describe("File Model test", () => {
|
|
152
|
+
it("should init files and load info", async () => {
|
|
153
|
+
const infoFile = "test_bloginfo.json";
|
|
154
|
+
const articlesFile = "test_articles.txt";
|
|
117
155
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
blog.database.dbname = "test_" + Date.now();
|
|
122
|
-
blog.setStyle(
|
|
123
|
-
"body { font-family: Arial, sans-serif; } h1 { color: #333; }"
|
|
124
|
-
);
|
|
125
|
-
await blog.init();
|
|
156
|
+
// Ensure clean state
|
|
157
|
+
if (fs.existsSync(infoFile)) fs.unlinkSync(infoFile);
|
|
158
|
+
if (fs.existsSync(articlesFile)) fs.unlinkSync(articlesFile);
|
|
126
159
|
|
|
127
|
-
|
|
160
|
+
await initFiles(infoFile, articlesFile);
|
|
128
161
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
if (event === "end") {
|
|
136
|
-
setTimeout(() => cb(), 20);
|
|
137
|
-
}
|
|
138
|
-
},
|
|
139
|
-
off: () => {},
|
|
140
|
-
};
|
|
141
|
-
const res = {
|
|
142
|
-
writeHead: () => {},
|
|
143
|
-
end: resolve,
|
|
144
|
-
};
|
|
145
|
-
blog.postArticle(req, res);
|
|
162
|
+
const info = await loadInfo(infoFile);
|
|
163
|
+
expect(info.title).toBe("Blog");
|
|
164
|
+
|
|
165
|
+
// Cleanup
|
|
166
|
+
fs.unlinkSync(infoFile);
|
|
167
|
+
fs.unlinkSync(articlesFile);
|
|
146
168
|
});
|
|
147
169
|
|
|
148
|
-
|
|
170
|
+
it("should read and write to articles.txt file", async () => {
|
|
171
|
+
const content = generateRandomContent(200);
|
|
172
|
+
const article = new Article("hello", content, new Date().toISOString());
|
|
173
|
+
const filename = "test_articles.txt";
|
|
149
174
|
|
|
150
|
-
|
|
151
|
-
|
|
175
|
+
await appendArticle(filename, article);
|
|
176
|
+
const articles = await loadArticles(filename);
|
|
177
|
+
|
|
178
|
+
// Compare properties since 'articles' contains plain objects, not Article instances
|
|
179
|
+
expect(articles.length).toBeGreaterThan(0);
|
|
180
|
+
const lastArticle = articles[articles.length - 1];
|
|
181
|
+
expect(lastArticle.title).toBe(article.title);
|
|
182
|
+
expect(lastArticle.content).toBe(article.content);
|
|
183
|
+
|
|
184
|
+
// Cleanup
|
|
185
|
+
if (fs.existsSync(filename)) fs.unlinkSync(filename);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it("should be empty articles.txt", async () => {
|
|
189
|
+
const filename = "test_articles_empty.txt";
|
|
190
|
+
const articles = await loadArticles(filename);
|
|
191
|
+
expect(articles.length).toBe(0);
|
|
192
|
+
// Cleanup
|
|
193
|
+
if (fs.existsSync(filename)) fs.unlinkSync(filename);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it("should read and write multiple articles to articles.txt file", async () => {
|
|
197
|
+
const filename = "test_articles.txt";
|
|
198
|
+
|
|
199
|
+
// Ensure clean state
|
|
200
|
+
if (fs.existsSync(filename)) fs.unlinkSync(filename);
|
|
201
|
+
|
|
202
|
+
try {
|
|
203
|
+
const articles_org = [];
|
|
204
|
+
const count = 20;
|
|
205
|
+
for (let i = 0; i < count; i++) {
|
|
206
|
+
const title = generateRandomContent(20);
|
|
207
|
+
const content = generateRandomContent(200);
|
|
208
|
+
const article = new Article(title, content, new Date().toISOString());
|
|
209
|
+
articles_org.push(article);
|
|
210
|
+
|
|
211
|
+
await appendArticle(filename, article);
|
|
212
|
+
}
|
|
213
|
+
const articles = await loadArticles(filename);
|
|
214
|
+
|
|
215
|
+
// Compare properties since 'articles' contains plain objects, not Article instances
|
|
216
|
+
expect(articles).toHaveLength(count);
|
|
217
|
+
|
|
218
|
+
for (let i = 0; i < articles.length; i++) {
|
|
219
|
+
expect(articles[i].title).toBe(articles_org[i].title);
|
|
220
|
+
expect(articles[i].content).toBe(articles_org[i].content);
|
|
221
|
+
expect(articles[i].createdAt).toBe(articles_org[i].createdAt);
|
|
222
|
+
}
|
|
223
|
+
} finally {
|
|
224
|
+
// Cleanup
|
|
225
|
+
if (fs.existsSync(filename)) fs.unlinkSync(filename);
|
|
226
|
+
}
|
|
227
|
+
});
|
|
152
228
|
});
|
|
153
229
|
});
|
package/test/styles.test.js
CHANGED
|
@@ -16,10 +16,26 @@ describe("Blog Stylesheet Test", () => {
|
|
|
16
16
|
expect(data).toContain("body");
|
|
17
17
|
expect(data).toContain("nav a");
|
|
18
18
|
expect(data).toContain(".datetime");
|
|
19
|
-
expect(data).toContain("font-style: normal");
|
|
20
19
|
expect(data).toContain("color: darkgray");
|
|
21
20
|
});
|
|
22
21
|
|
|
22
|
+
it("should load the stylesheet (.css) file from public", async () => {
|
|
23
|
+
const filepath = path.join(__dirname, "../public/styles.min.css");
|
|
24
|
+
|
|
25
|
+
const data = await fs.promises.readFile(filepath, "utf8");
|
|
26
|
+
console.log(data);
|
|
27
|
+
expect(data).toContain("font-family:Arial");
|
|
28
|
+
expect(data).toContain("h1");
|
|
29
|
+
expect(data).toContain(".grid{");
|
|
30
|
+
expect(data).toContain(".grid article");
|
|
31
|
+
expect(data).toContain("nav a");
|
|
32
|
+
expect(data).toContain(".datetime");
|
|
33
|
+
expect(data).toContain("nav a:visited{");
|
|
34
|
+
expect(data).toContain("@media screen");
|
|
35
|
+
expect(data).toContain("#wrapper{");
|
|
36
|
+
expect(data).not.toContain("color:darkgray");
|
|
37
|
+
});
|
|
38
|
+
|
|
23
39
|
it("should load and compile the stylesheet (.css) correctly", async () => {
|
|
24
40
|
const blog = new Blog();
|
|
25
41
|
blog.title = "My Blog";
|
|
@@ -38,7 +54,7 @@ describe("Blog Stylesheet Test", () => {
|
|
|
38
54
|
expect(publicCSS).toContain("body");
|
|
39
55
|
expect(publicCSS).toContain("nav a");
|
|
40
56
|
expect(publicCSS).toContain(".datetime");
|
|
41
|
-
expect(publicCSS).toContain("font-style:
|
|
57
|
+
expect(publicCSS).toContain("font-style:normal");
|
|
42
58
|
expect(publicCSS).toContain("color:");
|
|
43
59
|
});
|
|
44
60
|
|
|
@@ -60,7 +76,7 @@ describe("Blog Stylesheet Test", () => {
|
|
|
60
76
|
expect(publicCSS).toContain("body");
|
|
61
77
|
expect(publicCSS).toContain("nav a");
|
|
62
78
|
expect(publicCSS).toContain(".datetime");
|
|
63
|
-
expect(publicCSS).toContain("font-style:
|
|
79
|
+
expect(publicCSS).toContain("font-style:normal");
|
|
64
80
|
expect(publicCSS).toContain("color:");
|
|
65
81
|
});
|
|
66
82
|
});
|