@binarynoir/vitepress-blog 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +491 -2
- package/dist/chunk-DICBXOTS.js +1001 -0
- package/dist/chunk-DICBXOTS.js.map +1 -0
- package/dist/client.cjs +176 -0
- package/dist/client.cjs.map +1 -0
- package/dist/client.d.cts +51 -0
- package/dist/client.d.ts +51 -0
- package/dist/client.js +139 -0
- package/dist/client.js.map +1 -0
- package/dist/index.cjs +930 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +13 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +17 -0
- package/dist/index.js.map +1 -0
- package/dist/plugin-BjbV6nDK.d.cts +190 -0
- package/dist/plugin-BjbV6nDK.d.ts +190 -0
- package/dist/style.css +418 -0
- package/dist/vitepress.cjs +1243 -0
- package/dist/vitepress.cjs.map +1 -0
- package/dist/vitepress.d.cts +81 -0
- package/dist/vitepress.d.ts +81 -0
- package/dist/vitepress.js +234 -0
- package/dist/vitepress.js.map +1 -0
- package/package.json +108 -4
|
@@ -0,0 +1,1001 @@
|
|
|
1
|
+
// src/collect.ts
|
|
2
|
+
function isPost(page) {
|
|
3
|
+
return page.post !== null;
|
|
4
|
+
}
|
|
5
|
+
function isIndex(page) {
|
|
6
|
+
return page.index !== null;
|
|
7
|
+
}
|
|
8
|
+
function dirname(relativePath) {
|
|
9
|
+
return relativePath.includes("/") ? relativePath.slice(0, relativePath.lastIndexOf("/")) : "";
|
|
10
|
+
}
|
|
11
|
+
function isInFolders(relativePath, folders) {
|
|
12
|
+
return folders.length === 0 || folders.some((folder) => !folder || relativePath.startsWith(`${folder}/`));
|
|
13
|
+
}
|
|
14
|
+
function folderDepth(index) {
|
|
15
|
+
return Math.max(0, ...index.index.folders.map((folder) => folder ? folder.split("/").length : 0));
|
|
16
|
+
}
|
|
17
|
+
function hasAnyTag(post, tags) {
|
|
18
|
+
if (tags.length === 0) return true;
|
|
19
|
+
const wanted = new Set(tags.map((tag) => tag.toLowerCase()));
|
|
20
|
+
return post.post.tags.some((tag) => wanted.has(tag.toLowerCase()));
|
|
21
|
+
}
|
|
22
|
+
function blogCoversPost(index, post, sections) {
|
|
23
|
+
if (index.index.scope === "section") return isInFolders(post.relativePath, index.index.folders);
|
|
24
|
+
if (!isInFolders(post.relativePath, index.index.folders)) return false;
|
|
25
|
+
if (post.post.scope === "site") return true;
|
|
26
|
+
return !sections.some((section) => isInFolders(post.relativePath, section.index.folders));
|
|
27
|
+
}
|
|
28
|
+
function compareDates(a, b) {
|
|
29
|
+
if (a && b) return b.getTime() - a.getTime();
|
|
30
|
+
return a ? -1 : b ? 1 : 0;
|
|
31
|
+
}
|
|
32
|
+
function compareByDate(a, b) {
|
|
33
|
+
return compareDates(a.post.date, b.post.date) || a.post.title.localeCompare(b.post.title);
|
|
34
|
+
}
|
|
35
|
+
function pinStickyPosts(posts, limit) {
|
|
36
|
+
const sticky = posts.filter((post) => post.post.sticky);
|
|
37
|
+
const pinned = new Set(limit ? sticky.slice(0, limit) : sticky);
|
|
38
|
+
return { pinned: [...pinned], rest: posts.filter((post) => !pinned.has(post)) };
|
|
39
|
+
}
|
|
40
|
+
function listBlogPosts(pages, index, options) {
|
|
41
|
+
const sections = pages.filter(isIndex).filter((page) => page.index.scope === "section");
|
|
42
|
+
let posts = pages.filter(isPost).filter((post) => options.drafts || !post.post.draft).filter((post) => blogCoversPost(index, post, sections) && hasAnyTag(post, index.index.tags)).sort(compareByDate);
|
|
43
|
+
if (options.stickyFirst) {
|
|
44
|
+
const { pinned, rest } = pinStickyPosts(posts, index.index.stickyLimit);
|
|
45
|
+
posts = [...pinned, ...rest];
|
|
46
|
+
}
|
|
47
|
+
return options.applyLimit && index.index.limit ? posts.slice(0, index.index.limit) : posts;
|
|
48
|
+
}
|
|
49
|
+
function findHomeBlog(pages, post, drafts) {
|
|
50
|
+
const listing = pages.filter(isIndex).filter((index) => listBlogPosts(pages, index, { drafts, stickyFirst: false, applyLimit: false }).includes(post));
|
|
51
|
+
const sections = listing.filter((index) => index.index.scope === "section");
|
|
52
|
+
if (sections.length) return deepestFirst(sections, post.relativePath)[0];
|
|
53
|
+
return listing[0] ?? null;
|
|
54
|
+
}
|
|
55
|
+
function deepestFirst(sections, relativePath) {
|
|
56
|
+
const isAbove = (index) => Number(isInFolders(relativePath, [dirname(index.relativePath)]));
|
|
57
|
+
return [...sections].sort((a, b) => folderDepth(b) - folderDepth(a) || isAbove(b) - isAbove(a));
|
|
58
|
+
}
|
|
59
|
+
function findBlogForPage(pages, relativePath) {
|
|
60
|
+
const indexes = pages.filter(isIndex);
|
|
61
|
+
const covering = indexes.filter((index) => isInFolders(relativePath, index.index.folders));
|
|
62
|
+
const sections = covering.filter((index) => index.index.scope === "section");
|
|
63
|
+
if (sections.length) return deepestFirst(sections, relativePath)[0];
|
|
64
|
+
const main = indexes.filter((index) => index.index.scope === "site");
|
|
65
|
+
return main.find((index) => covering.includes(index)) ?? main[0] ?? null;
|
|
66
|
+
}
|
|
67
|
+
function findBlogsListing(pages, post) {
|
|
68
|
+
return pages.filter(isIndex).filter(
|
|
69
|
+
(index) => listBlogPosts(pages, index, { drafts: false, stickyFirst: false, applyLimit: false }).includes(post)
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// src/util.ts
|
|
74
|
+
var HTML_ESCAPES = {
|
|
75
|
+
"&": "&",
|
|
76
|
+
"<": "<",
|
|
77
|
+
">": ">",
|
|
78
|
+
'"': """,
|
|
79
|
+
"'": "'"
|
|
80
|
+
};
|
|
81
|
+
function escapeHtml(value) {
|
|
82
|
+
return value.replace(/[&<>"']/g, (character) => HTML_ESCAPES[character]);
|
|
83
|
+
}
|
|
84
|
+
function escapeMarkdown(text) {
|
|
85
|
+
const escaped = text.replace(/[\\`*_[\]#|~]/g, "\\$&").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
86
|
+
return /[{}]/.test(escaped) ? `<span v-pre>${escaped}</span>` : escaped;
|
|
87
|
+
}
|
|
88
|
+
function escapeTemplateText(text) {
|
|
89
|
+
const escaped = escapeHtml(text);
|
|
90
|
+
return /[{}]/.test(escaped) ? `<span v-pre>${escaped}</span>` : escaped;
|
|
91
|
+
}
|
|
92
|
+
function isExternalUrl(url) {
|
|
93
|
+
return /^(?:[a-z][a-z\d+.-]*:|\/\/)/i.test(url);
|
|
94
|
+
}
|
|
95
|
+
function toMarkdownUrl(url) {
|
|
96
|
+
return encodeURI(url).replace(/[()]/g, (character) => character === "(" ? "%28" : "%29");
|
|
97
|
+
}
|
|
98
|
+
function toPageLink(text, routePath) {
|
|
99
|
+
return `[${escapeMarkdown(text)}](${toMarkdownUrl(`/${routePath}`)})`;
|
|
100
|
+
}
|
|
101
|
+
function toUrlPath(routePath, cleanUrls) {
|
|
102
|
+
return `/${routePath.replace(/(^|\/)index\.md$/, "$1").replace(/\.md$/, cleanUrls ? "" : ".html")}`;
|
|
103
|
+
}
|
|
104
|
+
function joinUrl(base, urlPath) {
|
|
105
|
+
return `${base.replace(/\/+$/, "")}/${urlPath.replace(/^\/+/, "")}`;
|
|
106
|
+
}
|
|
107
|
+
function toIsoDate(date) {
|
|
108
|
+
return date.toISOString().slice(0, 10);
|
|
109
|
+
}
|
|
110
|
+
function formatDate(date, locale, format) {
|
|
111
|
+
return new Intl.DateTimeFormat(locale, { timeZone: "UTC", ...format }).format(date);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// src/feed.ts
|
|
115
|
+
import { createHash } from "crypto";
|
|
116
|
+
import { readFileSync, statSync } from "fs";
|
|
117
|
+
import path from "path";
|
|
118
|
+
function toAbsoluteUrl(urlPath, site) {
|
|
119
|
+
return joinUrl(site.siteUrl, joinUrl(site.base, urlPath));
|
|
120
|
+
}
|
|
121
|
+
function toPageUrl(routePath, site) {
|
|
122
|
+
return toAbsoluteUrl(toUrlPath(routePath, site.cleanUrls), site);
|
|
123
|
+
}
|
|
124
|
+
function toFeedPath(index) {
|
|
125
|
+
return path.posix.join(path.posix.dirname(index.routePath), index.index.feed.path);
|
|
126
|
+
}
|
|
127
|
+
var IMAGE_TYPES = {
|
|
128
|
+
".avif": "image/avif",
|
|
129
|
+
".gif": "image/gif",
|
|
130
|
+
".jpeg": "image/jpeg",
|
|
131
|
+
".jpg": "image/jpeg",
|
|
132
|
+
".png": "image/png",
|
|
133
|
+
".svg": "image/svg+xml",
|
|
134
|
+
".webp": "image/webp"
|
|
135
|
+
};
|
|
136
|
+
function toImageType(file) {
|
|
137
|
+
return IMAGE_TYPES[path.extname(file).toLowerCase()];
|
|
138
|
+
}
|
|
139
|
+
function resolvePostImage(post, site) {
|
|
140
|
+
const { image } = post.post;
|
|
141
|
+
if (!image) return null;
|
|
142
|
+
if (isExternalUrl(image)) return { url: image.startsWith("//") ? `https:${image}` : image, type: toImageType(image) };
|
|
143
|
+
if (/^[@~]/.test(image)) return null;
|
|
144
|
+
const file = image.startsWith("/") ? path.join(site.publicDir, decodeURI(image)) : path.resolve(path.dirname(post.absolutePath), decodeURI(image));
|
|
145
|
+
let bytes;
|
|
146
|
+
try {
|
|
147
|
+
bytes = statSync(file).size;
|
|
148
|
+
} catch {
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
if (image.startsWith("/")) return { url: toAbsoluteUrl(image, site), file, bytes, type: toImageType(file) };
|
|
152
|
+
const hash = createHash("sha256").update(readFileSync(file)).digest("hex").slice(0, 8);
|
|
153
|
+
const extension = path.extname(file);
|
|
154
|
+
const outputPath = path.posix.join(site.imagesDir, `${path.basename(file, extension)}-${hash}${extension}`);
|
|
155
|
+
return { url: toAbsoluteUrl(outputPath, site), file, outputPath, bytes, type: toImageType(file) };
|
|
156
|
+
}
|
|
157
|
+
function escapeXml(value) {
|
|
158
|
+
return value.replace(/[&<>"']/g, (character) => `&#${character.charCodeAt(0)};`);
|
|
159
|
+
}
|
|
160
|
+
function element(name, value, attributes = "") {
|
|
161
|
+
return value ? `<${name}${attributes}>${escapeXml(value)}</${name}>` : "";
|
|
162
|
+
}
|
|
163
|
+
function renderFeed({ index, posts, site, language, siteDescription }) {
|
|
164
|
+
const feed = index.index.feed;
|
|
165
|
+
const pageUrl = toPageUrl(index.routePath, site);
|
|
166
|
+
const newest = posts.map((post) => post.post.updated ?? post.post.date).find((date) => date !== null);
|
|
167
|
+
const items = posts.map((post) => {
|
|
168
|
+
const url = toPageUrl(post.routePath, site);
|
|
169
|
+
const image = resolvePostImage(post, site);
|
|
170
|
+
const lines = [
|
|
171
|
+
element("title", post.post.title),
|
|
172
|
+
element("link", url),
|
|
173
|
+
element("guid", url, ' isPermaLink="true"'),
|
|
174
|
+
post.post.date ? element("pubDate", post.post.date.toUTCString()) : "",
|
|
175
|
+
element("description", post.post.summary),
|
|
176
|
+
...post.post.authors.map(
|
|
177
|
+
(author) => element("dc:creator", author.email ? `${author.email} (${author.name})` : author.name)
|
|
178
|
+
),
|
|
179
|
+
...[post.post.category, ...post.post.tags].filter(Boolean).map((category) => element("category", category)),
|
|
180
|
+
image ? `<media:content url="${escapeXml(image.url)}" medium="image"${image.type ? ` type="${image.type}"` : ""} />` : "",
|
|
181
|
+
image?.bytes !== void 0 && image.type ? `<enclosure url="${escapeXml(image.url)}" length="${image.bytes}" type="${image.type}" />` : ""
|
|
182
|
+
];
|
|
183
|
+
return ` <item>
|
|
184
|
+
${lines.filter(Boolean).map((line) => ` ${line}`).join("\n")}
|
|
185
|
+
</item>`;
|
|
186
|
+
});
|
|
187
|
+
const channel = [
|
|
188
|
+
element("title", feed.title ?? index.title),
|
|
189
|
+
element("link", pageUrl),
|
|
190
|
+
element("description", feed.description ?? (index.description || siteDescription || index.title)),
|
|
191
|
+
`<atom:link href="${escapeXml(toAbsoluteUrl(toFeedPath(index), site))}" rel="self" type="application/rss+xml" />`,
|
|
192
|
+
element("language", language),
|
|
193
|
+
newest ? element("lastBuildDate", newest.toUTCString()) : "",
|
|
194
|
+
element("generator", "@binarynoir/vitepress-blog")
|
|
195
|
+
];
|
|
196
|
+
return [
|
|
197
|
+
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
198
|
+
'<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:media="http://search.yahoo.com/mrss/">',
|
|
199
|
+
" <channel>",
|
|
200
|
+
...channel.filter(Boolean).map((line) => ` ${line}`),
|
|
201
|
+
...items,
|
|
202
|
+
" </channel>",
|
|
203
|
+
"</rss>",
|
|
204
|
+
""
|
|
205
|
+
].join("\n");
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// src/types.ts
|
|
209
|
+
var DEFAULT_LABELS = {
|
|
210
|
+
by: "By",
|
|
211
|
+
updated: "Updated",
|
|
212
|
+
readingTime: "{n} min read",
|
|
213
|
+
newer: "Newer post",
|
|
214
|
+
older: "Older post",
|
|
215
|
+
allPosts: "All posts in {blog}",
|
|
216
|
+
draft: "Draft",
|
|
217
|
+
sticky: "Pinned",
|
|
218
|
+
noPosts: "No posts yet.",
|
|
219
|
+
undated: "Undated",
|
|
220
|
+
tags: "Keywords",
|
|
221
|
+
tagged: "Posts tagged {tag}",
|
|
222
|
+
showAll: "Show all posts",
|
|
223
|
+
noTagged: "No posts tagged {tag}.",
|
|
224
|
+
previous: "Previous",
|
|
225
|
+
next: "Next",
|
|
226
|
+
pages: "Pages",
|
|
227
|
+
subscribe: "Subscribe",
|
|
228
|
+
rssFeed: "RSS feed for {blog}"
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
// src/render.ts
|
|
232
|
+
import path2 from "path";
|
|
233
|
+
var BLOG_POSTS_PLACEHOLDER = "<!-- blog-posts -->";
|
|
234
|
+
function createRenderContext(options, pageDirectory, feeds = !!options.siteUrl) {
|
|
235
|
+
return {
|
|
236
|
+
locale: options.locale ?? "en-US",
|
|
237
|
+
dateFormat: options.dateFormat ?? { dateStyle: "long" },
|
|
238
|
+
labels: { ...DEFAULT_LABELS, ...options.labels },
|
|
239
|
+
pageDirectory,
|
|
240
|
+
feeds
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
var RSS_ICON = '<svg class="vp-blog-rss__icon" viewBox="0 0 24 24" width="14" height="14" aria-hidden="true"><circle cx="6" cy="18" r="2.5" fill="currentColor"></circle><path fill="currentColor" d="M3.5 10.5v3a7 7 0 0 1 7 7h3a10 10 0 0 0-10-10z"></path><path fill="currentColor" d="M3.5 3.5v3a14 14 0 0 1 14 14h3a17 17 0 0 0-17-17z"></path></svg>';
|
|
244
|
+
function renderRssLink(index, context, show = index.index.rssLink) {
|
|
245
|
+
if (!show || !context.feeds || !index.index.feed) return "";
|
|
246
|
+
const title = fill(context.labels.rssFeed, { blog: index.title }).replace(/"/g, """);
|
|
247
|
+
const url = toMarkdownUrl(`/${toFeedPath(index)}`);
|
|
248
|
+
return `<span class="vp-blog-rss">[${RSS_ICON} ${escapeMarkdown(context.labels.subscribe)}](${url} "${title}")</span>`;
|
|
249
|
+
}
|
|
250
|
+
function fill(template, values) {
|
|
251
|
+
return template.replace(/\{(\w+)\}/g, (match, key) => values[key] ?? match);
|
|
252
|
+
}
|
|
253
|
+
function toImageSource(post, pageDirectory) {
|
|
254
|
+
const { image } = post.post;
|
|
255
|
+
if (!image || isExternalUrl(image) || /^[/@~]/.test(image)) return image;
|
|
256
|
+
const absolute = path2.resolve(path2.dirname(post.absolutePath), image);
|
|
257
|
+
const relative = path2.relative(pageDirectory, absolute).split(path2.sep).join("/");
|
|
258
|
+
return relative.startsWith(".") ? relative : `./${relative}`;
|
|
259
|
+
}
|
|
260
|
+
function renderTime(date, context) {
|
|
261
|
+
return `<time datetime="${toIsoDate(date)}">${escapeTemplateText(formatDate(date, context.locale, context.dateFormat))}</time>`;
|
|
262
|
+
}
|
|
263
|
+
function renderAuthor(author) {
|
|
264
|
+
return author.url ? `[${escapeMarkdown(author.name)}](${toMarkdownUrl(author.url)})` : escapeMarkdown(author.name);
|
|
265
|
+
}
|
|
266
|
+
function renderAuthors(authors, context) {
|
|
267
|
+
if (authors.length === 0) return "";
|
|
268
|
+
const placeholders = authors.map((_, index) => `\0${index}\0`);
|
|
269
|
+
const joined = new Intl.ListFormat(context.locale, { type: "conjunction" }).format(placeholders);
|
|
270
|
+
const names = joined.replace(/\u0000(\d+)\u0000/g, (_, index) => renderAuthor(authors[Number(index)]));
|
|
271
|
+
return `${escapeMarkdown(context.labels.by)} ${names}`;
|
|
272
|
+
}
|
|
273
|
+
function renderBadge(kind, context) {
|
|
274
|
+
return `<span class="vp-blog-badge vp-blog-badge--${kind}">${escapeTemplateText(context.labels[kind])}</span>`;
|
|
275
|
+
}
|
|
276
|
+
function toTagUrl(blogRoutePath, tag) {
|
|
277
|
+
const value = encodeURIComponent(tag.toLowerCase()).replace(/[()]/g, (c) => c === "(" ? "%28" : "%29");
|
|
278
|
+
const page = blogRoutePath.replace(/(^|\/)index\.md$/, "$1");
|
|
279
|
+
return `${toMarkdownUrl(`/${page}`)}?tag=${value}`;
|
|
280
|
+
}
|
|
281
|
+
function renderTags(tags, blogRoutePath) {
|
|
282
|
+
return tags.map(
|
|
283
|
+
(tag) => blogRoutePath ? `[${escapeMarkdown(tag)}](${toTagUrl(blogRoutePath, tag)})` : `<span class="vp-blog-tag">${escapeTemplateText(tag)}</span>`
|
|
284
|
+
).join(" ");
|
|
285
|
+
}
|
|
286
|
+
function toTagsAttribute(post) {
|
|
287
|
+
return escapeHtml(post.post.tags.map((tag) => tag.toLowerCase()).join("|"));
|
|
288
|
+
}
|
|
289
|
+
function renderMeta(post, context, include) {
|
|
290
|
+
const { date, updated, readingMinutes, authors, draft, sticky } = post.post;
|
|
291
|
+
const parts = [];
|
|
292
|
+
if (include.badges) {
|
|
293
|
+
const badges = [draft && renderBadge("draft", context), sticky && renderBadge("sticky", context)];
|
|
294
|
+
const shown = badges.filter(Boolean).join(" ");
|
|
295
|
+
if (shown) parts.push(shown);
|
|
296
|
+
}
|
|
297
|
+
if (date) parts.push(renderTime(date, context));
|
|
298
|
+
if (include.updated && updated && (!date || updated.getTime() !== date.getTime())) {
|
|
299
|
+
parts.push(`${escapeMarkdown(context.labels.updated)} ${renderTime(updated, context)}`);
|
|
300
|
+
}
|
|
301
|
+
if (readingMinutes) parts.push(escapeMarkdown(fill(context.labels.readingTime, { n: String(readingMinutes) })));
|
|
302
|
+
if (include.authors) {
|
|
303
|
+
const names = renderAuthors(authors, context);
|
|
304
|
+
if (names) parts.push(names);
|
|
305
|
+
}
|
|
306
|
+
return parts.join(" \xB7 ");
|
|
307
|
+
}
|
|
308
|
+
function block(className, content) {
|
|
309
|
+
return content ? [`<div class="${className}">`, "", content, "", "</div>"] : [];
|
|
310
|
+
}
|
|
311
|
+
function imageStyle(post) {
|
|
312
|
+
return post.post.imagePosition ? ` style="--vp-blog-image-position: ${post.post.imagePosition}"` : "";
|
|
313
|
+
}
|
|
314
|
+
function renderCard(post, context, index) {
|
|
315
|
+
const link = `/${post.routePath}`;
|
|
316
|
+
const image = toImageSource(post, context.pageDirectory);
|
|
317
|
+
const modifiers = [
|
|
318
|
+
post.post.sticky && "sticky",
|
|
319
|
+
post.post.draft && "draft",
|
|
320
|
+
image && "with-image",
|
|
321
|
+
image && post.post.imageFit === "contain" && "contain"
|
|
322
|
+
].filter(Boolean).map((modifier) => ` vp-blog-card--${modifier}`).join("");
|
|
323
|
+
const alt = escapeMarkdown(post.post.imageAlt);
|
|
324
|
+
return [
|
|
325
|
+
`<article class="vp-blog-card${modifiers}"${imageStyle(post)} data-vp-blog-item data-tags="${toTagsAttribute(post)}">`,
|
|
326
|
+
...image ? block("vp-blog-card__image", `[})](${toMarkdownUrl(link)})`) : [],
|
|
327
|
+
'<div class="vp-blog-card__body">',
|
|
328
|
+
...block("vp-blog-card__title", toPageLink(post.post.title, post.routePath)),
|
|
329
|
+
...block("vp-blog-card__meta", renderMeta(post, context, { authors: true, updated: false, badges: true })),
|
|
330
|
+
...block("vp-blog-card__summary", escapeMarkdown(post.post.summary)),
|
|
331
|
+
...index.index.showTags ? block("vp-blog-card__tags", renderTags(post.post.tags, index.routePath)) : [],
|
|
332
|
+
"</div>",
|
|
333
|
+
"</article>"
|
|
334
|
+
];
|
|
335
|
+
}
|
|
336
|
+
function renderCompactItem(post, context) {
|
|
337
|
+
const date = post.post.date ? `${renderTime(post.post.date, context)} ` : "";
|
|
338
|
+
const badges = [post.post.draft && renderBadge("draft", context), post.post.sticky && renderBadge("sticky", context)].filter(Boolean).join(" ");
|
|
339
|
+
const marker = `<span hidden data-vp-blog-item data-tags="${toTagsAttribute(post)}"></span>`;
|
|
340
|
+
return `- ${marker}${date}${toPageLink(post.post.title, post.routePath)}${badges ? ` ${badges}` : ""}`;
|
|
341
|
+
}
|
|
342
|
+
function renderGroup(posts, index, context) {
|
|
343
|
+
const { layout } = index.index;
|
|
344
|
+
if (layout === "compact") {
|
|
345
|
+
return [
|
|
346
|
+
'<div class="vp-blog-list vp-blog-list--compact">',
|
|
347
|
+
"",
|
|
348
|
+
...posts.map((post) => renderCompactItem(post, context)),
|
|
349
|
+
"",
|
|
350
|
+
"</div>"
|
|
351
|
+
];
|
|
352
|
+
}
|
|
353
|
+
return [
|
|
354
|
+
`<div class="vp-blog-list vp-blog-list--${layout}">`,
|
|
355
|
+
...posts.flatMap((post) => renderCard(post, context, index)),
|
|
356
|
+
"</div>"
|
|
357
|
+
];
|
|
358
|
+
}
|
|
359
|
+
function renderYears(posts, index, context) {
|
|
360
|
+
const years = /* @__PURE__ */ new Map();
|
|
361
|
+
for (const post of posts) {
|
|
362
|
+
const year = post.post.date ? String(post.post.date.getUTCFullYear()) : context.labels.undated;
|
|
363
|
+
years.set(year, [...years.get(year) ?? [], post]);
|
|
364
|
+
}
|
|
365
|
+
const lines = [];
|
|
366
|
+
for (const [year, group] of years) {
|
|
367
|
+
lines.push(
|
|
368
|
+
'<section class="vp-blog-year">',
|
|
369
|
+
"",
|
|
370
|
+
`## ${escapeMarkdown(year)}`,
|
|
371
|
+
"",
|
|
372
|
+
...renderGroup(group, index, context),
|
|
373
|
+
"",
|
|
374
|
+
"</section>"
|
|
375
|
+
);
|
|
376
|
+
}
|
|
377
|
+
return lines;
|
|
378
|
+
}
|
|
379
|
+
function collectTags(posts) {
|
|
380
|
+
const tags = /* @__PURE__ */ new Map();
|
|
381
|
+
for (const post of posts) {
|
|
382
|
+
for (const tag of new Set(post.post.tags)) {
|
|
383
|
+
const entry = tags.get(tag.toLowerCase()) ?? { tag, count: 0 };
|
|
384
|
+
entry.count++;
|
|
385
|
+
tags.set(tag.toLowerCase(), entry);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
return [...tags.values()].sort((a, b) => a.tag.localeCompare(b.tag, void 0, { sensitivity: "base" }));
|
|
389
|
+
}
|
|
390
|
+
function renderTagList(posts, index, context) {
|
|
391
|
+
const tags = collectTags(posts);
|
|
392
|
+
if (!index.index.tagList || tags.length === 0) return [];
|
|
393
|
+
const links = tags.map(
|
|
394
|
+
({ tag, count }) => `[${escapeMarkdown(tag)} <span class="vp-blog-tags__count">${count}</span>](${toTagUrl(index.routePath, tag)})`
|
|
395
|
+
);
|
|
396
|
+
const label = `<span class="vp-blog-tags__label">${escapeTemplateText(context.labels.tags)}</span>`;
|
|
397
|
+
return [
|
|
398
|
+
`<nav class="vp-blog-tags" aria-label="${escapeHtml(context.labels.tags)}">`,
|
|
399
|
+
"",
|
|
400
|
+
`${label} ${links.join(" ")}`,
|
|
401
|
+
"",
|
|
402
|
+
"</nav>"
|
|
403
|
+
];
|
|
404
|
+
}
|
|
405
|
+
function renderClientAttributes(index, context) {
|
|
406
|
+
const { labels } = context;
|
|
407
|
+
const attributes = {
|
|
408
|
+
"data-page-size": String(index.index.pageSize),
|
|
409
|
+
"data-label-tagged": labels.tagged,
|
|
410
|
+
"data-label-show-all": labels.showAll,
|
|
411
|
+
"data-label-no-tagged": labels.noTagged,
|
|
412
|
+
"data-label-previous": labels.previous,
|
|
413
|
+
"data-label-next": labels.next,
|
|
414
|
+
"data-label-pages": labels.pages
|
|
415
|
+
};
|
|
416
|
+
return Object.entries(attributes).map(([name, value]) => `${name}="${escapeHtml(value)}"`).join(" ");
|
|
417
|
+
}
|
|
418
|
+
function renderPostList(pinned, rest, index, context) {
|
|
419
|
+
const all = [...pinned, ...rest];
|
|
420
|
+
if (all.length === 0) return `<p class="vp-blog-empty">${escapeTemplateText(context.labels.noPosts)}</p>
|
|
421
|
+
`;
|
|
422
|
+
const lines = [
|
|
423
|
+
`<div class="vp-blog" data-vp-blog ${renderClientAttributes(index, context)}>`,
|
|
424
|
+
...block("vp-blog-subscribe", renderRssLink(index, context)),
|
|
425
|
+
...renderTagList(all, index, context),
|
|
426
|
+
'<div class="vp-blog-filter" data-vp-blog-filter hidden></div>'
|
|
427
|
+
];
|
|
428
|
+
if (pinned.length)
|
|
429
|
+
lines.push(
|
|
430
|
+
'<section class="vp-blog-sticky" data-vp-blog-sticky>',
|
|
431
|
+
...renderGroup(pinned, index, context),
|
|
432
|
+
"</section>"
|
|
433
|
+
);
|
|
434
|
+
if (rest.length)
|
|
435
|
+
lines.push(...index.index.groupByYear ? renderYears(rest, index, context) : renderGroup(rest, index, context));
|
|
436
|
+
lines.push(`<nav class="vp-blog-pagination" data-vp-blog-pagination hidden></nav>`, "</div>");
|
|
437
|
+
return `${lines.join("\n")}
|
|
438
|
+
`;
|
|
439
|
+
}
|
|
440
|
+
function renderPostHeader(post, context, show, blogRoutePath = null) {
|
|
441
|
+
const lines = [];
|
|
442
|
+
if (show.byline) {
|
|
443
|
+
const meta = renderMeta(post, context, { authors: true, updated: true, badges: true });
|
|
444
|
+
const tags = post.post.tags.length ? `<span class="vp-blog-byline__tags">${renderTags(post.post.tags, blogRoutePath)}</span>` : "";
|
|
445
|
+
lines.push(...block("vp-blog-byline", [meta, tags].filter(Boolean).join("\n\n")));
|
|
446
|
+
}
|
|
447
|
+
if (show.cover && post.post.image) {
|
|
448
|
+
const image = /^[./@~]/.test(post.post.image) || isExternalUrl(post.post.image) ? post.post.image : `./${post.post.image}`;
|
|
449
|
+
lines.push(...block("vp-blog-cover", `})`));
|
|
450
|
+
}
|
|
451
|
+
return lines.join("\n");
|
|
452
|
+
}
|
|
453
|
+
function renderNavLink(kind, post, context) {
|
|
454
|
+
if (!post) return [`<div class="vp-blog-post-nav__${kind}"></div>`];
|
|
455
|
+
const label = `<span class="vp-blog-post-nav__label">${escapeTemplateText(context.labels[kind])}</span>`;
|
|
456
|
+
return block(`vp-blog-post-nav__${kind}`, `${label}<br>
|
|
457
|
+
${toPageLink(post.post.title, post.routePath)}`);
|
|
458
|
+
}
|
|
459
|
+
function renderPostNav(post, posts, blog, context) {
|
|
460
|
+
const position = posts.indexOf(post);
|
|
461
|
+
const newer = position > 0 ? posts[position - 1] : void 0;
|
|
462
|
+
const older = position >= 0 ? posts[position + 1] : void 0;
|
|
463
|
+
const lines = ['<nav class="vp-blog-post-nav">'];
|
|
464
|
+
if (newer || older) lines.push(...renderNavLink("newer", newer, context), ...renderNavLink("older", older, context));
|
|
465
|
+
lines.push(
|
|
466
|
+
...block("vp-blog-post-nav__all", toPageLink(fill(context.labels.allPosts, { blog: blog.title }), blog.routePath))
|
|
467
|
+
);
|
|
468
|
+
lines.push("</nav>");
|
|
469
|
+
return lines.join("\n");
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// src/frontmatter.ts
|
|
473
|
+
import path3 from "path";
|
|
474
|
+
import { parse } from "yaml";
|
|
475
|
+
var POST_FRONTMATTER_KEY = "blog";
|
|
476
|
+
var INDEX_FRONTMATTER_KEY = "blog-index";
|
|
477
|
+
var FRONTMATTER = /^---[ \t]*\r?\n(?:([\s\S]*?)\r?\n)?---[ \t]*(?:\r?\n|$)/;
|
|
478
|
+
var BLOG_KEY = /^blog(?:-index)?[ \t]*:/m;
|
|
479
|
+
var FENCED_CODE = /^[ \t]*(`{3,}|~{3,})[^\n]*\n[\s\S]*?(?:^[ \t]*\1[`~]*[ \t]*$|(?![\s\S]))/gm;
|
|
480
|
+
var HEADING_1 = /^ {0,3}#[ \t]+(.*?)(?:[ \t]+#+)?[ \t]*$/m;
|
|
481
|
+
var TAG_SYNTAX = /\(\(<?tag([|/])[^|/)\r\n]*(?:\1[^|/)\r\n]*){0,2}\)\)/g;
|
|
482
|
+
var MAX_SUMMARY_LENGTH = 200;
|
|
483
|
+
function readFrontmatter(raw) {
|
|
484
|
+
const source = raw.replace(/^/, "");
|
|
485
|
+
const match = FRONTMATTER.exec(source);
|
|
486
|
+
if (!match) return { data: {}, content: source };
|
|
487
|
+
const data = match[1] ? parse(match[1]) : null;
|
|
488
|
+
return {
|
|
489
|
+
data: data && typeof data === "object" && !Array.isArray(data) ? data : {},
|
|
490
|
+
content: source.slice(match[0].length)
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
function mayHaveBlogFrontmatter(raw) {
|
|
494
|
+
const frontmatter = FRONTMATTER.exec(raw.replace(/^/, ""))?.[1];
|
|
495
|
+
return frontmatter !== void 0 && BLOG_KEY.test(frontmatter);
|
|
496
|
+
}
|
|
497
|
+
function stripTagSyntax(text) {
|
|
498
|
+
return text.replace(TAG_SYNTAX, " ").replace(/\s+/g, " ").trim();
|
|
499
|
+
}
|
|
500
|
+
function toRecord(value) {
|
|
501
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
502
|
+
}
|
|
503
|
+
function toText(value) {
|
|
504
|
+
return typeof value === "string" ? value.trim() : typeof value === "number" ? String(value) : "";
|
|
505
|
+
}
|
|
506
|
+
function toBoolean(value) {
|
|
507
|
+
return typeof value === "boolean" ? value : void 0;
|
|
508
|
+
}
|
|
509
|
+
function toStringList(value) {
|
|
510
|
+
const values = Array.isArray(value) ? value : typeof value === "string" ? value.split(",") : [];
|
|
511
|
+
return values.map(toText).filter(Boolean);
|
|
512
|
+
}
|
|
513
|
+
function toDate(value) {
|
|
514
|
+
if (value instanceof Date) return Number.isNaN(value.getTime()) ? null : value;
|
|
515
|
+
if (typeof value !== "string" && typeof value !== "number") return null;
|
|
516
|
+
const date = new Date(value);
|
|
517
|
+
return Number.isNaN(date.getTime()) ? null : date;
|
|
518
|
+
}
|
|
519
|
+
function toAuthor(value, options) {
|
|
520
|
+
if (typeof value === "string") {
|
|
521
|
+
const name2 = value.trim();
|
|
522
|
+
if (!name2) return null;
|
|
523
|
+
const known = options.authors && Object.hasOwn(options.authors, name2) ? options.authors[name2] : void 0;
|
|
524
|
+
return known ? toAuthor(known, {}) : { name: name2 };
|
|
525
|
+
}
|
|
526
|
+
const record = toRecord(value);
|
|
527
|
+
const name = toText(record.name);
|
|
528
|
+
if (!name) return null;
|
|
529
|
+
const author = { name };
|
|
530
|
+
if (toText(record.url)) author.url = toText(record.url);
|
|
531
|
+
if (toText(record.email)) author.email = toText(record.email);
|
|
532
|
+
return author;
|
|
533
|
+
}
|
|
534
|
+
function toAuthors(value, options) {
|
|
535
|
+
const values = Array.isArray(value) ? value : value === void 0 ? [] : [value];
|
|
536
|
+
const authors = values.map((item) => toAuthor(item, options)).filter((author) => author !== null);
|
|
537
|
+
if (authors.length === 0 && options.defaultAuthor) {
|
|
538
|
+
const fallback = toAuthor(options.defaultAuthor, options);
|
|
539
|
+
if (fallback) authors.push(fallback);
|
|
540
|
+
}
|
|
541
|
+
return authors;
|
|
542
|
+
}
|
|
543
|
+
function toImagePosition(value) {
|
|
544
|
+
const parts = value.trim().toLowerCase().split(/\s+/).filter(Boolean);
|
|
545
|
+
const valid = /^(?:top|bottom|left|right|center|-?\d+(?:\.\d+)?(?:%|px|rem|em)?)$/;
|
|
546
|
+
return parts.length > 0 && parts.length <= 4 && parts.every((part) => valid.test(part)) ? parts.join(" ") : "";
|
|
547
|
+
}
|
|
548
|
+
function toScope(value) {
|
|
549
|
+
return value === "site" || value === "global" ? "site" : "section";
|
|
550
|
+
}
|
|
551
|
+
function withoutCode(content) {
|
|
552
|
+
return content.replace(FENCED_CODE, "");
|
|
553
|
+
}
|
|
554
|
+
function readHeading(content) {
|
|
555
|
+
return HEADING_1.exec(withoutCode(content))?.[1]?.replace(/[ \t]*\{#[^}]*\}$/, "").trim() ?? "";
|
|
556
|
+
}
|
|
557
|
+
function toPlainText(markdown) {
|
|
558
|
+
return markdown.replace(/!\[[^\]]*\]\([^)]*\)/g, "").replace(/\[([^\]]*)\]\([^)]*\)/g, "$1").replace(/<[^>]+>/g, "").replace(/[*_`~]/g, "").replace(/\s+/g, " ").trim();
|
|
559
|
+
}
|
|
560
|
+
function readExcerpt(content) {
|
|
561
|
+
for (const block2 of withoutCode(content).split(/\r?\n[ \t]*\r?\n/)) {
|
|
562
|
+
const text = block2.trim();
|
|
563
|
+
if (!text || /^(?:#|<|>|\||:::|\[\[|!\[|[-*+] |\d+[.)] |\{)/.test(text)) continue;
|
|
564
|
+
const plain = toPlainText(text);
|
|
565
|
+
if (!plain) continue;
|
|
566
|
+
return plain.length > MAX_SUMMARY_LENGTH ? `${plain.slice(0, MAX_SUMMARY_LENGTH - 1).trimEnd()}\u2026` : plain;
|
|
567
|
+
}
|
|
568
|
+
return "";
|
|
569
|
+
}
|
|
570
|
+
function estimateReadingMinutes(content, wordsPerMinute) {
|
|
571
|
+
if (wordsPerMinute <= 0) return 0;
|
|
572
|
+
const words = toPlainText(withoutCode(content)).split(" ").filter(Boolean).length;
|
|
573
|
+
return Math.max(1, Math.ceil(words / wordsPerMinute));
|
|
574
|
+
}
|
|
575
|
+
function resolvePostData(data, content, fallbackTitle, options = {}) {
|
|
576
|
+
const value = data[POST_FRONTMATTER_KEY];
|
|
577
|
+
if (!value) return null;
|
|
578
|
+
const post = toRecord(value);
|
|
579
|
+
const pick = (key) => post[key] ?? data[key];
|
|
580
|
+
const stripTags = options.stripTags ?? true;
|
|
581
|
+
const clean = (text) => stripTags ? stripTagSyntax(text) : text;
|
|
582
|
+
const image = toRecord(pick("image"));
|
|
583
|
+
return {
|
|
584
|
+
title: clean(toText(post.title) || toText(data.title) || readHeading(content)) || fallbackTitle,
|
|
585
|
+
summary: clean(toText(post.summary) || toText(data.description) || readExcerpt(content)),
|
|
586
|
+
date: toDate(pick("date")),
|
|
587
|
+
updated: toDate(pick("updated")),
|
|
588
|
+
image: toText(image.src) || toText(pick("image")),
|
|
589
|
+
imageAlt: toText(image.alt) || toText(post.imageAlt) || toText(data.imageAlt),
|
|
590
|
+
imageFit: (toText(image.fit) || toText(post.imageFit)) === "contain" ? "contain" : "cover",
|
|
591
|
+
imagePosition: toImagePosition(toText(image.position) || toText(post.imagePosition)),
|
|
592
|
+
authors: toAuthors(post.author ?? post.authors ?? data.author ?? data.authors, options),
|
|
593
|
+
tags: toStringList(pick("tags")),
|
|
594
|
+
category: toText(pick("category")),
|
|
595
|
+
scope: toScope(post.scope),
|
|
596
|
+
sticky: post.sticky === true,
|
|
597
|
+
draft: post.draft === true,
|
|
598
|
+
byline: toBoolean(post.byline),
|
|
599
|
+
cover: toBoolean(post.cover),
|
|
600
|
+
postNav: toBoolean(post.postNav),
|
|
601
|
+
readingMinutes: estimateReadingMinutes(content, options.wordsPerMinute ?? 200)
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
function toLayout(value) {
|
|
605
|
+
return value === "grid" || value === "compact" ? value : "list";
|
|
606
|
+
}
|
|
607
|
+
function defaultFeedPath(relativePath) {
|
|
608
|
+
const name = path3.posix.basename(relativePath, ".md");
|
|
609
|
+
return name === "index" ? "feed.xml" : `${name}-feed.xml`;
|
|
610
|
+
}
|
|
611
|
+
function toFeed(value, relativePath) {
|
|
612
|
+
if (value === false) return null;
|
|
613
|
+
const feed = toRecord(value);
|
|
614
|
+
const config = { path: toText(feed.path) || defaultFeedPath(relativePath) };
|
|
615
|
+
if (toText(feed.title)) config.title = toText(feed.title);
|
|
616
|
+
if (toText(feed.description)) config.description = toText(feed.description);
|
|
617
|
+
if (typeof feed.limit === "number" && feed.limit > 0) config.limit = Math.floor(feed.limit);
|
|
618
|
+
return config;
|
|
619
|
+
}
|
|
620
|
+
function resolveFolders(folders, pageRelativePath) {
|
|
621
|
+
return folders.map((folder) => {
|
|
622
|
+
const resolved = /^\.\.?(\/|$)/.test(folder) ? path3.posix.join(path3.posix.dirname(pageRelativePath), folder) : path3.posix.normalize(folder);
|
|
623
|
+
return resolved.replace(/^\/+|\/+$/g, "").replace(/^\.$/, "");
|
|
624
|
+
});
|
|
625
|
+
}
|
|
626
|
+
function toCount(value, fallback) {
|
|
627
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? Math.floor(value) : fallback;
|
|
628
|
+
}
|
|
629
|
+
function resolveIndexConfig(data, relativePath, defaults = {}) {
|
|
630
|
+
const value = data[INDEX_FRONTMATTER_KEY];
|
|
631
|
+
if (!value) return null;
|
|
632
|
+
const options = toRecord(value);
|
|
633
|
+
const scope = toScope(options.scope);
|
|
634
|
+
const folders = toStringList(options.folders);
|
|
635
|
+
const ownFolder = path3.posix.dirname(relativePath).replace(/^\.$/, "");
|
|
636
|
+
return {
|
|
637
|
+
scope,
|
|
638
|
+
folders: folders.length ? resolveFolders(folders, relativePath) : scope === "section" ? [ownFolder] : [],
|
|
639
|
+
tags: toStringList(options.tags),
|
|
640
|
+
limit: typeof options.limit === "number" && options.limit > 0 ? Math.floor(options.limit) : 0,
|
|
641
|
+
layout: toLayout(options.layout),
|
|
642
|
+
groupByYear: options.groupBy === "year",
|
|
643
|
+
showTags: options.showTags !== false,
|
|
644
|
+
tagList: options.tagList !== false,
|
|
645
|
+
pageSize: toCount(options.pageSize, toCount(defaults.pageSize, 10)),
|
|
646
|
+
stickyLimit: toCount(options.stickyLimit, toCount(defaults.stickyLimit, 0)),
|
|
647
|
+
rssLink: typeof options.rssLink === "boolean" ? options.rssLink : defaults.rssLink ?? true,
|
|
648
|
+
feed: toFeed(options.feed, relativePath)
|
|
649
|
+
};
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
// src/scan.ts
|
|
653
|
+
import { createHash as createHash2 } from "crypto";
|
|
654
|
+
import { readdirSync, readFileSync as readFileSync2, realpathSync, statSync as statSync2 } from "fs";
|
|
655
|
+
import path4 from "path";
|
|
656
|
+
var scanCache = /* @__PURE__ */ new Map();
|
|
657
|
+
function listMarkdownFiles(directory, ancestors = /* @__PURE__ */ new Set()) {
|
|
658
|
+
let realDirectory;
|
|
659
|
+
try {
|
|
660
|
+
realDirectory = realpathSync(directory);
|
|
661
|
+
} catch {
|
|
662
|
+
return [];
|
|
663
|
+
}
|
|
664
|
+
if (ancestors.has(realDirectory)) return [];
|
|
665
|
+
const chain = new Set(ancestors).add(realDirectory);
|
|
666
|
+
const files = [];
|
|
667
|
+
for (const entry of readdirSync(directory, { withFileTypes: true })) {
|
|
668
|
+
if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
|
|
669
|
+
const fullPath = path4.join(directory, entry.name);
|
|
670
|
+
let isDirectory = entry.isDirectory();
|
|
671
|
+
let isFile = entry.isFile();
|
|
672
|
+
if (entry.isSymbolicLink()) {
|
|
673
|
+
try {
|
|
674
|
+
const target = statSync2(fullPath);
|
|
675
|
+
isDirectory = target.isDirectory();
|
|
676
|
+
isFile = target.isFile();
|
|
677
|
+
} catch {
|
|
678
|
+
continue;
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
if (isDirectory) files.push(...listMarkdownFiles(fullPath, chain));
|
|
682
|
+
else if (isFile && entry.name.endsWith(".md")) files.push(fullPath);
|
|
683
|
+
}
|
|
684
|
+
return files;
|
|
685
|
+
}
|
|
686
|
+
function toFallbackTitle(relativePath) {
|
|
687
|
+
const parts = relativePath.replace(/\.md$/, "").split("/");
|
|
688
|
+
const name = parts.at(-1) === "index" && parts.length > 1 ? parts.at(-2) : parts.at(-1);
|
|
689
|
+
return name.replace(/[-_]+/g, " ");
|
|
690
|
+
}
|
|
691
|
+
var warned = /* @__PURE__ */ new Set();
|
|
692
|
+
function scanPage(raw, relativePath, options = {}) {
|
|
693
|
+
if (!mayHaveBlogFrontmatter(raw)) return null;
|
|
694
|
+
const { data, content } = readFrontmatter(raw);
|
|
695
|
+
const fallbackTitle = toFallbackTitle(relativePath);
|
|
696
|
+
const index = resolveIndexConfig(data, relativePath, options);
|
|
697
|
+
const post = index ? null : resolvePostData(data, content, fallbackTitle, options);
|
|
698
|
+
if (!index && !post) return null;
|
|
699
|
+
const rawTitle = typeof data.title === "string" && data.title.trim() || readHeading(content);
|
|
700
|
+
const title = options.stripTags ?? true ? stripTagSyntax(rawTitle) : rawTitle;
|
|
701
|
+
return {
|
|
702
|
+
relativePath,
|
|
703
|
+
title: post?.title ?? (title || fallbackTitle),
|
|
704
|
+
description: typeof data.description === "string" ? data.description.trim() : "",
|
|
705
|
+
post,
|
|
706
|
+
index
|
|
707
|
+
};
|
|
708
|
+
}
|
|
709
|
+
function toOptionsKey(options) {
|
|
710
|
+
return JSON.stringify([
|
|
711
|
+
options.authors,
|
|
712
|
+
options.defaultAuthor,
|
|
713
|
+
options.wordsPerMinute,
|
|
714
|
+
options.stripTags,
|
|
715
|
+
options.pageSize,
|
|
716
|
+
options.stickyLimit,
|
|
717
|
+
options.rssLink
|
|
718
|
+
]);
|
|
719
|
+
}
|
|
720
|
+
function indexBlogSource(sourceDirectory, options = {}, rewrites = {}) {
|
|
721
|
+
const pages = [];
|
|
722
|
+
const present = /* @__PURE__ */ new Set();
|
|
723
|
+
const optionsKey = toOptionsKey(options);
|
|
724
|
+
for (const absolutePath of listMarkdownFiles(sourceDirectory).sort()) {
|
|
725
|
+
present.add(absolutePath);
|
|
726
|
+
const stats = statSync2(absolutePath);
|
|
727
|
+
const relativePath = path4.relative(sourceDirectory, absolutePath).split(path4.sep).join("/");
|
|
728
|
+
let entry = scanCache.get(absolutePath);
|
|
729
|
+
if (!entry || entry.mtimeMs !== stats.mtimeMs || entry.size !== stats.size || entry.optionsKey !== optionsKey) {
|
|
730
|
+
let page = null;
|
|
731
|
+
try {
|
|
732
|
+
page = scanPage(readFileSync2(absolutePath, "utf8"), relativePath, options);
|
|
733
|
+
} catch (error) {
|
|
734
|
+
if (!warned.has(absolutePath)) {
|
|
735
|
+
warned.add(absolutePath);
|
|
736
|
+
console.warn(`[vitepress-blog] skipped ${relativePath}: ${error.message}`);
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
entry = { mtimeMs: stats.mtimeMs, size: stats.size, optionsKey, page };
|
|
740
|
+
scanCache.set(absolutePath, entry);
|
|
741
|
+
}
|
|
742
|
+
if (entry.page) pages.push({ ...entry.page, absolutePath, routePath: rewrites[relativePath] ?? relativePath });
|
|
743
|
+
}
|
|
744
|
+
for (const cached of scanCache.keys()) {
|
|
745
|
+
if (cached.startsWith(sourceDirectory + path4.sep) && !present.has(cached)) scanCache.delete(cached);
|
|
746
|
+
}
|
|
747
|
+
return pages;
|
|
748
|
+
}
|
|
749
|
+
function loadBlogPages(sourceDirectory, options = {}, info = {}) {
|
|
750
|
+
const built = info.pages ? new Set(info.pages) : null;
|
|
751
|
+
return indexBlogSource(sourceDirectory, options, info.rewrites?.map).filter(
|
|
752
|
+
(page) => !built || built.has(page.relativePath)
|
|
753
|
+
);
|
|
754
|
+
}
|
|
755
|
+
function fingerprintBlogSource(pages) {
|
|
756
|
+
const hash = createHash2("sha1");
|
|
757
|
+
for (const page of pages) {
|
|
758
|
+
const { mtimeMs, size } = statSync2(page.absolutePath);
|
|
759
|
+
hash.update(`${page.relativePath}:${mtimeMs}:${size}
|
|
760
|
+
`);
|
|
761
|
+
}
|
|
762
|
+
return hash.digest("hex").slice(0, 12);
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
// src/widget.ts
|
|
766
|
+
import path5 from "path";
|
|
767
|
+
import { parse as parse2 } from "yaml";
|
|
768
|
+
var widgetPages = /* @__PURE__ */ new Set();
|
|
769
|
+
var WIDGET_NAME = "blog-posts";
|
|
770
|
+
var WIDGET_OPEN = new RegExp(`^ {0,3}:{3,}[ \\t]*${WIDGET_NAME}[ \\t]*$`);
|
|
771
|
+
var WIDGET_CLOSE = /^ {0,3}:{3,}[ \t]*$/;
|
|
772
|
+
var FENCE = /^[ \t]*(`{3,}|~{3,})/;
|
|
773
|
+
var DEFAULT_LIMIT = 5;
|
|
774
|
+
function hasWidget(source) {
|
|
775
|
+
return source.includes(`${WIDGET_NAME}`) && source.split("\n").some((line) => WIDGET_OPEN.test(line));
|
|
776
|
+
}
|
|
777
|
+
function toRecord2(value) {
|
|
778
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
779
|
+
}
|
|
780
|
+
function toCount2(value) {
|
|
781
|
+
return typeof value === "number" && Number.isInteger(value) && value >= 0 ? value : void 0;
|
|
782
|
+
}
|
|
783
|
+
function findWidgetBlog(pages, blog, pageRelativePath) {
|
|
784
|
+
const indexes = pages.filter(isIndex);
|
|
785
|
+
if (!blog) return findBlogForPage(pages, pageRelativePath);
|
|
786
|
+
let target = /^\.\.?(\/|$)/.test(blog) ? path5.posix.join(path5.posix.dirname(pageRelativePath), blog) : blog.replace(/^\/+/, "");
|
|
787
|
+
target = target.replace(/\.html$/, "");
|
|
788
|
+
const candidates = target.endsWith(".md") ? [target] : target === "" || target.endsWith("/") ? [`${target}index.md`] : [`${target}.md`, `${target}/index.md`];
|
|
789
|
+
return indexes.find((index) => candidates.includes(index.routePath)) ?? indexes.find((index) => candidates.includes(index.relativePath)) ?? null;
|
|
790
|
+
}
|
|
791
|
+
function resolveWidgetConfig(body) {
|
|
792
|
+
let data = {};
|
|
793
|
+
let problem;
|
|
794
|
+
try {
|
|
795
|
+
data = toRecord2(body.trim() ? parse2(body) : {});
|
|
796
|
+
} catch (error) {
|
|
797
|
+
problem = `its settings are not valid YAML: ${error.message.split("\n")[0]}`;
|
|
798
|
+
}
|
|
799
|
+
const layout = data.layout === "list" || data.layout === "grid" || data.layout === "compact" ? data.layout : void 0;
|
|
800
|
+
return {
|
|
801
|
+
config: {
|
|
802
|
+
blog: typeof data.blog === "string" ? data.blog.trim() : "",
|
|
803
|
+
limit: toCount2(data.limit) ?? DEFAULT_LIMIT,
|
|
804
|
+
stickyLimit: toCount2(data.stickyLimit),
|
|
805
|
+
layout,
|
|
806
|
+
showTags: typeof data.showTags === "boolean" ? data.showTags : void 0,
|
|
807
|
+
tags: toStringList(data.tags),
|
|
808
|
+
more: data.more === false ? null : typeof data.more === "string" && data.more.trim() ? data.more.trim() : "",
|
|
809
|
+
rssLink: typeof data.rssLink === "boolean" ? data.rssLink : void 0
|
|
810
|
+
},
|
|
811
|
+
problem
|
|
812
|
+
};
|
|
813
|
+
}
|
|
814
|
+
function renderWidget(body, environment) {
|
|
815
|
+
const { pages, pageRelativePath, drafts, context, warn } = environment;
|
|
816
|
+
const { config, problem } = resolveWidgetConfig(body);
|
|
817
|
+
if (problem) warn(problem);
|
|
818
|
+
const blog = findWidgetBlog(pages, config.blog, pageRelativePath);
|
|
819
|
+
if (!blog) {
|
|
820
|
+
warn(config.blog ? `no blog page found at "${config.blog}"` : 'no blog covers this page: name one with "blog:"');
|
|
821
|
+
return "";
|
|
822
|
+
}
|
|
823
|
+
const stickyLimit = config.stickyLimit ?? blog.index.stickyLimit;
|
|
824
|
+
const wanted = new Set(config.tags.map((tag) => tag.toLowerCase()));
|
|
825
|
+
const posts = listBlogPosts(pages, blog, { drafts, stickyFirst: false, applyLimit: false }).filter(
|
|
826
|
+
(post) => wanted.size === 0 || post.post.tags.some((tag) => wanted.has(tag.toLowerCase()))
|
|
827
|
+
);
|
|
828
|
+
const { pinned, rest } = pinStickyPosts(posts, stickyLimit);
|
|
829
|
+
const shown = [...pinned, ...rest].slice(0, config.limit || void 0);
|
|
830
|
+
const view = {
|
|
831
|
+
...blog,
|
|
832
|
+
index: {
|
|
833
|
+
...blog.index,
|
|
834
|
+
layout: config.layout ?? blog.index.layout,
|
|
835
|
+
showTags: config.showTags ?? blog.index.showTags
|
|
836
|
+
}
|
|
837
|
+
};
|
|
838
|
+
const lines = [`<div class="vp-blog-widget">`];
|
|
839
|
+
if (shown.length) lines.push(...renderGroup(shown, view, context));
|
|
840
|
+
else lines.push(`<p class="vp-blog-empty">${escapeTemplateText(context.labels.noPosts)}</p>`);
|
|
841
|
+
const footer = [renderRssLink(blog, context, config.rssLink ?? blog.index.rssLink)];
|
|
842
|
+
if (config.more !== null) {
|
|
843
|
+
footer.push(toPageLink(config.more || context.labels.allPosts.replace("{blog}", blog.title), blog.routePath));
|
|
844
|
+
}
|
|
845
|
+
if (footer.some(Boolean)) {
|
|
846
|
+
lines.push('<div class="vp-blog-widget__footer">', "", footer.filter(Boolean).join(" "), "", "</div>");
|
|
847
|
+
}
|
|
848
|
+
lines.push("</div>");
|
|
849
|
+
return lines.join("\n");
|
|
850
|
+
}
|
|
851
|
+
function applyWidgets(source, environment) {
|
|
852
|
+
const lines = source.split("\n");
|
|
853
|
+
const output = [];
|
|
854
|
+
let fence = "";
|
|
855
|
+
for (let index = 0; index < lines.length; index++) {
|
|
856
|
+
const line = lines[index];
|
|
857
|
+
const marker = FENCE.exec(line)?.[1];
|
|
858
|
+
if (marker && (!fence || marker[0] === fence[0] && marker.length >= fence.length)) {
|
|
859
|
+
fence = fence ? "" : marker;
|
|
860
|
+
output.push(line);
|
|
861
|
+
continue;
|
|
862
|
+
}
|
|
863
|
+
if (fence || !WIDGET_OPEN.test(line)) {
|
|
864
|
+
output.push(line);
|
|
865
|
+
continue;
|
|
866
|
+
}
|
|
867
|
+
const close = lines.findIndex((candidate, at) => at > index && WIDGET_CLOSE.test(candidate));
|
|
868
|
+
if (close === -1) {
|
|
869
|
+
environment.warn(`"::: ${WIDGET_NAME}" on line ${index + 1} has no closing ":::"`);
|
|
870
|
+
output.push(line);
|
|
871
|
+
continue;
|
|
872
|
+
}
|
|
873
|
+
const lineWarn = (message) => environment.warn(`the block on line ${index + 1}: ${message}`);
|
|
874
|
+
output.push("", renderWidget(lines.slice(index + 1, close).join("\n"), { ...environment, warn: lineWarn }), "");
|
|
875
|
+
index = close;
|
|
876
|
+
}
|
|
877
|
+
return output.join("\n");
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
// src/plugin.ts
|
|
881
|
+
import path6 from "path";
|
|
882
|
+
var FENCE2 = /^[ \t]*(`{3,}|~{3,})/;
|
|
883
|
+
var HEADING_12 = /^ {0,3}#[ \t]+\S/;
|
|
884
|
+
function readSourceInfo() {
|
|
885
|
+
return globalThis.VITEPRESS_CONFIG ?? {};
|
|
886
|
+
}
|
|
887
|
+
function insertAfterTitle(source, insert) {
|
|
888
|
+
const lines = source.split("\n");
|
|
889
|
+
let fence = "";
|
|
890
|
+
for (const [index, line] of lines.entries()) {
|
|
891
|
+
const marker = FENCE2.exec(line)?.[1];
|
|
892
|
+
if (marker && (!fence || marker[0] === fence[0])) {
|
|
893
|
+
fence = fence ? "" : marker;
|
|
894
|
+
continue;
|
|
895
|
+
}
|
|
896
|
+
if (!fence && HEADING_12.test(line)) {
|
|
897
|
+
return [...lines.slice(0, index + 1), "", insert, "", ...lines.slice(index + 1)].join("\n");
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
return `${insert}
|
|
901
|
+
|
|
902
|
+
${source}`;
|
|
903
|
+
}
|
|
904
|
+
function hasFeeds(options) {
|
|
905
|
+
return !!(options.siteUrl ?? readSourceInfo().userConfig?.sitemap?.hostname);
|
|
906
|
+
}
|
|
907
|
+
function withPostList(source, page, pages, options) {
|
|
908
|
+
if (!isIndex(page)) return source;
|
|
909
|
+
const posts = listBlogPosts(pages, page, { drafts: options.drafts ?? false, stickyFirst: true, applyLimit: true });
|
|
910
|
+
const { pinned, rest } = pinStickyPosts(posts, page.index.stickyLimit);
|
|
911
|
+
const context = createRenderContext(options, path6.dirname(page.absolutePath), hasFeeds(options));
|
|
912
|
+
const generated = renderPostList(pinned, rest, page, context);
|
|
913
|
+
return source.includes(BLOG_POSTS_PLACEHOLDER) ? source.replace(BLOG_POSTS_PLACEHOLDER, () => `
|
|
914
|
+
${generated}
|
|
915
|
+
`) : `${source.replace(/\s*$/, "")}
|
|
916
|
+
|
|
917
|
+
${generated}`;
|
|
918
|
+
}
|
|
919
|
+
function withPostDecorations(source, page, pages, options) {
|
|
920
|
+
if (!isPost(page)) return source;
|
|
921
|
+
const context = createRenderContext(options, path6.dirname(page.absolutePath));
|
|
922
|
+
const drafts = options.drafts ?? false;
|
|
923
|
+
const blog = findHomeBlog(pages, page, drafts);
|
|
924
|
+
const header = renderPostHeader(
|
|
925
|
+
page,
|
|
926
|
+
context,
|
|
927
|
+
{ byline: page.post.byline ?? options.byline ?? true, cover: page.post.cover ?? options.cover ?? false },
|
|
928
|
+
blog?.routePath ?? null
|
|
929
|
+
);
|
|
930
|
+
let result = header ? insertAfterTitle(source, header) : source;
|
|
931
|
+
if (page.post.postNav ?? options.postNav ?? true) {
|
|
932
|
+
if (blog) {
|
|
933
|
+
const posts = listBlogPosts(pages, blog, { drafts, stickyFirst: false, applyLimit: false });
|
|
934
|
+
result = `${result.replace(/\s*$/, "")}
|
|
935
|
+
|
|
936
|
+
${renderPostNav(page, posts, blog, context)}
|
|
937
|
+
`;
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
return result;
|
|
941
|
+
}
|
|
942
|
+
var warned2 = /* @__PURE__ */ new Set();
|
|
943
|
+
function applyBlog({ src, env }, options) {
|
|
944
|
+
const frontmatter = env?.frontmatter;
|
|
945
|
+
const isBlogPage = !!(frontmatter?.blog || frontmatter?.["blog-index"]);
|
|
946
|
+
const widgets = hasWidget(src);
|
|
947
|
+
if (!isBlogPage && !widgets) return src;
|
|
948
|
+
if (typeof env.path !== "string" || typeof env.relativePath !== "string") return src;
|
|
949
|
+
const sourceDirectory = path6.resolve(env.path, ...env.relativePath.split("/").map(() => ".."));
|
|
950
|
+
const realPath = typeof env.realPath === "string" ? env.realPath : env.path;
|
|
951
|
+
const relativePath = path6.relative(sourceDirectory, realPath).split(path6.sep).join("/");
|
|
952
|
+
const pages = loadBlogPages(sourceDirectory, options, readSourceInfo());
|
|
953
|
+
let result = src;
|
|
954
|
+
if (widgets) {
|
|
955
|
+
widgetPages.add(realPath);
|
|
956
|
+
result = applyWidgets(result, {
|
|
957
|
+
pages,
|
|
958
|
+
pageRelativePath: relativePath,
|
|
959
|
+
drafts: options.drafts ?? false,
|
|
960
|
+
context: createRenderContext(options, path6.dirname(realPath), hasFeeds(options)),
|
|
961
|
+
warn: (message) => {
|
|
962
|
+
const key = `${relativePath}: ${message}`;
|
|
963
|
+
if (warned2.has(key)) return;
|
|
964
|
+
warned2.add(key);
|
|
965
|
+
console.warn(`[vitepress-blog] ${relativePath}: ${message}`);
|
|
966
|
+
}
|
|
967
|
+
});
|
|
968
|
+
}
|
|
969
|
+
const page = pages.find((candidate) => candidate.relativePath === relativePath);
|
|
970
|
+
if (!page) return result;
|
|
971
|
+
return page.index ? withPostList(result, page, pages, options) : withPostDecorations(result, page, pages, options);
|
|
972
|
+
}
|
|
973
|
+
function blogMarkdown(md, options = {}) {
|
|
974
|
+
md.core.ruler.before("normalize", "vitepress_blog", (state) => {
|
|
975
|
+
state.src = applyBlog(state, options);
|
|
976
|
+
});
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
export {
|
|
980
|
+
isPost,
|
|
981
|
+
isIndex,
|
|
982
|
+
listBlogPosts,
|
|
983
|
+
findBlogsListing,
|
|
984
|
+
joinUrl,
|
|
985
|
+
toPageUrl,
|
|
986
|
+
toFeedPath,
|
|
987
|
+
resolvePostImage,
|
|
988
|
+
renderFeed,
|
|
989
|
+
DEFAULT_LABELS,
|
|
990
|
+
BLOG_POSTS_PLACEHOLDER,
|
|
991
|
+
POST_FRONTMATTER_KEY,
|
|
992
|
+
INDEX_FRONTMATTER_KEY,
|
|
993
|
+
mayHaveBlogFrontmatter,
|
|
994
|
+
loadBlogPages,
|
|
995
|
+
fingerprintBlogSource,
|
|
996
|
+
widgetPages,
|
|
997
|
+
WIDGET_NAME,
|
|
998
|
+
hasWidget,
|
|
999
|
+
blogMarkdown
|
|
1000
|
+
};
|
|
1001
|
+
//# sourceMappingURL=chunk-DICBXOTS.js.map
|