@lexho111/plainblog 0.5.27 → 0.6.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/Article.js +73 -4
- package/Blog.js +348 -149
- package/Formatter.js +5 -12
- package/README.md +5 -1
- package/{blog → blog_test_empty.db} +0 -0
- package/blog_test_load.db +0 -0
- package/build-scripts.js +54 -0
- package/coverage/clover.xml +1043 -0
- package/coverage/coverage-final.json +20 -0
- package/coverage/lcov-report/base.css +224 -0
- package/coverage/lcov-report/block-navigation.js +87 -0
- package/coverage/lcov-report/favicon.png +0 -0
- package/coverage/lcov-report/index.html +161 -0
- package/coverage/lcov-report/package/Article.js.html +406 -0
- package/coverage/lcov-report/package/Blog.js.html +2635 -0
- package/coverage/lcov-report/package/Formatter.js.html +379 -0
- package/coverage/lcov-report/package/build-scripts.js.html +247 -0
- package/coverage/lcov-report/package/build-styles.js.html +367 -0
- package/coverage/lcov-report/package/index.html +191 -0
- package/coverage/lcov-report/package/model/APIModel.js.html +190 -0
- package/coverage/lcov-report/package/model/ArrayList.js.html +382 -0
- package/coverage/lcov-report/package/model/ArrayListHashMap.js.html +379 -0
- package/coverage/lcov-report/package/model/BinarySearchTree.js.html +856 -0
- package/coverage/lcov-report/package/model/BinarySearchTreeHashMap.js.html +346 -0
- package/coverage/lcov-report/package/model/DataModel.js.html +307 -0
- package/coverage/lcov-report/package/model/DatabaseModel.js.html +232 -0
- package/coverage/lcov-report/package/model/FileAdapter.js.html +394 -0
- package/coverage/lcov-report/package/model/FileList.js.html +244 -0
- package/coverage/lcov-report/package/model/FileModel.js.html +358 -0
- package/coverage/lcov-report/package/model/SequelizeAdapter.js.html +538 -0
- package/coverage/lcov-report/package/model/SqliteAdapter.js.html +247 -0
- package/coverage/lcov-report/package/model/datastructures/ArrayList.js.html +439 -0
- package/coverage/lcov-report/package/model/datastructures/ArrayListHashMap.js.html +196 -0
- package/coverage/lcov-report/package/model/datastructures/BinarySearchTree.js.html +913 -0
- package/coverage/lcov-report/package/model/datastructures/BinarySearchTreeHashMap.js.html +346 -0
- package/coverage/lcov-report/package/model/datastructures/FileList.js.html +244 -0
- package/coverage/lcov-report/package/model/datastructures/index.html +176 -0
- package/coverage/lcov-report/package/model/index.html +206 -0
- package/coverage/lcov-report/package/utilities.js.html +511 -0
- package/coverage/lcov-report/prettify.css +1 -0
- package/coverage/lcov-report/prettify.js +2 -0
- package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
- package/coverage/lcov-report/sorter.js +210 -0
- package/coverage/lcov.info +2063 -0
- package/index.js +25 -1
- package/model/DataModel.js +79 -0
- package/model/DatabaseModel.js +20 -8
- package/model/FileAdapter.js +43 -4
- package/model/FileModel.js +47 -9
- package/model/SequelizeAdapter.js +11 -3
- package/model/datastructures/ArrayList.js +118 -0
- package/model/datastructures/ArrayListHashMap.js +37 -0
- package/model/datastructures/ArrayListHashMap.js.bk +90 -0
- package/model/datastructures/BinarySearchTree.js +276 -0
- package/model/datastructures/BinarySearchTreeHashMap.js +89 -0
- package/model/datastructures/BinarySearchTreeTest.js +16 -0
- package/model/datastructures/FileList.js +53 -0
- package/package.json +11 -2
- package/postinstall.js +89 -0
- package/public/fetchData.js +0 -0
- package/public/scripts.min.js +2 -1
- package/public/styles.min.css +2 -2
- package/src/fetchData.js +150 -30
- package/src/styles.css +47 -0
- package/utilities.js +142 -0
package/Blog.js
CHANGED
|
@@ -2,16 +2,38 @@ import http from "http";
|
|
|
2
2
|
import crypto from "node:crypto";
|
|
3
3
|
import fs from "fs";
|
|
4
4
|
import { URLSearchParams } from "url";
|
|
5
|
+
import process from "node:process";
|
|
6
|
+
import path from "path";
|
|
7
|
+
import { fileURLToPath } from "url";
|
|
8
|
+
import pkg from "./package.json" with { type: "json" };
|
|
9
|
+
import createDebug from "debug";
|
|
5
10
|
import Article from "./Article.js";
|
|
6
11
|
import DatabaseModel from "./model/DatabaseModel.js";
|
|
7
12
|
import { fetchData, postData } from "./model/APIModel.js";
|
|
8
13
|
import { formatHTML, header, formatMarkdown, validate } from "./Formatter.js"; // import pkg from "./package.json" with { type: "json" };
|
|
9
|
-
import path from "path";
|
|
10
|
-
import { fileURLToPath } from "url";
|
|
11
14
|
import { compileStyles, mergeStyles } from "./build-styles.js";
|
|
12
|
-
import
|
|
15
|
+
import { compileScripts } from "./build-scripts.js";
|
|
16
|
+
import FileAdapter from "./model/FileAdapter.js";
|
|
17
|
+
import { table, log } from "./utilities.js";
|
|
18
|
+
|
|
19
|
+
import DataModel from "./model/DataModel.js";
|
|
20
|
+
import ArrayList from "./model/datastructures/ArrayList.js";
|
|
21
|
+
import ArrayListHashMap from "./model/datastructures/ArrayListHashMap.js";
|
|
22
|
+
import { BinarySearchTreeHashMap } from "./model/datastructures/BinarySearchTreeHashMap.js";
|
|
23
|
+
import FileList from "./model/datastructures/FileList.js";
|
|
24
|
+
|
|
25
|
+
// Initialize the debugger with a specific namespace
|
|
26
|
+
const debug = createDebug("plainblog:Blog");
|
|
13
27
|
|
|
14
28
|
export default class Blog {
|
|
29
|
+
#makeDataModel() {
|
|
30
|
+
return new BinarySearchTreeHashMap();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
setDataModel(datamodel) {
|
|
34
|
+
this.#articles = datamodel;
|
|
35
|
+
}
|
|
36
|
+
|
|
15
37
|
constructor() {
|
|
16
38
|
this.database = {
|
|
17
39
|
type: "file",
|
|
@@ -21,7 +43,7 @@ export default class Blog {
|
|
|
21
43
|
dbname: "articles.txt", // x
|
|
22
44
|
};
|
|
23
45
|
this.#title = "";
|
|
24
|
-
this.#articles =
|
|
46
|
+
this.#articles = new DataModel(this.#makeDataModel());
|
|
25
47
|
this.#server = null;
|
|
26
48
|
this.#password = "admin";
|
|
27
49
|
this.#styles = "body { font-family: Arial; }";
|
|
@@ -47,7 +69,7 @@ export default class Blog {
|
|
|
47
69
|
const json = {
|
|
48
70
|
version: this.#version,
|
|
49
71
|
title: this.#title,
|
|
50
|
-
articles: this.#articles,
|
|
72
|
+
articles: this.#articles.getAllArticles(),
|
|
51
73
|
server: serverInfo,
|
|
52
74
|
compiledStyles: this.compiledStyles,
|
|
53
75
|
sessions: this.sessions,
|
|
@@ -57,7 +79,7 @@ export default class Blog {
|
|
|
57
79
|
reloadStylesOnGET: this.reloadStylesOnGET,
|
|
58
80
|
};
|
|
59
81
|
|
|
60
|
-
return JSON.parse(JSON.stringify(json));
|
|
82
|
+
return JSON.parse(JSON.stringify(json)); // make json read-only
|
|
61
83
|
}
|
|
62
84
|
|
|
63
85
|
// Private fields
|
|
@@ -71,7 +93,7 @@ export default class Blog {
|
|
|
71
93
|
#articles = [];
|
|
72
94
|
#styles = "";
|
|
73
95
|
#stylesHash = "";
|
|
74
|
-
|
|
96
|
+
#scriptsHash = "";
|
|
75
97
|
#stylesheetPath = "";
|
|
76
98
|
compilestyle = false;
|
|
77
99
|
#initPromise = null;
|
|
@@ -111,7 +133,10 @@ export default class Blog {
|
|
|
111
133
|
*/
|
|
112
134
|
setDatabaseAdapter(adapter) {
|
|
113
135
|
if (!this.#databaseModel) {
|
|
114
|
-
|
|
136
|
+
if (this.database.type === "file") {
|
|
137
|
+
const adapter = new FileAdapter(this.database);
|
|
138
|
+
this.#databaseModel = new DatabaseModel(adapter);
|
|
139
|
+
}
|
|
115
140
|
}
|
|
116
141
|
this.#databaseModel.setDatabaseAdapter(adapter);
|
|
117
142
|
}
|
|
@@ -136,7 +161,7 @@ export default class Blog {
|
|
|
136
161
|
}
|
|
137
162
|
|
|
138
163
|
addArticle(article) {
|
|
139
|
-
this.#articles.
|
|
164
|
+
this.#articles.insert(article);
|
|
140
165
|
}
|
|
141
166
|
|
|
142
167
|
#isAuthenticated(req) {
|
|
@@ -146,6 +171,7 @@ export default class Blog {
|
|
|
146
171
|
}
|
|
147
172
|
|
|
148
173
|
async #handleLogin(req, res) {
|
|
174
|
+
debug("handle login");
|
|
149
175
|
const body = await new Promise((resolve, reject) => {
|
|
150
176
|
let data = "";
|
|
151
177
|
req.on("data", (chunk) => (data += chunk.toString()));
|
|
@@ -166,17 +192,18 @@ export default class Blog {
|
|
|
166
192
|
res.writeHead(401, { "Content-Type": "text/html" });
|
|
167
193
|
res.end(`${header("My Blog")}
|
|
168
194
|
<body>
|
|
169
|
-
<h1>Unauthorized</h1><p>Please enter the password.<form method="POST">
|
|
195
|
+
<h1>Unauthorized</h1><div class="box"><p>Please enter the password.<form method="POST">
|
|
170
196
|
<input type="password" name="password" placeholder="Password" />
|
|
171
|
-
<button style="margin: 2px;">Login</button></form>
|
|
197
|
+
<button style="margin: 2px;">Login</button></form></div>
|
|
172
198
|
</body></html>`);
|
|
173
199
|
}
|
|
174
200
|
}
|
|
175
201
|
|
|
176
202
|
#handleLogout(req, res) {
|
|
203
|
+
debug("handle logout");
|
|
177
204
|
if (req.headers.cookie) {
|
|
178
205
|
const params = new URLSearchParams(
|
|
179
|
-
req.headers.cookie.replace(/; /g, "&")
|
|
206
|
+
req.headers.cookie.replace(/; /g, "&"),
|
|
180
207
|
);
|
|
181
208
|
const sessionId = params.get("session");
|
|
182
209
|
if (this.sessions.has(sessionId)) {
|
|
@@ -194,105 +221,134 @@ export default class Blog {
|
|
|
194
221
|
async init() {
|
|
195
222
|
if (this.#initPromise) return this.#initPromise;
|
|
196
223
|
this.#initPromise = (async () => {
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
224
|
+
//this.loadStyles();
|
|
225
|
+
//this.loadScripts();
|
|
226
|
+
// if there is a stylesheet path provided, process it
|
|
227
|
+
if (this.#stylesheetPath != null && this.compilestyle) {
|
|
228
|
+
// read file from stylesheet path, compare checksums and write to public/styles.min.css
|
|
229
|
+
await this.#processStylesheets(this.#stylesheetPath);
|
|
230
|
+
}
|
|
231
|
+
if (!this.#stylesheetPath) {
|
|
232
|
+
// this.#styles
|
|
233
|
+
// src/styles.css
|
|
234
|
+
// compile and merge hardcoded styles in "this.#styles" with "src/styles.css" and write to file "styles.min.css"
|
|
235
|
+
// which will be imported by webbrowser via '<link rel="stylesheet" href="styles.min.css"...'
|
|
209
236
|
|
|
237
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
238
|
+
const __dirname = path.dirname(__filename);
|
|
239
|
+
const srcStylePath = path.join(__dirname, "src", "styles.css");
|
|
240
|
+
const publicStylePath = path.join(
|
|
241
|
+
process.cwd(),
|
|
242
|
+
"public",
|
|
243
|
+
"styles.min.css",
|
|
244
|
+
);
|
|
245
|
+
|
|
246
|
+
let publicHash = null;
|
|
247
|
+
let srcStyles = "";
|
|
248
|
+
|
|
249
|
+
await Promise.all([
|
|
250
|
+
fs.promises
|
|
251
|
+
.readFile(publicStylePath, "utf8")
|
|
252
|
+
.then((publicCSS) => {
|
|
253
|
+
const match = publicCSS.match(
|
|
254
|
+
/\/\* source-hash: ([a-f0-9]{64}) \*\//,
|
|
255
|
+
);
|
|
256
|
+
if (match) publicHash = match[1];
|
|
257
|
+
})
|
|
258
|
+
.catch((err) => console.error(err)), // public/styles.min.css doesn't exist, will be created.
|
|
259
|
+
fs.promises
|
|
260
|
+
.readFile(srcStylePath, "utf8")
|
|
261
|
+
.then((content) => {
|
|
262
|
+
srcStyles = content;
|
|
263
|
+
})
|
|
264
|
+
.catch((err) => {
|
|
265
|
+
if (err.code !== "ENOENT") console.error(err);
|
|
266
|
+
}), // ignore if src/styles.css doesn't exist
|
|
267
|
+
]);
|
|
268
|
+
|
|
269
|
+
const combinedStyles = this.#styles + srcStyles;
|
|
270
|
+
const srcHash = crypto
|
|
271
|
+
.createHash("sha256")
|
|
272
|
+
.update(combinedStyles)
|
|
273
|
+
.digest("hex");
|
|
274
|
+
|
|
275
|
+
if (srcHash !== publicHash && this.compilestyle) {
|
|
276
|
+
console.log("Styles have changed. Recompiling...");
|
|
277
|
+
const finalStyles = await mergeStyles(this.#styles, srcStyles);
|
|
278
|
+
try {
|
|
279
|
+
await fs.promises.mkdir(path.dirname(publicStylePath), {
|
|
280
|
+
recursive: true,
|
|
281
|
+
});
|
|
282
|
+
await fs.promises.writeFile(
|
|
283
|
+
publicStylePath,
|
|
284
|
+
finalStyles + `\n/* source-hash: ${srcHash} */`,
|
|
285
|
+
);
|
|
286
|
+
} catch (err) {
|
|
287
|
+
console.error("Failed to write styles to public folder:", err);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Process Scripts
|
|
210
293
|
const __filename = fileURLToPath(import.meta.url);
|
|
211
294
|
const __dirname = path.dirname(__filename);
|
|
212
|
-
const
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
let publicHash = null;
|
|
216
|
-
let srcStyles = "";
|
|
217
|
-
|
|
218
|
-
await Promise.all([
|
|
219
|
-
fs.promises
|
|
220
|
-
.readFile(publicStylePath, "utf8")
|
|
221
|
-
.then((publicCSS) => {
|
|
222
|
-
const match = publicCSS.match(
|
|
223
|
-
/\/\* source-hash: ([a-f0-9]{64}) \*\//
|
|
224
|
-
);
|
|
225
|
-
if (match) publicHash = match[1];
|
|
226
|
-
})
|
|
227
|
-
.catch((err) => console.error(err)), // public/styles.min.css doesn't exist, will be created.
|
|
228
|
-
fs.promises
|
|
229
|
-
.readFile(srcStylePath, "utf8")
|
|
230
|
-
.then((content) => {
|
|
231
|
-
srcStyles = content;
|
|
232
|
-
})
|
|
233
|
-
.catch((err) => console.error(err)), // ignore if src/styles.css doesn't exist
|
|
234
|
-
]);
|
|
235
|
-
|
|
236
|
-
const combinedStyles = this.#styles + srcStyles;
|
|
237
|
-
const srcHash = crypto
|
|
238
|
-
.createHash("sha256")
|
|
239
|
-
.update(combinedStyles)
|
|
240
|
-
.digest("hex");
|
|
295
|
+
const srcScriptPath = path.join(__dirname, "src", "fetchData.js");
|
|
296
|
+
await this.#processScripts(srcScriptPath);
|
|
241
297
|
|
|
242
|
-
if (
|
|
243
|
-
console.log("
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
298
|
+
if (this.#isExternalAPI) {
|
|
299
|
+
console.log("external API");
|
|
300
|
+
await this.#loadFromAPI();
|
|
301
|
+
} else {
|
|
302
|
+
console.log(`database: ${this.database.type}`);
|
|
303
|
+
if (!this.#databaseModel) {
|
|
304
|
+
if (this.database.type === "file") {
|
|
305
|
+
const adapter = new FileAdapter(this.database);
|
|
306
|
+
this.#databaseModel = new DatabaseModel(adapter);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
console.log(`connected to database`);
|
|
310
|
+
await this.#databaseModel.initialize();
|
|
311
|
+
this.#articles.setDatabase(this.#databaseModel);
|
|
312
|
+
const [dbTitle, dbArticles] = await Promise.all([
|
|
313
|
+
this.#databaseModel.getBlogTitle(),
|
|
314
|
+
this.#databaseModel.findAll(),
|
|
315
|
+
]);
|
|
316
|
+
|
|
317
|
+
debug("dbArticles.size(): %d", dbArticles.length);
|
|
318
|
+
//log(filename, "dbArticles.size(): " + dbArticles.length);
|
|
319
|
+
//log(filename, "dbArticles.size(): " + dbArticles.length, "Blog.js");
|
|
320
|
+
debug("all articles in Blog after loading from db");
|
|
321
|
+
|
|
322
|
+
// Displays a beautiful table in the console
|
|
323
|
+
//table(dbArticles)
|
|
324
|
+
|
|
325
|
+
if (dbArticles.length == 0) {
|
|
326
|
+
dbArticles.push(
|
|
327
|
+
new Article(
|
|
328
|
+
1,
|
|
329
|
+
"Sample Entry #1",
|
|
330
|
+
"Prow scuttle parrel provost Sail ho shrouds spirits boom mizzenmast yardarm. Pinnace holystone mizzenmast quarter crow's nest nipperkin grog yardarm hempen halter furl. Swab barque interloper chantey doubloon starboard grog black jack gangway rutters.",
|
|
331
|
+
new Date(),
|
|
332
|
+
),
|
|
333
|
+
);
|
|
334
|
+
dbArticles.push(
|
|
335
|
+
new Article(
|
|
336
|
+
2,
|
|
337
|
+
"Sample Entry #2",
|
|
338
|
+
"Deadlights jack lad schooner scallywag dance the hempen jig carouser broadside cable strike colors. Bring a spring upon her cable holystone blow the man down spanker Shiver me timbers to go on account lookout wherry doubloon chase. Belay yo-ho-ho keelhaul squiffy black spot yardarm spyglass sheet transom heave to.",
|
|
339
|
+
new Date(),
|
|
340
|
+
),
|
|
250
341
|
);
|
|
251
|
-
} catch (err) {
|
|
252
|
-
console.error("Failed to write styles to public folder:", err);
|
|
253
342
|
}
|
|
343
|
+
if (this.reloadStylesOnGET)
|
|
344
|
+
console.log("reload scripts and styles on GET-Request");
|
|
345
|
+
let title = "";
|
|
346
|
+
if (this.#title != null && this.#title.length > 0)
|
|
347
|
+
title = this.#title; // use blog title if set
|
|
348
|
+
else title = dbTitle; // use title from the database
|
|
349
|
+
const responseData = { title: title, articles: dbArticles };
|
|
350
|
+
this.#applyBlogData(responseData);
|
|
254
351
|
}
|
|
255
|
-
}
|
|
256
|
-
if (this.#isExternalAPI) {
|
|
257
|
-
console.log("external API");
|
|
258
|
-
await this.#loadFromAPI();
|
|
259
|
-
} else {
|
|
260
|
-
console.log(`database: ${this.database.type}`);
|
|
261
|
-
if (!this.#databaseModel) {
|
|
262
|
-
this.#databaseModel = new DatabaseModel(this.database);
|
|
263
|
-
}
|
|
264
|
-
console.log(`connected to database`);
|
|
265
|
-
await this.#databaseModel.initialize();
|
|
266
|
-
const [dbTitle, dbArticles] = await Promise.all([
|
|
267
|
-
this.#databaseModel.getBlogTitle(),
|
|
268
|
-
this.#databaseModel.findAll(),
|
|
269
|
-
]);
|
|
270
|
-
|
|
271
|
-
if (dbArticles.length == 0) {
|
|
272
|
-
dbArticles.push(
|
|
273
|
-
new Article(
|
|
274
|
-
"Sample Entry #1",
|
|
275
|
-
"Prow scuttle parrel provost Sail ho shrouds spirits boom mizzenmast yardarm. Pinnace holystone mizzenmast quarter crow's nest nipperkin grog yardarm hempen halter furl. Swab barque interloper chantey doubloon starboard grog black jack gangway rutters.",
|
|
276
|
-
new Date()
|
|
277
|
-
)
|
|
278
|
-
);
|
|
279
|
-
dbArticles.push(
|
|
280
|
-
new Article(
|
|
281
|
-
"Sample Entry #2",
|
|
282
|
-
"Deadlights jack lad schooner scallywag dance the hempen jig carouser broadside cable strike colors. Bring a spring upon her cable holystone blow the man down spanker Shiver me timbers to go on account lookout wherry doubloon chase. Belay yo-ho-ho keelhaul squiffy black spot yardarm spyglass sheet transom heave to.",
|
|
283
|
-
new Date()
|
|
284
|
-
)
|
|
285
|
-
);
|
|
286
|
-
}
|
|
287
|
-
if (this.reloadStylesOnGET)
|
|
288
|
-
console.log("reload scripts and styles on GET-Request");
|
|
289
|
-
let title = "";
|
|
290
|
-
if (this.#title != null && this.#title.length > 0)
|
|
291
|
-
title = this.#title; // use blog title if set
|
|
292
|
-
else title = dbTitle; // use title from the database
|
|
293
|
-
const responseData = { title: title, articles: dbArticles };
|
|
294
|
-
this.#applyBlogData(responseData);
|
|
295
|
-
}
|
|
296
352
|
})();
|
|
297
353
|
return this.#initPromise;
|
|
298
354
|
}
|
|
@@ -322,12 +378,10 @@ export default class Blog {
|
|
|
322
378
|
promises.push(postData(this.#apiUrl, newArticleData));
|
|
323
379
|
await Promise.all(promises);
|
|
324
380
|
// Add the article to the local list for immediate display
|
|
325
|
-
this.#articles.
|
|
381
|
+
this.#articles.insert(Article.createNew(title, content));
|
|
326
382
|
// remove sample entries
|
|
327
|
-
this.#articles
|
|
328
|
-
|
|
329
|
-
art.title !== "Sample Entry #1" && art.title !== "Sample Entry #2"
|
|
330
|
-
);
|
|
383
|
+
this.#articles.remove(1); // "Sample Entry #1"
|
|
384
|
+
this.#articles.remove(2); // "Sample Entry #2"
|
|
331
385
|
} catch (error) {
|
|
332
386
|
console.error("Failed to post new article to API:", error);
|
|
333
387
|
}
|
|
@@ -343,6 +397,7 @@ export default class Blog {
|
|
|
343
397
|
await this.init();
|
|
344
398
|
|
|
345
399
|
const server = http.createServer(async (req, res) => {
|
|
400
|
+
//debug("query %s", req.url);
|
|
346
401
|
// API routes
|
|
347
402
|
if (req.url.startsWith("/api")) {
|
|
348
403
|
await this.#jsonAPI(req, res);
|
|
@@ -355,9 +410,9 @@ export default class Blog {
|
|
|
355
410
|
res.writeHead(200, { "Content-Type": "text/html" });
|
|
356
411
|
res.end(`${header("My Blog")}
|
|
357
412
|
<body>
|
|
358
|
-
<h1>Login</h1><form method="POST">
|
|
413
|
+
<h1>Login</h1><div class="box"><form method="POST">
|
|
359
414
|
<input type="password" name="password" placeholder="Password" />
|
|
360
|
-
<button style="margin: 2px;">Login</button></form>
|
|
415
|
+
<button style="margin: 2px;">Login</button></form></div>
|
|
361
416
|
</body></html>`);
|
|
362
417
|
return;
|
|
363
418
|
} else if (req.method === "POST") {
|
|
@@ -387,6 +442,10 @@ export default class Blog {
|
|
|
387
442
|
if (this.#stylesheetPath != null && this.compilestyle) {
|
|
388
443
|
await this.#processStylesheets(this.#stylesheetPath);
|
|
389
444
|
}
|
|
445
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
446
|
+
const __dirname = path.dirname(__filename);
|
|
447
|
+
const srcScriptPath = path.join(__dirname, "src", "fetchData.js");
|
|
448
|
+
await this.#processScripts(srcScriptPath);
|
|
390
449
|
}
|
|
391
450
|
|
|
392
451
|
let loggedin = false;
|
|
@@ -410,12 +469,10 @@ export default class Blog {
|
|
|
410
469
|
} else {
|
|
411
470
|
// Try to serve static files from public folder
|
|
412
471
|
try {
|
|
413
|
-
const
|
|
414
|
-
const __dirname = path.dirname(__filename);
|
|
415
|
-
const publicDir = path.join(__dirname, "public");
|
|
472
|
+
const publicDir = path.join(process.cwd(), "public");
|
|
416
473
|
const parsedUrl = new URL(
|
|
417
474
|
req.url,
|
|
418
|
-
`http://${req.headers.host || "localhost"}
|
|
475
|
+
`http://${req.headers.host || "localhost"}`,
|
|
419
476
|
);
|
|
420
477
|
const filePath = path.join(publicDir, parsedUrl.pathname);
|
|
421
478
|
|
|
@@ -441,8 +498,11 @@ export default class Blog {
|
|
|
441
498
|
}
|
|
442
499
|
}
|
|
443
500
|
} catch (err) {
|
|
444
|
-
|
|
445
|
-
//
|
|
501
|
+
// ENOENT (file not found) is expected, leading to a 404.
|
|
502
|
+
// Only log other, unexpected errors.
|
|
503
|
+
if (err.code !== "ENOENT") {
|
|
504
|
+
console.error(err);
|
|
505
|
+
}
|
|
446
506
|
}
|
|
447
507
|
|
|
448
508
|
// Error 404
|
|
@@ -456,7 +516,8 @@ export default class Blog {
|
|
|
456
516
|
return new Promise((resolve, reject) => {
|
|
457
517
|
const errorHandler = (err) => reject(err);
|
|
458
518
|
this.#server.once("error", errorHandler);
|
|
459
|
-
this.#server.listen(port, "0.0.0.0", () => {
|
|
519
|
+
this.#server.listen(port, "0.0.0.0", () => {
|
|
520
|
+
// <-- for docker 0.0.0.0, localhost 127.0.0.1
|
|
460
521
|
this.#server.removeListener("error", errorHandler);
|
|
461
522
|
console.log(`server running at http://localhost:${port}/`);
|
|
462
523
|
resolve(); // Resolve the promise when the server is listening
|
|
@@ -482,18 +543,42 @@ export default class Blog {
|
|
|
482
543
|
|
|
483
544
|
/** Populates the blog's title and articles from a data object. */
|
|
484
545
|
#applyBlogData(data) {
|
|
485
|
-
|
|
546
|
+
debug("applyBlogData");
|
|
547
|
+
if (this.#articles.storage.clear) {
|
|
548
|
+
this.#articles.storage.clear();
|
|
549
|
+
} else {
|
|
550
|
+
//this.#articles.setDataModel(this.#makeDataModel()); // Fallback if clear() isn't implemented
|
|
551
|
+
}
|
|
486
552
|
this.#title = data.title;
|
|
487
553
|
// Assuming data contains a title and an array of articles with title and content
|
|
488
|
-
if (data.articles && Array.isArray(data.articles)) {
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
554
|
+
if (this.#articles && data.articles && Array.isArray(data.articles)) {
|
|
555
|
+
if (this.#articles.getStorageName().includes("BinarySearchTree")) {
|
|
556
|
+
debug("using special insert method for BST");
|
|
557
|
+
const insertBalanced = (start, end) => {
|
|
558
|
+
if (start > end) return;
|
|
559
|
+
const mid = Math.floor((start + end) / 2);
|
|
560
|
+
const articleData = data.articles[mid];
|
|
561
|
+
const article = new Article(
|
|
562
|
+
articleData.id,
|
|
563
|
+
articleData.title,
|
|
564
|
+
articleData.content,
|
|
565
|
+
articleData.createdAt,
|
|
566
|
+
);
|
|
567
|
+
this.addArticle(article);
|
|
568
|
+
insertBalanced(start, mid - 1);
|
|
569
|
+
insertBalanced(mid + 1, end);
|
|
570
|
+
};
|
|
571
|
+
insertBalanced(0, data.articles.length - 1);
|
|
572
|
+
} else {
|
|
573
|
+
for (const articleData of data.articles) {
|
|
574
|
+
const article = new Article(
|
|
575
|
+
articleData.id,
|
|
576
|
+
articleData.title,
|
|
577
|
+
articleData.content,
|
|
578
|
+
articleData.createdAt,
|
|
579
|
+
);
|
|
580
|
+
this.addArticle(article);
|
|
581
|
+
}
|
|
497
582
|
}
|
|
498
583
|
}
|
|
499
584
|
}
|
|
@@ -519,22 +604,31 @@ export default class Blog {
|
|
|
519
604
|
};
|
|
520
605
|
res.end(JSON.stringify(data));
|
|
521
606
|
}
|
|
607
|
+
// Search
|
|
608
|
+
if (url.searchParams.has("q")) {
|
|
609
|
+
const query = url.searchParams.get("q");
|
|
610
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
611
|
+
const results = this.#articles.search(query);
|
|
612
|
+
res.end(JSON.stringify({ articles: results }));
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
522
615
|
// GET all blog data
|
|
523
616
|
if (pathname === "/api/articles") {
|
|
524
617
|
// Use 'offset' param as startId (filter) to get items starting at ID
|
|
525
|
-
const
|
|
526
|
-
const
|
|
527
|
-
|
|
528
|
-
const
|
|
529
|
-
const
|
|
618
|
+
const pLimit = parseInt(url.searchParams.get("limit"));
|
|
619
|
+
const limit = !isNaN(pLimit) ? pLimit : null;
|
|
620
|
+
|
|
621
|
+
const start = url.searchParams.get("startdate");
|
|
622
|
+
const end = url.searchParams.get("enddate");
|
|
623
|
+
//const parsedStart = parseDateParam(qStartdate);
|
|
624
|
+
//const parsedEnd = parseDateParam(qEnddate, true);
|
|
625
|
+
|
|
626
|
+
//const effectiveStart = parsedStart !== null ? parsedStart : startID;
|
|
627
|
+
//const effectiveEnd = parsedEnd !== null ? parsedEnd : endID;
|
|
628
|
+
|
|
530
629
|
// controller
|
|
531
630
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
532
|
-
const dbArticles = await this.#
|
|
533
|
-
limit,
|
|
534
|
-
0,
|
|
535
|
-
startID,
|
|
536
|
-
endID
|
|
537
|
-
);
|
|
631
|
+
const dbArticles = await this.#articles.findAll(start, end, limit);
|
|
538
632
|
const responseData = {
|
|
539
633
|
title: this.title, // Keep the title from the original constant
|
|
540
634
|
articles: dbArticles,
|
|
@@ -561,6 +655,40 @@ export default class Blog {
|
|
|
561
655
|
// extern
|
|
562
656
|
res.writeHead(201, { "Content-Type": "application/json" });
|
|
563
657
|
res.end(JSON.stringify(newArticle));
|
|
658
|
+
} else if (req.method === "DELETE" && pathname === "/api/articles") {
|
|
659
|
+
if (!this.#isAuthenticated(req)) {
|
|
660
|
+
res.writeHead(403, { "Content-Type": "application/json" });
|
|
661
|
+
res.end(JSON.stringify({ error: "Forbidden" }));
|
|
662
|
+
return;
|
|
663
|
+
}
|
|
664
|
+
const id = url.searchParams.get("id");
|
|
665
|
+
if (id) {
|
|
666
|
+
this.#articles.remove(parseInt(id));
|
|
667
|
+
if (this.#databaseModel) {
|
|
668
|
+
await this.#databaseModel.remove(parseInt(id));
|
|
669
|
+
}
|
|
670
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
671
|
+
res.end(JSON.stringify({ status: "deleted", id }));
|
|
672
|
+
}
|
|
673
|
+
} else if (req.method === "PUT" && pathname === "/api/articles") {
|
|
674
|
+
if (!this.#isAuthenticated(req)) {
|
|
675
|
+
res.writeHead(403, { "Content-Type": "application/json" });
|
|
676
|
+
res.end(JSON.stringify({ error: "Forbidden" }));
|
|
677
|
+
return;
|
|
678
|
+
}
|
|
679
|
+
const id = url.searchParams.get("id");
|
|
680
|
+
const body = await new Promise((resolve) => {
|
|
681
|
+
let data = "";
|
|
682
|
+
req.on("data", (chunk) => (data += chunk));
|
|
683
|
+
req.on("end", () => resolve(data));
|
|
684
|
+
});
|
|
685
|
+
const { title, content } = JSON.parse(body);
|
|
686
|
+
this.#articles.update(parseInt(id), title, content);
|
|
687
|
+
if (this.#databaseModel) {
|
|
688
|
+
await this.#databaseModel.update(parseInt(id), { title, content });
|
|
689
|
+
}
|
|
690
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
691
|
+
res.end(JSON.stringify({ status: "updated", id }));
|
|
564
692
|
}
|
|
565
693
|
}
|
|
566
694
|
|
|
@@ -574,7 +702,7 @@ export default class Blog {
|
|
|
574
702
|
print() {
|
|
575
703
|
const data = {
|
|
576
704
|
title: this.title,
|
|
577
|
-
articles: this.#articles,
|
|
705
|
+
articles: this.#articles.getAllArticles(),
|
|
578
706
|
};
|
|
579
707
|
const markdown = formatMarkdown(data);
|
|
580
708
|
console.log(markdown);
|
|
@@ -582,9 +710,13 @@ export default class Blog {
|
|
|
582
710
|
|
|
583
711
|
/** render this blog content to valid html */
|
|
584
712
|
async toHTML(loggedin) {
|
|
713
|
+
const articles = this.#articles.getAllArticles();
|
|
714
|
+
const articles_after = articles.slice(0, 50);
|
|
715
|
+
// prettier-ignore
|
|
716
|
+
debug("slice articles from %d to %d", articles.length, articles_after.length);
|
|
585
717
|
const data = {
|
|
586
718
|
title: this.title,
|
|
587
|
-
articles:
|
|
719
|
+
articles: articles_after,
|
|
588
720
|
loggedin,
|
|
589
721
|
login: "",
|
|
590
722
|
};
|
|
@@ -592,6 +724,12 @@ export default class Blog {
|
|
|
592
724
|
if (loggedin) data.login = `<a href="/logout">logout</a>`;
|
|
593
725
|
else data.login = `<a href="/login">login</a>`;
|
|
594
726
|
|
|
727
|
+
//debug("typeof data: %o", typeof data);
|
|
728
|
+
//debug("typeof data.articles: %o", typeof data.articles);
|
|
729
|
+
//debug("typeof data.articles: %O", data.articles);
|
|
730
|
+
const article = data.articles[0];
|
|
731
|
+
debug("first article: ");
|
|
732
|
+
debug("%d %s %s", article.id, article.title, article.getContentVeryShort());
|
|
595
733
|
const html = formatHTML(data);
|
|
596
734
|
if (validate(html)) return html;
|
|
597
735
|
throw new Error("Error. Invalid HTML!");
|
|
@@ -612,7 +750,7 @@ export default class Blog {
|
|
|
612
750
|
(f) =>
|
|
613
751
|
typeof f === "string" &&
|
|
614
752
|
(f.endsWith(".scss") || f.endsWith(".css")) &&
|
|
615
|
-
!f.endsWith(".min.css")
|
|
753
|
+
!f.endsWith(".min.css"),
|
|
616
754
|
);
|
|
617
755
|
//const scriptFiles = files.filter((f) => f.endsWith(".js") && !f.endsWith(".min.js"));
|
|
618
756
|
|
|
@@ -624,7 +762,7 @@ export default class Blog {
|
|
|
624
762
|
const content = await fs.promises.readFile(f, "utf-8");
|
|
625
763
|
if (content == "") throw new Error("Invalid Filepath or empty file!");
|
|
626
764
|
return { path: f, content };
|
|
627
|
-
})
|
|
765
|
+
}),
|
|
628
766
|
);
|
|
629
767
|
|
|
630
768
|
// compute hash
|
|
@@ -633,9 +771,9 @@ export default class Blog {
|
|
|
633
771
|
.update(
|
|
634
772
|
fileData
|
|
635
773
|
.map((f) =>
|
|
636
|
-
crypto.createHash("sha256").update(f.content).digest("hex")
|
|
774
|
+
crypto.createHash("sha256").update(f.content).digest("hex"),
|
|
637
775
|
)
|
|
638
|
-
.join("")
|
|
776
|
+
.join(""),
|
|
639
777
|
)
|
|
640
778
|
.digest("hex");
|
|
641
779
|
|
|
@@ -648,17 +786,78 @@ export default class Blog {
|
|
|
648
786
|
this.compiledStyles = await compileStyles(fileData);
|
|
649
787
|
|
|
650
788
|
// generate a file
|
|
651
|
-
const
|
|
652
|
-
const __dirname = path.dirname(__filename);
|
|
653
|
-
const publicDir = path.join(__dirname, "public");
|
|
789
|
+
const publicDir = path.join(process.cwd(), "public");
|
|
654
790
|
|
|
791
|
+
await fs.promises.mkdir(publicDir, { recursive: true });
|
|
655
792
|
await fs.promises.writeFile(
|
|
656
793
|
path.join(publicDir, "styles.min.css"),
|
|
657
|
-
this.compiledStyles + `\n/* source-hash: ${currentHash}
|
|
794
|
+
this.compiledStyles + `\n/* source-hash: ${currentHash} */`,
|
|
658
795
|
);
|
|
659
796
|
} else {
|
|
660
797
|
console.log("styles are up-to-date");
|
|
661
798
|
}
|
|
662
799
|
}
|
|
663
800
|
}
|
|
801
|
+
|
|
802
|
+
/**
|
|
803
|
+
* read files, compare checksums, compile and write to public/scripts.min.js
|
|
804
|
+
* @param {string|string[]} files - File path(s) to process.
|
|
805
|
+
*/
|
|
806
|
+
async #processScripts(files) {
|
|
807
|
+
// Normalize input to array
|
|
808
|
+
const fileList = Array.isArray(files) ? files : [files];
|
|
809
|
+
const scriptFiles = fileList.filter(
|
|
810
|
+
(f) =>
|
|
811
|
+
typeof f === "string" && f.endsWith(".js") && !f.endsWith(".min.js"),
|
|
812
|
+
);
|
|
813
|
+
|
|
814
|
+
if (scriptFiles.length > 0) {
|
|
815
|
+
const fileData = await Promise.all(
|
|
816
|
+
scriptFiles.map(async (f) => {
|
|
817
|
+
const content = await fs.promises.readFile(f, "utf-8");
|
|
818
|
+
if (content == "") throw new Error("Invalid Filepath or empty file!");
|
|
819
|
+
return { path: f, content };
|
|
820
|
+
}),
|
|
821
|
+
);
|
|
822
|
+
|
|
823
|
+
const currentHash = crypto
|
|
824
|
+
.createHash("sha256")
|
|
825
|
+
.update(
|
|
826
|
+
fileData
|
|
827
|
+
.map((f) =>
|
|
828
|
+
crypto.createHash("sha256").update(f.content).digest("hex"),
|
|
829
|
+
)
|
|
830
|
+
.join(""),
|
|
831
|
+
)
|
|
832
|
+
.digest("hex");
|
|
833
|
+
|
|
834
|
+
if (!this.#scriptsHash) {
|
|
835
|
+
try {
|
|
836
|
+
const publicDir = path.join(process.cwd(), "public");
|
|
837
|
+
const existing = await fs.promises.readFile(
|
|
838
|
+
path.join(publicDir, "scripts.min.js"),
|
|
839
|
+
"utf-8",
|
|
840
|
+
);
|
|
841
|
+
const match = existing.match(/\/\* source-hash: ([a-f0-9]{64}) \*\//);
|
|
842
|
+
if (match) this.#scriptsHash = match[1];
|
|
843
|
+
} catch (err) {
|
|
844
|
+
// ignore
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
if (currentHash !== this.#scriptsHash) {
|
|
849
|
+
console.log("Script assets have changed. Recompiling...");
|
|
850
|
+
this.#scriptsHash = currentHash;
|
|
851
|
+
|
|
852
|
+
const compiledScripts = await compileScripts(fileData);
|
|
853
|
+
|
|
854
|
+
const publicDir = path.join(process.cwd(), "public");
|
|
855
|
+
await fs.promises.mkdir(publicDir, { recursive: true });
|
|
856
|
+
await fs.promises.writeFile(
|
|
857
|
+
path.join(publicDir, "scripts.min.js"),
|
|
858
|
+
compiledScripts + `\n/* source-hash: ${currentHash} */`,
|
|
859
|
+
);
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
}
|
|
664
863
|
}
|