@binarynoir/vitepress-blog 0.1.0 → 0.2.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,7 +250,9 @@ 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",
@@ -183,10 +275,18 @@ var DEFAULT_LABELS = {
183
275
  // src/render.ts
184
276
  var BLOG_POSTS_PLACEHOLDER = "<!-- blog-posts -->";
185
277
  function createRenderContext(options, pageDirectory, feeds = !!options.siteUrl) {
278
+ const locale = options.locale ?? "en-US";
279
+ const labels = { ...DEFAULT_LABELS, ...options.labels };
186
280
  return {
187
- locale: options.locale ?? "en-US",
188
- dateFormat: options.dateFormat ?? { dateStyle: "long" },
189
- labels: { ...DEFAULT_LABELS, ...options.labels },
281
+ locale,
282
+ dates: {
283
+ locale,
284
+ timeZone: resolveTimeZone(options.timeZone),
285
+ dateFormat: options.dateFormat ?? { dateStyle: "long" },
286
+ timeFormat: options.timeFormat ?? { hour: "numeric", minute: "2-digit", timeZoneName: "short" },
287
+ dateTime: labels.dateTime
288
+ },
289
+ labels,
190
290
  pageDirectory,
191
291
  feeds
192
292
  };
@@ -208,8 +308,10 @@ function toImageSource(post, pageDirectory) {
208
308
  const relative = import_node_path2.default.relative(pageDirectory, absolute).split(import_node_path2.default.sep).join("/");
209
309
  return relative.startsWith(".") ? relative : `./${relative}`;
210
310
  }
211
- function renderTime(date, context) {
212
- return `<time datetime="${toIsoDate(date)}">${escapeTemplateText(formatDate(date, context.locale, context.dateFormat))}</time>`;
311
+ function renderTime(date, hasTime, context) {
312
+ const value = { date, hasTime };
313
+ const attribute = toDateTimeAttribute(value, context.dates.timeZone);
314
+ return `<time datetime="${attribute}">${escapeTemplateText(formatPostDate(value, context.dates))}</time>`;
213
315
  }
214
316
  function renderAuthor(author) {
215
317
  return author.url ? `[${escapeMarkdown(author.name)}](${toMarkdownUrl(author.url)})` : escapeMarkdown(author.name);
@@ -238,16 +340,19 @@ function toTagsAttribute(post) {
238
340
  return escapeHtml(post.post.tags.map((tag) => tag.toLowerCase()).join("|"));
239
341
  }
240
342
  function renderMeta(post, context, include) {
241
- const { date, updated, readingMinutes, authors, draft, sticky } = post.post;
343
+ const { date, dateHasTime, updated, updatedHasTime, readingMinutes, authors, draft, sticky } = post.post;
242
344
  const parts = [];
243
345
  if (include.badges) {
244
346
  const badges = [draft && renderBadge("draft", context), sticky && renderBadge("sticky", context)];
245
347
  const shown = badges.filter(Boolean).join(" ");
246
348
  if (shown) parts.push(shown);
247
349
  }
248
- if (date) parts.push(renderTime(date, context));
350
+ if (date) {
351
+ const time = renderTime(date, dateHasTime, context);
352
+ parts.push(include.updated ? `${escapeMarkdown(context.labels.published)} ${time}` : time);
353
+ }
249
354
  if (include.updated && updated && (!date || updated.getTime() !== date.getTime())) {
250
- parts.push(`${escapeMarkdown(context.labels.updated)} ${renderTime(updated, context)}`);
355
+ parts.push(`${escapeMarkdown(context.labels.updated)} ${renderTime(updated, updatedHasTime, context)}`);
251
356
  }
252
357
  if (readingMinutes) parts.push(escapeMarkdown(fill(context.labels.readingTime, { n: String(readingMinutes) })));
253
358
  if (include.authors) {
@@ -285,7 +390,7 @@ function renderCard(post, context, index) {
285
390
  ];
286
391
  }
287
392
  function renderCompactItem(post, context) {
288
- const date = post.post.date ? `${renderTime(post.post.date, context)} ` : "";
393
+ const date = post.post.date ? `${renderTime(post.post.date, post.post.dateHasTime, context)} ` : "";
289
394
  const badges = [post.post.draft && renderBadge("draft", context), post.post.sticky && renderBadge("sticky", context)].filter(Boolean).join(" ");
290
395
  const marker = `<span hidden data-vp-blog-item data-tags="${toTagsAttribute(post)}"></span>`;
291
396
  return `- ${marker}${date}${toPageLink(post.post.title, post.routePath)}${badges ? ` ${badges}` : ""}`;
@@ -310,7 +415,7 @@ function renderGroup(posts, index, context) {
310
415
  function renderYears(posts, index, context) {
311
416
  const years = /* @__PURE__ */ new Map();
312
417
  for (const post of posts) {
313
- const year = post.post.date ? String(post.post.date.getUTCFullYear()) : context.labels.undated;
418
+ const year = post.post.date ? String(yearIn(post.post.date, context.dates.timeZone)) : context.labels.undated;
314
419
  years.set(year, [...years.get(year) ?? [], post]);
315
420
  }
316
421
  const lines = [];
@@ -466,12 +571,6 @@ function toStringList(value) {
466
571
  const values = Array.isArray(value) ? value : typeof value === "string" ? value.split(",") : [];
467
572
  return values.map(toText).filter(Boolean);
468
573
  }
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
574
  function toAuthor(value, options) {
476
575
  if (typeof value === "string") {
477
576
  const name2 = value.trim();
@@ -536,11 +635,16 @@ function resolvePostData(data, content, fallbackTitle, options = {}) {
536
635
  const stripTags = options.stripTags ?? true;
537
636
  const clean = (text) => stripTags ? stripTagSyntax(text) : text;
538
637
  const image = toRecord(pick("image"));
638
+ const timeZone = resolveTimeZone(options.timeZone);
639
+ const date = parsePostDate(pick("date"), timeZone);
640
+ const updated = parsePostDate(pick("updated"), timeZone);
539
641
  return {
540
642
  title: clean(toText(post.title) || toText(data.title) || readHeading(content)) || fallbackTitle,
541
643
  summary: clean(toText(post.summary) || toText(data.description) || readExcerpt(content)),
542
- date: toDate(pick("date")),
543
- updated: toDate(pick("updated")),
644
+ date: date?.date ?? null,
645
+ dateHasTime: date?.hasTime ?? false,
646
+ updated: updated?.date ?? null,
647
+ updatedHasTime: updated?.hasTime ?? false,
544
648
  image: toText(image.src) || toText(pick("image")),
545
649
  imageAlt: toText(image.alt) || toText(post.imageAlt) || toText(data.imageAlt),
546
650
  imageFit: (toText(image.fit) || toText(post.imageFit)) === "contain" ? "contain" : "cover",
@@ -667,7 +771,8 @@ function toOptionsKey(options) {
667
771
  options.stripTags,
668
772
  options.pageSize,
669
773
  options.stickyLimit,
670
- options.rssLink
774
+ options.rssLink,
775
+ options.timeZone
671
776
  ]);
672
777
  }
673
778
  function indexBlogSource(sourceDirectory, options = {}, rewrites = {}) {