@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 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
- await this.processAssets(this.assetFiles);
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.reloadScriptsStylesOnGET = false;
209
- if(this.reloadScriptsStylesOnGET) console.log("reload scripts and styles on GET-Request");
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.reloadScriptsStylesOnGET) {
302
- this.loadScripts();
303
- this.loadStyles();
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
- this.addArticle(new Article(articleData.title, articleData.content, articleData.createdAt));
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(req.url === "/api" || req.url === "/api/") {
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 (req.url === "/api/articles") {
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" && req.url === "/api/articles") {
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
- let body = "";
422
- req.on("data", (chunk) => (body += chunk.toString()));
423
- req.on("end", async () => {
424
- const newArticle = JSON.parse(body);
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 finalScripts = this.scripts + this.compiledScripts;
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.map((article) => article.toHTML()).join("")}
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
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
- // Configuration
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
- for (const file of scssFiles) {
56
- try {
57
- const result = sass.compile(file, {
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
- const request$ = defer(() => fetch(apiUrl)).pipe(
10
- // 1. Check response status inside the stream
11
- mergeMap(async (response) => {
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
- // 2. Retry 3 times, waiting 1s, 2s, 3s respectively
17
- retry({
18
- count: 3,
19
- delay: (error, retryCount) => timer(retryCount * 1000),
20
- }),
21
- // 3. Handle final failure
22
- catchError((error) => {
23
- console.error("Failed to fetch data after retries:", error);
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 */
@@ -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(limit = 10, offset = 0) {
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", "DESC"],
98
- ["id", "DESC"],
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.8",
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
- /*! gruntproject 2025-12-22 */
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}
@@ -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
+ }
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file