@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.
package/dist/index.cjs CHANGED
@@ -112,6 +112,102 @@ function findBlogForPage(pages, relativePath) {
112
112
  // src/render.ts
113
113
  var import_node_path2 = __toESM(require("path"), 1);
114
114
 
115
+ // src/dates.ts
116
+ 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;
117
+ function isTimeZone(timeZone) {
118
+ try {
119
+ new Intl.DateTimeFormat("en-US", { timeZone });
120
+ return true;
121
+ } catch {
122
+ return false;
123
+ }
124
+ }
125
+ var warnedZones = /* @__PURE__ */ new Set();
126
+ function resolveTimeZone(timeZone) {
127
+ if (!timeZone) return "UTC";
128
+ if (isTimeZone(timeZone)) return timeZone;
129
+ if (!warnedZones.has(timeZone)) {
130
+ warnedZones.add(timeZone);
131
+ console.warn(
132
+ `[vitepress-blog] "${timeZone}" is not a time zone; using UTC. Use an IANA name such as "America/New_York".`
133
+ );
134
+ }
135
+ return "UTC";
136
+ }
137
+ var offsetFormats = /* @__PURE__ */ new Map();
138
+ function zoneOffset(instant, timeZone) {
139
+ let format = offsetFormats.get(timeZone);
140
+ if (!format) {
141
+ format = new Intl.DateTimeFormat("en-US", {
142
+ timeZone,
143
+ hourCycle: "h23",
144
+ year: "numeric",
145
+ month: "2-digit",
146
+ day: "2-digit",
147
+ hour: "2-digit",
148
+ minute: "2-digit",
149
+ second: "2-digit"
150
+ });
151
+ offsetFormats.set(timeZone, format);
152
+ }
153
+ const parts = Object.fromEntries(format.formatToParts(instant).map(({ type, value }) => [type, Number(value)]));
154
+ const wall = Date.UTC(parts.year, parts.month - 1, parts.day, parts.hour, parts.minute, parts.second);
155
+ return wall - Math.floor(instant / 1e3) * 1e3;
156
+ }
157
+ function fromZonedTime(wall, timeZone) {
158
+ const first = wall - zoneOffset(wall, timeZone);
159
+ const second = wall - zoneOffset(first, timeZone);
160
+ return second;
161
+ }
162
+ function parsePostDate(value, timeZone = "UTC") {
163
+ if (value instanceof Date) {
164
+ if (Number.isNaN(value.getTime())) return null;
165
+ const midnight = value.getUTCHours() === 0 && value.getUTCMinutes() === 0 && value.getUTCSeconds() === 0;
166
+ return { date: value, hasTime: !midnight };
167
+ }
168
+ if (typeof value !== "string") return null;
169
+ const match = DATE_TIME.exec(value.trim());
170
+ if (!match) return null;
171
+ const [, year, month, day, hour, minute, second = "0", millisecond = "0", offset] = match;
172
+ const hasTime = hour !== void 0;
173
+ const wall = Date.UTC(
174
+ Number(year),
175
+ Number(month) - 1,
176
+ Number(day),
177
+ hasTime ? Number(hour) : 0,
178
+ hasTime ? Number(minute) : 0,
179
+ Number(second),
180
+ Number(millisecond.padEnd(3, "0"))
181
+ );
182
+ const check = new Date(wall);
183
+ if (check.getUTCMonth() !== Number(month) - 1 || check.getUTCDate() !== Number(day)) return null;
184
+ if (!offset) return { date: new Date(fromZonedTime(wall, timeZone)), hasTime };
185
+ if (offset.toUpperCase() === "Z") return { date: new Date(wall), hasTime };
186
+ const sign = offset.startsWith("-") ? -1 : 1;
187
+ const digits = offset.slice(1).replace(":", "");
188
+ const minutes = Number(digits.slice(0, 2)) * 60 + Number(digits.slice(2));
189
+ return { date: new Date(wall - sign * minutes * 6e4), hasTime };
190
+ }
191
+ function formatPostDate({ date, hasTime }, display) {
192
+ const { locale, timeZone } = display;
193
+ const day = new Intl.DateTimeFormat(locale, { timeZone, ...display.dateFormat }).format(date);
194
+ if (!hasTime) return day;
195
+ const time = new Intl.DateTimeFormat(locale, { timeZone, ...display.timeFormat }).format(date);
196
+ return display.dateTime.replace("{date}", day).replace("{time}", time);
197
+ }
198
+ function toZonedIsoDate(date, timeZone) {
199
+ const parts = Object.fromEntries(
200
+ new Intl.DateTimeFormat("en-US", { timeZone, year: "numeric", month: "2-digit", day: "2-digit" }).formatToParts(date).map(({ type, value }) => [type, value])
201
+ );
202
+ return `${parts.year}-${parts.month}-${parts.day}`;
203
+ }
204
+ function toDateTimeAttribute({ date, hasTime }, timeZone) {
205
+ return hasTime ? date.toISOString() : toZonedIsoDate(date, timeZone);
206
+ }
207
+ function yearIn(date, timeZone) {
208
+ return Number(toZonedIsoDate(date, timeZone).slice(0, 4));
209
+ }
210
+
115
211
  // src/feed.ts
116
212
  var import_node_crypto = require("crypto");
117
213
  var import_node_fs = require("fs");
@@ -145,12 +241,6 @@ function toMarkdownUrl(url) {
145
241
  function toPageLink(text, routePath) {
146
242
  return `[${escapeMarkdown(text)}](${toMarkdownUrl(`/${routePath}`)})`;
147
243
  }
148
- function toIsoDate(date) {
149
- return date.toISOString().slice(0, 10);
150
- }
151
- function formatDate(date, locale, format) {
152
- return new Intl.DateTimeFormat(locale, { timeZone: "UTC", ...format }).format(date);
153
- }
154
244
 
155
245
  // src/feed.ts
156
246
  function toFeedPath(index) {
@@ -160,13 +250,20 @@ function toFeedPath(index) {
160
250
  // src/types.ts
161
251
  var DEFAULT_LABELS = {
162
252
  by: "By",
253
+ published: "Published",
163
254
  updated: "Updated",
255
+ dateTime: "{date} at {time}",
164
256
  readingTime: "{n} min read",
165
257
  newer: "Newer post",
166
258
  older: "Older post",
167
259
  allPosts: "All posts in {blog}",
168
260
  draft: "Draft",
169
261
  sticky: "Pinned",
262
+ announcement: "Announcement",
263
+ announcements: "Announcements",
264
+ noAnnouncements: "No announcements.",
265
+ announcementCount: "{n} announcements",
266
+ dismiss: "Dismiss",
170
267
  noPosts: "No posts yet.",
171
268
  undated: "Undated",
172
269
  tags: "Keywords",
@@ -183,10 +280,18 @@ var DEFAULT_LABELS = {
183
280
  // src/render.ts
184
281
  var BLOG_POSTS_PLACEHOLDER = "<!-- blog-posts -->";
185
282
  function createRenderContext(options, pageDirectory, feeds = !!options.siteUrl) {
283
+ const locale = options.locale ?? "en-US";
284
+ const labels = { ...DEFAULT_LABELS, ...options.labels };
186
285
  return {
187
- locale: options.locale ?? "en-US",
188
- dateFormat: options.dateFormat ?? { dateStyle: "long" },
189
- labels: { ...DEFAULT_LABELS, ...options.labels },
286
+ locale,
287
+ dates: {
288
+ locale,
289
+ timeZone: resolveTimeZone(options.timeZone),
290
+ dateFormat: options.dateFormat ?? { dateStyle: "long" },
291
+ timeFormat: options.timeFormat ?? { hour: "numeric", minute: "2-digit", timeZoneName: "short" },
292
+ dateTime: labels.dateTime
293
+ },
294
+ labels,
190
295
  pageDirectory,
191
296
  feeds
192
297
  };
@@ -208,8 +313,10 @@ function toImageSource(post, pageDirectory) {
208
313
  const relative = import_node_path2.default.relative(pageDirectory, absolute).split(import_node_path2.default.sep).join("/");
209
314
  return relative.startsWith(".") ? relative : `./${relative}`;
210
315
  }
211
- function renderTime(date, context) {
212
- return `<time datetime="${toIsoDate(date)}">${escapeTemplateText(formatDate(date, context.locale, context.dateFormat))}</time>`;
316
+ function renderTime(date, hasTime, context) {
317
+ const value = { date, hasTime };
318
+ const attribute = toDateTimeAttribute(value, context.dates.timeZone);
319
+ return `<time datetime="${attribute}">${escapeTemplateText(formatPostDate(value, context.dates))}</time>`;
213
320
  }
214
321
  function renderAuthor(author) {
215
322
  return author.url ? `[${escapeMarkdown(author.name)}](${toMarkdownUrl(author.url)})` : escapeMarkdown(author.name);
@@ -237,17 +344,30 @@ function renderTags(tags, blogRoutePath) {
237
344
  function toTagsAttribute(post) {
238
345
  return escapeHtml(post.post.tags.map((tag) => tag.toLowerCase()).join("|"));
239
346
  }
347
+ function toItemAttributes(post) {
348
+ return `data-vp-blog-item data-tags="${toTagsAttribute(post)}"${post.post.announcement ? " data-announcement" : ""}`;
349
+ }
350
+ function renderBadges(post, context) {
351
+ const { draft, sticky, announcement } = post.post;
352
+ return [
353
+ draft && renderBadge("draft", context),
354
+ sticky && renderBadge("sticky", context),
355
+ announcement && renderBadge("announcement", context)
356
+ ].filter(Boolean).join(" ");
357
+ }
240
358
  function renderMeta(post, context, include) {
241
- const { date, updated, readingMinutes, authors, draft, sticky } = post.post;
359
+ const { date, dateHasTime, updated, updatedHasTime, readingMinutes, authors } = post.post;
242
360
  const parts = [];
243
361
  if (include.badges) {
244
- const badges = [draft && renderBadge("draft", context), sticky && renderBadge("sticky", context)];
245
- const shown = badges.filter(Boolean).join(" ");
362
+ const shown = renderBadges(post, context);
246
363
  if (shown) parts.push(shown);
247
364
  }
248
- if (date) parts.push(renderTime(date, context));
365
+ if (date) {
366
+ const time = renderTime(date, dateHasTime, context);
367
+ parts.push(include.updated ? `${escapeMarkdown(context.labels.published)} ${time}` : time);
368
+ }
249
369
  if (include.updated && updated && (!date || updated.getTime() !== date.getTime())) {
250
- parts.push(`${escapeMarkdown(context.labels.updated)} ${renderTime(updated, context)}`);
370
+ parts.push(`${escapeMarkdown(context.labels.updated)} ${renderTime(updated, updatedHasTime, context)}`);
251
371
  }
252
372
  if (readingMinutes) parts.push(escapeMarkdown(fill(context.labels.readingTime, { n: String(readingMinutes) })));
253
373
  if (include.authors) {
@@ -268,12 +388,13 @@ function renderCard(post, context, index) {
268
388
  const modifiers = [
269
389
  post.post.sticky && "sticky",
270
390
  post.post.draft && "draft",
391
+ post.post.announcement && "announcement",
271
392
  image && "with-image",
272
393
  image && post.post.imageFit === "contain" && "contain"
273
394
  ].filter(Boolean).map((modifier) => ` vp-blog-card--${modifier}`).join("");
274
395
  const alt = escapeMarkdown(post.post.imageAlt);
275
396
  return [
276
- `<article class="vp-blog-card${modifiers}"${imageStyle(post)} data-vp-blog-item data-tags="${toTagsAttribute(post)}">`,
397
+ `<article class="vp-blog-card${modifiers}"${imageStyle(post)} ${toItemAttributes(post)}>`,
277
398
  ...image ? block("vp-blog-card__image", `[![${alt}](${toMarkdownUrl(image)})](${toMarkdownUrl(link)})`) : [],
278
399
  '<div class="vp-blog-card__body">',
279
400
  ...block("vp-blog-card__title", toPageLink(post.post.title, post.routePath)),
@@ -285,9 +406,9 @@ function renderCard(post, context, index) {
285
406
  ];
286
407
  }
287
408
  function renderCompactItem(post, context) {
288
- const date = post.post.date ? `${renderTime(post.post.date, context)} ` : "";
289
- const badges = [post.post.draft && renderBadge("draft", context), post.post.sticky && renderBadge("sticky", context)].filter(Boolean).join(" ");
290
- const marker = `<span hidden data-vp-blog-item data-tags="${toTagsAttribute(post)}"></span>`;
409
+ const date = post.post.date ? `${renderTime(post.post.date, post.post.dateHasTime, context)} ` : "";
410
+ const badges = renderBadges(post, context);
411
+ const marker = `<span hidden ${toItemAttributes(post)}></span>`;
291
412
  return `- ${marker}${date}${toPageLink(post.post.title, post.routePath)}${badges ? ` ${badges}` : ""}`;
292
413
  }
293
414
  function renderGroup(posts, index, context) {
@@ -310,7 +431,7 @@ function renderGroup(posts, index, context) {
310
431
  function renderYears(posts, index, context) {
311
432
  const years = /* @__PURE__ */ new Map();
312
433
  for (const post of posts) {
313
- const year = post.post.date ? String(post.post.date.getUTCFullYear()) : context.labels.undated;
434
+ const year = post.post.date ? String(yearIn(post.post.date, context.dates.timeZone)) : context.labels.undated;
314
435
  years.set(year, [...years.get(year) ?? [], post]);
315
436
  }
316
437
  const lines = [];
@@ -362,7 +483,9 @@ function renderClientAttributes(index, context) {
362
483
  "data-label-no-tagged": labels.noTagged,
363
484
  "data-label-previous": labels.previous,
364
485
  "data-label-next": labels.next,
365
- "data-label-pages": labels.pages
486
+ "data-label-pages": labels.pages,
487
+ "data-label-announcements": labels.announcements,
488
+ "data-label-no-announcements": labels.noAnnouncements
366
489
  };
367
490
  return Object.entries(attributes).map(([name, value]) => `${name}="${escapeHtml(value)}"`).join(" ");
368
491
  }
@@ -466,12 +589,6 @@ function toStringList(value) {
466
589
  const values = Array.isArray(value) ? value : typeof value === "string" ? value.split(",") : [];
467
590
  return values.map(toText).filter(Boolean);
468
591
  }
469
- function toDate(value) {
470
- if (value instanceof Date) return Number.isNaN(value.getTime()) ? null : value;
471
- if (typeof value !== "string" && typeof value !== "number") return null;
472
- const date = new Date(value);
473
- return Number.isNaN(date.getTime()) ? null : date;
474
- }
475
592
  function toAuthor(value, options) {
476
593
  if (typeof value === "string") {
477
594
  const name2 = value.trim();
@@ -536,11 +653,17 @@ function resolvePostData(data, content, fallbackTitle, options = {}) {
536
653
  const stripTags = options.stripTags ?? true;
537
654
  const clean = (text) => stripTags ? stripTagSyntax(text) : text;
538
655
  const image = toRecord(pick("image"));
656
+ const timeZone = resolveTimeZone(options.timeZone);
657
+ const date = parsePostDate(pick("date"), timeZone);
658
+ const updated = parsePostDate(pick("updated"), timeZone);
659
+ const announcementText = typeof post.announcement === "string" ? clean(post.announcement.trim()) : "";
539
660
  return {
540
661
  title: clean(toText(post.title) || toText(data.title) || readHeading(content)) || fallbackTitle,
541
662
  summary: clean(toText(post.summary) || toText(data.description) || readExcerpt(content)),
542
- date: toDate(pick("date")),
543
- updated: toDate(pick("updated")),
663
+ date: date?.date ?? null,
664
+ dateHasTime: date?.hasTime ?? false,
665
+ updated: updated?.date ?? null,
666
+ updatedHasTime: updated?.hasTime ?? false,
544
667
  image: toText(image.src) || toText(pick("image")),
545
668
  imageAlt: toText(image.alt) || toText(post.imageAlt) || toText(data.imageAlt),
546
669
  imageFit: (toText(image.fit) || toText(post.imageFit)) === "contain" ? "contain" : "cover",
@@ -551,6 +674,8 @@ function resolvePostData(data, content, fallbackTitle, options = {}) {
551
674
  scope: toScope(post.scope),
552
675
  sticky: post.sticky === true,
553
676
  draft: post.draft === true,
677
+ announcement: post.announcement === true || !!announcementText,
678
+ announcementText,
554
679
  byline: toBoolean(post.byline),
555
680
  cover: toBoolean(post.cover),
556
681
  postNav: toBoolean(post.postNav),
@@ -667,7 +792,8 @@ function toOptionsKey(options) {
667
792
  options.stripTags,
668
793
  options.pageSize,
669
794
  options.stickyLimit,
670
- options.rssLink
795
+ options.rssLink,
796
+ options.timeZone
671
797
  ]);
672
798
  }
673
799
  function indexBlogSource(sourceDirectory, options = {}, rewrites = {}) {