@lexho111/plainblog 0.5.28 → 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 +341 -140
- package/Formatter.js +2 -11
- package/README.md +1 -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 +10 -2
- package/public/fetchData.js +0 -0
- package/public/scripts.min.js +2 -1
- package/public/styles.min.css +2 -1
- package/src/fetchData.js +150 -30
- package/src/styles.css +29 -0
- package/utilities.js +142 -0
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import createDebug from "debug";
|
|
2
|
+
import Article from "../../Article.js";
|
|
3
|
+
|
|
4
|
+
const debug = createDebug("plainblog:BinarySearchTree");
|
|
5
|
+
|
|
6
|
+
class BlogNode {
|
|
7
|
+
constructor(article) {
|
|
8
|
+
this.article = article;
|
|
9
|
+
this.left = null;
|
|
10
|
+
this.right = null;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export class BinarySearchTree {
|
|
15
|
+
constructor() {
|
|
16
|
+
this.root = null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// O(n) - Linear search (Base class has no HashMap)
|
|
20
|
+
contains(id) {
|
|
21
|
+
return this.find(id) !== null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Insert a new article
|
|
25
|
+
insert(article) {
|
|
26
|
+
// Note: The tree is now sorted by date. ID-based methods will not work correctly.
|
|
27
|
+
// if (!article.id) article.id = this.getNextID();
|
|
28
|
+
// Add to the Set
|
|
29
|
+
const newNode = new BlogNode(article);
|
|
30
|
+
if (!this.root) {
|
|
31
|
+
this.root = newNode;
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
let current = this.root;
|
|
35
|
+
const newTime = newNode.article.createdAt;
|
|
36
|
+
while (true) {
|
|
37
|
+
// Using getTime() for numeric comparison. Duplicates go to the right.
|
|
38
|
+
if (newTime < current.article.createdAt) {
|
|
39
|
+
if (!current.left) {
|
|
40
|
+
current.left = newNode;
|
|
41
|
+
break;
|
|
42
|
+
}
|
|
43
|
+
current = current.left;
|
|
44
|
+
} else {
|
|
45
|
+
if (!current.right) {
|
|
46
|
+
current.right = newNode;
|
|
47
|
+
break;
|
|
48
|
+
}
|
|
49
|
+
current = current.right;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
update(id, title, content) {
|
|
55
|
+
const article = this.find(id);
|
|
56
|
+
article.title = title;
|
|
57
|
+
article.content = content;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
search(query) {
|
|
61
|
+
debug("search %s ", query);
|
|
62
|
+
const lower = query.toLowerCase();
|
|
63
|
+
return this.getAllArticles().filter(
|
|
64
|
+
(a) =>
|
|
65
|
+
a.title.toLowerCase().includes(lower) ||
|
|
66
|
+
a.content.toLowerCase().includes(lower),
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
_findMinNode(node) {
|
|
71
|
+
while (node.left) node = node.left;
|
|
72
|
+
return node;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Search for a specific article by ID
|
|
76
|
+
find(id) {
|
|
77
|
+
// Tree is sorted by Date, so we must traverse all nodes to find by ID (O(n))
|
|
78
|
+
return this._findNodeDFS(this.root, id);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
_findNodeDFS(node, id) {
|
|
82
|
+
if (!node) return null;
|
|
83
|
+
if (id === node.article.id) return node.article;
|
|
84
|
+
const leftResult = this._findNodeDFS(node.left, id);
|
|
85
|
+
if (leftResult) return leftResult;
|
|
86
|
+
return this._findNodeDFS(node.right, id);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Get all articles in order (Sorted by ID)
|
|
90
|
+
getAllArticles(order = "newest") {
|
|
91
|
+
const articles = [];
|
|
92
|
+
if (this.root) {
|
|
93
|
+
this._inOrder(this.root, articles, order);
|
|
94
|
+
}
|
|
95
|
+
return articles;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
_inOrder(node, articles, order) {
|
|
99
|
+
const stack = [];
|
|
100
|
+
let current = node;
|
|
101
|
+
|
|
102
|
+
if (order === "newest") {
|
|
103
|
+
while (current || stack.length > 0) {
|
|
104
|
+
while (current) {
|
|
105
|
+
stack.push(current);
|
|
106
|
+
current = current.right;
|
|
107
|
+
}
|
|
108
|
+
current = stack.pop();
|
|
109
|
+
articles.push(current.article);
|
|
110
|
+
current = current.left;
|
|
111
|
+
}
|
|
112
|
+
} else {
|
|
113
|
+
while (current || stack.length > 0) {
|
|
114
|
+
while (current) {
|
|
115
|
+
stack.push(current);
|
|
116
|
+
current = current.left;
|
|
117
|
+
}
|
|
118
|
+
current = stack.pop();
|
|
119
|
+
articles.push(current.article);
|
|
120
|
+
current = current.right;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
getRange(startdate, enddate, limit) {
|
|
126
|
+
const results = [];
|
|
127
|
+
// Optimization: Convert to timestamps once to avoid Date object creation/coercion in recursion
|
|
128
|
+
const start = startdate
|
|
129
|
+
? new Date(
|
|
130
|
+
startdate instanceof Date
|
|
131
|
+
? startdate
|
|
132
|
+
: isNaN(startdate)
|
|
133
|
+
? startdate
|
|
134
|
+
: parseInt(startdate),
|
|
135
|
+
).getTime()
|
|
136
|
+
: 0;
|
|
137
|
+
const end = enddate
|
|
138
|
+
? new Date(
|
|
139
|
+
enddate instanceof Date
|
|
140
|
+
? enddate
|
|
141
|
+
: isNaN(enddate)
|
|
142
|
+
? enddate
|
|
143
|
+
: parseInt(enddate),
|
|
144
|
+
).getTime()
|
|
145
|
+
: Infinity;
|
|
146
|
+
this._rangeSearch(this.root, start, end, limit, results);
|
|
147
|
+
return results;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
_rangeSearch(node, start, end, limit, results) {
|
|
151
|
+
// Stop if node is null or we have reached the limit
|
|
152
|
+
if (!node || (limit !== null && results.length >= limit)) return;
|
|
153
|
+
|
|
154
|
+
// This is an efficient reverse in-order traversal for a date-sorted tree.
|
|
155
|
+
// It prunes branches that are outside the specified date range.
|
|
156
|
+
|
|
157
|
+
// 1. Traverse right (newer posts) if there's a possibility of finding matches.
|
|
158
|
+
if (node.article.createdAt <= end) {
|
|
159
|
+
this._rangeSearch(node.right, start, end, limit, results);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (limit !== null && results.length >= limit) return;
|
|
163
|
+
|
|
164
|
+
// 2. Check if the current node is within the range.
|
|
165
|
+
if (node.article.createdAt >= start && node.article.createdAt <= end) {
|
|
166
|
+
results.push(node.article);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (limit !== null && results.length >= limit) return;
|
|
170
|
+
|
|
171
|
+
// 3. Traverse left (older posts) if there's a possibility of finding matches.
|
|
172
|
+
if (node.article.createdAt > start) {
|
|
173
|
+
this._rangeSearch(node.left, start, end, limit, results);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Private recursive helper
|
|
178
|
+
_countNodes(node) {
|
|
179
|
+
// 1. Base Case: If the branch is empty, return 0
|
|
180
|
+
if (!node) {
|
|
181
|
+
return 0;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// 2. Recursive Step:
|
|
185
|
+
// Count the current node (1)
|
|
186
|
+
// PLUS whatever the left side has
|
|
187
|
+
// PLUS whatever the right side has
|
|
188
|
+
return 1 + this._countNodes(node.left) + this._countNodes(node.right);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
remove(data) {
|
|
192
|
+
const id = data instanceof Article ? data.id : data;
|
|
193
|
+
const date = data instanceof Article ? data.createdAt : null;
|
|
194
|
+
this.root = this._removeIterative(this.root, id, date);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
_removeIterative(root, id, date) {
|
|
198
|
+
let parent = null;
|
|
199
|
+
let current = root;
|
|
200
|
+
let isLeftChild = false;
|
|
201
|
+
|
|
202
|
+
// 1. Find the node
|
|
203
|
+
if (date !== null) {
|
|
204
|
+
// Optimized search using BST property (Date)
|
|
205
|
+
while (current) {
|
|
206
|
+
if (current.article.id == id) break;
|
|
207
|
+
parent = current;
|
|
208
|
+
if (date < current.article.createdAt) {
|
|
209
|
+
current = current.left;
|
|
210
|
+
isLeftChild = true;
|
|
211
|
+
} else {
|
|
212
|
+
current = current.right;
|
|
213
|
+
isLeftChild = false;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
} else {
|
|
217
|
+
// Full scan required (Iterative DFS) if we don't have the date
|
|
218
|
+
const stack = [{ node: root, parent: null, isLeft: false }];
|
|
219
|
+
current = null;
|
|
220
|
+
while (stack.length > 0) {
|
|
221
|
+
const item = stack.pop();
|
|
222
|
+
if (item.node.article.id == id) {
|
|
223
|
+
current = item.node;
|
|
224
|
+
parent = item.parent;
|
|
225
|
+
isLeftChild = item.isLeft;
|
|
226
|
+
break;
|
|
227
|
+
}
|
|
228
|
+
if (item.node.right)
|
|
229
|
+
stack.push({
|
|
230
|
+
node: item.node.right,
|
|
231
|
+
parent: item.node,
|
|
232
|
+
isLeft: false,
|
|
233
|
+
});
|
|
234
|
+
if (item.node.left)
|
|
235
|
+
stack.push({ node: item.node.left, parent: item.node, isLeft: true });
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (!current) return root; // Not found
|
|
240
|
+
|
|
241
|
+
// 2. Delete node
|
|
242
|
+
if (!current.left && !current.right) {
|
|
243
|
+
if (!parent) return null; // Root was deleted
|
|
244
|
+
if (isLeftChild) parent.left = null;
|
|
245
|
+
else parent.right = null;
|
|
246
|
+
} else if (!current.right) {
|
|
247
|
+
if (!parent) return current.left;
|
|
248
|
+
if (isLeftChild) parent.left = current.left;
|
|
249
|
+
else parent.right = current.left;
|
|
250
|
+
} else if (!current.left) {
|
|
251
|
+
if (!parent) return current.right;
|
|
252
|
+
if (isLeftChild) parent.left = current.right;
|
|
253
|
+
else parent.right = current.right;
|
|
254
|
+
} else {
|
|
255
|
+
// Two children: Find successor (min in right subtree)
|
|
256
|
+
let successorParent = current;
|
|
257
|
+
let successor = current.right;
|
|
258
|
+
while (successor.left) {
|
|
259
|
+
successorParent = successor;
|
|
260
|
+
successor = successor.left;
|
|
261
|
+
}
|
|
262
|
+
current.article = successor.article;
|
|
263
|
+
if (successorParent === current) successorParent.right = successor.right;
|
|
264
|
+
else successorParent.left = successor.right;
|
|
265
|
+
}
|
|
266
|
+
return root;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
clear() {
|
|
270
|
+
this.root = null;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
size() {
|
|
274
|
+
return this._countNodes(this.root);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import createDebug from "debug";
|
|
2
|
+
import { BinarySearchTree } from "./BinarySearchTree.js";
|
|
3
|
+
|
|
4
|
+
// Initialize the debugger with a specific namespace
|
|
5
|
+
const debug = createDebug("plainblog:BinarySearchTreeHashMap");
|
|
6
|
+
|
|
7
|
+
export class BinarySearchTreeHashMap extends BinarySearchTree {
|
|
8
|
+
constructor() {
|
|
9
|
+
super();
|
|
10
|
+
this.ids = new Map(); // Fast lookup storage (ID -> Article)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// O(1) - Instant lookup
|
|
14
|
+
contains(id) {
|
|
15
|
+
return this.ids.has(id);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Insert a new article
|
|
19
|
+
insert(article) {
|
|
20
|
+
// Note: The tree is now sorted by date. ID-based methods will not work correctly.
|
|
21
|
+
// if (!article.id) article.id = this.getNextID();
|
|
22
|
+
// Add to the Map
|
|
23
|
+
this.ids.set(article.id, article);
|
|
24
|
+
super.insert(article);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
update(id, title, content) {
|
|
28
|
+
// Check for both string and number ID
|
|
29
|
+
let article = this.ids.get(id);
|
|
30
|
+
if (!article) article = this.ids.get(parseInt(id));
|
|
31
|
+
|
|
32
|
+
if (article) {
|
|
33
|
+
article.title = title;
|
|
34
|
+
article.content = content;
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
search(query) {
|
|
41
|
+
debug("search %s ", query);
|
|
42
|
+
const lower = query.toLowerCase();
|
|
43
|
+
const results = this.getAllArticles().filter(
|
|
44
|
+
(a) =>
|
|
45
|
+
a.title.toLowerCase().includes(lower) ||
|
|
46
|
+
a.content.toLowerCase().includes(lower),
|
|
47
|
+
);
|
|
48
|
+
debug("%d results found", results.length);
|
|
49
|
+
return results;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Search for a specific article by ID
|
|
53
|
+
find(id) {
|
|
54
|
+
return this.ids.get(id);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Get all articles in order (Sorted by ID)
|
|
58
|
+
getAllArticles(order = "newest") {
|
|
59
|
+
return super.getAllArticles(order);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
getRange(startdate, enddate, limit) {
|
|
63
|
+
return super.getRange(startdate, enddate, limit);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
remove(data) {
|
|
67
|
+
const id = data && data.id ? data.id : data;
|
|
68
|
+
|
|
69
|
+
let article = this.ids.get(id);
|
|
70
|
+
if (!article) article = this.ids.get(parseInt(id));
|
|
71
|
+
|
|
72
|
+
if (article) {
|
|
73
|
+
this.ids.delete(article.id);
|
|
74
|
+
return super.remove(article); // Pass article to use optimized date-based removal
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
this.ids.delete(id);
|
|
78
|
+
return super.remove(data);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
clear() {
|
|
82
|
+
super.clear();
|
|
83
|
+
this.ids.clear();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
size() {
|
|
87
|
+
return super.size();
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import Article from "../../Article.js";
|
|
2
|
+
import { BinarySearchTree } from "./BinarySearchTree.js";
|
|
3
|
+
|
|
4
|
+
const bst = new BinarySearchTree();
|
|
5
|
+
|
|
6
|
+
// Adding articles
|
|
7
|
+
//myBlog.insert(new Article(101, "Hello World", "Welcome to my JS blog."));
|
|
8
|
+
//myBlog.insert(new Article(50, "Intro to Trees", "Binary trees are useful..."));
|
|
9
|
+
//myBlog.insert(new Article(150, "JavaScript in 2026", "The state of JS today."));
|
|
10
|
+
|
|
11
|
+
// Find a specific article
|
|
12
|
+
const post = bst.find(101);
|
|
13
|
+
console.log("Found:", post.title);
|
|
14
|
+
|
|
15
|
+
// List all articles (Sorted by ID)
|
|
16
|
+
console.log("All Articles:", bst.getAllArticles());
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import {
|
|
3
|
+
saveInfo,
|
|
4
|
+
loadInfo,
|
|
5
|
+
appendArticleSync, // Change to synchronous version
|
|
6
|
+
loadArticlesSync, // Change to synchronous version
|
|
7
|
+
initFiles,
|
|
8
|
+
} from "../FileModel.js";
|
|
9
|
+
|
|
10
|
+
export default class FileList {
|
|
11
|
+
constructor(filepath) {
|
|
12
|
+
this.filepath = filepath;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
clear() {
|
|
16
|
+
// Optionally: fs.writeFileSync(this.filepath, "");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Removed 'async'
|
|
20
|
+
insert(newArticle) {
|
|
21
|
+
/*if (!newArticle.createdAt) {
|
|
22
|
+
newArticle.createdAt = new Date().toISOString();
|
|
23
|
+
}
|
|
24
|
+
if (!newArticle.id) {
|
|
25
|
+
newArticle.id = Date.now();
|
|
26
|
+
}
|
|
27
|
+
// Must call a synchronous function here
|
|
28
|
+
appendArticleSync(this.filepath, newArticle);*/
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
remove(article) {}
|
|
32
|
+
|
|
33
|
+
contains(id) {}
|
|
34
|
+
|
|
35
|
+
// Removed 'async'
|
|
36
|
+
getAllArticles() {
|
|
37
|
+
return loadArticlesSync(this.filepath);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
size() {
|
|
41
|
+
try {
|
|
42
|
+
const content = fs.readFileSync(this.filepath, "utf8");
|
|
43
|
+
// filter(Boolean) prevents empty last lines from being counted as articles
|
|
44
|
+
return content.split("\n").filter((line) => line.trim()).length;
|
|
45
|
+
} catch (err) {
|
|
46
|
+
return 0;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
length() {
|
|
51
|
+
return this.size();
|
|
52
|
+
}
|
|
53
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lexho111/plainblog",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "A tool for creating and serving a minimalist, single-page blog.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
"dev": "node index.js",
|
|
9
9
|
"postinstall": "node postinstall.js",
|
|
10
10
|
"test": "node --experimental-vm-modules node_modules/jest/bin/jest.js",
|
|
11
|
+
"coverage": "node --experimental-vm-modules node_modules/jest/bin/jest.js --coverage & start coverage/lcov-report/index.html",
|
|
12
|
+
"view-coverage": "start coverage/lcov-report/index.html",
|
|
11
13
|
"lint": "eslint ."
|
|
12
14
|
},
|
|
13
15
|
"keywords": [
|
|
@@ -18,6 +20,8 @@
|
|
|
18
20
|
"author": "lexho111",
|
|
19
21
|
"license": "ISC",
|
|
20
22
|
"devDependencies": {
|
|
23
|
+
"@babel/core": "^7.28.6",
|
|
24
|
+
"@babel/preset-env": "^7.28.6",
|
|
21
25
|
"@eslint/js": "^9.39.2",
|
|
22
26
|
"@types/node": "^25.0.3",
|
|
23
27
|
"autoprefixer": "^10.4.23",
|
|
@@ -30,8 +34,12 @@
|
|
|
30
34
|
"pg-hstore": "^2.3.4",
|
|
31
35
|
"postcss": "^8.5.6",
|
|
32
36
|
"sequelize": "^6.37.7",
|
|
33
|
-
"sqlite3": "^5.
|
|
37
|
+
"sqlite3": "^5.0.2",
|
|
34
38
|
"typescript": "^5.9.3",
|
|
39
|
+
"uglify-js": "^3.19.3",
|
|
35
40
|
"w3c-css-validator": "^1.4.1"
|
|
41
|
+
},
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"debug": "^4.4.3"
|
|
36
44
|
}
|
|
37
45
|
}
|
|
File without changes
|
package/public/scripts.min.js
CHANGED
|
@@ -1 +1,2 @@
|
|
|
1
|
-
|
|
1
|
+
let isLoading=!1;async function handleScroll(){isLoading||document.documentElement.scrollHeight-window.innerHeight-window.scrollY<100&&await loadMore()}async function loadMore(){console.log("load more");var e=document.getElementById("articles"),t=Array.from(e.querySelectorAll("article"));let n=NaN;for(let e=t.length-1;0<=e;e--){var a=t[e].getAttribute("data-date");if(a){a=parseInt(a,10);if(!isNaN(a)){n=a;break}}}if(console.log("lastTimestamp "+n),isNaN(n))window.removeEventListener("scroll",handleScroll);else{var e=n,o=(isLoading=!0,`/api/articles?enddate=${e}&limit=51`);console.log("load more "+o),console.log("endDate: "+new Date(e));try{var l=await fetch(o);if(l.ok){var i=(await l.json()).articles||[];if(0<i.length){let e=0;for(var r of i)fillWithContent(r.title,r.content,r.createdAt,r.id)&&e++;e<50&&window.removeEventListener("scroll",handleScroll)}else window.removeEventListener("scroll",handleScroll)}}catch(e){console.error("Failed to load articles:",e)}isLoading=!1}}function getFormattedDate(e){return e.getFullYear()+`/${String(e.getMonth()+1).padStart(2,"0")}/${String(e.getDate()).padStart(2,"0")} ${String(e.getHours()).padStart(2,"0")}:`+String(e.getMinutes()).padStart(2,"0")}function fillWithContent(n,a,e,o){if(o&&document.querySelector(`#articles > article[data-id='${o}']`))return console.log("article found. not adding it."),console.log(o+` ${n} `+e),!1;let l=document.createElement("article");var t,i,r,d,e=new Date(e),c=(null!=o&&l.setAttribute("data-id",o),l.setAttribute("data-date",e.getTime()),document.createElement("h2")),s=document.createElement("span"),m=document.createElement("p");function u(e){var t;"delete"===e?confirm("Delete this article?")&&fetch("/api/articles?id="+o,{method:"DELETE"}).then(e=>{e.ok?l.remove():alert("Error: "+e.statusText)}):"edit"===e&&(e=prompt("New Title",n),t=prompt("New Content",a),e)&&t&&fetch("/api/articles?id="+o,{method:"PUT",body:JSON.stringify({title:e,content:t})}).then(e=>{e.ok?window.location.reload():alert("Error: "+e.statusText)})}return null!==document.querySelector('a[href="/logout"]')&&(t=document.createElement("div"),i=document.createElement("div"),r=document.createElement("div"),d=document.createElement("div"),t.setAttribute("class","buttons"),i.setAttribute("class","button edit"),r.setAttribute("class","button delete"),d.setAttribute("class","button delete"),i.textContent="edit",r.textContent="delete",d.textContent="something",t.appendChild(i),t.appendChild(r),t.appendChild(d),i.addEventListener("click",()=>u("edit")),r.addEventListener("click",()=>u("delete")),l.appendChild(t)),c.textContent=n,s.textContent=getFormattedDate(e),m.textContent=a,l.appendChild(c),l.appendChild(s),l.appendChild(m),document.getElementById("articles").appendChild(l),!0}document.addEventListener("DOMContentLoaded",()=>{window.addEventListener("scroll",handleScroll);var e=document.getElementById("search");e&&e.addEventListener("input",async e=>{var e=e.target.value;0<e.length?(e=await(await fetch("/api/articles?q="+encodeURIComponent(e))).json(),document.getElementById("articles").innerHTML="",e.articles.forEach(e=>fillWithContent(e.title,e.content,e.createdAt,e.id))):window.location.reload()})});
|
|
2
|
+
/* source-hash: 2b1ef8919234bc5888e0b423e7411b07aab439a1feaebec95b729fe308f2fa32 */
|
package/public/styles.min.css
CHANGED
|
@@ -1 +1,2 @@
|
|
|
1
|
-
body{font-family:Arial;margin:0}nav{margin-top:10px}.box,h1,nav{margin-left:10px}.grid{border:0 solid #000;display:grid;
|
|
1
|
+
body{font-family:Arial,sans-serif}h1{color:#333}body{font-family:Arial;margin:0}nav{margin-top:10px}.box,h1,nav{margin-left:10px}.grid{border:0 solid #000;display:grid;gap:.25rem;grid-template-columns:1fr}.grid article{border:0 solid #ccc;border-radius:4px;min-width:0;overflow-wrap:break-word;padding:.25rem}.grid article h2{color:#353535;margin-bottom:5px}.grid article .datetime{color:#757575;margin:0}.grid article p{margin-bottom:0;margin-top:10px}article a,article a:visited,h1{color:#696969}nav a{color:#3b40c1;font-size:20px;text-decoration:underline}nav a:visited{color:#3b40c1;text-decoration-color:#3b40c1}#wrapper{max-width:500px;width:100%}.hide-image{display:none}@media screen and (max-width:1000px){*{font-size:4vw}#wrapper{box-sizing:border-box;max-width:100%;padding:0 10px;width:100%}}h2{margin-top:0}.buttons,h2{border:0 solid #000}.buttons{height:25px;margin-bottom:0;width:100%}.button{border:1px solid #000;cursor:pointer;float:left;height:19px;padding:2px;-webkit-user-select:none;-moz-user-select:none;user-select:none;width:100px}.button:focus{outline:2px solid orange}.edit{background-color:blue}.delete{background-color:red}
|
|
2
|
+
/* source-hash: 22048a393d87cbbefd7a61fb300fff916360a30934da974ee2fd45739e446492 */
|