@binarynoir/vitepress-blog 0.1.1 → 0.3.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.
@@ -1,3 +1,14 @@
1
+ import {
2
+ escapeHtml,
3
+ escapeMarkdown,
4
+ escapeTemplateText,
5
+ isExternalUrl,
6
+ joinUrl,
7
+ toMarkdownUrl,
8
+ toPageLink,
9
+ toUrlPath
10
+ } from "./chunk-42Z6H7IN.js";
11
+
1
12
  // src/collect.ts
2
13
  function isPost(page) {
3
14
  return page.post !== null;
@@ -69,46 +80,16 @@ function findBlogsListing(pages, post) {
69
80
  (index) => listBlogPosts(pages, index, { drafts: false, stickyFirst: false, applyLimit: false }).includes(post)
70
81
  );
71
82
  }
72
-
73
- // src/util.ts
74
- var HTML_ESCAPES = {
75
- "&": "&",
76
- "<": "&lt;",
77
- ">": "&gt;",
78
- '"': "&quot;",
79
- "'": "&#39;"
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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
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")}`;
83
+ function listAnnouncements(pages, drafts) {
84
+ return pages.filter(isPost).filter((post) => post.post.announcement && (drafts || !post.post.draft)).sort(compareByDate);
103
85
  }
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);
86
+ function findAnnouncementBlog(pages, announcements, drafts) {
87
+ if (announcements.length === 0) return null;
88
+ const listingAll = pages.filter(isIndex).filter((index) => {
89
+ const listed = listBlogPosts(pages, index, { drafts, stickyFirst: false, applyLimit: false });
90
+ return announcements.every((post) => listed.includes(post));
91
+ });
92
+ return listingAll.find((index) => index.index.scope === "site") ?? listingAll[0] ?? findHomeBlog(pages, announcements[0], drafts);
112
93
  }
113
94
 
114
95
  // src/feed.ts
@@ -208,13 +189,20 @@ ${lines.filter(Boolean).map((line) => ` ${line}`).join("\n")}
208
189
  // src/types.ts
209
190
  var DEFAULT_LABELS = {
210
191
  by: "By",
192
+ published: "Published",
211
193
  updated: "Updated",
194
+ dateTime: "{date} at {time}",
212
195
  readingTime: "{n} min read",
213
196
  newer: "Newer post",
214
197
  older: "Older post",
215
198
  allPosts: "All posts in {blog}",
216
199
  draft: "Draft",
217
200
  sticky: "Pinned",
201
+ announcement: "Announcement",
202
+ announcements: "Announcements",
203
+ noAnnouncements: "No announcements.",
204
+ announcementCount: "{n} announcements",
205
+ dismiss: "Dismiss",
218
206
  noPosts: "No posts yet.",
219
207
  undated: "Undated",
220
208
  tags: "Keywords",
@@ -230,12 +218,118 @@ var DEFAULT_LABELS = {
230
218
 
231
219
  // src/render.ts
232
220
  import path2 from "path";
221
+
222
+ // src/dates.ts
223
+ var DATE_TIME = /^(\d{4})-(\d{2})-(\d{2})(?:[T ](\d{2}):(\d{2})(?::(\d{2})(?:\.(\d{1,3})\d*)?)?)?\s*(Z|[+-]\d{2}:?\d{2})?$/i;
224
+ function isTimeZone(timeZone) {
225
+ try {
226
+ new Intl.DateTimeFormat("en-US", { timeZone });
227
+ return true;
228
+ } catch {
229
+ return false;
230
+ }
231
+ }
232
+ var warnedZones = /* @__PURE__ */ new Set();
233
+ function resolveTimeZone(timeZone) {
234
+ if (!timeZone) return "UTC";
235
+ if (isTimeZone(timeZone)) return timeZone;
236
+ if (!warnedZones.has(timeZone)) {
237
+ warnedZones.add(timeZone);
238
+ console.warn(
239
+ `[vitepress-blog] "${timeZone}" is not a time zone; using UTC. Use an IANA name such as "America/New_York".`
240
+ );
241
+ }
242
+ return "UTC";
243
+ }
244
+ var offsetFormats = /* @__PURE__ */ new Map();
245
+ function zoneOffset(instant, timeZone) {
246
+ let format = offsetFormats.get(timeZone);
247
+ if (!format) {
248
+ format = new Intl.DateTimeFormat("en-US", {
249
+ timeZone,
250
+ hourCycle: "h23",
251
+ year: "numeric",
252
+ month: "2-digit",
253
+ day: "2-digit",
254
+ hour: "2-digit",
255
+ minute: "2-digit",
256
+ second: "2-digit"
257
+ });
258
+ offsetFormats.set(timeZone, format);
259
+ }
260
+ const parts = Object.fromEntries(format.formatToParts(instant).map(({ type, value }) => [type, Number(value)]));
261
+ const wall = Date.UTC(parts.year, parts.month - 1, parts.day, parts.hour, parts.minute, parts.second);
262
+ return wall - Math.floor(instant / 1e3) * 1e3;
263
+ }
264
+ function fromZonedTime(wall, timeZone) {
265
+ const first = wall - zoneOffset(wall, timeZone);
266
+ const second = wall - zoneOffset(first, timeZone);
267
+ return second;
268
+ }
269
+ function parsePostDate(value, timeZone = "UTC") {
270
+ if (value instanceof Date) {
271
+ if (Number.isNaN(value.getTime())) return null;
272
+ const midnight = value.getUTCHours() === 0 && value.getUTCMinutes() === 0 && value.getUTCSeconds() === 0;
273
+ return { date: value, hasTime: !midnight };
274
+ }
275
+ if (typeof value !== "string") return null;
276
+ const match = DATE_TIME.exec(value.trim());
277
+ if (!match) return null;
278
+ const [, year, month, day, hour, minute, second = "0", millisecond = "0", offset] = match;
279
+ const hasTime = hour !== void 0;
280
+ const wall = Date.UTC(
281
+ Number(year),
282
+ Number(month) - 1,
283
+ Number(day),
284
+ hasTime ? Number(hour) : 0,
285
+ hasTime ? Number(minute) : 0,
286
+ Number(second),
287
+ Number(millisecond.padEnd(3, "0"))
288
+ );
289
+ const check = new Date(wall);
290
+ if (check.getUTCMonth() !== Number(month) - 1 || check.getUTCDate() !== Number(day)) return null;
291
+ if (!offset) return { date: new Date(fromZonedTime(wall, timeZone)), hasTime };
292
+ if (offset.toUpperCase() === "Z") return { date: new Date(wall), hasTime };
293
+ const sign = offset.startsWith("-") ? -1 : 1;
294
+ const digits = offset.slice(1).replace(":", "");
295
+ const minutes = Number(digits.slice(0, 2)) * 60 + Number(digits.slice(2));
296
+ return { date: new Date(wall - sign * minutes * 6e4), hasTime };
297
+ }
298
+ function formatPostDate({ date, hasTime }, display) {
299
+ const { locale, timeZone } = display;
300
+ const day = new Intl.DateTimeFormat(locale, { timeZone, ...display.dateFormat }).format(date);
301
+ if (!hasTime) return day;
302
+ const time = new Intl.DateTimeFormat(locale, { timeZone, ...display.timeFormat }).format(date);
303
+ return display.dateTime.replace("{date}", day).replace("{time}", time);
304
+ }
305
+ function toZonedIsoDate(date, timeZone) {
306
+ const parts = Object.fromEntries(
307
+ new Intl.DateTimeFormat("en-US", { timeZone, year: "numeric", month: "2-digit", day: "2-digit" }).formatToParts(date).map(({ type, value }) => [type, value])
308
+ );
309
+ return `${parts.year}-${parts.month}-${parts.day}`;
310
+ }
311
+ function toDateTimeAttribute({ date, hasTime }, timeZone) {
312
+ return hasTime ? date.toISOString() : toZonedIsoDate(date, timeZone);
313
+ }
314
+ function yearIn(date, timeZone) {
315
+ return Number(toZonedIsoDate(date, timeZone).slice(0, 4));
316
+ }
317
+
318
+ // src/render.ts
233
319
  var BLOG_POSTS_PLACEHOLDER = "<!-- blog-posts -->";
234
320
  function createRenderContext(options, pageDirectory, feeds = !!options.siteUrl) {
321
+ const locale = options.locale ?? "en-US";
322
+ const labels = { ...DEFAULT_LABELS, ...options.labels };
235
323
  return {
236
- locale: options.locale ?? "en-US",
237
- dateFormat: options.dateFormat ?? { dateStyle: "long" },
238
- labels: { ...DEFAULT_LABELS, ...options.labels },
324
+ locale,
325
+ dates: {
326
+ locale,
327
+ timeZone: resolveTimeZone(options.timeZone),
328
+ dateFormat: options.dateFormat ?? { dateStyle: "long" },
329
+ timeFormat: options.timeFormat ?? { hour: "numeric", minute: "2-digit", timeZoneName: "short" },
330
+ dateTime: labels.dateTime
331
+ },
332
+ labels,
239
333
  pageDirectory,
240
334
  feeds
241
335
  };
@@ -257,8 +351,10 @@ function toImageSource(post, pageDirectory) {
257
351
  const relative = path2.relative(pageDirectory, absolute).split(path2.sep).join("/");
258
352
  return relative.startsWith(".") ? relative : `./${relative}`;
259
353
  }
260
- function renderTime(date, context) {
261
- return `<time datetime="${toIsoDate(date)}">${escapeTemplateText(formatDate(date, context.locale, context.dateFormat))}</time>`;
354
+ function renderTime(date, hasTime, context) {
355
+ const value = { date, hasTime };
356
+ const attribute = toDateTimeAttribute(value, context.dates.timeZone);
357
+ return `<time datetime="${attribute}">${escapeTemplateText(formatPostDate(value, context.dates))}</time>`;
262
358
  }
263
359
  function renderAuthor(author) {
264
360
  return author.url ? `[${escapeMarkdown(author.name)}](${toMarkdownUrl(author.url)})` : escapeMarkdown(author.name);
@@ -286,17 +382,30 @@ function renderTags(tags, blogRoutePath) {
286
382
  function toTagsAttribute(post) {
287
383
  return escapeHtml(post.post.tags.map((tag) => tag.toLowerCase()).join("|"));
288
384
  }
385
+ function toItemAttributes(post) {
386
+ return `data-vp-blog-item data-tags="${toTagsAttribute(post)}"${post.post.announcement ? " data-announcement" : ""}`;
387
+ }
388
+ function renderBadges(post, context) {
389
+ const { draft, sticky, announcement } = post.post;
390
+ return [
391
+ draft && renderBadge("draft", context),
392
+ sticky && renderBadge("sticky", context),
393
+ announcement && renderBadge("announcement", context)
394
+ ].filter(Boolean).join(" ");
395
+ }
289
396
  function renderMeta(post, context, include) {
290
- const { date, updated, readingMinutes, authors, draft, sticky } = post.post;
397
+ const { date, dateHasTime, updated, updatedHasTime, readingMinutes, authors } = post.post;
291
398
  const parts = [];
292
399
  if (include.badges) {
293
- const badges = [draft && renderBadge("draft", context), sticky && renderBadge("sticky", context)];
294
- const shown = badges.filter(Boolean).join(" ");
400
+ const shown = renderBadges(post, context);
295
401
  if (shown) parts.push(shown);
296
402
  }
297
- if (date) parts.push(renderTime(date, context));
403
+ if (date) {
404
+ const time = renderTime(date, dateHasTime, context);
405
+ parts.push(include.updated ? `${escapeMarkdown(context.labels.published)} ${time}` : time);
406
+ }
298
407
  if (include.updated && updated && (!date || updated.getTime() !== date.getTime())) {
299
- parts.push(`${escapeMarkdown(context.labels.updated)} ${renderTime(updated, context)}`);
408
+ parts.push(`${escapeMarkdown(context.labels.updated)} ${renderTime(updated, updatedHasTime, context)}`);
300
409
  }
301
410
  if (readingMinutes) parts.push(escapeMarkdown(fill(context.labels.readingTime, { n: String(readingMinutes) })));
302
411
  if (include.authors) {
@@ -317,12 +426,13 @@ function renderCard(post, context, index) {
317
426
  const modifiers = [
318
427
  post.post.sticky && "sticky",
319
428
  post.post.draft && "draft",
429
+ post.post.announcement && "announcement",
320
430
  image && "with-image",
321
431
  image && post.post.imageFit === "contain" && "contain"
322
432
  ].filter(Boolean).map((modifier) => ` vp-blog-card--${modifier}`).join("");
323
433
  const alt = escapeMarkdown(post.post.imageAlt);
324
434
  return [
325
- `<article class="vp-blog-card${modifiers}"${imageStyle(post)} data-vp-blog-item data-tags="${toTagsAttribute(post)}">`,
435
+ `<article class="vp-blog-card${modifiers}"${imageStyle(post)} ${toItemAttributes(post)}>`,
326
436
  ...image ? block("vp-blog-card__image", `[![${alt}](${toMarkdownUrl(image)})](${toMarkdownUrl(link)})`) : [],
327
437
  '<div class="vp-blog-card__body">',
328
438
  ...block("vp-blog-card__title", toPageLink(post.post.title, post.routePath)),
@@ -334,9 +444,9 @@ function renderCard(post, context, index) {
334
444
  ];
335
445
  }
336
446
  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>`;
447
+ const date = post.post.date ? `${renderTime(post.post.date, post.post.dateHasTime, context)} ` : "";
448
+ const badges = renderBadges(post, context);
449
+ const marker = `<span hidden ${toItemAttributes(post)}></span>`;
340
450
  return `- ${marker}${date}${toPageLink(post.post.title, post.routePath)}${badges ? ` ${badges}` : ""}`;
341
451
  }
342
452
  function renderGroup(posts, index, context) {
@@ -359,7 +469,7 @@ function renderGroup(posts, index, context) {
359
469
  function renderYears(posts, index, context) {
360
470
  const years = /* @__PURE__ */ new Map();
361
471
  for (const post of posts) {
362
- const year = post.post.date ? String(post.post.date.getUTCFullYear()) : context.labels.undated;
472
+ const year = post.post.date ? String(yearIn(post.post.date, context.dates.timeZone)) : context.labels.undated;
363
473
  years.set(year, [...years.get(year) ?? [], post]);
364
474
  }
365
475
  const lines = [];
@@ -411,7 +521,9 @@ function renderClientAttributes(index, context) {
411
521
  "data-label-no-tagged": labels.noTagged,
412
522
  "data-label-previous": labels.previous,
413
523
  "data-label-next": labels.next,
414
- "data-label-pages": labels.pages
524
+ "data-label-pages": labels.pages,
525
+ "data-label-announcements": labels.announcements,
526
+ "data-label-no-announcements": labels.noAnnouncements
415
527
  };
416
528
  return Object.entries(attributes).map(([name, value]) => `${name}="${escapeHtml(value)}"`).join(" ");
417
529
  }
@@ -510,12 +622,6 @@ function toStringList(value) {
510
622
  const values = Array.isArray(value) ? value : typeof value === "string" ? value.split(",") : [];
511
623
  return values.map(toText).filter(Boolean);
512
624
  }
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
625
  function toAuthor(value, options) {
520
626
  if (typeof value === "string") {
521
627
  const name2 = value.trim();
@@ -580,11 +686,17 @@ function resolvePostData(data, content, fallbackTitle, options = {}) {
580
686
  const stripTags = options.stripTags ?? true;
581
687
  const clean = (text) => stripTags ? stripTagSyntax(text) : text;
582
688
  const image = toRecord(pick("image"));
689
+ const timeZone = resolveTimeZone(options.timeZone);
690
+ const date = parsePostDate(pick("date"), timeZone);
691
+ const updated = parsePostDate(pick("updated"), timeZone);
692
+ const announcementText = typeof post.announcement === "string" ? clean(post.announcement.trim()) : "";
583
693
  return {
584
694
  title: clean(toText(post.title) || toText(data.title) || readHeading(content)) || fallbackTitle,
585
695
  summary: clean(toText(post.summary) || toText(data.description) || readExcerpt(content)),
586
- date: toDate(pick("date")),
587
- updated: toDate(pick("updated")),
696
+ date: date?.date ?? null,
697
+ dateHasTime: date?.hasTime ?? false,
698
+ updated: updated?.date ?? null,
699
+ updatedHasTime: updated?.hasTime ?? false,
588
700
  image: toText(image.src) || toText(pick("image")),
589
701
  imageAlt: toText(image.alt) || toText(post.imageAlt) || toText(data.imageAlt),
590
702
  imageFit: (toText(image.fit) || toText(post.imageFit)) === "contain" ? "contain" : "cover",
@@ -595,6 +707,8 @@ function resolvePostData(data, content, fallbackTitle, options = {}) {
595
707
  scope: toScope(post.scope),
596
708
  sticky: post.sticky === true,
597
709
  draft: post.draft === true,
710
+ announcement: post.announcement === true || !!announcementText,
711
+ announcementText,
598
712
  byline: toBoolean(post.byline),
599
713
  cover: toBoolean(post.cover),
600
714
  postNav: toBoolean(post.postNav),
@@ -714,7 +828,8 @@ function toOptionsKey(options) {
714
828
  options.stripTags,
715
829
  options.pageSize,
716
830
  options.stickyLimit,
717
- options.rssLink
831
+ options.rssLink,
832
+ options.timeZone
718
833
  ]);
719
834
  }
720
835
  function indexBlogSource(sourceDirectory, options = {}, rewrites = {}) {
@@ -981,7 +1096,8 @@ export {
981
1096
  isIndex,
982
1097
  listBlogPosts,
983
1098
  findBlogsListing,
984
- joinUrl,
1099
+ listAnnouncements,
1100
+ findAnnouncementBlog,
985
1101
  toPageUrl,
986
1102
  toFeedPath,
987
1103
  resolvePostImage,
@@ -998,4 +1114,4 @@ export {
998
1114
  hasWidget,
999
1115
  blogMarkdown
1000
1116
  };
1001
- //# sourceMappingURL=chunk-DICBXOTS.js.map
1117
+ //# sourceMappingURL=chunk-LYTJBMVK.js.map