@lexho111/plainblog 0.3.8 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Article.js +2 -0
- package/Blog.js +109 -31
- package/Formatter.js +12 -2
- package/blog.db +0 -0
- package/blog.db-journal +0 -0
- package/build-styles.js +4 -32
- package/lexho111-plainblog-0.4.1.tgz +0 -0
- package/model/APIModel.js +16 -20
- package/model/DatabaseModel.js +19 -4
- package/package.json +8 -7
- package/scripts.min.js +1 -2
- package/src/loader.js +94 -0
- package/src/styles.css +48 -0
- package/styles.min.css +1 -2
- package/test/server.test.js +14 -2
- package/test/styles.test.js +107 -0
- package/test/stylesheets/styles.css +29 -0
- package/test/stylesheets/styles.scss +34 -0
- package/test_1767173763155.db +0 -0
- package/test_1767260493607.db +0 -0
- package/test_1767281040439.db +0 -0
- package/test_1767281442334.db +0 -0
- package/test_1767286038587.db +0 -0
- package/test_1767286127364.db +0 -0
- package/test_1767286366239.db +0 -0
- package/test_1767286503638.db +0 -0
- package/test_1767286637739.db +0 -0
- package/test_1767292219862.db +0 -0
- package/test_1767292355190.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/Article.js
CHANGED
|
@@ -27,10 +27,12 @@ export default class Article {
|
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
toHTML() {
|
|
30
|
+
const moreButton = this.content.length > 400 ? `<a href="#">more</a>` : "";
|
|
30
31
|
return ` <article>
|
|
31
32
|
<h2>${this.title}</h2>
|
|
32
33
|
<span class="datetime">${this.getFormattedDate()}</span>
|
|
33
34
|
<p>${this.getContentShort()}</p>
|
|
35
|
+
${moreButton}
|
|
34
36
|
</article>`;
|
|
35
37
|
}
|
|
36
38
|
}
|
package/Blog.js
CHANGED
|
@@ -42,7 +42,7 @@ export default class Blog {
|
|
|
42
42
|
console.log(`version: ${version}`);
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
loadScripts() {
|
|
45
|
+
/*loadScripts() {
|
|
46
46
|
const __filename = fileURLToPath(import.meta.url);
|
|
47
47
|
const __dirname = path.dirname(__filename);
|
|
48
48
|
try {
|
|
@@ -62,7 +62,7 @@ export default class Blog {
|
|
|
62
62
|
this.compiledScripts = "";
|
|
63
63
|
this.#scriptsHash = "";
|
|
64
64
|
}
|
|
65
|
-
}
|
|
65
|
+
}*/
|
|
66
66
|
|
|
67
67
|
loadStyles() {
|
|
68
68
|
console.log("load styles")
|
|
@@ -87,6 +87,18 @@ export default class Blog {
|
|
|
87
87
|
}
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
+
loadScripts() {
|
|
91
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
92
|
+
const __dirname = path.dirname(__filename);
|
|
93
|
+
try {
|
|
94
|
+
this.scripts = fs.readFileSync(path.join(__dirname, "scripts.min.js"), "utf-8");
|
|
95
|
+
//console.log(this.scripts)
|
|
96
|
+
} catch (err) {
|
|
97
|
+
console.error(err);
|
|
98
|
+
this.scripts = "";
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
90
102
|
// Private fields
|
|
91
103
|
#server = null;
|
|
92
104
|
#password = null;
|
|
@@ -188,8 +200,9 @@ export default class Blog {
|
|
|
188
200
|
this.loadStyles();
|
|
189
201
|
this.loadScripts();
|
|
190
202
|
//const assetFiles = await this.#findAssetFiles();
|
|
191
|
-
if(this.assetFiles != null)
|
|
192
|
-
|
|
203
|
+
if(this.assetFiles != null) {
|
|
204
|
+
await this.processAssets(this.assetFiles);
|
|
205
|
+
}
|
|
193
206
|
if (this.#isExternalAPI) {
|
|
194
207
|
console.log("external API");
|
|
195
208
|
await this.loadFromAPI();
|
|
@@ -205,8 +218,8 @@ export default class Blog {
|
|
|
205
218
|
dbArticles.push(new Article("Sample Entry #1", "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.", new Date()));
|
|
206
219
|
dbArticles.push(new Article("Sample Entry #2", "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.", new Date()));
|
|
207
220
|
}
|
|
208
|
-
this.
|
|
209
|
-
if(this.
|
|
221
|
+
this.reloadStylesOnGET = false;
|
|
222
|
+
if(this.reloadStylesOnGET) console.log("reload scripts and styles on GET-Request");
|
|
210
223
|
let title = "";
|
|
211
224
|
if (this.#title != null && this.#title.length > 0) title = this.#title; // use blog title if set
|
|
212
225
|
else title = dbTitle; // use title from the database
|
|
@@ -298,9 +311,13 @@ export default class Blog {
|
|
|
298
311
|
// load articles
|
|
299
312
|
|
|
300
313
|
// reload styles and scripts on (every) request
|
|
301
|
-
if(this.
|
|
302
|
-
this.
|
|
303
|
-
|
|
314
|
+
if(this.reloadStylesOnGET) {
|
|
315
|
+
if (this.assetFiles) {
|
|
316
|
+
await this.processAssets(this.assetFiles);
|
|
317
|
+
} else {
|
|
318
|
+
this.loadStyles();
|
|
319
|
+
this.loadScripts();
|
|
320
|
+
}
|
|
304
321
|
}
|
|
305
322
|
|
|
306
323
|
let loggedin = false;
|
|
@@ -313,10 +330,11 @@ export default class Blog {
|
|
|
313
330
|
}
|
|
314
331
|
|
|
315
332
|
try {
|
|
316
|
-
const html = this.toHTML(loggedin); // render this blog to HTML
|
|
333
|
+
const html = await this.toHTML(loggedin); // render this blog to HTML
|
|
317
334
|
res.writeHead(200, { "Content-Type": "text/html; charset=UTF-8" });
|
|
318
335
|
res.end(html);
|
|
319
336
|
} catch (err) {
|
|
337
|
+
console.error(err);
|
|
320
338
|
res.writeHead(500, { "Content-Type": "text/plain" });
|
|
321
339
|
res.end("Internal Server Error");
|
|
322
340
|
}
|
|
@@ -358,7 +376,9 @@ export default class Blog {
|
|
|
358
376
|
// Assuming the API returns an array of objects with title and content
|
|
359
377
|
if (data.articles && Array.isArray(data.articles)) {
|
|
360
378
|
for (const articleData of data.articles) {
|
|
361
|
-
|
|
379
|
+
const article = new Article(articleData.title, articleData.content, articleData.createdAt);
|
|
380
|
+
article.id = articleData.id;
|
|
381
|
+
this.addArticle(article);
|
|
362
382
|
}
|
|
363
383
|
}
|
|
364
384
|
}
|
|
@@ -390,8 +410,11 @@ export default class Blog {
|
|
|
390
410
|
// controller
|
|
391
411
|
/** everything that happens in /api */
|
|
392
412
|
async #jsonAPI(req, res) {
|
|
413
|
+
const url = new URL(req.url, `http://${req.headers.host || "localhost"}`);
|
|
414
|
+
const pathname = url.pathname;
|
|
415
|
+
|
|
393
416
|
if(req.method === "GET") {
|
|
394
|
-
if(
|
|
417
|
+
if(pathname === "/api" || pathname === "/api/") {
|
|
395
418
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
396
419
|
const data = {
|
|
397
420
|
title: this.title
|
|
@@ -399,37 +422,42 @@ export default class Blog {
|
|
|
399
422
|
res.end(JSON.stringify(data));
|
|
400
423
|
}
|
|
401
424
|
// GET all blog data
|
|
402
|
-
if (
|
|
425
|
+
if (pathname === "/api/articles") {
|
|
426
|
+
// Use 'offset' param as startId (filter) to get items starting at ID
|
|
427
|
+
const pStartID = parseInt(url.searchParams.get("startID"));
|
|
428
|
+
const startID = !isNaN(pStartID) ? pStartID : null;
|
|
429
|
+
const pEndID = parseInt(url.searchParams.get("endID"));
|
|
430
|
+
const endID = !isNaN(pEndID) ? pEndID : null;
|
|
431
|
+
const limit = parseInt(url.searchParams.get("limit")) || 10;
|
|
403
432
|
// controller
|
|
404
433
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
405
|
-
const dbArticles = await this.#databaseModel.findAll();
|
|
434
|
+
const dbArticles = await this.#databaseModel.findAll(limit, 0, startID, endID);
|
|
406
435
|
const responseData = {
|
|
407
436
|
title: this.title, // Keep the title from the original constant
|
|
408
437
|
articles: dbArticles,
|
|
409
438
|
};
|
|
410
|
-
console.log(`responseData: ${responseData}`);
|
|
411
439
|
res.end(JSON.stringify(responseData));
|
|
412
440
|
}
|
|
413
441
|
|
|
414
442
|
// POST a new article
|
|
415
|
-
} else if (req.method === "POST" &&
|
|
443
|
+
} else if (req.method === "POST" && pathname === "/api/articles") {
|
|
416
444
|
if (!this.isAuthenticated(req)) {
|
|
417
445
|
res.writeHead(403, { "Content-Type": "application/json" });
|
|
418
446
|
res.end(JSON.stringify({ error: "Forbidden" }));
|
|
419
447
|
return;
|
|
420
448
|
}
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
// local
|
|
427
|
-
await this.#databaseModel.save(newArticle); // --> to api server
|
|
428
|
-
// extern
|
|
429
|
-
|
|
430
|
-
res.writeHead(201, { "Content-Type": "application/json" });
|
|
431
|
-
res.end(JSON.stringify(newArticle));
|
|
449
|
+
const body = await new Promise((resolve, reject) => {
|
|
450
|
+
let data = "";
|
|
451
|
+
req.on("data", (chunk) => (data += chunk.toString()));
|
|
452
|
+
req.on("end", () => resolve(data));
|
|
453
|
+
req.on("error", reject);
|
|
432
454
|
});
|
|
455
|
+
const newArticle = JSON.parse(body);
|
|
456
|
+
// local
|
|
457
|
+
await this.#databaseModel.save(newArticle); // --> to api server
|
|
458
|
+
// extern
|
|
459
|
+
res.writeHead(201, { "Content-Type": "application/json" });
|
|
460
|
+
res.end(JSON.stringify(newArticle));
|
|
433
461
|
}
|
|
434
462
|
}
|
|
435
463
|
|
|
@@ -449,8 +477,32 @@ export default class Blog {
|
|
|
449
477
|
console.log(markdown);
|
|
450
478
|
}
|
|
451
479
|
|
|
480
|
+
/*async isItFileOrArray(filesInput) {
|
|
481
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
482
|
+
const files = Array.isArray(filesInput) ? filesInput : [filesInput];
|
|
483
|
+
const fileData = await Promise.all(files.map(async (f) => {
|
|
484
|
+
const filepath = path.isAbsolute(f) ? f : path.join(__dirname, f);
|
|
485
|
+
const content = await fs.promises.readFile(filepath, "utf8");
|
|
486
|
+
return { path: filepath, content };
|
|
487
|
+
}));
|
|
488
|
+
return fileData;
|
|
489
|
+
}*/
|
|
490
|
+
|
|
491
|
+
/*computeHashsum(fileData) {
|
|
492
|
+
return crypto
|
|
493
|
+
.createHash("sha256")
|
|
494
|
+
.update(
|
|
495
|
+
fileData
|
|
496
|
+
.map((f) =>
|
|
497
|
+
crypto.createHash("sha256").update(f.content).digest("hex")
|
|
498
|
+
)
|
|
499
|
+
.join("")
|
|
500
|
+
)
|
|
501
|
+
.digest("hex");
|
|
502
|
+
}*/
|
|
503
|
+
|
|
452
504
|
/** render this blog content to valid html */
|
|
453
|
-
toHTML(loggedin) {
|
|
505
|
+
async toHTML(loggedin) {
|
|
454
506
|
const data = {
|
|
455
507
|
title: this.title,
|
|
456
508
|
articles: this.articles,
|
|
@@ -460,9 +512,32 @@ export default class Blog {
|
|
|
460
512
|
|
|
461
513
|
if(loggedin) data.login = `<a href="/logout">logout</a>`;
|
|
462
514
|
else data.login = `<a href="/login">login</a>`;
|
|
515
|
+
|
|
516
|
+
/*const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
517
|
+
const filepath = path.join(__dirname, "test/stylesheets/styles.css");
|
|
518
|
+
const fileData = await fs.promises.readFile(filepath, 'utf8');
|
|
519
|
+
console.log(fileData);
|
|
520
|
+
this.compiledStyles = fileData;*/
|
|
521
|
+
|
|
522
|
+
//await this.processAssets(this.assetFiles);
|
|
523
|
+
|
|
524
|
+
// is it file or array of file?
|
|
525
|
+
/*if (this.assetFiles) {
|
|
526
|
+
const fileData = await this.isItFileOrArray(this.assetFiles);
|
|
527
|
+
|
|
528
|
+
const currentHash = this.computeHashsum(fileData);
|
|
529
|
+
console.log(`currentHash: ${currentHash}`)
|
|
530
|
+
if(currentHash !== this.#stylesHash) {
|
|
531
|
+
console.log("Style assets have changed. Recompiling...");
|
|
532
|
+
this.#stylesHash = currentHash;
|
|
533
|
+
this.compiledStyles = await compileStyles(fileData);
|
|
534
|
+
}
|
|
535
|
+
}*/
|
|
536
|
+
|
|
537
|
+
this.loadScripts();
|
|
538
|
+
|
|
463
539
|
const finalStyles = this.styles + this.compiledStyles;
|
|
464
|
-
const
|
|
465
|
-
const html = formatHTML(data, finalScripts, finalStyles);
|
|
540
|
+
const html = formatHTML(data, this.scripts, finalStyles);
|
|
466
541
|
if (validate(html)) return html;
|
|
467
542
|
throw new Error("Error. Invalid HTML!");
|
|
468
543
|
}
|
|
@@ -487,6 +562,7 @@ export default class Blog {
|
|
|
487
562
|
* @param {string[]} files - Array of file paths to process.
|
|
488
563
|
*/
|
|
489
564
|
async processAssets(files) {
|
|
565
|
+
|
|
490
566
|
// Normalize input to array (handles string or array)
|
|
491
567
|
const fileList = Array.isArray(files) ? files : [files];
|
|
492
568
|
|
|
@@ -501,7 +577,8 @@ export default class Blog {
|
|
|
501
577
|
if (styleFiles.length > 0) {
|
|
502
578
|
const fileData = await Promise.all(
|
|
503
579
|
styleFiles.sort().map(async (f) => {
|
|
504
|
-
const content = await fs.promises.readFile(f);
|
|
580
|
+
const content = await fs.promises.readFile(f, "utf-8");
|
|
581
|
+
if(content == "") throw new Error("Invalid Filepath or empty file!");
|
|
505
582
|
return { path: f, content };
|
|
506
583
|
})
|
|
507
584
|
);
|
|
@@ -522,6 +599,7 @@ export default class Blog {
|
|
|
522
599
|
|
|
523
600
|
// Compile styles using the standalone script
|
|
524
601
|
this.compiledStyles = await compileStyles(fileData);
|
|
602
|
+
//console.log(`compiledStyles: ${this.compiledStyles}`)
|
|
525
603
|
|
|
526
604
|
await fs.promises.writeFile(
|
|
527
605
|
path.join(__dirname, "styles.min.css"),
|
package/Formatter.js
CHANGED
|
@@ -30,8 +30,18 @@ export function formatHTML(data, script, style) {
|
|
|
30
30
|
<h1>${data.title}</h1>
|
|
31
31
|
<div style="max-width: 500px; width: 100%;">
|
|
32
32
|
${form}
|
|
33
|
-
<section class="grid">
|
|
34
|
-
${data.articles
|
|
33
|
+
<section id="articles" class="grid">
|
|
34
|
+
${data.articles
|
|
35
|
+
.map((article) => {
|
|
36
|
+
let html = article.toHTML();
|
|
37
|
+
if (article.id != null)
|
|
38
|
+
html = html.replace(
|
|
39
|
+
"<article",
|
|
40
|
+
`<article data-id="${article.id}" `
|
|
41
|
+
);
|
|
42
|
+
return html;
|
|
43
|
+
})
|
|
44
|
+
.join("")}
|
|
35
45
|
</section>
|
|
36
46
|
</div>
|
|
37
47
|
<script>
|
package/blog.db
CHANGED
|
Binary file
|
package/blog.db-journal
ADDED
|
Binary file
|
package/build-styles.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { promises as fs } from "fs";
|
|
2
1
|
import path from "path";
|
|
3
2
|
import { pathToFileURL } from "url";
|
|
4
3
|
import * as sass from "sass";
|
|
@@ -6,21 +5,7 @@ import postcss from "postcss";
|
|
|
6
5
|
import autoprefixer from "autoprefixer";
|
|
7
6
|
import cssnano from "cssnano";
|
|
8
7
|
|
|
9
|
-
//
|
|
10
|
-
const srcDir = path.join("gulp_frontend", "src");
|
|
11
|
-
|
|
12
|
-
// Helper to find files recursively
|
|
13
|
-
async function getFiles(dir) {
|
|
14
|
-
const dirents = await fs.readdir(dir, { withFileTypes: true });
|
|
15
|
-
const files = await Promise.all(
|
|
16
|
-
dirents.map((dirent) => {
|
|
17
|
-
const res = path.resolve(dir, dirent.name);
|
|
18
|
-
return dirent.isDirectory() ? getFiles(res) : res;
|
|
19
|
-
})
|
|
20
|
-
);
|
|
21
|
-
return Array.prototype.concat(...files);
|
|
22
|
-
}
|
|
23
|
-
|
|
8
|
+
// array of files or a single file
|
|
24
9
|
export async function compileStyles(fileData) {
|
|
25
10
|
try {
|
|
26
11
|
let combinedCss = "";
|
|
@@ -34,7 +19,6 @@ export async function compileStyles(fileData) {
|
|
|
34
19
|
for (const file of scssFiles) {
|
|
35
20
|
try {
|
|
36
21
|
const result = sass.compileString(file.content.toString(), {
|
|
37
|
-
loadPaths: [srcDir],
|
|
38
22
|
style: "expanded",
|
|
39
23
|
url: pathToFileURL(file.path),
|
|
40
24
|
});
|
|
@@ -46,22 +30,10 @@ export async function compileStyles(fileData) {
|
|
|
46
30
|
);
|
|
47
31
|
}
|
|
48
32
|
}
|
|
49
|
-
} else {
|
|
50
|
-
const allFiles = await getFiles(srcDir);
|
|
51
|
-
const scssFiles = allFiles
|
|
52
|
-
.filter((f) => f.endsWith(".scss") && !path.basename(f).startsWith("_"))
|
|
53
|
-
.sort();
|
|
54
33
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
loadPaths: [srcDir],
|
|
59
|
-
style: "expanded",
|
|
60
|
-
});
|
|
61
|
-
combinedCss += result.css + "\n";
|
|
62
|
-
} catch (err) {
|
|
63
|
-
console.error(`Error compiling ${path.basename(file)}:`, err.message);
|
|
64
|
-
}
|
|
34
|
+
const cssFiles = fileData.filter((f) => f.path.endsWith(".css"));
|
|
35
|
+
for (const file of cssFiles) {
|
|
36
|
+
combinedCss += file.content + "\n";
|
|
65
37
|
}
|
|
66
38
|
}
|
|
67
39
|
|
|
Binary file
|
package/model/APIModel.js
CHANGED
|
@@ -1,31 +1,27 @@
|
|
|
1
1
|
import fetch from "node-fetch";
|
|
2
|
-
import { defer, firstValueFrom, timer, of } from "rxjs";
|
|
3
|
-
import { retry, mergeMap, catchError } from "rxjs/operators";
|
|
4
2
|
|
|
5
3
|
// EXTERNAL DATA API
|
|
6
4
|
|
|
7
5
|
/** fetch data from an URL */
|
|
8
6
|
export async function fetchData(apiUrl) {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
7
|
+
let retryCount = 0;
|
|
8
|
+
const maxRetries = 3;
|
|
9
|
+
|
|
10
|
+
while (true) {
|
|
11
|
+
try {
|
|
12
|
+
const response = await fetch(apiUrl);
|
|
12
13
|
if (!response.ok)
|
|
13
14
|
throw new Error(`HTTP error! status: ${response.status}`);
|
|
14
|
-
return response.json();
|
|
15
|
-
})
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
return of(null);
|
|
25
|
-
})
|
|
26
|
-
);
|
|
27
|
-
|
|
28
|
-
return firstValueFrom(request$);
|
|
15
|
+
return await response.json();
|
|
16
|
+
} catch (error) {
|
|
17
|
+
retryCount++;
|
|
18
|
+
if (retryCount > maxRetries) {
|
|
19
|
+
console.error("Failed to fetch data after retries:", error);
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
await new Promise((resolve) => setTimeout(resolve, retryCount * 1000));
|
|
23
|
+
}
|
|
24
|
+
}
|
|
29
25
|
}
|
|
30
26
|
|
|
31
27
|
/** post data to an URL */
|
package/model/DatabaseModel.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Sequelize, DataTypes } from "sequelize";
|
|
1
|
+
import { Sequelize, DataTypes, Op } from "sequelize";
|
|
2
2
|
|
|
3
3
|
export default class DatabaseModel {
|
|
4
4
|
#username;
|
|
@@ -91,11 +91,26 @@ export default class DatabaseModel {
|
|
|
91
91
|
}
|
|
92
92
|
|
|
93
93
|
// model
|
|
94
|
-
async findAll(
|
|
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
|
+
}
|
|
95
109
|
const options = {
|
|
110
|
+
where,
|
|
96
111
|
order: [
|
|
97
|
-
["createdAt",
|
|
98
|
-
["id",
|
|
112
|
+
["createdAt", order],
|
|
113
|
+
["id", order],
|
|
99
114
|
],
|
|
100
115
|
limit,
|
|
101
116
|
offset,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lexho111/plainblog",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"description": "A tool for creating and serving a minimalist, single-page blog.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -16,19 +16,20 @@
|
|
|
16
16
|
"author": "lexho111",
|
|
17
17
|
"license": "ISC",
|
|
18
18
|
"dependencies": {
|
|
19
|
+
"autoprefixer": "^10.4.23",
|
|
20
|
+
"cssnano": "^7.1.2",
|
|
19
21
|
"node-fetch": "^3.3.2",
|
|
20
22
|
"pg": "^8.16.3",
|
|
21
23
|
"pg-hstore": "^2.3.4",
|
|
22
|
-
"sequelize": "^6.37.7",
|
|
23
|
-
"sqlite3": "^5.1.7",
|
|
24
|
-
"autoprefixer": "^10.4.23",
|
|
25
|
-
"cssnano": "^7.1.2",
|
|
26
24
|
"postcss": "^8.4.35",
|
|
27
|
-
"sass": "^1.97.1"
|
|
25
|
+
"sass": "^1.97.1",
|
|
26
|
+
"sequelize": "^6.37.7",
|
|
27
|
+
"sqlite3": "^5.1.7"
|
|
28
28
|
},
|
|
29
29
|
"devDependencies": {
|
|
30
30
|
"eslint": "^9.8.0",
|
|
31
31
|
"eslint-plugin-jest": "^28.6.0",
|
|
32
|
-
"jest": "^29.7.0"
|
|
32
|
+
"jest": "^29.7.0",
|
|
33
|
+
"dom-parser": "^1.1.5"
|
|
33
34
|
}
|
|
34
35
|
}
|
package/scripts.min.js
CHANGED
|
@@ -1,2 +1 @@
|
|
|
1
|
-
|
|
2
|
-
function generateRandomContent(t){let n="";for(let e=0;e<t;e++){var o=Math.random()*("z".charCodeAt(0)-"A".charCodeAt(0)),o=String.fromCharCode("A".charCodeAt(0)+o);n+=o}return n}function fillWithContent(){var e=document.getElementById("title");document.getElementById("content").value=generateRandomContent(200),e.value=generateRandomContent(50)}
|
|
1
|
+
function _createForOfIteratorHelper(e,t){var r,n,o,a,i="undefined"!=typeof Symbol&&e[Symbol.iterator]||e["@@iterator"];if(i)return o=!(n=!0),{s:function(){i=i.call(e)},n:function(){var e=i.next();return n=e.done,e},e:function(e){o=!0,r=e},f:function(){try{n||null==i.return||i.return()}finally{if(o)throw r}}};if(Array.isArray(e)||(i=_unsupportedIterableToArray(e))||t&&e&&"number"==typeof e.length)return i&&(e=i),a=0,{s:t=function(){},n:function(){return a>=e.length?{done:!0}:{done:!1,value:e[a++]}},e:function(e){throw e},f:t};throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}function _unsupportedIterableToArray(e,t){var r;if(e)return"string"==typeof e?_arrayLikeToArray(e,t):"Map"===(r="Object"===(r={}.toString.call(e).slice(8,-1))&&e.constructor?e.constructor.name:r)||"Set"===r?Array.from(e):"Arguments"===r||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(r)?_arrayLikeToArray(e,t):void 0}function _arrayLikeToArray(e,t){(null==t||t>e.length)&&(t=e.length);for(var r=0,n=Array(t);r<t;r++)n[r]=e[r];return n}function _regenerator(){var g,e="function"==typeof Symbol?Symbol:{},t=e.iterator||"@@iterator",r=e.toStringTag||"@@toStringTag";function n(e,t,r,n){var o,a,i,c,l,u,f,s,d,t=t&&t.prototype instanceof h?t:h,t=Object.create(t.prototype);return _regeneratorDefine2(t,"_invoke",(o=e,a=r,f=n||[],s=!1,d={p:u=0,n:0,v:g,a:p,f:p.bind(g,4),d:function(e,t){return i=e,c=0,l=g,d.n=t,y}},function(e,t,r){if(1<u)throw TypeError("Generator is already running");for(s&&1===t&&p(t,r),c=t,l=r;(m=c<2?g:l)||!s;){i||(c?c<3?(1<c&&(d.n=-1),p(c,l)):d.n=l:d.v=l);try{if(u=2,i){if(m=i[e=c?e:"next"]){if(!(m=m.call(i,l)))throw TypeError("iterator result is not an object");if(!m.done)return m;l=m.value,c<2&&(c=0)}else 1===c&&(m=i.return)&&m.call(i),c<2&&(l=TypeError("The iterator does not provide a '"+e+"' method"),c=1);i=g}else if((m=(s=d.n<0)?l:o.call(a,d))!==y)break}catch(e){i=g,c=1,l=e}finally{u=1}}return{value:m,done:s}}),!0),t;function p(e,t){for(c=e,l=t,m=0;!s&&u&&!r&&m<f.length;m++){var r,n=f[m],o=d.p,a=n[2];3<e?(r=a===t)&&(l=n[(c=n[4])?5:c=3],n[4]=n[5]=g):n[0]<=o&&((r=e<2&&o<n[1])?(c=0,d.v=t,d.n=n[1]):o<a&&(r=e<3||n[0]>t||a<t)&&(n[4]=e,n[5]=t,d.n=a,c=0))}if(r||1<e)return y;throw s=!0,t}}var y={};function h(){}function o(){}function a(){}var m=Object.getPrototypeOf,e=[][t]?m(m([][t]())):(_regeneratorDefine2(m={},t,function(){return this}),m),i=a.prototype=h.prototype=Object.create(e);function c(e){return Object.setPrototypeOf?Object.setPrototypeOf(e,a):(e.__proto__=a,_regeneratorDefine2(e,r,"GeneratorFunction")),e.prototype=Object.create(i),e}return _regeneratorDefine2(i,"constructor",o.prototype=a),_regeneratorDefine2(a,"constructor",o),_regeneratorDefine2(a,r,o.displayName="GeneratorFunction"),_regeneratorDefine2(i),_regeneratorDefine2(i,r,"Generator"),_regeneratorDefine2(i,t,function(){return this}),_regeneratorDefine2(i,"toString",function(){return"[object Generator]"}),(_regenerator=function(){return{w:n,m:c}})()}function _regeneratorDefine2(e,t,r,n){var a=Object.defineProperty;try{a({},"",{})}catch(e){a=0}(_regeneratorDefine2=function(e,t,r,n){function o(t,r){_regeneratorDefine2(e,t,function(e){return this._invoke(t,r,e)})}t?a?a(e,t,{value:r,enumerable:!n,configurable:!n,writable:!n}):e[t]=r:(o("next",0),o("throw",1),o("return",2))})(e,t,r,n)}function asyncGeneratorStep(e,t,r,n,o,a,i){try{var c=e[a](i),l=c.value}catch(e){return void r(e)}c.done?t(l):Promise.resolve(l).then(n,o)}function _asyncToGenerator(c){return function(){var e=this,i=arguments;return new Promise(function(t,r){var n=c.apply(e,i);function o(e){asyncGeneratorStep(n,t,r,o,a,"next",e)}function a(e){asyncGeneratorStep(n,t,r,o,a,"throw",e)}o(void 0)})}}var isLoading=!1;function handleScroll(){return _handleScroll.apply(this,arguments)}function _handleScroll(){return(_handleScroll=_asyncToGenerator(_regenerator().m(function e(){return _regenerator().w(function(e){for(;;)switch(e.n){case 0:if(isLoading)return e.a(2);e.n=1;break;case 1:if(document.documentElement.scrollHeight-window.innerHeight-window.scrollY<100)return e.n=2,loadMore();e.n=2;break;case 2:return e.a(2)}},e)}))).apply(this,arguments)}function loadMore(){return _loadMore.apply(this,arguments)}function _loadMore(){return(_loadMore=_asyncToGenerator(_regenerator().m(function e(){var t,r,n,o,a,i,c,l;return _regenerator().w(function(e){for(;;)switch(e.p=e.n){case 0:if(console.log("load more"),t=document.getElementById("articles"),t=t.querySelectorAll("article"),t=t[t.length-1],console.log("lastArticle",t),t){e.n=1;break}return e.a(2);case 1:if(r=parseInt(t.getAttribute("data-id")),console.log("lastId ".concat(r)),isNaN(r))return e.a(2);e.n=2;break;case 2:if((n=r-1)<1)return console.log("nextId < 1 ".concat(n<1)),window.removeEventListener("scroll",handleScroll),e.a(2);e.n=3;break;case 3:return isLoading=!0,o="/api/articles?startID=".concat(n,"&limit=5"),console.log("load more ".concat(o)),e.p=4,e.n=5,fetch(o);case 5:if((o=e.v).ok)return e.n=6,o.json();e.n=7;break;case 6:if((l=e.v).articles&&0<l.articles.length){a=_createForOfIteratorHelper(l.articles);try{for(a.s();!(i=a.n()).done;)fillWithContent((c=i.value).title,c.content,c.createdAt,c.id)}catch(e){a.e(e)}finally{a.f()}}case 7:e.n=9;break;case 8:e.p=8,l=e.v,console.error("Failed to load articles:",l);case 9:isLoading=!1;case 10:return e.a(2)}},e,null,[[4,8]])}))).apply(this,arguments)}function getFormattedDate(e){var t=e.getFullYear(),r=String(e.getMonth()+1).padStart(2,"0"),n=String(e.getDate()).padStart(2,"0"),o=String(e.getHours()).padStart(2,"0"),e=String(e.getMinutes()).padStart(2,"0");return"".concat(t,"/").concat(r,"/").concat(n," ").concat(o,":").concat(e)}function fillWithContent(e,t,r,n){var o=document.createElement("article"),n=(n&&o.setAttribute("data-id",n),document.createElement("h2")),a=document.createElement("span"),i=document.createElement("p");n.textContent=e,a.textContent=getFormattedDate(new Date(r)),i.textContent=t,o.appendChild(n),o.appendChild(a),o.appendChild(i),document.getElementById("articles").appendChild(o)}function test(){console.log("123")}document.addEventListener("DOMContentLoaded",function(){window.addEventListener("scroll",handleScroll)});
|
package/src/loader.js
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
let isLoading = false;
|
|
2
|
+
|
|
3
|
+
document.addEventListener("DOMContentLoaded", () => {
|
|
4
|
+
window.addEventListener("scroll", handleScroll);
|
|
5
|
+
});
|
|
6
|
+
|
|
7
|
+
async function handleScroll() {
|
|
8
|
+
if (isLoading) return;
|
|
9
|
+
const scrollable = document.documentElement.scrollHeight - window.innerHeight;
|
|
10
|
+
const scrolled = window.scrollY;
|
|
11
|
+
|
|
12
|
+
/*console.log(`scrollable: ${scrollable}`);
|
|
13
|
+
console.log(`scrolled: ${scrolled}`);
|
|
14
|
+
console.log(`scrollable - scrolled: ${scrollable - scrolled}`);*/
|
|
15
|
+
|
|
16
|
+
// Load more when user is near the bottom (within 100px)
|
|
17
|
+
if (scrollable - scrolled < 100) {
|
|
18
|
+
await loadMore();
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function loadMore() {
|
|
23
|
+
console.log(`load more`);
|
|
24
|
+
const articlesContainer = document.getElementById("articles");
|
|
25
|
+
const articles = articlesContainer.querySelectorAll("article");
|
|
26
|
+
const lastArticle = articles[articles.length - 1];
|
|
27
|
+
console.log("lastArticle", lastArticle);
|
|
28
|
+
if (!lastArticle) return;
|
|
29
|
+
|
|
30
|
+
const lastId = parseInt(lastArticle.getAttribute("data-id"));
|
|
31
|
+
console.log(`lastId ${lastId}`);
|
|
32
|
+
if (isNaN(lastId)) return;
|
|
33
|
+
|
|
34
|
+
// Fetch older items: startID should be lastId - 1 (since we want older/smaller IDs)
|
|
35
|
+
const nextId = lastId - 1;
|
|
36
|
+
if (nextId < 1) {
|
|
37
|
+
console.log(`nextId < 1 ${nextId < 1}`);
|
|
38
|
+
window.removeEventListener("scroll", handleScroll);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
isLoading = true;
|
|
43
|
+
const url = `/api/articles?startID=${nextId}&limit=5`;
|
|
44
|
+
console.log(`load more ${url}`);
|
|
45
|
+
try {
|
|
46
|
+
const response = await fetch(url);
|
|
47
|
+
if (response.ok) {
|
|
48
|
+
const result = await response.json();
|
|
49
|
+
if (result.articles && result.articles.length > 0) {
|
|
50
|
+
for (const article of result.articles) {
|
|
51
|
+
fillWithContent(
|
|
52
|
+
article.title,
|
|
53
|
+
article.content,
|
|
54
|
+
article.createdAt,
|
|
55
|
+
article.id
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
} else {
|
|
59
|
+
// No more articles to load
|
|
60
|
+
//window.removeEventListener("scroll", handleScroll);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
} catch (error) {
|
|
64
|
+
console.error("Failed to load articles:", error);
|
|
65
|
+
}
|
|
66
|
+
isLoading = false;
|
|
67
|
+
}
|
|
68
|
+
function getFormattedDate(date) {
|
|
69
|
+
const year = date.getFullYear();
|
|
70
|
+
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
71
|
+
const day = String(date.getDate()).padStart(2, "0");
|
|
72
|
+
const hours = String(date.getHours()).padStart(2, "0");
|
|
73
|
+
const minutes = String(date.getMinutes()).padStart(2, "0");
|
|
74
|
+
return `${year}/${month}/${day} ${hours}:${minutes}`;
|
|
75
|
+
}
|
|
76
|
+
function fillWithContent(title, content, date, id) {
|
|
77
|
+
const article = document.createElement("article");
|
|
78
|
+
if (id) article.setAttribute("data-id", id);
|
|
79
|
+
|
|
80
|
+
const heading = document.createElement("h2");
|
|
81
|
+
const time = document.createElement("span");
|
|
82
|
+
const text = document.createElement("p");
|
|
83
|
+
|
|
84
|
+
heading.textContent = title;
|
|
85
|
+
time.textContent = getFormattedDate(new Date(date));
|
|
86
|
+
text.textContent = content;
|
|
87
|
+
|
|
88
|
+
//article.textContent = "article1";
|
|
89
|
+
article.appendChild(heading);
|
|
90
|
+
article.appendChild(time);
|
|
91
|
+
article.appendChild(text);
|
|
92
|
+
const articles = document.getElementById("articles");
|
|
93
|
+
articles.appendChild(article);
|
|
94
|
+
}
|
package/src/styles.css
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
.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
|
+
}
|
package/styles.min.css
CHANGED
|
@@ -1,2 +1 @@
|
|
|
1
|
-
.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}h1{color:#696969}nav a{color:#3b40c1;font-size:20px;text-decoration:underline}nav a:visited{color:#3b40c1;text-decoration-color:#3b40c1}
|
|
2
|
-
/* source-hash: cfa91156b4c55874a3e63989247cc5cb22e7ed78a4e66a9d8cc24f9b9c598bd0 */
|
|
1
|
+
.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}
|
package/test/server.test.js
CHANGED
|
@@ -21,13 +21,14 @@ describe("server test", () => {
|
|
|
21
21
|
// Use beforeAll to set up and start the server once before any tests run
|
|
22
22
|
beforeAll(async () => {
|
|
23
23
|
//blog.database.type = "sqlite";
|
|
24
|
+
blog.database.dbname = "test_server_db";
|
|
24
25
|
blog.setStyle(
|
|
25
26
|
"body { font-family: Arial, sans-serif; } h1 { color: #333; }"
|
|
26
27
|
);
|
|
27
28
|
await blog.init();
|
|
28
29
|
// Await it to ensure the server is running before tests start.
|
|
29
30
|
await blog.startServer(PORT);
|
|
30
|
-
});
|
|
31
|
+
}, 10000);
|
|
31
32
|
|
|
32
33
|
// Use afterAll to shut down the server once after all tests have finished
|
|
33
34
|
afterAll(async () => {
|
|
@@ -105,6 +106,17 @@ describe("server test", () => {
|
|
|
105
106
|
const responseData = await response.json();
|
|
106
107
|
expect(responseData).toEqual(newArticle);
|
|
107
108
|
|
|
108
|
-
expect(blog.toHTML()).toContain(newArticle.content); // does blog contain my new article?
|
|
109
|
+
expect(await blog.toHTML()).toContain(newArticle.content); // does blog contain my new article?
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test("performance: server should respond within acceptable time (e.g. 500ms)", async () => {
|
|
113
|
+
const start = Date.now();
|
|
114
|
+
const response = await fetch(`${apiBaseUrl}/api/articles`);
|
|
115
|
+
const end = Date.now();
|
|
116
|
+
const duration = end - start;
|
|
117
|
+
|
|
118
|
+
expect(response.status).toBe(200);
|
|
119
|
+
console.log(`Request took ${duration}ms`);
|
|
120
|
+
expect(duration).toBeLessThan(500); // Fail if request takes longer than 500ms
|
|
109
121
|
});
|
|
110
122
|
});
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import Blog from "../Blog.js";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { fileURLToPath } from "url";
|
|
4
|
+
import { parseFromString } from "dom-parser";
|
|
5
|
+
import fs from "node:fs";
|
|
6
|
+
|
|
7
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
|
|
9
|
+
describe("Blog Stylesheet Test", () => {
|
|
10
|
+
it("should load and compile the sass stylesheet (.scss) correctly", async () => {
|
|
11
|
+
const blog = new Blog();
|
|
12
|
+
blog.title = "My Blog";
|
|
13
|
+
blog.database.dbname = "test_styles_1";
|
|
14
|
+
|
|
15
|
+
blog.stylesheetPath = "test/stylesheets/styles.scss";
|
|
16
|
+
|
|
17
|
+
await blog.init();
|
|
18
|
+
const html = await blog.toHTML();
|
|
19
|
+
|
|
20
|
+
expect(html).toContain(".grid");
|
|
21
|
+
expect(html).toContain("article");
|
|
22
|
+
expect(html).toContain("nav");
|
|
23
|
+
expect(html).toContain("nav a:visited");
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("should load and compile the sass stylesheet (.scss) correctly [Array]", async () => {
|
|
27
|
+
const blog = new Blog();
|
|
28
|
+
blog.title = "My Blog";
|
|
29
|
+
blog.database.dbname = "test_styles_2";
|
|
30
|
+
|
|
31
|
+
blog.stylesheetPath = ["test/stylesheets/styles.scss"];
|
|
32
|
+
|
|
33
|
+
await blog.init();
|
|
34
|
+
const html = await blog.toHTML();
|
|
35
|
+
|
|
36
|
+
expect(html).toContain(".grid");
|
|
37
|
+
expect(html).toContain("article");
|
|
38
|
+
expect(html).toContain("nav");
|
|
39
|
+
expect(html).toContain("nav a:visited");
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("should load the stylesheet (.css) file", async () => {
|
|
43
|
+
const filepath = path.join(__dirname, "stylesheets/styles.css");
|
|
44
|
+
|
|
45
|
+
const data = await fs.promises.readFile(filepath, "utf8");
|
|
46
|
+
console.log(data);
|
|
47
|
+
expect(data).toContain("body");
|
|
48
|
+
expect(data).toContain("nav a");
|
|
49
|
+
expect(data).toContain(".datetime");
|
|
50
|
+
expect(data).toContain("font-style: normal");
|
|
51
|
+
expect(data).toContain("color: darkgray");
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("should load and compile the stylesheet (.css) correctly", async () => {
|
|
55
|
+
const blog = new Blog();
|
|
56
|
+
blog.title = "My Blog";
|
|
57
|
+
blog.database.dbname = "test_styles_3";
|
|
58
|
+
blog.stylesheetPath = "test/stylesheets/styles.css";
|
|
59
|
+
await blog.init();
|
|
60
|
+
const html = await blog.toHTML();
|
|
61
|
+
|
|
62
|
+
// dom-parser fails on <!DOCTYPE html>, so we remove it before parsing
|
|
63
|
+
const doc = parseFromString(
|
|
64
|
+
html.replace("<!DOCTYPE html>", ""),
|
|
65
|
+
"text/html"
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
// Find all style tags
|
|
69
|
+
const styleTags = doc.getElementsByTagName("style");
|
|
70
|
+
const css = styleTags.map((tag) => tag.innerHTML).join("\n");
|
|
71
|
+
console.log(css);
|
|
72
|
+
|
|
73
|
+
expect(html).not.toContain("<style>body { font-family: Arial; }</style>");
|
|
74
|
+
expect(css).toContain("body");
|
|
75
|
+
expect(css).toContain("nav a");
|
|
76
|
+
expect(css).toContain(".datetime");
|
|
77
|
+
expect(css).toContain("font-style:normal");
|
|
78
|
+
expect(css).toContain("color:");
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("should load and compile the stylesheet (.css) correctly [Array]", async () => {
|
|
82
|
+
const blog = new Blog();
|
|
83
|
+
blog.title = "My Blog";
|
|
84
|
+
blog.database.dbname = "test_styles_4";
|
|
85
|
+
blog.stylesheetPath = ["test/stylesheets/styles.css"];
|
|
86
|
+
await blog.init();
|
|
87
|
+
const html = await blog.toHTML();
|
|
88
|
+
|
|
89
|
+
// dom-parser fails on <!DOCTYPE html>, so we remove it before parsing
|
|
90
|
+
const doc = parseFromString(
|
|
91
|
+
html.replace("<!DOCTYPE html>", ""),
|
|
92
|
+
"text/html"
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
// Find all style tags
|
|
96
|
+
const styleTags = doc.getElementsByTagName("style");
|
|
97
|
+
const css = styleTags.map((tag) => tag.innerHTML).join("\n");
|
|
98
|
+
console.log(css);
|
|
99
|
+
|
|
100
|
+
expect(html).not.toContain("<style>body { font-family: Arial; }</style>");
|
|
101
|
+
expect(css).toContain("body");
|
|
102
|
+
expect(css).toContain("nav a");
|
|
103
|
+
expect(css).toContain(".datetime");
|
|
104
|
+
expect(css).toContain("font-style:normal");
|
|
105
|
+
expect(css).toContain("color:");
|
|
106
|
+
});
|
|
107
|
+
});
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
body {
|
|
2
|
+
background-color: rgb(253, 253, 253);
|
|
3
|
+
font-family: Arial;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
nav a {
|
|
7
|
+
text-decoration: underline;
|
|
8
|
+
color: rgb(59, 64, 193);
|
|
9
|
+
font-size: 20px;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.datetime {
|
|
13
|
+
font-style: normal;
|
|
14
|
+
color: rgb(67, 67, 67);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
h2 {
|
|
18
|
+
margin: 0;
|
|
19
|
+
margin-bottom: 5px;
|
|
20
|
+
color: darkgray;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
p {
|
|
24
|
+
margin-top: 10px;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
span {
|
|
28
|
+
margin: 0;
|
|
29
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
.grid {
|
|
2
|
+
display: grid;
|
|
3
|
+
grid-template-columns: 1fr;
|
|
4
|
+
gap: 0.25rem;
|
|
5
|
+
border: 0px solid black;
|
|
6
|
+
|
|
7
|
+
article {
|
|
8
|
+
padding: 0.25rem;
|
|
9
|
+
border: 0px solid #ccc;
|
|
10
|
+
border-radius: 4px;
|
|
11
|
+
min-width: 0; /* Allow grid items to shrink */
|
|
12
|
+
overflow-wrap: break-word; /* Break long words */
|
|
13
|
+
p {
|
|
14
|
+
margin-bottom: 0;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
h1 {
|
|
20
|
+
color: rgb(105, 105, 105);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
nav {
|
|
24
|
+
a {
|
|
25
|
+
text-decoration: underline;
|
|
26
|
+
color: rgb(59, 64, 193);
|
|
27
|
+
font-size: 20px;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
a:visited {
|
|
31
|
+
color: rgb(59, 64, 193);
|
|
32
|
+
text-decoration-color: rgb(59, 64, 193);
|
|
33
|
+
}
|
|
34
|
+
}
|
package/test_1767173763155.db
CHANGED
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/test_styles_1.db
ADDED
|
Binary file
|
package/test_styles_2.db
ADDED
|
Binary file
|
package/test_styles_3.db
ADDED
|
Binary file
|
package/test_styles_4.db
ADDED
|
Binary file
|