@binarynoir/vitepress-blog 0.2.0 → 0.4.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/client.cjs CHANGED
@@ -30,147 +30,363 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
30
30
  // src/client.ts
31
31
  var client_exports = {};
32
32
  __export(client_exports, {
33
+ BlogAnnouncementBanner: () => BlogAnnouncementBanner,
33
34
  applyBlogView: () => applyBlogView,
34
35
  applyBlogViews: () => applyBlogViews,
36
+ readFilter: () => readFilter,
35
37
  useBlog: () => useBlog
36
38
  });
37
39
  module.exports = __toCommonJS(client_exports);
38
- var vitepress = __toESM(require("vitepress"), 1);
39
- var import_vue = require("vue");
40
+ var vitepress2 = __toESM(require("vitepress"), 1);
41
+ var import_vue2 = require("vue");
40
42
 
41
43
  // src/view.ts
42
44
  var ITEM = "[data-vp-blog-item]";
45
+ var FILTER_LINKS = ".vp-blog-facet a, .vp-blog-card__category a, .vp-blog-card__tags a";
43
46
  function readLabels(blog) {
44
47
  const { dataset } = blog;
45
48
  return {
46
- tagged: dataset.labelTagged ?? "Posts tagged {tag}",
47
49
  showAll: dataset.labelShowAll ?? "Show all posts",
48
- noTagged: dataset.labelNoTagged ?? "No posts tagged {tag}.",
49
50
  previous: dataset.labelPrevious ?? "Previous",
50
51
  next: dataset.labelNext ?? "Next",
51
- pages: dataset.labelPages ?? "Pages"
52
+ pages: dataset.labelPages ?? "Pages",
53
+ announcements: dataset.labelAnnouncements ?? "Announcements",
54
+ category: dataset.labelCategory ?? "Category",
55
+ tag: dataset.labelTag ?? "Keyword",
56
+ postCount: dataset.labelPostCount ?? "{n} posts",
57
+ postCountOne: dataset.labelPostCountOne ?? "{n} post",
58
+ noMatches: dataset.labelNoMatches ?? "No posts match these filters.",
59
+ removeFilter: dataset.labelRemoveFilter ?? "Remove filter: {filter}"
60
+ };
61
+ }
62
+ function readFilter(url) {
63
+ const text = (name) => (url.searchParams.get(name) ?? "").trim().toLowerCase();
64
+ const month = text("month");
65
+ const year = text("year");
66
+ return {
67
+ category: text("category"),
68
+ tag: text("tag"),
69
+ date: /^\d{4}-(?:0[1-9]|1[0-2])$/.test(month) ? month : /^\d{4}$/.test(year) ? year : "",
70
+ announcements: url.searchParams.has("announcements")
52
71
  };
53
72
  }
54
73
  function itemElement(marker) {
55
74
  return marker.tagName === "ARTICLE" ? marker : marker.closest("li") ?? marker;
56
75
  }
57
- function hasTag(marker, tag) {
58
- return !tag || (marker.getAttribute("data-tags") ?? "").split("|").includes(tag);
76
+ function matches(marker, filter) {
77
+ const { category, tag, date, announcements } = filter;
78
+ return (!tag || (marker.getAttribute("data-tags") ?? "").split("|").includes(tag)) && (!category || marker.getAttribute("data-category") === category) && // `2026` matches every month of 2026; `2026-10` only October.
79
+ (!date || (marker.getAttribute("data-month") ?? "").startsWith(date)) && (!announcements || marker.hasAttribute("data-announcement"));
59
80
  }
60
- function toHref(url, tag, page) {
81
+ function toHref(url, filter, page = 1) {
61
82
  const next = new URL(url.href);
62
83
  next.hash = "";
63
- if (tag) next.searchParams.set("tag", tag);
64
- else next.searchParams.delete("tag");
65
- if (page > 1) next.searchParams.set("page", String(page));
66
- else next.searchParams.delete("page");
67
- return next.pathname + next.search;
68
- }
69
- function fillWithTag(element, template, tag) {
70
- const [before, after = ""] = template.split("{tag}");
71
- const strong = document.createElement("strong");
72
- strong.textContent = tag;
73
- element.replaceChildren(document.createTextNode(before), ...template.includes("{tag}") ? [strong] : [], after);
84
+ const set = (name, value) => value ? next.searchParams.set(name, value) : next.searchParams.delete(name);
85
+ set("category", filter.category);
86
+ set("tag", filter.tag);
87
+ set("year", filter.date.length === 4 ? filter.date : "");
88
+ set("month", filter.date.length === 7 ? filter.date : "");
89
+ if (filter.announcements) next.searchParams.set("announcements", "");
90
+ else next.searchParams.delete("announcements");
91
+ set("page", page > 1 ? String(page) : "");
92
+ return next.pathname + next.search.replace(/([?&]announcements)=(?=&|$)/, "$1");
93
+ }
94
+ function readFilterLink(anchor) {
95
+ if (!anchor.dataset.vpBlogFacet) {
96
+ let params;
97
+ try {
98
+ params = new URL(anchor.href, location.href).searchParams;
99
+ } catch {
100
+ return null;
101
+ }
102
+ const param = ["category", "tag", "month", "year"].find((name) => params.has(name));
103
+ if (!param) return null;
104
+ anchor.dataset.vpBlogFacet = param === "year" || param === "month" ? "date" : param;
105
+ anchor.dataset.vpBlogValue = (params.get(param) ?? "").trim().toLowerCase();
106
+ }
107
+ return { facet: anchor.dataset.vpBlogFacet, value: anchor.dataset.vpBlogValue ?? "" };
108
+ }
109
+ function withValue(filter, { facet, value }) {
110
+ return { ...filter, [facet]: value };
111
+ }
112
+ function isActive({ facet, value }, filter) {
113
+ if (facet !== "date") return filter[facet] === value;
114
+ return value.length === 4 ? filter.date.startsWith(value) : filter.date === value;
115
+ }
116
+ function toggle(filter, link2) {
117
+ if (link2.facet !== "date")
118
+ return withValue(filter, { ...link2, value: filter[link2.facet] === link2.value ? "" : link2.value });
119
+ if (filter.date !== link2.value) return withValue(filter, link2);
120
+ return { ...filter, date: link2.value.length === 7 ? link2.value.slice(0, 4) : "" };
121
+ }
122
+ function markActive(anchor, active) {
123
+ anchor.classList.toggle("is-active", active);
124
+ if (active) anchor.setAttribute("aria-current", "true");
125
+ else anchor.removeAttribute("aria-current");
126
+ }
127
+ function syncBrowse(blog, url, filter, markers) {
128
+ const browse = blog.querySelector(".vp-blog-browse");
129
+ if (!browse) return;
130
+ for (const anchor of browse.querySelectorAll(".vp-blog-facet a")) {
131
+ const link2 = readFilterLink(anchor);
132
+ if (!link2) continue;
133
+ const active = isActive(link2, filter);
134
+ const count = markers.filter((marker) => matches(marker, withValue(filter, link2))).length;
135
+ anchor.href = toHref(url, toggle(filter, link2));
136
+ markActive(anchor, active);
137
+ anchor.classList.toggle("is-empty", count === 0 && !active);
138
+ const counter = anchor.querySelector(".vp-blog-facet__count");
139
+ if (counter) counter.textContent = String(count);
140
+ }
141
+ const months = [...browse.querySelectorAll('[data-vp-blog-facet="month"]')];
142
+ for (const row of months) row.hidden = months.length > 1 && row.dataset.year !== filter.date.slice(0, 4);
143
+ }
144
+ function findName(blog, facet, value) {
145
+ for (const anchor of blog.querySelectorAll(FILTER_LINKS)) {
146
+ const link2 = readFilterLink(anchor);
147
+ if (link2?.facet === facet && link2.value === value) {
148
+ const name = (anchor.querySelector(".vp-blog-facet__name") ?? anchor).textContent?.trim();
149
+ if (name) return name;
150
+ }
151
+ }
152
+ return value;
153
+ }
154
+ function formatDate(date, locale) {
155
+ if (date.length === 4) return date;
156
+ const [year, month] = date.split("-").map(Number);
157
+ try {
158
+ return new Intl.DateTimeFormat(locale, { timeZone: "UTC", month: "long", year: "numeric" }).format(
159
+ Date.UTC(year, month - 1, 1, 12)
160
+ );
161
+ } catch {
162
+ return date;
163
+ }
164
+ }
165
+ function element(tag, className, text = "") {
166
+ const node = document.createElement(tag);
167
+ node.className = className;
168
+ node.textContent = text;
169
+ return node;
74
170
  }
75
171
  function link(text, href, className) {
76
- const anchor = document.createElement("a");
77
- anchor.className = className;
172
+ const anchor = element("a", className, text);
78
173
  anchor.href = href;
79
- anchor.textContent = text;
80
174
  return anchor;
81
175
  }
82
- function renderFilter(slot, url, tag, matches, labels) {
83
- slot.hidden = !tag;
84
- if (!tag) return slot.replaceChildren();
85
- const message = document.createElement("span");
86
- fillWithTag(message, matches ? labels.tagged : labels.noTagged, tag);
87
- slot.replaceChildren(message, " ", link(labels.showAll, toHref(url, "", 1), "vp-blog-filter__clear"));
176
+ function listActiveFilters(blog, filter, labels) {
177
+ const active = [];
178
+ if (filter.announcements) {
179
+ active.push({ kind: "", name: labels.announcements, without: { ...filter, announcements: false } });
180
+ }
181
+ if (filter.category) {
182
+ const name = findName(blog, "category", filter.category);
183
+ active.push({ kind: labels.category, name, without: { ...filter, category: "" } });
184
+ }
185
+ if (filter.tag) {
186
+ active.push({ kind: labels.tag, name: findName(blog, "tag", filter.tag), without: { ...filter, tag: "" } });
187
+ }
188
+ if (filter.date) {
189
+ const name = formatDate(filter.date, blog.dataset.locale || document.documentElement.lang || "en-US");
190
+ active.push({ kind: "", name, without: { ...filter, date: "" } });
191
+ }
192
+ return active;
88
193
  }
89
- function renderPagination(slot, url, tag, page, pageCount, labels) {
194
+ function renderFilter(slot, blog, url, filter, count, labels) {
195
+ const active = listActiveFilters(blog, filter, labels);
196
+ slot.hidden = active.length === 0;
197
+ if (slot.hidden) return slot.replaceChildren();
198
+ const summary = count === 0 ? labels.noMatches : count === 1 ? labels.postCountOne : labels.postCount;
199
+ const parts = [element("span", "vp-blog-filter__count", summary.replace("{n}", String(count)))];
200
+ for (const { kind, name, without } of active) {
201
+ const chip = link("", toHref(url, without), "vp-blog-filter__chip");
202
+ chip.setAttribute("aria-label", labels.removeFilter.replace("{filter}", kind ? `${kind} ${name}` : name));
203
+ if (kind) chip.append(element("span", "vp-blog-filter__kind", kind), " ");
204
+ const remove = element("span", "vp-blog-filter__remove", "\xD7");
205
+ remove.setAttribute("aria-hidden", "true");
206
+ chip.append(name, remove);
207
+ parts.push(chip);
208
+ }
209
+ const clear = { category: "", tag: "", date: "", announcements: false };
210
+ parts.push(link(labels.showAll, toHref(url, clear), "vp-blog-filter__clear"));
211
+ slot.replaceChildren(...parts);
212
+ }
213
+ function renderPagination(slot, url, filter, page, pageCount, labels) {
90
214
  slot.hidden = pageCount < 2;
91
215
  slot.setAttribute("aria-label", labels.pages);
92
216
  if (pageCount < 2) return slot.replaceChildren();
93
217
  const parts = [];
94
- if (page > 1) parts.push(link(labels.previous, toHref(url, tag, page - 1), "vp-blog-pagination__previous"));
218
+ if (page > 1) parts.push(link(labels.previous, toHref(url, filter, page - 1), "vp-blog-pagination__previous"));
95
219
  for (let number = 1; number <= pageCount; number++) {
96
220
  if (number === page) {
97
- const current = document.createElement("span");
98
- current.className = "vp-blog-pagination__page is-current";
221
+ const current = element("span", "vp-blog-pagination__page is-current", String(number));
99
222
  current.setAttribute("aria-current", "page");
100
- current.textContent = String(number);
101
223
  parts.push(current);
102
224
  } else {
103
- parts.push(link(String(number), toHref(url, tag, number), "vp-blog-pagination__page"));
225
+ parts.push(link(String(number), toHref(url, filter, number), "vp-blog-pagination__page"));
104
226
  }
105
227
  }
106
- if (page < pageCount) parts.push(link(labels.next, toHref(url, tag, page + 1), "vp-blog-pagination__next"));
228
+ if (page < pageCount) parts.push(link(labels.next, toHref(url, filter, page + 1), "vp-blog-pagination__next"));
107
229
  slot.replaceChildren(...parts);
108
230
  }
109
- function markActiveTags(blog, tag) {
110
- for (const anchor of blog.querySelectorAll(".vp-blog-tags a, .vp-blog-card__tags a")) {
111
- let linkTag = "";
112
- try {
113
- linkTag = new URL(anchor.href, location.href).searchParams.get("tag") ?? "";
114
- } catch {
115
- }
116
- const active = !!tag && linkTag.toLowerCase() === tag;
117
- anchor.classList.toggle("is-active", active);
118
- if (active) anchor.setAttribute("aria-current", "true");
119
- else anchor.removeAttribute("aria-current");
231
+ function markCardLinks(blog, filter) {
232
+ for (const anchor of blog.querySelectorAll(".vp-blog-card__category a, .vp-blog-card__tags a")) {
233
+ const link2 = readFilterLink(anchor);
234
+ markActive(anchor, !!link2 && isActive(link2, filter));
120
235
  }
121
236
  }
122
237
  function applyBlogView(blog, url) {
123
238
  const labels = readLabels(blog);
124
- const tag = (url.searchParams.get("tag") ?? "").trim().toLowerCase();
239
+ const filter = readFilter(url);
125
240
  const pageSize = Math.max(0, Number(blog.dataset.pageSize) || 0);
126
241
  const markers = [...blog.querySelectorAll(ITEM)];
127
- const isSticky = (marker) => !!marker.closest("[data-vp-blog-sticky]");
128
- const sticky = markers.filter((marker) => isSticky(marker) && hasTag(marker, tag));
129
- const rest = markers.filter((marker) => !isSticky(marker) && hasTag(marker, tag));
242
+ const isPinned = (marker) => !!marker.closest("[data-vp-blog-pinned]");
243
+ const pinned = markers.filter((marker) => isPinned(marker) && matches(marker, filter));
244
+ const rest = markers.filter((marker) => !isPinned(marker) && matches(marker, filter));
130
245
  const pageCount = pageSize ? Math.max(1, Math.ceil(rest.length / pageSize)) : 1;
131
246
  const requested = Number.parseInt(url.searchParams.get("page") ?? "1", 10);
132
247
  const page = Math.min(pageCount, Math.max(1, Number.isFinite(requested) ? requested : 1));
133
248
  const shown = /* @__PURE__ */ new Set([
134
- ...page === 1 ? sticky : [],
249
+ ...page === 1 ? pinned : [],
135
250
  ...pageSize ? rest.slice((page - 1) * pageSize, page * pageSize) : rest
136
251
  ]);
137
252
  for (const marker of markers) itemElement(marker).hidden = !shown.has(marker);
138
- for (const group of blog.querySelectorAll("[data-vp-blog-sticky], .vp-blog-year")) {
253
+ for (const group of blog.querySelectorAll("[data-vp-blog-pinned], .vp-blog-group")) {
139
254
  group.hidden = ![...group.querySelectorAll(ITEM)].some((marker) => shown.has(marker));
140
255
  }
141
- const filter = blog.querySelector("[data-vp-blog-filter]");
142
- if (filter) renderFilter(filter, url, tag, sticky.length + rest.length, labels);
256
+ const banner = blog.querySelector("[data-vp-blog-filter]");
257
+ if (banner) renderFilter(banner, blog, url, filter, pinned.length + rest.length, labels);
143
258
  const pagination = blog.querySelector("[data-vp-blog-pagination]");
144
- if (pagination) renderPagination(pagination, url, tag, page, pageCount, labels);
145
- markActiveTags(blog, tag);
146
- return { tag, page, pageCount };
259
+ if (pagination) renderPagination(pagination, url, filter, page, pageCount, labels);
260
+ syncBrowse(blog, url, filter, markers);
261
+ markCardLinks(blog, filter);
262
+ return { ...filter, page, pageCount };
147
263
  }
148
264
  var lastViews = /* @__PURE__ */ new WeakMap();
265
+ function viewChanged(previous, view) {
266
+ return ["category", "tag", "date", "announcements", "page"].some((key) => previous[key] !== view[key]);
267
+ }
149
268
  function applyBlogViews(root = document, url = new URL(location.href)) {
150
269
  for (const blog of root.querySelectorAll("[data-vp-blog]")) {
151
270
  const previous = lastViews.get(blog);
152
271
  const view = applyBlogView(blog, url);
153
272
  lastViews.set(blog, view);
154
- if (previous && (previous.page !== view.page || previous.tag !== view.tag) && blog.getBoundingClientRect().top < 0) {
273
+ if (previous && viewChanged(previous, view) && blog.getBoundingClientRect().top < 0) {
155
274
  blog.scrollIntoView({ block: "start" });
156
275
  }
157
276
  }
158
277
  }
159
278
 
279
+ // src/banner.ts
280
+ var vitepress = __toESM(require("vitepress"), 1);
281
+ var import_vue = require("vue");
282
+
283
+ // src/util.ts
284
+ var ANNOUNCEMENTS_KEY = "blogAnnouncements";
285
+ var DISMISSED_STORAGE_KEY = "vp-blog-announcements-dismissed";
286
+ var DISMISSED_CLASS = "vp-blog-announcement-dismissed";
287
+
288
+ // src/banner.ts
289
+ var closeIcon = () => (0, import_vue.h)("svg", { viewBox: "0 0 24 24", width: 16, height: 16, "aria-hidden": "true" }, [
290
+ (0, import_vue.h)("path", {
291
+ fill: "currentColor",
292
+ d: "M18.3 5.71a1 1 0 0 0-1.41 0L12 10.59 7.11 5.7A1 1 0 0 0 5.7 7.11L10.59 12 5.7 16.89a1 1 0 1 0 1.41 1.41L12 13.41l4.89 4.89a1 1 0 0 0 1.41-1.41L13.41 12l4.89-4.89a1 1 0 0 0 0-1.4z"
293
+ })
294
+ ]);
295
+ function readDismissed() {
296
+ try {
297
+ const value = JSON.parse(localStorage.getItem(DISMISSED_STORAGE_KEY) ?? "[]");
298
+ return Array.isArray(value) ? value.filter((id) => typeof id === "string") : [];
299
+ } catch {
300
+ return [];
301
+ }
302
+ }
303
+ function writeDismissed(ids) {
304
+ try {
305
+ localStorage.setItem(DISMISSED_STORAGE_KEY, JSON.stringify(ids));
306
+ } catch {
307
+ }
308
+ }
309
+ var BlogAnnouncementBanner = (0, import_vue.defineComponent)({
310
+ name: "BlogAnnouncementBanner",
311
+ setup() {
312
+ if (typeof vitepress.useData !== "function") return () => null;
313
+ const { site } = vitepress.useData();
314
+ const data = (0, import_vue.computed)(
315
+ () => site.value[ANNOUNCEMENTS_KEY]
316
+ );
317
+ const dismissed = (0, import_vue.ref)(null);
318
+ (0, import_vue.onMounted)(() => {
319
+ dismissed.value = readDismissed();
320
+ });
321
+ const latest = (0, import_vue.computed)(() => data.value?.items[0]);
322
+ const active = (0, import_vue.computed)(() => (data.value?.items ?? []).filter((item) => !dismissed.value?.includes(item.id)));
323
+ const dismiss = () => {
324
+ const ids = (data.value?.items ?? []).map((item) => item.id);
325
+ dismissed.value = ids;
326
+ writeDismissed(ids);
327
+ document.documentElement.classList.add(DISMISSED_CLASS);
328
+ };
329
+ return () => {
330
+ const announcement = latest.value;
331
+ if (!data.value || !announcement || dismissed.value?.includes(announcement.id)) return null;
332
+ const { labels, listLink } = data.value;
333
+ const count = active.value.length;
334
+ const countLabel = labels.announcementCount.replace("{n}", String(count));
335
+ const badge = count > 1 ? listLink ? (0, import_vue.h)(
336
+ "a",
337
+ {
338
+ class: "vp-blog-announcement__count",
339
+ href: vitepress.withBase(listLink),
340
+ title: countLabel,
341
+ "aria-label": countLabel
342
+ },
343
+ String(count)
344
+ ) : (0, import_vue.h)(
345
+ "span",
346
+ { class: "vp-blog-announcement__count", title: countLabel, "aria-label": countLabel },
347
+ String(count)
348
+ ) : null;
349
+ return (0, import_vue.h)("div", { class: "vp-blog-announcement", role: "region", "aria-label": labels.announcement }, [
350
+ (0, import_vue.h)("div", { class: "vp-blog-announcement__content" }, [
351
+ (0, import_vue.h)(
352
+ "a",
353
+ { class: "vp-blog-announcement__text", href: vitepress.withBase(announcement.link) },
354
+ announcement.text
355
+ ),
356
+ badge
357
+ ]),
358
+ (0, import_vue.h)(
359
+ "button",
360
+ {
361
+ class: "vp-blog-announcement__dismiss",
362
+ type: "button",
363
+ title: labels.dismiss,
364
+ "aria-label": labels.dismiss,
365
+ onClick: dismiss
366
+ },
367
+ [closeIcon()]
368
+ )
369
+ ]);
370
+ };
371
+ }
372
+ });
373
+
160
374
  // src/client.ts
161
375
  function useBlog() {
162
- if (!vitepress.inBrowser) return;
163
- const route = vitepress.useRoute();
164
- const apply = () => (0, import_vue.nextTick)(() => applyBlogViews());
165
- (0, import_vue.onMounted)(() => {
166
- (0, import_vue.watch)(() => [route.path, route.query], apply, { immediate: true });
376
+ if (!vitepress2.inBrowser) return;
377
+ const route = vitepress2.useRoute();
378
+ const apply = () => (0, import_vue2.nextTick)(() => applyBlogViews());
379
+ (0, import_vue2.onMounted)(() => {
380
+ (0, import_vue2.watch)(() => [route.path, route.query], apply, { immediate: true });
167
381
  });
168
- vitepress.onContentUpdated(apply);
382
+ vitepress2.onContentUpdated(apply);
169
383
  }
170
384
  // Annotate the CommonJS export names for ESM import in node:
171
385
  0 && (module.exports = {
386
+ BlogAnnouncementBanner,
172
387
  applyBlogView,
173
388
  applyBlogViews,
389
+ readFilter,
174
390
  useBlog
175
391
  });
176
392
  //# sourceMappingURL=client.cjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/client.ts","../src/view.ts"],"sourcesContent":["// A namespace import, not named ones: during the server-side build Node loads\n// VitePress's node entry, which has no `inBrowser`, `useRoute` or\n// `onContentUpdated`. Named imports would fail to link there; on the\n// namespace they are just `undefined`.\nimport * as vitepress from 'vitepress';\nimport { nextTick, onMounted, watch } from 'vue';\nimport { applyBlogViews } from './view.js';\n\n/**\n * Turns on keyword filtering (`?tag=`) and pagination (`?page=`) on blog\n * pages. Call it from your theme's `setup()`:\n *\n * ```ts\n * // .vitepress/theme/index.ts\n * import DefaultTheme from 'vitepress/theme';\n * import { useBlog } from '@binarynoir/vitepress-blog/client';\n * import '@binarynoir/vitepress-blog/style.css';\n *\n * export default {\n * extends: DefaultTheme,\n * setup() {\n * useBlog();\n * },\n * };\n * ```\n *\n * Keyword and page links are ordinary links: VitePress's router follows them\n * without reloading the page, and this re-applies the view whenever the path\n * or query string changes. It runs after the page has hydrated, so the\n * server-rendered HTML (every post) and the first client render agree.\n * Does nothing during the server-side build.\n */\nexport function useBlog(): void {\n if (!vitepress.inBrowser) return;\n const route = vitepress.useRoute();\n const apply = () => nextTick(() => applyBlogViews());\n onMounted(() => {\n watch(() => [route.path, route.query], apply, { immediate: true });\n });\n // `vitepress dev` swaps a page's content in place when it is edited.\n vitepress.onContentUpdated(apply);\n}\n\nexport { applyBlogView, applyBlogViews, type BlogViewState } from './view.js';\n","/**\n * Keyword filtering and pagination for blog pages, in the browser. The build\n * writes every post into the page (see `renderPostList`); this hides the ones\n * the URL's `?tag=` and `?page=` leave out, and fills in the filter banner and\n * the page links. Without it, every post simply shows.\n */\n\nconst ITEM = '[data-vp-blog-item]';\n\ninterface BlogLabels {\n tagged: string;\n showAll: string;\n noTagged: string;\n previous: string;\n next: string;\n pages: string;\n}\n\nfunction readLabels(blog: HTMLElement): BlogLabels {\n const { dataset } = blog;\n return {\n tagged: dataset.labelTagged ?? 'Posts tagged {tag}',\n showAll: dataset.labelShowAll ?? 'Show all posts',\n noTagged: dataset.labelNoTagged ?? 'No posts tagged {tag}.',\n previous: dataset.labelPrevious ?? 'Previous',\n next: dataset.labelNext ?? 'Next',\n pages: dataset.labelPages ?? 'Pages',\n };\n}\n\n/** The element a marker stands for: a card is its own marker; a compact list item holds one. */\nfunction itemElement(marker: Element): HTMLElement {\n return (marker.tagName === 'ARTICLE' ? marker : (marker.closest('li') ?? marker)) as HTMLElement;\n}\n\nfunction hasTag(marker: Element, tag: string): boolean {\n return !tag || (marker.getAttribute('data-tags') ?? '').split('|').includes(tag);\n}\n\n/** `url` with `tag` and `page` set (or removed when empty or page 1), as a path the router can follow. */\nfunction toHref(url: URL, tag: string, page: number): string {\n const next = new URL(url.href);\n next.hash = '';\n if (tag) next.searchParams.set('tag', tag);\n else next.searchParams.delete('tag');\n if (page > 1) next.searchParams.set('page', String(page));\n else next.searchParams.delete('page');\n return next.pathname + next.search;\n}\n\n/** `template` with `{tag}` replaced by a `<strong>` holding the tag, as text: the tag comes from the URL. */\nfunction fillWithTag(element: HTMLElement, template: string, tag: string): void {\n const [before, after = ''] = template.split('{tag}');\n const strong = document.createElement('strong');\n strong.textContent = tag;\n element.replaceChildren(document.createTextNode(before), ...(template.includes('{tag}') ? [strong] : []), after);\n}\n\nfunction link(text: string, href: string, className: string): HTMLAnchorElement {\n const anchor = document.createElement('a');\n anchor.className = className;\n anchor.href = href;\n anchor.textContent = text;\n return anchor;\n}\n\nfunction renderFilter(slot: HTMLElement, url: URL, tag: string, matches: number, labels: BlogLabels): void {\n slot.hidden = !tag;\n if (!tag) return slot.replaceChildren();\n const message = document.createElement('span');\n fillWithTag(message, matches ? labels.tagged : labels.noTagged, tag);\n slot.replaceChildren(message, ' ', link(labels.showAll, toHref(url, '', 1), 'vp-blog-filter__clear'));\n}\n\nfunction renderPagination(\n slot: HTMLElement,\n url: URL,\n tag: string,\n page: number,\n pageCount: number,\n labels: BlogLabels,\n): void {\n slot.hidden = pageCount < 2;\n slot.setAttribute('aria-label', labels.pages);\n if (pageCount < 2) return slot.replaceChildren();\n\n const parts: Node[] = [];\n if (page > 1) parts.push(link(labels.previous, toHref(url, tag, page - 1), 'vp-blog-pagination__previous'));\n for (let number = 1; number <= pageCount; number++) {\n if (number === page) {\n const current = document.createElement('span');\n current.className = 'vp-blog-pagination__page is-current';\n current.setAttribute('aria-current', 'page');\n current.textContent = String(number);\n parts.push(current);\n } else {\n parts.push(link(String(number), toHref(url, tag, number), 'vp-blog-pagination__page'));\n }\n }\n if (page < pageCount) parts.push(link(labels.next, toHref(url, tag, page + 1), 'vp-blog-pagination__next'));\n slot.replaceChildren(...parts);\n}\n\n/** Marks the keyword links (in the keyword list and on cards) that match the active keyword. */\nfunction markActiveTags(blog: HTMLElement, tag: string): void {\n for (const anchor of blog.querySelectorAll<HTMLAnchorElement>('.vp-blog-tags a, .vp-blog-card__tags a')) {\n let linkTag = '';\n try {\n linkTag = new URL(anchor.href, location.href).searchParams.get('tag') ?? '';\n } catch {\n // Leave a link that isn't a URL unmarked.\n }\n const active = !!tag && linkTag.toLowerCase() === tag;\n anchor.classList.toggle('is-active', active);\n if (active) anchor.setAttribute('aria-current', 'true');\n else anchor.removeAttribute('aria-current');\n }\n}\n\nexport interface BlogViewState {\n tag: string;\n page: number;\n pageCount: number;\n}\n\n/**\n * Shows the posts of one blog page that `url` asks for: those with its\n * `?tag=` keyword (any case), and of those, the ones on its `?page=`. Pinned\n * sticky posts only show on the first page and aren't counted in the page\n * size. Returns what is showing.\n */\nexport function applyBlogView(blog: HTMLElement, url: URL): BlogViewState {\n const labels = readLabels(blog);\n const tag = (url.searchParams.get('tag') ?? '').trim().toLowerCase();\n const pageSize = Math.max(0, Number(blog.dataset.pageSize) || 0);\n\n const markers = [...blog.querySelectorAll(ITEM)];\n const isSticky = (marker: Element) => !!marker.closest('[data-vp-blog-sticky]');\n const sticky = markers.filter((marker) => isSticky(marker) && hasTag(marker, tag));\n const rest = markers.filter((marker) => !isSticky(marker) && hasTag(marker, tag));\n\n const pageCount = pageSize ? Math.max(1, Math.ceil(rest.length / pageSize)) : 1;\n const requested = Number.parseInt(url.searchParams.get('page') ?? '1', 10);\n const page = Math.min(pageCount, Math.max(1, Number.isFinite(requested) ? requested : 1));\n\n const shown = new Set<Element>([\n ...(page === 1 ? sticky : []),\n ...(pageSize ? rest.slice((page - 1) * pageSize, page * pageSize) : rest),\n ]);\n for (const marker of markers) itemElement(marker).hidden = !shown.has(marker);\n\n // A sticky section or year with nothing showing is hidden, heading and all.\n for (const group of blog.querySelectorAll<HTMLElement>('[data-vp-blog-sticky], .vp-blog-year')) {\n group.hidden = ![...group.querySelectorAll(ITEM)].some((marker) => shown.has(marker));\n }\n\n const filter = blog.querySelector<HTMLElement>('[data-vp-blog-filter]');\n if (filter) renderFilter(filter, url, tag, sticky.length + rest.length, labels);\n const pagination = blog.querySelector<HTMLElement>('[data-vp-blog-pagination]');\n if (pagination) renderPagination(pagination, url, tag, page, pageCount, labels);\n markActiveTags(blog, tag);\n\n return { tag, page, pageCount };\n}\n\nconst lastViews = new WeakMap<HTMLElement, BlogViewState>();\n\n/**\n * Applies `url` to every blog page in `root`. When the page or keyword of a\n * blog already on screen changes, it is scrolled back to its top.\n */\nexport function applyBlogViews(root: ParentNode = document, url: URL = new URL(location.href)): void {\n for (const blog of root.querySelectorAll<HTMLElement>('[data-vp-blog]')) {\n const previous = lastViews.get(blog);\n const view = applyBlogView(blog, url);\n lastViews.set(blog, view);\n if (\n previous &&\n (previous.page !== view.page || previous.tag !== view.tag) &&\n blog.getBoundingClientRect().top < 0\n ) {\n blog.scrollIntoView({ block: 'start' });\n }\n }\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/client.ts","../src/view.ts","../src/banner.ts","../src/util.ts"],"sourcesContent":["// A namespace import, not named ones: during the server-side build Node loads\n// VitePress's node entry, which has no `inBrowser`, `useRoute` or\n// `onContentUpdated`. Named imports would fail to link there; on the\n// namespace they are just `undefined`.\nimport * as vitepress from 'vitepress';\nimport { nextTick, onMounted, watch } from 'vue';\nimport { applyBlogViews } from './view.js';\n\n/**\n * Turns on filtering (`?category=`, `?tag=`, `?year=`, `?month=`) and pagination (`?page=`) on blog\n * pages. Call it from your theme's `setup()`:\n *\n * ```ts\n * // .vitepress/theme/index.ts\n * import DefaultTheme from 'vitepress/theme';\n * import { useBlog } from '@binarynoir/vitepress-blog/client';\n * import '@binarynoir/vitepress-blog/style.css';\n *\n * export default {\n * extends: DefaultTheme,\n * setup() {\n * useBlog();\n * },\n * };\n * ```\n *\n * Filter and page links are ordinary links: VitePress's router follows them\n * without reloading the page, and this re-applies the view whenever the path\n * or query string changes. It runs after the page has hydrated, so the\n * server-rendered HTML (every post) and the first client render agree.\n * Does nothing during the server-side build.\n */\nexport function useBlog(): void {\n if (!vitepress.inBrowser) return;\n const route = vitepress.useRoute();\n const apply = () => nextTick(() => applyBlogViews());\n onMounted(() => {\n watch(() => [route.path, route.query], apply, { immediate: true });\n });\n // `vitepress dev` swaps a page's content in place when it is edited.\n vitepress.onContentUpdated(apply);\n}\n\nexport { BlogAnnouncementBanner } from './banner.js';\nexport { applyBlogView, applyBlogViews, readFilter, type BlogFilter, type BlogViewState } from './view.js';\n","/**\n * Filtering and pagination for blog pages, in the browser. The build writes\n * every post into the page (see `renderPostList`); this hides the ones the\n * URL's `?category=`, `?tag=`, `?year=`, `?month=`, `?announcements` and\n * `?page=` leave out, keeps the browse panel's chips and counts in step with\n * them, and fills in the active filters and the page links. Without it, every\n * post simply shows.\n */\n\nconst ITEM = '[data-vp-blog-item]';\n/** Links that filter the blog: the browse panel's chips, and the category and keyword links on cards. */\nconst FILTER_LINKS = '.vp-blog-facet a, .vp-blog-card__category a, .vp-blog-card__tags a';\n\ninterface BlogLabels {\n showAll: string;\n previous: string;\n next: string;\n pages: string;\n announcements: string;\n category: string;\n tag: string;\n postCount: string;\n postCountOne: string;\n noMatches: string;\n removeFilter: string;\n}\n\nfunction readLabels(blog: HTMLElement): BlogLabels {\n const { dataset } = blog;\n return {\n showAll: dataset.labelShowAll ?? 'Show all posts',\n previous: dataset.labelPrevious ?? 'Previous',\n next: dataset.labelNext ?? 'Next',\n pages: dataset.labelPages ?? 'Pages',\n announcements: dataset.labelAnnouncements ?? 'Announcements',\n category: dataset.labelCategory ?? 'Category',\n tag: dataset.labelTag ?? 'Keyword',\n postCount: dataset.labelPostCount ?? '{n} posts',\n postCountOne: dataset.labelPostCountOne ?? '{n} post',\n noMatches: dataset.labelNoMatches ?? 'No posts match these filters.',\n removeFilter: dataset.labelRemoveFilter ?? 'Remove filter: {filter}',\n };\n}\n\n/**\n * What the URL filters by. Every filter must match. The category and keyword\n * are lowercased; `date` is `''`, a year (`2026`) or a month (`2026-10`).\n */\nexport interface BlogFilter {\n category: string;\n tag: string;\n date: string;\n /** Whether only announcements show (`?announcements`). */\n announcements: boolean;\n}\n\ntype Facet = 'category' | 'tag' | 'date';\n\n/** Reads the filter from `?category=`, `?tag=`, `?year=` and `?month=`. A month names its year too, so it wins over `?year=`. */\nexport function readFilter(url: URL): BlogFilter {\n const text = (name: string) => (url.searchParams.get(name) ?? '').trim().toLowerCase();\n const month = text('month');\n const year = text('year');\n return {\n category: text('category'),\n tag: text('tag'),\n date: /^\\d{4}-(?:0[1-9]|1[0-2])$/.test(month) ? month : /^\\d{4}$/.test(year) ? year : '',\n announcements: url.searchParams.has('announcements'),\n };\n}\n\n/** The element a marker stands for: a card is its own marker; a compact list item holds one. */\nfunction itemElement(marker: Element): HTMLElement {\n return (marker.tagName === 'ARTICLE' ? marker : (marker.closest('li') ?? marker)) as HTMLElement;\n}\n\nfunction matches(marker: Element, filter: BlogFilter): boolean {\n const { category, tag, date, announcements } = filter;\n return (\n (!tag || (marker.getAttribute('data-tags') ?? '').split('|').includes(tag)) &&\n (!category || marker.getAttribute('data-category') === category) &&\n // `2026` matches every month of 2026; `2026-10` only October.\n (!date || (marker.getAttribute('data-month') ?? '').startsWith(date)) &&\n (!announcements || marker.hasAttribute('data-announcement'))\n );\n}\n\n/** `url` with the filter and `page` set (or removed when empty or page 1), as a path the router can follow. */\nfunction toHref(url: URL, filter: BlogFilter, page = 1): string {\n const next = new URL(url.href);\n next.hash = '';\n const set = (name: string, value: string) =>\n value ? next.searchParams.set(name, value) : next.searchParams.delete(name);\n set('category', filter.category);\n set('tag', filter.tag);\n set('year', filter.date.length === 4 ? filter.date : '');\n set('month', filter.date.length === 7 ? filter.date : '');\n if (filter.announcements) next.searchParams.set('announcements', '');\n else next.searchParams.delete('announcements');\n set('page', page > 1 ? String(page) : '');\n // A bare `?announcements`, not `?announcements=`.\n return next.pathname + next.search.replace(/([?&]announcements)=(?=&|$)/, '$1');\n}\n\n/** The filter a link applies, as the build wrote it: `?category=x`, `?tag=x`, `?year=2026` or `?month=2026-10`. */\ninterface FilterLink {\n facet: Facet;\n value: string;\n}\n\n/**\n * The filter a link stands for. It is read from the link's URL the first time\n * and remembered, because the browse panel's links are then rewritten to\n * combine it with the other active filters.\n */\nfunction readFilterLink(anchor: HTMLAnchorElement): FilterLink | null {\n if (!anchor.dataset.vpBlogFacet) {\n let params: URLSearchParams;\n try {\n params = new URL(anchor.href, location.href).searchParams;\n } catch {\n return null;\n }\n const param = (['category', 'tag', 'month', 'year'] as const).find((name) => params.has(name));\n if (!param) return null;\n anchor.dataset.vpBlogFacet = param === 'year' || param === 'month' ? 'date' : param;\n anchor.dataset.vpBlogValue = (params.get(param) ?? '').trim().toLowerCase();\n }\n return { facet: anchor.dataset.vpBlogFacet as Facet, value: anchor.dataset.vpBlogValue ?? '' };\n}\n\nfunction withValue(filter: BlogFilter, { facet, value }: FilterLink): BlogFilter {\n return { ...filter, [facet]: value };\n}\n\n/** Whether the link's filter is applied. A year is also active while one of its months is. */\nfunction isActive({ facet, value }: FilterLink, filter: BlogFilter): boolean {\n if (facet !== 'date') return filter[facet] === value;\n return value.length === 4 ? filter.date.startsWith(value) : filter.date === value;\n}\n\n/**\n * Where a browse chip leads: its filter added to the active ones (replacing\n * any other value of the same kind), or, when it is the active one, taken off\n * again. An active month steps back out to its year.\n */\nfunction toggle(filter: BlogFilter, link: FilterLink): BlogFilter {\n if (link.facet !== 'date')\n return withValue(filter, { ...link, value: filter[link.facet] === link.value ? '' : link.value });\n if (filter.date !== link.value) return withValue(filter, link);\n return { ...filter, date: link.value.length === 7 ? link.value.slice(0, 4) : '' };\n}\n\nfunction markActive(anchor: HTMLAnchorElement, active: boolean): void {\n anchor.classList.toggle('is-active', active);\n if (active) anchor.setAttribute('aria-current', 'true');\n else anchor.removeAttribute('aria-current');\n}\n\n/**\n * Brings the browse panel in step with the filter. Each chip links to the\n * view with it toggled, and counts the posts it would show with the other\n * filters kept, so a reader can see where every next step leads; chips that\n * would show nothing are dimmed. The months of the year in view show below\n * the years.\n */\nfunction syncBrowse(blog: HTMLElement, url: URL, filter: BlogFilter, markers: Element[]): void {\n const browse = blog.querySelector<HTMLElement>('.vp-blog-browse');\n if (!browse) return;\n for (const anchor of browse.querySelectorAll<HTMLAnchorElement>('.vp-blog-facet a')) {\n const link = readFilterLink(anchor);\n if (!link) continue;\n const active = isActive(link, filter);\n const count = markers.filter((marker) => matches(marker, withValue(filter, link))).length;\n anchor.href = toHref(url, toggle(filter, link));\n markActive(anchor, active);\n anchor.classList.toggle('is-empty', count === 0 && !active);\n const counter = anchor.querySelector('.vp-blog-facet__count');\n if (counter) counter.textContent = String(count);\n }\n // With only one year, its months always show.\n const months = [...browse.querySelectorAll<HTMLElement>('[data-vp-blog-facet=\"month\"]')];\n for (const row of months) row.hidden = months.length > 1 && row.dataset.year !== filter.date.slice(0, 4);\n}\n\n/** The name a filter value is shown with: as a chip or card link spells it, else as the URL gave it. */\nfunction findName(blog: HTMLElement, facet: 'category' | 'tag', value: string): string {\n for (const anchor of blog.querySelectorAll<HTMLAnchorElement>(FILTER_LINKS)) {\n const link = readFilterLink(anchor);\n if (link?.facet === facet && link.value === value) {\n const name = (anchor.querySelector('.vp-blog-facet__name') ?? anchor).textContent?.trim();\n if (name) return name;\n }\n }\n return value;\n}\n\n/** A year as written, or a month as \"October 2026\" in the blog's locale. */\nfunction formatDate(date: string, locale: string): string {\n if (date.length === 4) return date;\n const [year, month] = date.split('-').map(Number);\n try {\n return new Intl.DateTimeFormat(locale, { timeZone: 'UTC', month: 'long', year: 'numeric' }).format(\n Date.UTC(year, month - 1, 1, 12),\n );\n } catch {\n return date;\n }\n}\n\nfunction element(tag: string, className: string, text = ''): HTMLElement {\n const node = document.createElement(tag);\n node.className = className;\n node.textContent = text;\n return node;\n}\n\nfunction link(text: string, href: string, className: string): HTMLAnchorElement {\n const anchor = element('a', className, text) as HTMLAnchorElement;\n anchor.href = href;\n return anchor;\n}\n\n/** One active filter: what kind it is (`''` for a date or announcements), its name, and the view without it. */\ninterface ActiveFilter {\n kind: string;\n name: string;\n without: BlogFilter;\n}\n\nfunction listActiveFilters(blog: HTMLElement, filter: BlogFilter, labels: BlogLabels): ActiveFilter[] {\n const active: ActiveFilter[] = [];\n if (filter.announcements) {\n active.push({ kind: '', name: labels.announcements, without: { ...filter, announcements: false } });\n }\n if (filter.category) {\n const name = findName(blog, 'category', filter.category);\n active.push({ kind: labels.category, name, without: { ...filter, category: '' } });\n }\n if (filter.tag) {\n active.push({ kind: labels.tag, name: findName(blog, 'tag', filter.tag), without: { ...filter, tag: '' } });\n }\n if (filter.date) {\n const name = formatDate(filter.date, blog.dataset.locale || document.documentElement.lang || 'en-US');\n active.push({ kind: '', name, without: { ...filter, date: '' } });\n }\n return active;\n}\n\n/**\n * The active filters, above the posts: how many posts match, a chip for each\n * filter that removes just that one, and a link that clears them all. All of\n * it is written as text: the values come from the URL.\n */\nfunction renderFilter(\n slot: HTMLElement,\n blog: HTMLElement,\n url: URL,\n filter: BlogFilter,\n count: number,\n labels: BlogLabels,\n): void {\n const active = listActiveFilters(blog, filter, labels);\n slot.hidden = active.length === 0;\n if (slot.hidden) return slot.replaceChildren();\n\n const summary = count === 0 ? labels.noMatches : count === 1 ? labels.postCountOne : labels.postCount;\n const parts: Node[] = [element('span', 'vp-blog-filter__count', summary.replace('{n}', String(count)))];\n for (const { kind, name, without } of active) {\n const chip = link('', toHref(url, without), 'vp-blog-filter__chip');\n chip.setAttribute('aria-label', labels.removeFilter.replace('{filter}', kind ? `${kind} ${name}` : name));\n if (kind) chip.append(element('span', 'vp-blog-filter__kind', kind), ' ');\n const remove = element('span', 'vp-blog-filter__remove', '×');\n remove.setAttribute('aria-hidden', 'true');\n chip.append(name, remove);\n parts.push(chip);\n }\n const clear = { category: '', tag: '', date: '', announcements: false };\n parts.push(link(labels.showAll, toHref(url, clear), 'vp-blog-filter__clear'));\n slot.replaceChildren(...parts);\n}\n\nfunction renderPagination(\n slot: HTMLElement,\n url: URL,\n filter: BlogFilter,\n page: number,\n pageCount: number,\n labels: BlogLabels,\n): void {\n slot.hidden = pageCount < 2;\n slot.setAttribute('aria-label', labels.pages);\n if (pageCount < 2) return slot.replaceChildren();\n\n const parts: Node[] = [];\n if (page > 1) parts.push(link(labels.previous, toHref(url, filter, page - 1), 'vp-blog-pagination__previous'));\n for (let number = 1; number <= pageCount; number++) {\n if (number === page) {\n const current = element('span', 'vp-blog-pagination__page is-current', String(number));\n current.setAttribute('aria-current', 'page');\n parts.push(current);\n } else {\n parts.push(link(String(number), toHref(url, filter, number), 'vp-blog-pagination__page'));\n }\n }\n if (page < pageCount) parts.push(link(labels.next, toHref(url, filter, page + 1), 'vp-blog-pagination__next'));\n slot.replaceChildren(...parts);\n}\n\n/** Marks the category and keyword links on cards that match the active filter. */\nfunction markCardLinks(blog: HTMLElement, filter: BlogFilter): void {\n for (const anchor of blog.querySelectorAll<HTMLAnchorElement>('.vp-blog-card__category a, .vp-blog-card__tags a')) {\n const link = readFilterLink(anchor);\n markActive(anchor, !!link && isActive(link, filter));\n }\n}\n\nexport interface BlogViewState extends BlogFilter {\n page: number;\n pageCount: number;\n}\n\n/**\n * Shows the posts of one blog page that `url` asks for: those matching every\n * filter it gives (category and keyword in any case, a year or a month, only\n * announcements), and of those, the ones on its `?page=`. Pinned posts\n * only show on the first page and aren't counted in the page size. Returns\n * what is showing.\n */\nexport function applyBlogView(blog: HTMLElement, url: URL): BlogViewState {\n const labels = readLabels(blog);\n const filter = readFilter(url);\n const pageSize = Math.max(0, Number(blog.dataset.pageSize) || 0);\n\n const markers = [...blog.querySelectorAll(ITEM)];\n const isPinned = (marker: Element) => !!marker.closest('[data-vp-blog-pinned]');\n const pinned = markers.filter((marker) => isPinned(marker) && matches(marker, filter));\n const rest = markers.filter((marker) => !isPinned(marker) && matches(marker, filter));\n\n const pageCount = pageSize ? Math.max(1, Math.ceil(rest.length / pageSize)) : 1;\n const requested = Number.parseInt(url.searchParams.get('page') ?? '1', 10);\n const page = Math.min(pageCount, Math.max(1, Number.isFinite(requested) ? requested : 1));\n\n const shown = new Set<Element>([\n ...(page === 1 ? pinned : []),\n ...(pageSize ? rest.slice((page - 1) * pageSize, page * pageSize) : rest),\n ]);\n for (const marker of markers) itemElement(marker).hidden = !shown.has(marker);\n\n // A pinned section, year or month with nothing showing is hidden, heading and all.\n for (const group of blog.querySelectorAll<HTMLElement>('[data-vp-blog-pinned], .vp-blog-group')) {\n group.hidden = ![...group.querySelectorAll(ITEM)].some((marker) => shown.has(marker));\n }\n\n const banner = blog.querySelector<HTMLElement>('[data-vp-blog-filter]');\n if (banner) renderFilter(banner, blog, url, filter, pinned.length + rest.length, labels);\n const pagination = blog.querySelector<HTMLElement>('[data-vp-blog-pagination]');\n if (pagination) renderPagination(pagination, url, filter, page, pageCount, labels);\n syncBrowse(blog, url, filter, markers);\n markCardLinks(blog, filter);\n\n return { ...filter, page, pageCount };\n}\n\nconst lastViews = new WeakMap<HTMLElement, BlogViewState>();\n\nfunction viewChanged(previous: BlogViewState, view: BlogViewState): boolean {\n return (['category', 'tag', 'date', 'announcements', 'page'] as const).some((key) => previous[key] !== view[key]);\n}\n\n/**\n * Applies `url` to every blog page in `root`. When the page or a filter of a\n * blog already on screen changes, it is scrolled back to its top.\n */\nexport function applyBlogViews(root: ParentNode = document, url: URL = new URL(location.href)): void {\n for (const blog of root.querySelectorAll<HTMLElement>('[data-vp-blog]')) {\n const previous = lastViews.get(blog);\n const view = applyBlogView(blog, url);\n lastViews.set(blog, view);\n if (previous && viewChanged(previous, view) && blog.getBoundingClientRect().top < 0) {\n blog.scrollIntoView({ block: 'start' });\n }\n }\n}\n","// A namespace import, as in `client.ts`: loaded by Node without the Vite\n// plugin, VitePress's node entry has no `useData` or `withBase`.\nimport * as vitepress from 'vitepress';\nimport { computed, defineComponent, h, onMounted, ref } from 'vue';\nimport type { BlogAnnouncementData } from './types.js';\nimport { ANNOUNCEMENTS_KEY, DISMISSED_CLASS, DISMISSED_STORAGE_KEY } from './util.js';\n\nconst closeIcon = () =>\n h('svg', { viewBox: '0 0 24 24', width: 16, height: 16, 'aria-hidden': 'true' }, [\n h('path', {\n fill: 'currentColor',\n d: 'M18.3 5.71a1 1 0 0 0-1.41 0L12 10.59 7.11 5.7A1 1 0 0 0 5.7 7.11L10.59 12 5.7 16.89a1 1 0 1 0 1.41 1.41L12 13.41l4.89 4.89a1 1 0 0 0 1.41-1.41L13.41 12l4.89-4.89a1 1 0 0 0 0-1.4z',\n }),\n ]);\n\nfunction readDismissed(): string[] {\n try {\n const value: unknown = JSON.parse(localStorage.getItem(DISMISSED_STORAGE_KEY) ?? '[]');\n return Array.isArray(value) ? value.filter((id): id is string => typeof id === 'string') : [];\n } catch {\n return [];\n }\n}\n\nfunction writeDismissed(ids: string[]): void {\n try {\n localStorage.setItem(DISMISSED_STORAGE_KEY, JSON.stringify(ids));\n } catch {\n // Storage blocked (a private window, say): dismissed until the next page load.\n }\n}\n\n/**\n * The announcement banner: one line across the top of the page, above the\n * nav bar, with the newest announcement linked to its post. With more than\n * one not yet dismissed, a badge counts them and links to the blog page\n * filtered to announcements. Its close button dismisses every current one;\n * a newer announcement brings it back. Put it in the theme's `layout-top` slot:\n *\n * ```ts\n * // .vitepress/theme/index.ts\n * import { h } from 'vue';\n * import DefaultTheme from 'vitepress/theme';\n * import { BlogAnnouncementBanner } from '@binarynoir/vitepress-blog/client';\n *\n * export default {\n * extends: DefaultTheme,\n * Layout: () => h(DefaultTheme.Layout, null, { 'layout-top': () => h(BlogAnnouncementBanner) }),\n * };\n * ```\n *\n * It reads what `blogVitePlugin` (or `withBlog`) puts on the site data, and\n * renders nothing without it.\n */\nexport const BlogAnnouncementBanner = defineComponent({\n name: 'BlogAnnouncementBanner',\n setup() {\n if (typeof vitepress.useData !== 'function') return () => null;\n const { site } = vitepress.useData();\n const data = computed(\n () => (site.value as unknown as Record<string, unknown>)[ANNOUNCEMENTS_KEY] as BlogAnnouncementData | undefined,\n );\n // Unknown until mounted: the server can't read storage, so the first render\n // matches its HTML, and the `<head>` script has already hidden it if dismissed.\n const dismissed = ref<string[] | null>(null);\n onMounted(() => {\n dismissed.value = readDismissed();\n });\n\n const latest = computed(() => data.value?.items[0]);\n const active = computed(() => (data.value?.items ?? []).filter((item) => !dismissed.value?.includes(item.id)));\n\n const dismiss = () => {\n const ids = (data.value?.items ?? []).map((item) => item.id);\n dismissed.value = ids;\n writeDismissed(ids);\n document.documentElement.classList.add(DISMISSED_CLASS);\n };\n\n return () => {\n const announcement = latest.value;\n if (!data.value || !announcement || dismissed.value?.includes(announcement.id)) return null;\n const { labels, listLink } = data.value;\n const count = active.value.length;\n const countLabel = labels.announcementCount.replace('{n}', String(count));\n const badge =\n count > 1\n ? listLink\n ? h(\n 'a',\n {\n class: 'vp-blog-announcement__count',\n href: vitepress.withBase(listLink),\n title: countLabel,\n 'aria-label': countLabel,\n },\n String(count),\n )\n : h(\n 'span',\n { class: 'vp-blog-announcement__count', title: countLabel, 'aria-label': countLabel },\n String(count),\n )\n : null;\n return h('div', { class: 'vp-blog-announcement', role: 'region', 'aria-label': labels.announcement }, [\n h('div', { class: 'vp-blog-announcement__content' }, [\n h(\n 'a',\n { class: 'vp-blog-announcement__text', href: vitepress.withBase(announcement.link) },\n announcement.text,\n ),\n badge,\n ]),\n h(\n 'button',\n {\n class: 'vp-blog-announcement__dismiss',\n type: 'button',\n title: labels.dismiss,\n 'aria-label': labels.dismiss,\n onClick: dismiss,\n },\n [closeIcon()],\n ),\n ]);\n };\n },\n});\n","const HTML_ESCAPES: Record<string, string> = {\n '&': '&amp;',\n '<': '&lt;',\n '>': '&gt;',\n '\"': '&quot;',\n \"'\": '&#39;',\n};\n\n/** Escapes the characters that are special in HTML text and attribute values. */\nexport function escapeHtml(value: string): string {\n return value.replace(/[&<>\"']/g, (character) => HTML_ESCAPES[character]);\n}\n\n/**\n * Escapes text so it stays literal in Markdown and in VitePress's Vue\n * templates. Text with braces goes in a `v-pre` span, since Vue would otherwise\n * evaluate `{{ }}` even when the Markdown escaped it.\n */\nexport function escapeMarkdown(text: string): string {\n const escaped = text\n .replace(/[\\\\`*_[\\]#|~]/g, '\\\\$&')\n .replace(/&/g, '&amp;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;');\n return /[{}]/.test(escaped) ? `<span v-pre>${escaped}</span>` : escaped;\n}\n\n/** HTML text that Vue leaves alone: braces go in a `v-pre` span. */\nexport function escapeTemplateText(text: string): string {\n const escaped = escapeHtml(text);\n return /[{}]/.test(escaped) ? `<span v-pre>${escaped}</span>` : escaped;\n}\n\n/** A URL with a scheme (`https:`, `mailto:`) or protocol-relative (`//cdn`). */\nexport function isExternalUrl(url: string): boolean {\n return /^(?:[a-z][a-z\\d+.-]*:|\\/\\/)/i.test(url);\n}\n\n/** Encodes a URL for a Markdown link destination: spaces and parentheses would end it early. */\nexport function toMarkdownUrl(url: string): string {\n return encodeURI(url).replace(/[()]/g, (character) => (character === '(' ? '%28' : '%29'));\n}\n\n/** A Markdown link to a page by its served path, which VitePress then checks and gives the site's base. */\nexport function toPageLink(text: string, routePath: string): string {\n return `[${escapeMarkdown(text)}](${toMarkdownUrl(`/${routePath}`)})`;\n}\n\n/** A page's URL path, without the site's base: `news/index.md` is `/news/`, `news/launch.md` is `/news/launch`. */\nexport function toUrlPath(routePath: string, cleanUrls: boolean): string {\n return `/${routePath.replace(/(^|\\/)index\\.md$/, '$1').replace(/\\.md$/, cleanUrls ? '' : '.html')}`;\n}\n\n/** `base` and `urlPath` joined with exactly one slash between them. */\nexport function joinUrl(base: string, urlPath: string): string {\n return `${base.replace(/\\/+$/, '')}/${urlPath.replace(/^\\/+/, '')}`;\n}\n\n/** Where the announcement banner's data sits on VitePress's site data: `useData().site.value.blogAnnouncements`. */\nexport const ANNOUNCEMENTS_KEY = 'blogAnnouncements';\n\n/** The `localStorage` key holding the ids of the announcements a visitor dismissed, as a JSON array. */\nexport const DISMISSED_STORAGE_KEY = 'vp-blog-announcements-dismissed';\n\n/** The class on `<html>` that hides a dismissed banner before the page hydrates. */\nexport const DISMISSED_CLASS = 'vp-blog-announcement-dismissed';\n"],"mappings":"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","names":["vitepress","import_vue","link"]}