@binarynoir/vitepress-blog 0.3.0 → 0.5.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/README.md +162 -83
- package/dist/{chunk-LYTJBMVK.js → chunk-FPFH7ORO.js} +216 -75
- package/dist/chunk-FPFH7ORO.js.map +1 -0
- package/dist/client.cjs +228 -56
- package/dist/client.cjs.map +1 -1
- package/dist/client.d.cts +26 -14
- package/dist/client.d.ts +26 -14
- package/dist/client.js +227 -56
- package/dist/client.js.map +1 -1
- package/dist/index.cjs +213 -72
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/{plugin-Bb4rRwO8.d.cts → plugin-kzO68Abz.d.cts} +60 -22
- package/dist/{plugin-Bb4rRwO8.d.ts → plugin-kzO68Abz.d.ts} +60 -22
- package/dist/style.css +282 -26
- package/dist/vitepress.cjs +216 -75
- package/dist/vitepress.cjs.map +1 -1
- package/dist/vitepress.d.cts +2 -2
- package/dist/vitepress.d.ts +2 -2
- package/dist/vitepress.js +2 -2
- package/dist/vitepress.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-LYTJBMVK.js.map +0 -1
package/dist/client.d.ts
CHANGED
|
@@ -27,34 +27,46 @@ declare const BlogAnnouncementBanner: vue.DefineComponent<{}, () => vue.VNode<vu
|
|
|
27
27
|
}> | null, {}, {}, {}, vue.ComponentOptionsMixin, vue.ComponentOptionsMixin, {}, string, vue.PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, vue.ComponentProvideOptions, true, {}, any>;
|
|
28
28
|
|
|
29
29
|
/**
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
30
|
+
* Filtering and pagination for blog pages, in the browser. The build writes
|
|
31
|
+
* every post into the page (see `renderPostList`); this hides the ones the
|
|
32
|
+
* URL's `?category=`, `?tag=`, `?year=`, `?month=`, `?announcements` and
|
|
33
|
+
* `?page=` leave out, keeps the browse panel's chips and counts in step with
|
|
34
|
+
* them, and fills in the active filters and the page links. Without it, every
|
|
35
|
+
* post simply shows.
|
|
34
36
|
*/
|
|
35
|
-
|
|
37
|
+
/**
|
|
38
|
+
* What the URL filters by. Every filter must match. The category and keyword
|
|
39
|
+
* are lowercased; `date` is `''`, a year (`2026`) or a month (`2026-10`).
|
|
40
|
+
*/
|
|
41
|
+
interface BlogFilter {
|
|
42
|
+
category: string;
|
|
36
43
|
tag: string;
|
|
44
|
+
date: string;
|
|
37
45
|
/** Whether only announcements show (`?announcements`). */
|
|
38
46
|
announcements: boolean;
|
|
47
|
+
}
|
|
48
|
+
/** Reads the filter from `?category=`, `?tag=`, `?year=` and `?month=`. A month names its year too, so it wins over `?year=`. */
|
|
49
|
+
declare function readFilter(url: URL): BlogFilter;
|
|
50
|
+
interface BlogViewState extends BlogFilter {
|
|
39
51
|
page: number;
|
|
40
52
|
pageCount: number;
|
|
41
53
|
}
|
|
42
54
|
/**
|
|
43
|
-
* Shows the posts of one blog page that `url` asks for: those
|
|
44
|
-
*
|
|
45
|
-
* of those, the ones on its `?page=`. Pinned
|
|
46
|
-
*
|
|
47
|
-
*
|
|
55
|
+
* Shows the posts of one blog page that `url` asks for: those matching every
|
|
56
|
+
* filter it gives (category and keyword in any case, a year or a month, only
|
|
57
|
+
* announcements), and of those, the ones on its `?page=`. Pinned posts
|
|
58
|
+
* only show on the first page and aren't counted in the page size. Returns
|
|
59
|
+
* what is showing.
|
|
48
60
|
*/
|
|
49
61
|
declare function applyBlogView(blog: HTMLElement, url: URL): BlogViewState;
|
|
50
62
|
/**
|
|
51
|
-
* Applies `url` to every blog page in `root`. When the page or
|
|
63
|
+
* Applies `url` to every blog page in `root`. When the page or a filter of a
|
|
52
64
|
* blog already on screen changes, it is scrolled back to its top.
|
|
53
65
|
*/
|
|
54
66
|
declare function applyBlogViews(root?: ParentNode, url?: URL): void;
|
|
55
67
|
|
|
56
68
|
/**
|
|
57
|
-
* Turns on
|
|
69
|
+
* Turns on filtering (`?category=`, `?tag=`, `?year=`, `?month=`) and pagination (`?page=`) on blog
|
|
58
70
|
* pages. Call it from your theme's `setup()`:
|
|
59
71
|
*
|
|
60
72
|
* ```ts
|
|
@@ -71,7 +83,7 @@ declare function applyBlogViews(root?: ParentNode, url?: URL): void;
|
|
|
71
83
|
* };
|
|
72
84
|
* ```
|
|
73
85
|
*
|
|
74
|
-
*
|
|
86
|
+
* Filter and page links are ordinary links: VitePress's router follows them
|
|
75
87
|
* without reloading the page, and this re-applies the view whenever the path
|
|
76
88
|
* or query string changes. It runs after the page has hydrated, so the
|
|
77
89
|
* server-rendered HTML (every post) and the first client render agree.
|
|
@@ -79,4 +91,4 @@ declare function applyBlogViews(root?: ParentNode, url?: URL): void;
|
|
|
79
91
|
*/
|
|
80
92
|
declare function useBlog(): void;
|
|
81
93
|
|
|
82
|
-
export { BlogAnnouncementBanner, type BlogViewState, applyBlogView, applyBlogViews, useBlog };
|
|
94
|
+
export { BlogAnnouncementBanner, type BlogFilter, type BlogViewState, applyBlogView, applyBlogViews, readFilter, useBlog };
|
package/dist/client.js
CHANGED
|
@@ -10,65 +10,241 @@ import { nextTick, onMounted as onMounted2, watch } from "vue";
|
|
|
10
10
|
|
|
11
11
|
// src/view.ts
|
|
12
12
|
var ITEM = "[data-vp-blog-item]";
|
|
13
|
+
var FILTER_LINKS = ".vp-blog-facet__chips a, .vp-blog-card__category a, .vp-blog-card__tags a";
|
|
13
14
|
function readLabels(blog) {
|
|
14
15
|
const { dataset } = blog;
|
|
15
16
|
return {
|
|
16
|
-
tagged: dataset.labelTagged ?? "Posts tagged {tag}",
|
|
17
17
|
showAll: dataset.labelShowAll ?? "Show all posts",
|
|
18
|
-
noTagged: dataset.labelNoTagged ?? "No posts tagged {tag}.",
|
|
19
18
|
previous: dataset.labelPrevious ?? "Previous",
|
|
20
19
|
next: dataset.labelNext ?? "Next",
|
|
21
20
|
pages: dataset.labelPages ?? "Pages",
|
|
22
21
|
announcements: dataset.labelAnnouncements ?? "Announcements",
|
|
23
|
-
|
|
22
|
+
category: dataset.labelCategory ?? "Category",
|
|
23
|
+
tag: dataset.labelTag ?? "Keyword",
|
|
24
|
+
postCount: dataset.labelPostCount ?? "{n} posts",
|
|
25
|
+
postCountOne: dataset.labelPostCountOne ?? "{n} post",
|
|
26
|
+
noMatches: dataset.labelNoMatches ?? "No posts match these filters.",
|
|
27
|
+
removeFilter: dataset.labelRemoveFilter ?? "Remove filter: {filter}",
|
|
28
|
+
more: dataset.labelMore ?? "+{n} more",
|
|
29
|
+
less: dataset.labelLess ?? "Show fewer",
|
|
30
|
+
filter: dataset.labelFilter ?? "Filter {row}",
|
|
31
|
+
noFilterMatches: dataset.labelNoFilterMatches ?? "Nothing matches."
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
function readFilter(url) {
|
|
35
|
+
const text = (name) => (url.searchParams.get(name) ?? "").trim().toLowerCase();
|
|
36
|
+
const month = text("month");
|
|
37
|
+
const year = text("year");
|
|
38
|
+
return {
|
|
39
|
+
category: text("category"),
|
|
40
|
+
tag: text("tag"),
|
|
41
|
+
date: /^\d{4}-(?:0[1-9]|1[0-2])$/.test(month) ? month : /^\d{4}$/.test(year) ? year : "",
|
|
42
|
+
announcements: url.searchParams.has("announcements")
|
|
24
43
|
};
|
|
25
44
|
}
|
|
26
45
|
function itemElement(marker) {
|
|
27
46
|
return marker.tagName === "ARTICLE" ? marker : marker.closest("li") ?? marker;
|
|
28
47
|
}
|
|
29
|
-
function hasTag(marker, tag) {
|
|
30
|
-
return !tag || (marker.getAttribute("data-tags") ?? "").split("|").includes(tag);
|
|
31
|
-
}
|
|
32
48
|
function matches(marker, filter) {
|
|
33
|
-
|
|
49
|
+
const { category, tag, date, announcements } = filter;
|
|
50
|
+
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.
|
|
51
|
+
(!date || (marker.getAttribute("data-month") ?? "").startsWith(date)) && (!announcements || marker.hasAttribute("data-announcement"));
|
|
34
52
|
}
|
|
35
|
-
function toHref(url, filter, page) {
|
|
53
|
+
function toHref(url, filter, page = 1) {
|
|
36
54
|
const next = new URL(url.href);
|
|
37
55
|
next.hash = "";
|
|
38
|
-
|
|
39
|
-
|
|
56
|
+
const set = (name, value) => value ? next.searchParams.set(name, value) : next.searchParams.delete(name);
|
|
57
|
+
set("category", filter.category);
|
|
58
|
+
set("tag", filter.tag);
|
|
59
|
+
set("year", filter.date.length === 4 ? filter.date : "");
|
|
60
|
+
set("month", filter.date.length === 7 ? filter.date : "");
|
|
40
61
|
if (filter.announcements) next.searchParams.set("announcements", "");
|
|
41
62
|
else next.searchParams.delete("announcements");
|
|
42
|
-
|
|
43
|
-
else next.searchParams.delete("page");
|
|
63
|
+
set("page", page > 1 ? String(page) : "");
|
|
44
64
|
return next.pathname + next.search.replace(/([?&]announcements)=(?=&|$)/, "$1");
|
|
45
65
|
}
|
|
46
|
-
function
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
66
|
+
function readFilterLink(anchor) {
|
|
67
|
+
if (!anchor.dataset.vpBlogFacet) {
|
|
68
|
+
let params;
|
|
69
|
+
try {
|
|
70
|
+
params = new URL(anchor.href, location.href).searchParams;
|
|
71
|
+
} catch {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
const param = ["category", "tag", "month", "year"].find((name) => params.has(name));
|
|
75
|
+
if (!param) return null;
|
|
76
|
+
anchor.dataset.vpBlogFacet = param === "year" || param === "month" ? "date" : param;
|
|
77
|
+
anchor.dataset.vpBlogValue = (params.get(param) ?? "").trim().toLowerCase();
|
|
78
|
+
}
|
|
79
|
+
return { facet: anchor.dataset.vpBlogFacet, value: anchor.dataset.vpBlogValue ?? "" };
|
|
80
|
+
}
|
|
81
|
+
function withValue(filter, { facet, value }) {
|
|
82
|
+
return { ...filter, [facet]: value };
|
|
83
|
+
}
|
|
84
|
+
function isActive({ facet, value }, filter) {
|
|
85
|
+
if (facet !== "date") return filter[facet] === value;
|
|
86
|
+
return value.length === 4 ? filter.date.startsWith(value) : filter.date === value;
|
|
87
|
+
}
|
|
88
|
+
function toggle(filter, link2) {
|
|
89
|
+
if (link2.facet !== "date")
|
|
90
|
+
return withValue(filter, { ...link2, value: filter[link2.facet] === link2.value ? "" : link2.value });
|
|
91
|
+
if (filter.date !== link2.value) return withValue(filter, link2);
|
|
92
|
+
return { ...filter, date: link2.value.length === 7 ? link2.value.slice(0, 4) : "" };
|
|
93
|
+
}
|
|
94
|
+
function markActive(anchor, active) {
|
|
95
|
+
anchor.classList.toggle("is-active", active);
|
|
96
|
+
if (active) anchor.setAttribute("aria-current", "true");
|
|
97
|
+
else anchor.removeAttribute("aria-current");
|
|
98
|
+
}
|
|
99
|
+
var FILTER_FIELD_MIN = 30;
|
|
100
|
+
function chipName(anchor) {
|
|
101
|
+
return (anchor.querySelector(".vp-blog-facet__name") ?? anchor).textContent?.trim() ?? "";
|
|
102
|
+
}
|
|
103
|
+
function rowChips(row) {
|
|
104
|
+
return [...row.querySelectorAll(".vp-blog-facet__chips a")];
|
|
105
|
+
}
|
|
106
|
+
function setUpRow(row, labels) {
|
|
107
|
+
if (row.querySelector(".vp-blog-facet__more")) return;
|
|
108
|
+
const chips = row.querySelector(".vp-blog-facet__chips");
|
|
109
|
+
if (!chips) return;
|
|
110
|
+
const anchors = rowChips(row);
|
|
111
|
+
[...anchors].sort((a, b) => chipName(a).localeCompare(chipName(b), void 0, { sensitivity: "base" })).forEach((anchor, position) => anchor.dataset.vpBlogAlphabetical = String(position));
|
|
112
|
+
const more = element("button", "vp-blog-facet__more");
|
|
113
|
+
more.type = "button";
|
|
114
|
+
more.addEventListener("click", () => {
|
|
115
|
+
row.classList.toggle("is-open");
|
|
116
|
+
layoutRow(row, labels);
|
|
117
|
+
});
|
|
118
|
+
chips.after(more);
|
|
119
|
+
if (anchors.length <= FILTER_FIELD_MIN) return;
|
|
120
|
+
const field = document.createElement("input");
|
|
121
|
+
field.type = "search";
|
|
122
|
+
field.className = "vp-blog-facet__filter";
|
|
123
|
+
field.placeholder = labels.filter.replace("{row}", row.getAttribute("aria-label") ?? "");
|
|
124
|
+
field.setAttribute("aria-label", field.placeholder);
|
|
125
|
+
field.addEventListener("input", () => layoutRow(row, labels));
|
|
126
|
+
field.addEventListener("keydown", (event) => {
|
|
127
|
+
if (event.key !== "Escape" || !field.value) return;
|
|
128
|
+
field.value = "";
|
|
129
|
+
layoutRow(row, labels);
|
|
130
|
+
});
|
|
131
|
+
const empty = element("p", "vp-blog-facet__empty", labels.noFilterMatches);
|
|
132
|
+
chips.prepend(field);
|
|
133
|
+
chips.append(empty);
|
|
134
|
+
}
|
|
135
|
+
function layoutRow(row, labels) {
|
|
136
|
+
const limit = Number(row.dataset.limit) || 0;
|
|
137
|
+
if (!limit) return;
|
|
138
|
+
setUpRow(row, labels);
|
|
139
|
+
const open = row.classList.contains("is-open");
|
|
140
|
+
const field = row.querySelector(".vp-blog-facet__filter");
|
|
141
|
+
const query = open && field ? field.value.trim().toLowerCase() : "";
|
|
142
|
+
const anchors = rowChips(row);
|
|
143
|
+
let shown = 0;
|
|
144
|
+
anchors.forEach((anchor, rank) => {
|
|
145
|
+
const visible = open ? !query || chipName(anchor).toLowerCase().includes(query) : rank < limit || anchor.classList.contains("is-active");
|
|
146
|
+
anchor.hidden = !visible;
|
|
147
|
+
anchor.style.order = open ? anchor.dataset.vpBlogAlphabetical ?? "" : "";
|
|
148
|
+
if (visible) shown++;
|
|
149
|
+
});
|
|
150
|
+
const more = row.querySelector(".vp-blog-facet__more");
|
|
151
|
+
more.hidden = !open && shown === anchors.length;
|
|
152
|
+
more.textContent = open ? labels.less : labels.more.replace("{n}", String(anchors.length - shown));
|
|
153
|
+
more.setAttribute("aria-expanded", String(open));
|
|
154
|
+
if (field) field.hidden = !open;
|
|
155
|
+
const empty = row.querySelector(".vp-blog-facet__empty");
|
|
156
|
+
if (empty) empty.hidden = !(open && shown === 0);
|
|
157
|
+
}
|
|
158
|
+
function syncBrowse(blog, url, filter, markers, labels) {
|
|
159
|
+
const browse = blog.querySelector(".vp-blog-browse");
|
|
160
|
+
if (!browse) return;
|
|
161
|
+
for (const anchor of browse.querySelectorAll(".vp-blog-facet__chips a")) {
|
|
162
|
+
const link2 = readFilterLink(anchor);
|
|
163
|
+
if (!link2) continue;
|
|
164
|
+
const active = isActive(link2, filter);
|
|
165
|
+
const count = markers.filter((marker) => matches(marker, withValue(filter, link2))).length;
|
|
166
|
+
anchor.href = toHref(url, toggle(filter, link2));
|
|
167
|
+
markActive(anchor, active);
|
|
168
|
+
anchor.classList.toggle("is-empty", count === 0 && !active);
|
|
169
|
+
const counter = anchor.querySelector(".vp-blog-facet__count");
|
|
170
|
+
if (counter) counter.textContent = String(count);
|
|
171
|
+
}
|
|
172
|
+
const months = [...browse.querySelectorAll('[data-vp-blog-facet="month"]')];
|
|
173
|
+
for (const row of months) row.hidden = months.length > 1 && row.dataset.year !== filter.date.slice(0, 4);
|
|
174
|
+
for (const row of browse.querySelectorAll(".vp-blog-facet")) {
|
|
175
|
+
layoutRow(row, labels);
|
|
176
|
+
const chips = row.querySelector(".vp-blog-facet__chips");
|
|
177
|
+
if (chips) row.classList.toggle("is-scrollable", chips.scrollWidth > chips.clientWidth + 1);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
function findName(blog, facet, value) {
|
|
181
|
+
for (const anchor of blog.querySelectorAll(FILTER_LINKS)) {
|
|
182
|
+
const link2 = readFilterLink(anchor);
|
|
183
|
+
if (link2?.facet === facet && link2.value === value) {
|
|
184
|
+
const name = (anchor.querySelector(".vp-blog-facet__name") ?? anchor).textContent?.trim();
|
|
185
|
+
if (name) return name;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return value;
|
|
189
|
+
}
|
|
190
|
+
function formatDate(date, locale) {
|
|
191
|
+
if (date.length === 4) return date;
|
|
192
|
+
const [year, month] = date.split("-").map(Number);
|
|
193
|
+
try {
|
|
194
|
+
return new Intl.DateTimeFormat(locale, { timeZone: "UTC", month: "long", year: "numeric" }).format(
|
|
195
|
+
Date.UTC(year, month - 1, 1, 12)
|
|
196
|
+
);
|
|
197
|
+
} catch {
|
|
198
|
+
return date;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
function element(tag, className, text = "") {
|
|
202
|
+
const node = document.createElement(tag);
|
|
203
|
+
node.className = className;
|
|
204
|
+
node.textContent = text;
|
|
205
|
+
return node;
|
|
51
206
|
}
|
|
52
207
|
function link(text, href, className) {
|
|
53
|
-
const anchor =
|
|
54
|
-
anchor.className = className;
|
|
208
|
+
const anchor = element("a", className, text);
|
|
55
209
|
anchor.href = href;
|
|
56
|
-
anchor.textContent = text;
|
|
57
210
|
return anchor;
|
|
58
211
|
}
|
|
59
|
-
function
|
|
60
|
-
const
|
|
61
|
-
|
|
212
|
+
function listActiveFilters(blog, filter, labels) {
|
|
213
|
+
const active = [];
|
|
214
|
+
if (filter.announcements) {
|
|
215
|
+
active.push({ kind: "", name: labels.announcements, without: { ...filter, announcements: false } });
|
|
216
|
+
}
|
|
217
|
+
if (filter.category) {
|
|
218
|
+
const name = findName(blog, "category", filter.category);
|
|
219
|
+
active.push({ kind: labels.category, name, without: { ...filter, category: "" } });
|
|
220
|
+
}
|
|
221
|
+
if (filter.tag) {
|
|
222
|
+
active.push({ kind: labels.tag, name: findName(blog, "tag", filter.tag), without: { ...filter, tag: "" } });
|
|
223
|
+
}
|
|
224
|
+
if (filter.date) {
|
|
225
|
+
const name = formatDate(filter.date, blog.dataset.locale || document.documentElement.lang || "en-US");
|
|
226
|
+
active.push({ kind: "", name, without: { ...filter, date: "" } });
|
|
227
|
+
}
|
|
228
|
+
return active;
|
|
229
|
+
}
|
|
230
|
+
function renderFilter(slot, blog, url, filter, count, labels) {
|
|
231
|
+
const active = listActiveFilters(blog, filter, labels);
|
|
232
|
+
slot.hidden = active.length === 0;
|
|
62
233
|
if (slot.hidden) return slot.replaceChildren();
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
234
|
+
const summary = count === 0 ? labels.noMatches : count === 1 ? labels.postCountOne : labels.postCount;
|
|
235
|
+
const parts = [element("span", "vp-blog-filter__count", summary.replace("{n}", String(count)))];
|
|
236
|
+
for (const { kind, name, without } of active) {
|
|
237
|
+
const chip = link("", toHref(url, without), "vp-blog-filter__chip");
|
|
238
|
+
chip.setAttribute("aria-label", labels.removeFilter.replace("{filter}", kind ? `${kind} ${name}` : name));
|
|
239
|
+
if (kind) chip.append(element("span", "vp-blog-filter__kind", kind), " ");
|
|
240
|
+
const remove = element("span", "vp-blog-filter__remove", "\xD7");
|
|
241
|
+
remove.setAttribute("aria-hidden", "true");
|
|
242
|
+
chip.append(name, remove);
|
|
243
|
+
parts.push(chip);
|
|
69
244
|
}
|
|
70
|
-
const clear = { tag: "", announcements: false };
|
|
71
|
-
|
|
245
|
+
const clear = { category: "", tag: "", date: "", announcements: false };
|
|
246
|
+
parts.push(link(labels.showAll, toHref(url, clear), "vp-blog-filter__clear"));
|
|
247
|
+
slot.replaceChildren(...parts);
|
|
72
248
|
}
|
|
73
249
|
function renderPagination(slot, url, filter, page, pageCount, labels) {
|
|
74
250
|
slot.hidden = pageCount < 2;
|
|
@@ -78,10 +254,8 @@ function renderPagination(slot, url, filter, page, pageCount, labels) {
|
|
|
78
254
|
if (page > 1) parts.push(link(labels.previous, toHref(url, filter, page - 1), "vp-blog-pagination__previous"));
|
|
79
255
|
for (let number = 1; number <= pageCount; number++) {
|
|
80
256
|
if (number === page) {
|
|
81
|
-
const current =
|
|
82
|
-
current.className = "vp-blog-pagination__page is-current";
|
|
257
|
+
const current = element("span", "vp-blog-pagination__page is-current", String(number));
|
|
83
258
|
current.setAttribute("aria-current", "page");
|
|
84
|
-
current.textContent = String(number);
|
|
85
259
|
parts.push(current);
|
|
86
260
|
} else {
|
|
87
261
|
parts.push(link(String(number), toHref(url, filter, number), "vp-blog-pagination__page"));
|
|
@@ -90,53 +264,49 @@ function renderPagination(slot, url, filter, page, pageCount, labels) {
|
|
|
90
264
|
if (page < pageCount) parts.push(link(labels.next, toHref(url, filter, page + 1), "vp-blog-pagination__next"));
|
|
91
265
|
slot.replaceChildren(...parts);
|
|
92
266
|
}
|
|
93
|
-
function
|
|
94
|
-
for (const anchor of blog.querySelectorAll(".vp-blog-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
linkTag = new URL(anchor.href, location.href).searchParams.get("tag") ?? "";
|
|
98
|
-
} catch {
|
|
99
|
-
}
|
|
100
|
-
const active = !!tag && linkTag.toLowerCase() === tag;
|
|
101
|
-
anchor.classList.toggle("is-active", active);
|
|
102
|
-
if (active) anchor.setAttribute("aria-current", "true");
|
|
103
|
-
else anchor.removeAttribute("aria-current");
|
|
267
|
+
function markCardLinks(blog, filter) {
|
|
268
|
+
for (const anchor of blog.querySelectorAll(".vp-blog-card__category a, .vp-blog-card__tags a")) {
|
|
269
|
+
const link2 = readFilterLink(anchor);
|
|
270
|
+
markActive(anchor, !!link2 && isActive(link2, filter));
|
|
104
271
|
}
|
|
105
272
|
}
|
|
106
273
|
function applyBlogView(blog, url) {
|
|
107
274
|
const labels = readLabels(blog);
|
|
108
|
-
const
|
|
109
|
-
const filter = { tag, announcements: url.searchParams.has("announcements") };
|
|
275
|
+
const filter = readFilter(url);
|
|
110
276
|
const pageSize = Math.max(0, Number(blog.dataset.pageSize) || 0);
|
|
111
277
|
const markers = [...blog.querySelectorAll(ITEM)];
|
|
112
|
-
const
|
|
113
|
-
const
|
|
114
|
-
const rest = markers.filter((marker) => !
|
|
278
|
+
const isPinned = (marker) => !!marker.closest("[data-vp-blog-pinned]");
|
|
279
|
+
const pinned = markers.filter((marker) => isPinned(marker) && matches(marker, filter));
|
|
280
|
+
const rest = markers.filter((marker) => !isPinned(marker) && matches(marker, filter));
|
|
115
281
|
const pageCount = pageSize ? Math.max(1, Math.ceil(rest.length / pageSize)) : 1;
|
|
116
282
|
const requested = Number.parseInt(url.searchParams.get("page") ?? "1", 10);
|
|
117
283
|
const page = Math.min(pageCount, Math.max(1, Number.isFinite(requested) ? requested : 1));
|
|
118
284
|
const shown = /* @__PURE__ */ new Set([
|
|
119
|
-
...page === 1 ?
|
|
285
|
+
...page === 1 ? pinned : [],
|
|
120
286
|
...pageSize ? rest.slice((page - 1) * pageSize, page * pageSize) : rest
|
|
121
287
|
]);
|
|
122
288
|
for (const marker of markers) itemElement(marker).hidden = !shown.has(marker);
|
|
123
|
-
for (const group of blog.querySelectorAll("[data-vp-blog-
|
|
289
|
+
for (const group of blog.querySelectorAll("[data-vp-blog-pinned], .vp-blog-group")) {
|
|
124
290
|
group.hidden = ![...group.querySelectorAll(ITEM)].some((marker) => shown.has(marker));
|
|
125
291
|
}
|
|
126
292
|
const banner = blog.querySelector("[data-vp-blog-filter]");
|
|
127
|
-
if (banner) renderFilter(banner, url, filter,
|
|
293
|
+
if (banner) renderFilter(banner, blog, url, filter, pinned.length + rest.length, labels);
|
|
128
294
|
const pagination = blog.querySelector("[data-vp-blog-pagination]");
|
|
129
295
|
if (pagination) renderPagination(pagination, url, filter, page, pageCount, labels);
|
|
130
|
-
|
|
131
|
-
|
|
296
|
+
syncBrowse(blog, url, filter, markers, labels);
|
|
297
|
+
markCardLinks(blog, filter);
|
|
298
|
+
return { ...filter, page, pageCount };
|
|
132
299
|
}
|
|
133
300
|
var lastViews = /* @__PURE__ */ new WeakMap();
|
|
301
|
+
function viewChanged(previous, view) {
|
|
302
|
+
return ["category", "tag", "date", "announcements", "page"].some((key) => previous[key] !== view[key]);
|
|
303
|
+
}
|
|
134
304
|
function applyBlogViews(root = document, url = new URL(location.href)) {
|
|
135
305
|
for (const blog of root.querySelectorAll("[data-vp-blog]")) {
|
|
136
306
|
const previous = lastViews.get(blog);
|
|
137
307
|
const view = applyBlogView(blog, url);
|
|
138
308
|
lastViews.set(blog, view);
|
|
139
|
-
if (previous && (previous
|
|
309
|
+
if (previous && viewChanged(previous, view) && blog.getBoundingClientRect().top < 0) {
|
|
140
310
|
blog.scrollIntoView({ block: "start" });
|
|
141
311
|
}
|
|
142
312
|
}
|
|
@@ -244,6 +414,7 @@ export {
|
|
|
244
414
|
BlogAnnouncementBanner,
|
|
245
415
|
applyBlogView,
|
|
246
416
|
applyBlogViews,
|
|
417
|
+
readFilter,
|
|
247
418
|
useBlog
|
|
248
419
|
};
|
|
249
420
|
//# sourceMappingURL=client.js.map
|
package/dist/client.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/client.ts","../src/view.ts","../src/banner.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 { BlogAnnouncementBanner } from './banner.js';\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=`, `?announcements` and `?page=` leave out, and fills in the\n * filter banner and 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 announcements: string;\n noAnnouncements: 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 announcements: dataset.labelAnnouncements ?? 'Announcements',\n noAnnouncements: dataset.labelNoAnnouncements ?? 'No 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 hasTag(marker: Element, tag: string): boolean {\n return !tag || (marker.getAttribute('data-tags') ?? '').split('|').includes(tag);\n}\n\n/** What the URL filters by: a keyword, lowercased, and whether only announcements show. */\ninterface Filter {\n tag: string;\n announcements: boolean;\n}\n\nfunction matches(marker: Element, filter: Filter): boolean {\n return hasTag(marker, filter.tag) && (!filter.announcements || marker.hasAttribute('data-announcement'));\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: Filter, page: number): string {\n const next = new URL(url.href);\n next.hash = '';\n if (filter.tag) next.searchParams.set('tag', filter.tag);\n else next.searchParams.delete('tag');\n if (filter.announcements) next.searchParams.set('announcements', '');\n else next.searchParams.delete('announcements');\n if (page > 1) next.searchParams.set('page', String(page));\n else next.searchParams.delete('page');\n // A bare `?announcements`, not `?announcements=`.\n return next.pathname + next.search.replace(/([?&]announcements)=(?=&|$)/, '$1');\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, filter: Filter, count: number, labels: BlogLabels): void {\n const { tag, announcements } = filter;\n slot.hidden = !tag && !announcements;\n if (slot.hidden) return slot.replaceChildren();\n const message = document.createElement('span');\n if (tag) {\n fillWithTag(message, count ? labels.tagged : labels.noTagged, tag);\n if (announcements) message.prepend(`${labels.announcements} · `);\n } else {\n message.textContent = count ? labels.announcements : labels.noAnnouncements;\n }\n const clear = { tag: '', announcements: false };\n slot.replaceChildren(message, ' ', link(labels.showAll, toHref(url, clear, 1), 'vp-blog-filter__clear'));\n}\n\nfunction renderPagination(\n slot: HTMLElement,\n url: URL,\n filter: Filter,\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 = 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, 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 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 /** Whether only announcements show (`?announcements`). */\n announcements: boolean;\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), only announcements with `?announcements`, and\n * 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 filter: Filter = { tag, announcements: url.searchParams.has('announcements') };\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) && matches(marker, filter));\n const rest = markers.filter((marker) => !isSticky(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 ? 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 banner = blog.querySelector<HTMLElement>('[data-vp-blog-filter]');\n if (banner) renderFilter(banner, url, filter, sticky.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 markActiveTags(blog, tag);\n\n return { tag, announcements: filter.announcements, 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 || previous.announcements !== view.announcements) &&\n blog.getBoundingClientRect().top < 0\n ) {\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"],"mappings":"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","names":["vitepress","onMounted","onMounted"]}
|
|
1
|
+
{"version":3,"sources":["../src/client.ts","../src/view.ts","../src/banner.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__chips 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 more: string;\n less: string;\n filter: string;\n noFilterMatches: 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 more: dataset.labelMore ?? '+{n} more',\n less: dataset.labelLess ?? 'Show fewer',\n filter: dataset.labelFilter ?? 'Filter {row}',\n noFilterMatches: dataset.labelNoFilterMatches ?? 'Nothing matches.',\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/** A row with more chips than this gets a field to narrow them, when it is open. */\nconst FILTER_FIELD_MIN = 30;\n\nfunction chipName(anchor: HTMLAnchorElement): string {\n return (anchor.querySelector('.vp-blog-facet__name') ?? anchor).textContent?.trim() ?? '';\n}\n\nfunction rowChips(row: HTMLElement): HTMLAnchorElement[] {\n return [...row.querySelectorAll<HTMLAnchorElement>('.vp-blog-facet__chips a')];\n}\n\n/**\n * Adds a long row's controls, once: a \"+N more\" button after its chips, in\n * view even while a narrow screen's row is scrolled, and for a very long row, a field that narrows them. Chips also\n * learn their place in alphabetical order, which an open row lists them in.\n */\nfunction setUpRow(row: HTMLElement, labels: BlogLabels): void {\n if (row.querySelector('.vp-blog-facet__more')) return;\n const chips = row.querySelector<HTMLElement>('.vp-blog-facet__chips');\n if (!chips) return;\n const anchors = rowChips(row);\n [...anchors]\n .sort((a, b) => chipName(a).localeCompare(chipName(b), undefined, { sensitivity: 'base' }))\n .forEach((anchor, position) => (anchor.dataset.vpBlogAlphabetical = String(position)));\n\n const more = element('button', 'vp-blog-facet__more') as HTMLButtonElement;\n more.type = 'button';\n more.addEventListener('click', () => {\n row.classList.toggle('is-open');\n layoutRow(row, labels);\n });\n chips.after(more);\n\n if (anchors.length <= FILTER_FIELD_MIN) return;\n const field = document.createElement('input');\n field.type = 'search';\n field.className = 'vp-blog-facet__filter';\n field.placeholder = labels.filter.replace('{row}', row.getAttribute('aria-label') ?? '');\n field.setAttribute('aria-label', field.placeholder);\n field.addEventListener('input', () => layoutRow(row, labels));\n field.addEventListener('keydown', (event) => {\n if (event.key !== 'Escape' || !field.value) return;\n field.value = '';\n layoutRow(row, labels);\n });\n const empty = element('p', 'vp-blog-facet__empty', labels.noFilterMatches);\n chips.prepend(field);\n chips.append(empty);\n}\n\n/**\n * Shows as much of a row as it should. A row with a `data-limit` shows that\n * many chips, most used first, plus any active one, then \"+N more\". Open, it\n * lists every chip alphabetically, narrowed to those matching its filter\n * field. Rows without a limit show every chip.\n */\nfunction layoutRow(row: HTMLElement, labels: BlogLabels): void {\n const limit = Number(row.dataset.limit) || 0;\n if (!limit) return;\n setUpRow(row, labels);\n const open = row.classList.contains('is-open');\n const field = row.querySelector<HTMLInputElement>('.vp-blog-facet__filter');\n const query = open && field ? field.value.trim().toLowerCase() : '';\n const anchors = rowChips(row);\n let shown = 0;\n anchors.forEach((anchor, rank) => {\n const visible = open\n ? !query || chipName(anchor).toLowerCase().includes(query)\n : rank < limit || anchor.classList.contains('is-active');\n anchor.hidden = !visible;\n anchor.style.order = open ? (anchor.dataset.vpBlogAlphabetical ?? '') : '';\n if (visible) shown++;\n });\n const more = row.querySelector<HTMLButtonElement>('.vp-blog-facet__more')!;\n more.hidden = !open && shown === anchors.length;\n more.textContent = open ? labels.less : labels.more.replace('{n}', String(anchors.length - shown));\n more.setAttribute('aria-expanded', String(open));\n if (field) field.hidden = !open;\n const empty = row.querySelector<HTMLElement>('.vp-blog-facet__empty');\n if (empty) empty.hidden = !(open && shown === 0);\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[], labels: BlogLabels): 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__chips 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 for (const row of browse.querySelectorAll<HTMLElement>('.vp-blog-facet')) {\n layoutRow(row, labels);\n // On narrow screens a row is one line that scrolls sideways; its end fades while there is more.\n const chips = row.querySelector<HTMLElement>('.vp-blog-facet__chips');\n if (chips) row.classList.toggle('is-scrollable', chips.scrollWidth > chips.clientWidth + 1);\n }\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, labels);\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"],"mappings":"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","names":["vitepress","onMounted","link","onMounted"]}
|