folderblog 0.0.2 → 0.0.3
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/dist/{chunk-24MKFHML.cjs → chunk-2TZSVPNP.cjs} +5 -0
- package/dist/{chunk-HMQIQUPB.cjs → chunk-6TFXNIO6.cjs} +108 -0
- package/dist/{chunk-ZRUBI3GH.js → chunk-B43UAOPC.js} +106 -1
- package/dist/{chunk-XP5J4LFJ.js → chunk-D26H5722.js} +5 -0
- package/dist/chunk-E7PYGJA7.cjs +39 -0
- package/dist/{chunk-QA4KPPTA.cjs → chunk-J3Y3HEBF.cjs} +84 -13
- package/dist/{chunk-PARGDJNY.js → chunk-K76XLEC7.js} +1 -1
- package/dist/{chunk-IXP35S24.js → chunk-LPPBVXJ7.js} +83 -12
- package/dist/chunk-Q6EXKX6K.js +17 -0
- package/dist/{chunk-4ZJGUMHS.cjs → chunk-Q6EYTOTM.cjs} +2 -2
- package/dist/chunk-UCXXH2MP.cjs +20 -0
- package/dist/chunk-XQD3UUL5.js +34 -0
- package/dist/cli/bin.cjs +5 -5
- package/dist/cli/bin.js +4 -4
- package/dist/cli/index.cjs +5 -5
- package/dist/cli/index.js +4 -4
- package/dist/config-ADPY6IQS.d.cts +473 -0
- package/dist/config-Dctsdeo6.d.ts +473 -0
- package/dist/index.cjs +157 -187
- package/dist/index.d.cts +4 -3
- package/dist/index.d.ts +4 -3
- package/dist/index.js +16 -69
- package/dist/local/index.cjs +785 -0
- package/dist/local/index.d.cts +268 -0
- package/dist/local/index.d.ts +268 -0
- package/dist/local/index.js +772 -0
- package/dist/output-0P0br3Jc.d.cts +452 -0
- package/dist/output-0P0br3Jc.d.ts +452 -0
- package/dist/plugins/embed-cloudflare-ai.cjs +166 -0
- package/dist/plugins/embed-cloudflare-ai.d.cts +73 -0
- package/dist/plugins/embed-cloudflare-ai.d.ts +73 -0
- package/dist/plugins/embed-cloudflare-ai.js +156 -0
- package/dist/plugins/embed-transformers.cjs +121 -0
- package/dist/plugins/embed-transformers.d.cts +55 -0
- package/dist/plugins/embed-transformers.d.ts +55 -0
- package/dist/plugins/embed-transformers.js +113 -0
- package/dist/plugins/similarity.cjs +19 -0
- package/dist/plugins/similarity.d.cts +41 -0
- package/dist/plugins/similarity.d.ts +41 -0
- package/dist/plugins/similarity.js +2 -0
- package/dist/processor/index.cjs +123 -111
- package/dist/processor/index.d.cts +6 -2
- package/dist/processor/index.d.ts +6 -2
- package/dist/processor/index.js +3 -3
- package/dist/processor/plugins.cjs +24 -12
- package/dist/processor/plugins.d.cts +4 -2
- package/dist/processor/plugins.d.ts +4 -2
- package/dist/processor/plugins.js +1 -1
- package/dist/processor/types.cjs +16 -16
- package/dist/processor/types.d.cts +3 -2
- package/dist/processor/types.d.ts +3 -2
- package/dist/processor/types.js +1 -1
- package/dist/seo/index.cjs +289 -0
- package/dist/seo/index.d.cts +95 -0
- package/dist/seo/index.d.ts +95 -0
- package/dist/seo/index.js +274 -0
- package/dist/server/index.cjs +2 -5
- package/dist/server/index.js +2 -5
- package/package.json +36 -1
- package/dist/config-DFr-htlO.d.cts +0 -887
- package/dist/config-DFr-htlO.d.ts +0 -887
|
@@ -0,0 +1,772 @@
|
|
|
1
|
+
import { FolderBlogError, NotFoundError } from '../chunk-XQD3UUL5.js';
|
|
2
|
+
import { loadJsonFile } from '../chunk-Q6EXKX6K.js';
|
|
3
|
+
import { isBrokenLinkIssue, isSlugConflictIssue, isMissingMediaIssue } from '../chunk-D26H5722.js';
|
|
4
|
+
import '../chunk-3RG5ZIWI.js';
|
|
5
|
+
import { resolve, join } from 'path';
|
|
6
|
+
import { existsSync, readFileSync } from 'fs';
|
|
7
|
+
import MiniSearch from 'minisearch';
|
|
8
|
+
|
|
9
|
+
// src/local/cache.ts
|
|
10
|
+
var LRUCache = class {
|
|
11
|
+
map = /* @__PURE__ */ new Map();
|
|
12
|
+
maxSize;
|
|
13
|
+
constructor(maxSize = 500) {
|
|
14
|
+
this.maxSize = Math.max(1, maxSize);
|
|
15
|
+
}
|
|
16
|
+
get(key) {
|
|
17
|
+
const value = this.map.get(key);
|
|
18
|
+
if (value === void 0) return void 0;
|
|
19
|
+
this.map.delete(key);
|
|
20
|
+
this.map.set(key, value);
|
|
21
|
+
return value;
|
|
22
|
+
}
|
|
23
|
+
set(key, value) {
|
|
24
|
+
this.map.delete(key);
|
|
25
|
+
if (this.map.size >= this.maxSize) {
|
|
26
|
+
const oldest = this.map.keys().next().value;
|
|
27
|
+
if (oldest !== void 0) {
|
|
28
|
+
this.map.delete(oldest);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
this.map.set(key, value);
|
|
32
|
+
}
|
|
33
|
+
has(key) {
|
|
34
|
+
return this.map.has(key);
|
|
35
|
+
}
|
|
36
|
+
clear() {
|
|
37
|
+
this.map.clear();
|
|
38
|
+
}
|
|
39
|
+
get size() {
|
|
40
|
+
return this.map.size;
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
function getPostDate(post) {
|
|
44
|
+
return post.frontmatter?.date || post.metadata?.createdAt || "";
|
|
45
|
+
}
|
|
46
|
+
function comparePostsFeaturedDate(a, b) {
|
|
47
|
+
const aFeatured = a.frontmatter?.featured === true;
|
|
48
|
+
const bFeatured = b.frontmatter?.featured === true;
|
|
49
|
+
if (aFeatured && !bFeatured) return -1;
|
|
50
|
+
if (!aFeatured && bFeatured) return 1;
|
|
51
|
+
const aDate = new Date(getPostDate(a) || 0);
|
|
52
|
+
const bDate = new Date(getPostDate(b) || 0);
|
|
53
|
+
return bDate.getTime() - aDate.getTime();
|
|
54
|
+
}
|
|
55
|
+
var PostStore = class {
|
|
56
|
+
posts;
|
|
57
|
+
bySlug;
|
|
58
|
+
byHash;
|
|
59
|
+
byPath;
|
|
60
|
+
cache;
|
|
61
|
+
_slugMap;
|
|
62
|
+
_slugs;
|
|
63
|
+
constructor(dir, cache) {
|
|
64
|
+
this.cache = cache;
|
|
65
|
+
const postsPath = join(dir, "posts.json");
|
|
66
|
+
if (!existsSync(postsPath)) {
|
|
67
|
+
throw new FolderBlogError(
|
|
68
|
+
`posts.json not found in ${dir} \u2014 did you run the build?`
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
let data;
|
|
72
|
+
try {
|
|
73
|
+
data = JSON.parse(readFileSync(postsPath, "utf-8"));
|
|
74
|
+
} catch (e) {
|
|
75
|
+
throw new FolderBlogError(
|
|
76
|
+
`Failed to parse posts.json in ${dir}: ${e instanceof Error ? e.message : String(e)}`
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
this.posts = data.sort((a, b) => {
|
|
80
|
+
return getPostDate(b).localeCompare(getPostDate(a));
|
|
81
|
+
});
|
|
82
|
+
this.bySlug = /* @__PURE__ */ new Map();
|
|
83
|
+
this.byHash = /* @__PURE__ */ new Map();
|
|
84
|
+
this.byPath = /* @__PURE__ */ new Map();
|
|
85
|
+
for (const post of this.posts) {
|
|
86
|
+
this.bySlug.set(post.slug, post);
|
|
87
|
+
this.byHash.set(post.hash, post);
|
|
88
|
+
this.byPath.set(post.originalPath, post);
|
|
89
|
+
}
|
|
90
|
+
const defaultSlugMap = {};
|
|
91
|
+
for (const post of this.posts) defaultSlugMap[post.slug] = post.hash;
|
|
92
|
+
this._slugMap = loadJsonFile(join(dir, "posts-slug-map.json"), defaultSlugMap);
|
|
93
|
+
this._slugs = Object.keys(this._slugMap);
|
|
94
|
+
}
|
|
95
|
+
/** Resolve a slug or hash to a hash, using the built-in indexes */
|
|
96
|
+
resolveHash(slugOrHash) {
|
|
97
|
+
if (this.byHash.has(slugOrHash)) return slugOrHash;
|
|
98
|
+
const post = this.bySlug.get(slugOrHash);
|
|
99
|
+
return post?.hash;
|
|
100
|
+
}
|
|
101
|
+
list() {
|
|
102
|
+
return this.posts;
|
|
103
|
+
}
|
|
104
|
+
lookup(cachePrefix, map, key) {
|
|
105
|
+
const cacheKey = `${cachePrefix}:${key}`;
|
|
106
|
+
if (this.cache) {
|
|
107
|
+
const cached = this.cache.get(cacheKey);
|
|
108
|
+
if (cached) return cached;
|
|
109
|
+
}
|
|
110
|
+
const post = map.get(key);
|
|
111
|
+
if (!post) throw new NotFoundError("Post", key);
|
|
112
|
+
if (this.cache) this.cache.set(cacheKey, post);
|
|
113
|
+
return post;
|
|
114
|
+
}
|
|
115
|
+
get(slug) {
|
|
116
|
+
return this.lookup("slug", this.bySlug, slug);
|
|
117
|
+
}
|
|
118
|
+
getByHash(hash) {
|
|
119
|
+
return this.lookup("hash", this.byHash, hash);
|
|
120
|
+
}
|
|
121
|
+
getByPath(path) {
|
|
122
|
+
return this.lookup("path", this.byPath, path);
|
|
123
|
+
}
|
|
124
|
+
recent(count = 10) {
|
|
125
|
+
return this.posts.slice(0, count);
|
|
126
|
+
}
|
|
127
|
+
get count() {
|
|
128
|
+
return this.posts.length;
|
|
129
|
+
}
|
|
130
|
+
/** Slug → hash mapping */
|
|
131
|
+
get slugMap() {
|
|
132
|
+
return this._slugMap;
|
|
133
|
+
}
|
|
134
|
+
/** All available slugs (cached) */
|
|
135
|
+
slugs() {
|
|
136
|
+
return this._slugs;
|
|
137
|
+
}
|
|
138
|
+
/** Query posts by frontmatter values */
|
|
139
|
+
where(query) {
|
|
140
|
+
return this.posts.filter((post) => {
|
|
141
|
+
for (const [key, value] of Object.entries(query)) {
|
|
142
|
+
const fm = post.frontmatter?.[key];
|
|
143
|
+
if (fm !== value) return false;
|
|
144
|
+
}
|
|
145
|
+
return true;
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
/** Shorthand for where({ featured: true }) */
|
|
149
|
+
featured() {
|
|
150
|
+
return this.where({ featured: true });
|
|
151
|
+
}
|
|
152
|
+
/** Published posts: not draft, not hidden, not future-dated */
|
|
153
|
+
published() {
|
|
154
|
+
const now = /* @__PURE__ */ new Date();
|
|
155
|
+
return this.posts.filter((post) => {
|
|
156
|
+
if (post.frontmatter?.draft === true) return false;
|
|
157
|
+
if (post.frontmatter?.hidden === true) return false;
|
|
158
|
+
const dateStr = post.frontmatter?.date;
|
|
159
|
+
if (dateStr) {
|
|
160
|
+
if (new Date(dateStr).getTime() > now.getTime()) return false;
|
|
161
|
+
}
|
|
162
|
+
return true;
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
/** Sort posts: featured first, then by date descending */
|
|
166
|
+
sorted(posts) {
|
|
167
|
+
return [...posts ?? this.posts].sort(comparePostsFeaturedDate);
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
function extractSection(post) {
|
|
171
|
+
const path = post.originalPath || "";
|
|
172
|
+
const parts = path.split("/");
|
|
173
|
+
if (parts.length >= 2) {
|
|
174
|
+
return parts[1]?.replace(/^\d+-/, "") || "general";
|
|
175
|
+
}
|
|
176
|
+
return "general";
|
|
177
|
+
}
|
|
178
|
+
function isScheduled(post) {
|
|
179
|
+
const dateStr = post.frontmatter?.date;
|
|
180
|
+
if (!dateStr) return false;
|
|
181
|
+
return new Date(dateStr).getTime() > Date.now();
|
|
182
|
+
}
|
|
183
|
+
function isDraft(post) {
|
|
184
|
+
return post.frontmatter?.draft === true;
|
|
185
|
+
}
|
|
186
|
+
function isHidden(post) {
|
|
187
|
+
return post.frontmatter?.hidden === true;
|
|
188
|
+
}
|
|
189
|
+
function isPublished(post) {
|
|
190
|
+
return !isDraft(post) && !isHidden(post) && !isScheduled(post);
|
|
191
|
+
}
|
|
192
|
+
var DEFAULT_BOOST = {
|
|
193
|
+
title: 3,
|
|
194
|
+
slug: 2,
|
|
195
|
+
excerpt: 2
|
|
196
|
+
};
|
|
197
|
+
var SearchEngine = class {
|
|
198
|
+
index;
|
|
199
|
+
postsBySlug;
|
|
200
|
+
defaultFuzzy;
|
|
201
|
+
defaultPrefix;
|
|
202
|
+
defaultBoost;
|
|
203
|
+
constructor(posts, options) {
|
|
204
|
+
this.defaultBoost = options?.boost ?? DEFAULT_BOOST;
|
|
205
|
+
this.defaultFuzzy = options?.fuzzy ?? 0.2;
|
|
206
|
+
this.defaultPrefix = options?.prefix ?? true;
|
|
207
|
+
this.postsBySlug = /* @__PURE__ */ new Map();
|
|
208
|
+
for (const post of posts) {
|
|
209
|
+
this.postsBySlug.set(post.slug, post);
|
|
210
|
+
}
|
|
211
|
+
this.index = new MiniSearch({
|
|
212
|
+
fields: ["title", "slug", "excerpt", "plainText"],
|
|
213
|
+
storeFields: ["title", "slug", "excerpt"],
|
|
214
|
+
idField: "slug",
|
|
215
|
+
searchOptions: {
|
|
216
|
+
boost: this.defaultBoost,
|
|
217
|
+
fuzzy: this.defaultFuzzy,
|
|
218
|
+
prefix: this.defaultPrefix
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
this.index.addAll(
|
|
222
|
+
posts.map((p) => ({
|
|
223
|
+
slug: p.slug,
|
|
224
|
+
title: p.title,
|
|
225
|
+
excerpt: p.excerpt,
|
|
226
|
+
plainText: p.plainText
|
|
227
|
+
}))
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
search(query, options) {
|
|
231
|
+
if (!query.trim()) return [];
|
|
232
|
+
const results = this.index.search(query, {
|
|
233
|
+
boost: options?.boost ?? this.defaultBoost,
|
|
234
|
+
fuzzy: options?.fuzzy ?? this.defaultFuzzy,
|
|
235
|
+
prefix: options?.prefix ?? this.defaultPrefix
|
|
236
|
+
});
|
|
237
|
+
const limit = options?.limit ?? 10;
|
|
238
|
+
return results.slice(0, limit).map((r) => ({
|
|
239
|
+
slug: r.id,
|
|
240
|
+
title: r.title,
|
|
241
|
+
excerpt: r.excerpt,
|
|
242
|
+
score: r.score
|
|
243
|
+
}));
|
|
244
|
+
}
|
|
245
|
+
autoSuggest(term, limit = 5) {
|
|
246
|
+
if (!term.trim()) return [];
|
|
247
|
+
const suggestions = this.index.autoSuggest(term, {
|
|
248
|
+
boost: this.defaultBoost,
|
|
249
|
+
fuzzy: this.defaultFuzzy,
|
|
250
|
+
prefix: this.defaultPrefix
|
|
251
|
+
});
|
|
252
|
+
const seen = /* @__PURE__ */ new Set();
|
|
253
|
+
const results = [];
|
|
254
|
+
for (const suggestion of suggestions) {
|
|
255
|
+
if (results.length >= limit) break;
|
|
256
|
+
const searchResults = this.index.search(suggestion.suggestion, {
|
|
257
|
+
boost: this.defaultBoost,
|
|
258
|
+
fuzzy: false,
|
|
259
|
+
prefix: true
|
|
260
|
+
});
|
|
261
|
+
for (const r of searchResults) {
|
|
262
|
+
if (results.length >= limit) break;
|
|
263
|
+
const slug = r.id;
|
|
264
|
+
if (seen.has(slug)) continue;
|
|
265
|
+
seen.add(slug);
|
|
266
|
+
const post = this.postsBySlug.get(slug);
|
|
267
|
+
if (post) {
|
|
268
|
+
results.push({ slug, title: post.title });
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
return results;
|
|
273
|
+
}
|
|
274
|
+
};
|
|
275
|
+
var SimilarityService = class {
|
|
276
|
+
data;
|
|
277
|
+
lookup;
|
|
278
|
+
defaultTopN;
|
|
279
|
+
constructor(dir, lookup, options) {
|
|
280
|
+
this.defaultTopN = options?.topN ?? 5;
|
|
281
|
+
this.lookup = lookup;
|
|
282
|
+
const simPath = join(dir, "similarity.json");
|
|
283
|
+
const fileExists = existsSync(simPath);
|
|
284
|
+
const shouldLoad = options?.enabled ?? fileExists;
|
|
285
|
+
this.data = shouldLoad && fileExists ? loadJsonFile(simPath, null) : null;
|
|
286
|
+
}
|
|
287
|
+
get available() {
|
|
288
|
+
return this.data !== null;
|
|
289
|
+
}
|
|
290
|
+
getSimilar(slugOrHash, count) {
|
|
291
|
+
if (!this.data) return [];
|
|
292
|
+
const n = count ?? this.defaultTopN;
|
|
293
|
+
const hash = this.lookup.resolveHash(slugOrHash);
|
|
294
|
+
if (!hash) return [];
|
|
295
|
+
const entries = this.data[hash];
|
|
296
|
+
if (!entries) return [];
|
|
297
|
+
return entries.slice(0, n).map((entry) => {
|
|
298
|
+
const post = this.lookup.byHash.get(entry.hash);
|
|
299
|
+
if (!post) return null;
|
|
300
|
+
return { post, score: entry.score };
|
|
301
|
+
}).filter((r) => r !== null);
|
|
302
|
+
}
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
// src/local/tags.ts
|
|
306
|
+
var CountedIndex = class {
|
|
307
|
+
counts = /* @__PURE__ */ new Map();
|
|
308
|
+
posts = /* @__PURE__ */ new Map();
|
|
309
|
+
add(key, post) {
|
|
310
|
+
const normalized = key.trim().toLowerCase();
|
|
311
|
+
if (!normalized) return;
|
|
312
|
+
this.counts.set(normalized, (this.counts.get(normalized) ?? 0) + 1);
|
|
313
|
+
const arr = this.posts.get(normalized);
|
|
314
|
+
if (arr) {
|
|
315
|
+
arr.push(post);
|
|
316
|
+
} else {
|
|
317
|
+
this.posts.set(normalized, [post]);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
lookup(key) {
|
|
321
|
+
return this.posts.get(key.trim().toLowerCase()) ?? [];
|
|
322
|
+
}
|
|
323
|
+
};
|
|
324
|
+
var TagIndex = class {
|
|
325
|
+
tags;
|
|
326
|
+
cats;
|
|
327
|
+
constructor(posts) {
|
|
328
|
+
this.tags = new CountedIndex();
|
|
329
|
+
this.cats = new CountedIndex();
|
|
330
|
+
for (const post of posts) {
|
|
331
|
+
const fm = post.frontmatter;
|
|
332
|
+
for (const tag of Array.isArray(fm?.tags) ? fm.tags : []) {
|
|
333
|
+
this.tags.add(tag, post);
|
|
334
|
+
}
|
|
335
|
+
const categories = Array.isArray(fm?.categories) ? [...fm.categories] : [];
|
|
336
|
+
if (typeof fm?.category === "string" && fm.category.trim()) {
|
|
337
|
+
categories.push(fm.category);
|
|
338
|
+
}
|
|
339
|
+
for (const cat of categories) {
|
|
340
|
+
this.cats.add(cat, post);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
listTags() {
|
|
345
|
+
return Array.from(this.tags.counts.entries()).map(([tag, count]) => ({ tag, count })).sort((a, b) => b.count - a.count);
|
|
346
|
+
}
|
|
347
|
+
postsByTag(tag) {
|
|
348
|
+
return this.tags.lookup(tag);
|
|
349
|
+
}
|
|
350
|
+
listCategories() {
|
|
351
|
+
return Array.from(this.cats.counts.entries()).map(([category, count]) => ({ category, count })).sort((a, b) => b.count - a.count);
|
|
352
|
+
}
|
|
353
|
+
postsByCategory(category) {
|
|
354
|
+
return this.cats.lookup(category);
|
|
355
|
+
}
|
|
356
|
+
get tagCount() {
|
|
357
|
+
return this.tags.counts.size;
|
|
358
|
+
}
|
|
359
|
+
get categoryCount() {
|
|
360
|
+
return this.cats.counts.size;
|
|
361
|
+
}
|
|
362
|
+
};
|
|
363
|
+
var MediaStore = class {
|
|
364
|
+
items;
|
|
365
|
+
byPath;
|
|
366
|
+
constructor(dir) {
|
|
367
|
+
this.items = loadJsonFile(join(dir, "media.json"), []);
|
|
368
|
+
this.byPath = /* @__PURE__ */ new Map();
|
|
369
|
+
for (const item of this.items) {
|
|
370
|
+
this.byPath.set(item.originalPath, item);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
list() {
|
|
374
|
+
return this.items;
|
|
375
|
+
}
|
|
376
|
+
get(path) {
|
|
377
|
+
const item = this.byPath.get(path);
|
|
378
|
+
if (!item) throw new NotFoundError("Media", path);
|
|
379
|
+
return item;
|
|
380
|
+
}
|
|
381
|
+
get count() {
|
|
382
|
+
return this.items.length;
|
|
383
|
+
}
|
|
384
|
+
};
|
|
385
|
+
var GraphService = class {
|
|
386
|
+
/** hash → set of hashes this post links to */
|
|
387
|
+
outgoing;
|
|
388
|
+
/** hash → set of hashes that link to this post */
|
|
389
|
+
incoming;
|
|
390
|
+
lookup;
|
|
391
|
+
available;
|
|
392
|
+
constructor(dir, lookup) {
|
|
393
|
+
this.lookup = lookup;
|
|
394
|
+
this.outgoing = /* @__PURE__ */ new Map();
|
|
395
|
+
this.incoming = /* @__PURE__ */ new Map();
|
|
396
|
+
const posts = lookup.list();
|
|
397
|
+
let hasLinks = false;
|
|
398
|
+
for (const post of posts) {
|
|
399
|
+
if (post.links && post.links.length > 0) {
|
|
400
|
+
hasLinks = true;
|
|
401
|
+
const out = new Set(post.links);
|
|
402
|
+
this.outgoing.set(post.hash, out);
|
|
403
|
+
for (const targetHash of out) {
|
|
404
|
+
let inc = this.incoming.get(targetHash);
|
|
405
|
+
if (!inc) {
|
|
406
|
+
inc = /* @__PURE__ */ new Set();
|
|
407
|
+
this.incoming.set(targetHash, inc);
|
|
408
|
+
}
|
|
409
|
+
inc.add(post.hash);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
const graphPath = join(dir, "graph.json");
|
|
414
|
+
if (existsSync(graphPath)) {
|
|
415
|
+
try {
|
|
416
|
+
const graphData = JSON.parse(readFileSync(graphPath, "utf-8"));
|
|
417
|
+
for (const edge of graphData.edges) {
|
|
418
|
+
if (edge.type === "POST_LINKS_TO_POST") {
|
|
419
|
+
hasLinks = true;
|
|
420
|
+
let out = this.outgoing.get(edge.source);
|
|
421
|
+
if (!out) {
|
|
422
|
+
out = /* @__PURE__ */ new Set();
|
|
423
|
+
this.outgoing.set(edge.source, out);
|
|
424
|
+
}
|
|
425
|
+
out.add(edge.target);
|
|
426
|
+
let inc = this.incoming.get(edge.target);
|
|
427
|
+
if (!inc) {
|
|
428
|
+
inc = /* @__PURE__ */ new Set();
|
|
429
|
+
this.incoming.set(edge.target, inc);
|
|
430
|
+
}
|
|
431
|
+
inc.add(edge.source);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
} catch {
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
this.available = hasLinks;
|
|
438
|
+
}
|
|
439
|
+
getRelated(slugOrHash, count = 5, fallbackSimilar) {
|
|
440
|
+
const hash = this.lookup.resolveHash(slugOrHash);
|
|
441
|
+
if (!hash) return [];
|
|
442
|
+
const sourcePost = this.lookup.byHash.get(hash);
|
|
443
|
+
const sourceTags = new Set(
|
|
444
|
+
sourcePost.frontmatter?.tags ?? []
|
|
445
|
+
);
|
|
446
|
+
const outgoing = this.outgoing.get(hash) ?? /* @__PURE__ */ new Set();
|
|
447
|
+
const incoming = this.incoming.get(hash) ?? /* @__PURE__ */ new Set();
|
|
448
|
+
if (outgoing.size === 0 && incoming.size === 0) {
|
|
449
|
+
if (fallbackSimilar) {
|
|
450
|
+
return fallbackSimilar(slugOrHash, count).map((r) => ({
|
|
451
|
+
post: r.post,
|
|
452
|
+
score: r.score,
|
|
453
|
+
reasons: ["similarity"]
|
|
454
|
+
}));
|
|
455
|
+
}
|
|
456
|
+
return [];
|
|
457
|
+
}
|
|
458
|
+
const scores = /* @__PURE__ */ new Map();
|
|
459
|
+
const addScore = (targetHash, points, reason) => {
|
|
460
|
+
if (targetHash === hash) return;
|
|
461
|
+
if (!this.lookup.byHash.has(targetHash)) return;
|
|
462
|
+
const entry = scores.get(targetHash) ?? { score: 0, reasons: [] };
|
|
463
|
+
entry.score += points;
|
|
464
|
+
entry.reasons.push(reason);
|
|
465
|
+
scores.set(targetHash, entry);
|
|
466
|
+
};
|
|
467
|
+
for (const target of outgoing) {
|
|
468
|
+
if (incoming.has(target)) {
|
|
469
|
+
addScore(target, 3, "mutual-link");
|
|
470
|
+
} else {
|
|
471
|
+
addScore(target, 2, "outgoing-link");
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
for (const source of incoming) {
|
|
475
|
+
if (!outgoing.has(source)) {
|
|
476
|
+
addScore(source, 1, "incoming-link");
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
if (sourceTags.size > 0) {
|
|
480
|
+
for (const post of this.lookup.list()) {
|
|
481
|
+
if (post.hash === hash) continue;
|
|
482
|
+
const postTags = post.frontmatter?.tags ?? [];
|
|
483
|
+
let shared = 0;
|
|
484
|
+
for (const tag of postTags) {
|
|
485
|
+
if (sourceTags.has(tag)) shared++;
|
|
486
|
+
}
|
|
487
|
+
if (shared > 0) {
|
|
488
|
+
addScore(post.hash, shared * 0.5, `shared-tags(${shared})`);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
return Array.from(scores.entries()).sort((a, b) => b[1].score - a[1].score).slice(0, count).map(([targetHash, { score, reasons }]) => ({
|
|
493
|
+
post: this.lookup.byHash.get(targetHash),
|
|
494
|
+
score,
|
|
495
|
+
reasons
|
|
496
|
+
}));
|
|
497
|
+
}
|
|
498
|
+
};
|
|
499
|
+
|
|
500
|
+
// src/local/issues.ts
|
|
501
|
+
function asArray(value) {
|
|
502
|
+
return Array.isArray(value) ? value : [value];
|
|
503
|
+
}
|
|
504
|
+
var IssueIndex = class {
|
|
505
|
+
report;
|
|
506
|
+
constructor(report) {
|
|
507
|
+
this.report = report;
|
|
508
|
+
}
|
|
509
|
+
/** All issues */
|
|
510
|
+
all() {
|
|
511
|
+
return this.report.issues;
|
|
512
|
+
}
|
|
513
|
+
/** The summary (counts, affected files, etc.) */
|
|
514
|
+
get summary() {
|
|
515
|
+
return this.report.summary;
|
|
516
|
+
}
|
|
517
|
+
/** Build metadata (start/end time, version) */
|
|
518
|
+
get metadata() {
|
|
519
|
+
return this.report.metadata;
|
|
520
|
+
}
|
|
521
|
+
/** Total issue count */
|
|
522
|
+
get count() {
|
|
523
|
+
return this.report.summary.totalIssues;
|
|
524
|
+
}
|
|
525
|
+
/** Whether there are any errors */
|
|
526
|
+
get hasErrors() {
|
|
527
|
+
return this.report.summary.errorCount > 0;
|
|
528
|
+
}
|
|
529
|
+
/** Whether there are any warnings */
|
|
530
|
+
get hasWarnings() {
|
|
531
|
+
return this.report.summary.warningCount > 0;
|
|
532
|
+
}
|
|
533
|
+
// --------------------------------------------------------------------------
|
|
534
|
+
// Filter by severity
|
|
535
|
+
// --------------------------------------------------------------------------
|
|
536
|
+
errors() {
|
|
537
|
+
return this.report.issues.filter((i) => i.severity === "error");
|
|
538
|
+
}
|
|
539
|
+
warnings() {
|
|
540
|
+
return this.report.issues.filter((i) => i.severity === "warning");
|
|
541
|
+
}
|
|
542
|
+
info() {
|
|
543
|
+
return this.report.issues.filter((i) => i.severity === "info");
|
|
544
|
+
}
|
|
545
|
+
// --------------------------------------------------------------------------
|
|
546
|
+
// Filter by category (typed returns)
|
|
547
|
+
// --------------------------------------------------------------------------
|
|
548
|
+
brokenLinks() {
|
|
549
|
+
return this.report.issues.filter(isBrokenLinkIssue);
|
|
550
|
+
}
|
|
551
|
+
slugConflicts() {
|
|
552
|
+
return this.report.issues.filter(isSlugConflictIssue);
|
|
553
|
+
}
|
|
554
|
+
missingMedia() {
|
|
555
|
+
return this.report.issues.filter(isMissingMediaIssue);
|
|
556
|
+
}
|
|
557
|
+
/** Filter by category name */
|
|
558
|
+
byCategory(category) {
|
|
559
|
+
return this.report.issues.filter((i) => i.category === category);
|
|
560
|
+
}
|
|
561
|
+
/** Filter by module name */
|
|
562
|
+
byModule(module) {
|
|
563
|
+
return this.report.issues.filter((i) => i.module === module);
|
|
564
|
+
}
|
|
565
|
+
/** Filter by file path */
|
|
566
|
+
byFile(filePath) {
|
|
567
|
+
return this.report.issues.filter((i) => i.filePath === filePath);
|
|
568
|
+
}
|
|
569
|
+
/** Generic filter with multiple criteria (AND) */
|
|
570
|
+
filter(options) {
|
|
571
|
+
let filtered = [...this.report.issues];
|
|
572
|
+
if (options.severity) {
|
|
573
|
+
const vals = asArray(options.severity);
|
|
574
|
+
filtered = filtered.filter((i) => vals.includes(i.severity));
|
|
575
|
+
}
|
|
576
|
+
if (options.category) {
|
|
577
|
+
const vals = asArray(options.category);
|
|
578
|
+
filtered = filtered.filter((i) => vals.includes(i.category));
|
|
579
|
+
}
|
|
580
|
+
if (options.module) {
|
|
581
|
+
const vals = asArray(options.module);
|
|
582
|
+
filtered = filtered.filter((i) => vals.includes(i.module));
|
|
583
|
+
}
|
|
584
|
+
if (options.filePath) {
|
|
585
|
+
const vals = asArray(options.filePath);
|
|
586
|
+
filtered = filtered.filter((i) => i.filePath != null && vals.includes(i.filePath));
|
|
587
|
+
}
|
|
588
|
+
return filtered;
|
|
589
|
+
}
|
|
590
|
+
/** Get affected file paths */
|
|
591
|
+
affectedFiles() {
|
|
592
|
+
const files = /* @__PURE__ */ new Set();
|
|
593
|
+
for (const issue of this.report.issues) {
|
|
594
|
+
if (issue.filePath) files.add(issue.filePath);
|
|
595
|
+
}
|
|
596
|
+
return [...files];
|
|
597
|
+
}
|
|
598
|
+
/** Human-readable summary string */
|
|
599
|
+
summaryString() {
|
|
600
|
+
const { errorCount, warningCount, infoCount } = this.report.summary;
|
|
601
|
+
const parts = [];
|
|
602
|
+
if (errorCount > 0) parts.push(`${errorCount} error${errorCount > 1 ? "s" : ""}`);
|
|
603
|
+
if (warningCount > 0) parts.push(`${warningCount} warning${warningCount > 1 ? "s" : ""}`);
|
|
604
|
+
if (infoCount > 0) parts.push(`${infoCount} info`);
|
|
605
|
+
return parts.length > 0 ? `${parts.join(", ")}` : "No issues";
|
|
606
|
+
}
|
|
607
|
+
};
|
|
608
|
+
|
|
609
|
+
// src/local/view.ts
|
|
610
|
+
function toPost(p) {
|
|
611
|
+
return {
|
|
612
|
+
fileName: p.fileName,
|
|
613
|
+
slug: p.slug,
|
|
614
|
+
hash: p.hash,
|
|
615
|
+
frontmatter: {
|
|
616
|
+
...p.frontmatter,
|
|
617
|
+
title: p.frontmatter?.title || p.title
|
|
618
|
+
},
|
|
619
|
+
firstParagraphText: p.excerpt ?? "",
|
|
620
|
+
plain: p.plainText ?? "",
|
|
621
|
+
html: p.content ?? "",
|
|
622
|
+
toc: (p.toc ?? []).map((t) => ({
|
|
623
|
+
id: t.slug,
|
|
624
|
+
title: t.text,
|
|
625
|
+
depth: t.depth
|
|
626
|
+
})),
|
|
627
|
+
originalFilePath: p.originalPath,
|
|
628
|
+
wordCount: p.wordCount ?? 0,
|
|
629
|
+
firstImage: p.firstImage,
|
|
630
|
+
cover: p.frontmatter?.cover ?? void 0,
|
|
631
|
+
links: p.links
|
|
632
|
+
};
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// src/local/index.ts
|
|
636
|
+
var FolderBlogLocal = class {
|
|
637
|
+
postStore;
|
|
638
|
+
searchEngine;
|
|
639
|
+
similarityService;
|
|
640
|
+
graphService;
|
|
641
|
+
tagIndex;
|
|
642
|
+
mediaStore;
|
|
643
|
+
issueReport;
|
|
644
|
+
issueIndex;
|
|
645
|
+
debug;
|
|
646
|
+
mapPostFn;
|
|
647
|
+
constructor(options) {
|
|
648
|
+
const dir = resolve(options.dir);
|
|
649
|
+
this.debug = options.debug ?? false;
|
|
650
|
+
this.mapPostFn = options.mapPost ?? null;
|
|
651
|
+
if (this.debug) console.log(`[folderblog/local] Loading from ${dir}`);
|
|
652
|
+
const cache = options.cache?.enabled ?? true ? new LRUCache(options.cache?.maxSize ?? 500) : null;
|
|
653
|
+
this.postStore = new PostStore(dir, cache);
|
|
654
|
+
const allPosts = this.postStore.list();
|
|
655
|
+
if (this.debug) console.log(`[folderblog/local] Loaded ${allPosts.length} posts`);
|
|
656
|
+
const searchEnabled = options.search?.enabled ?? true;
|
|
657
|
+
this.searchEngine = searchEnabled ? new SearchEngine(allPosts, {
|
|
658
|
+
boost: options.search?.boost,
|
|
659
|
+
fuzzy: options.search?.fuzzy,
|
|
660
|
+
prefix: options.search?.prefix
|
|
661
|
+
}) : null;
|
|
662
|
+
this.similarityService = new SimilarityService(dir, this.postStore, {
|
|
663
|
+
enabled: options.similarity?.enabled,
|
|
664
|
+
topN: options.similarity?.topN
|
|
665
|
+
});
|
|
666
|
+
this.graphService = new GraphService(dir, this.postStore);
|
|
667
|
+
this.tagIndex = new TagIndex(allPosts);
|
|
668
|
+
this.mediaStore = new MediaStore(dir);
|
|
669
|
+
this.issueReport = loadJsonFile(join(dir, "processor-issues.json"), {
|
|
670
|
+
issues: [],
|
|
671
|
+
summary: { totalIssues: 0, errorCount: 0, warningCount: 0, infoCount: 0, filesAffected: 0, categoryCounts: {}, moduleCounts: {} },
|
|
672
|
+
metadata: { processStartTime: "", processEndTime: "" }
|
|
673
|
+
});
|
|
674
|
+
this.issueIndex = new IssueIndex(this.issueReport);
|
|
675
|
+
const postStore = this.postStore;
|
|
676
|
+
const tagIdx = this.tagIndex;
|
|
677
|
+
const mediaStr = this.mediaStore;
|
|
678
|
+
const mapPost = this.mapPostFn;
|
|
679
|
+
const mp = mapPost ?? ((post) => post);
|
|
680
|
+
this.posts = {
|
|
681
|
+
list: () => postStore.list().map(mp),
|
|
682
|
+
get: (slug) => mp(postStore.get(slug)),
|
|
683
|
+
getByHash: (hash) => mp(postStore.getByHash(hash)),
|
|
684
|
+
getByPath: (path) => mp(postStore.getByPath(path)),
|
|
685
|
+
recent: (count) => postStore.recent(count).map(mp),
|
|
686
|
+
filter: (predicate) => postStore.list().map(mp).filter(predicate),
|
|
687
|
+
where: (query) => postStore.where(query).map(mp),
|
|
688
|
+
featured: () => postStore.featured().map(mp),
|
|
689
|
+
published: () => postStore.published().map(mp),
|
|
690
|
+
sorted: (posts) => {
|
|
691
|
+
if (posts) {
|
|
692
|
+
return [...posts].sort(
|
|
693
|
+
(a, b) => comparePostsFeaturedDate(a, b)
|
|
694
|
+
);
|
|
695
|
+
}
|
|
696
|
+
return postStore.sorted().map(mp);
|
|
697
|
+
},
|
|
698
|
+
slugs: () => postStore.slugs(),
|
|
699
|
+
get count() {
|
|
700
|
+
return postStore.count;
|
|
701
|
+
},
|
|
702
|
+
get slugMap() {
|
|
703
|
+
return postStore.slugMap;
|
|
704
|
+
}
|
|
705
|
+
};
|
|
706
|
+
this.tags = {
|
|
707
|
+
list: () => tagIdx.listTags(),
|
|
708
|
+
posts: (tag) => tagIdx.postsByTag(tag).map(mp)
|
|
709
|
+
};
|
|
710
|
+
this.categories = {
|
|
711
|
+
list: () => tagIdx.listCategories(),
|
|
712
|
+
posts: (category) => tagIdx.postsByCategory(category).map(mp)
|
|
713
|
+
};
|
|
714
|
+
this.media = {
|
|
715
|
+
list: () => mediaStr.list(),
|
|
716
|
+
get: (path) => mediaStr.get(path)
|
|
717
|
+
};
|
|
718
|
+
if (this.debug) {
|
|
719
|
+
console.log(`[folderblog/local] Search: ${searchEnabled ? "enabled" : "disabled"}`);
|
|
720
|
+
console.log(`[folderblog/local] Similarity: ${this.similarityService.available ? "available" : "unavailable"}`);
|
|
721
|
+
console.log(`[folderblog/local] Graph: ${this.graphService.available ? "available" : "unavailable"}`);
|
|
722
|
+
console.log(`[folderblog/local] Tags: ${this.tagIndex.tagCount}, Categories: ${this.tagIndex.categoryCount}`);
|
|
723
|
+
console.log(`[folderblog/local] Media: ${this.mediaStore.count}`);
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
search(query, options) {
|
|
727
|
+
if (!this.searchEngine) return [];
|
|
728
|
+
return this.searchEngine.search(query, options);
|
|
729
|
+
}
|
|
730
|
+
searchAutocomplete(term, limit) {
|
|
731
|
+
if (!this.searchEngine) return [];
|
|
732
|
+
return this.searchEngine.autoSuggest(term, limit);
|
|
733
|
+
}
|
|
734
|
+
similar(slugOrHash, count) {
|
|
735
|
+
return this.similarityService.getSimilar(slugOrHash, count);
|
|
736
|
+
}
|
|
737
|
+
related(slugOrHash, count) {
|
|
738
|
+
return this.graphService.getRelated(
|
|
739
|
+
slugOrHash,
|
|
740
|
+
count,
|
|
741
|
+
(s, c) => this.similar(s, c)
|
|
742
|
+
);
|
|
743
|
+
}
|
|
744
|
+
// Namespace objects — initialized in constructor
|
|
745
|
+
posts;
|
|
746
|
+
tags;
|
|
747
|
+
categories;
|
|
748
|
+
media;
|
|
749
|
+
// --- Stats ---
|
|
750
|
+
get stats() {
|
|
751
|
+
return {
|
|
752
|
+
posts: this.postStore.count,
|
|
753
|
+
media: this.mediaStore.count,
|
|
754
|
+
tags: this.tagIndex.tagCount,
|
|
755
|
+
categories: this.tagIndex.categoryCount,
|
|
756
|
+
hasSearch: this.searchEngine !== null,
|
|
757
|
+
hasSimilarity: this.similarityService.available
|
|
758
|
+
};
|
|
759
|
+
}
|
|
760
|
+
// --- Issues ---
|
|
761
|
+
get issues() {
|
|
762
|
+
return this.issueReport;
|
|
763
|
+
}
|
|
764
|
+
get report() {
|
|
765
|
+
return this.issueIndex;
|
|
766
|
+
}
|
|
767
|
+
};
|
|
768
|
+
function createLocalClient(options) {
|
|
769
|
+
return new FolderBlogLocal(options);
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
export { IssueIndex, createLocalClient, extractSection, isDraft, isHidden, isPublished, isScheduled, toPost };
|