@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/README.md +238 -88
- package/dist/chunk-42Z6H7IN.js +52 -0
- package/dist/chunk-42Z6H7IN.js.map +1 -0
- package/dist/{chunk-PWNWSYI4.js → chunk-ZM5FOUT4.js} +229 -106
- package/dist/chunk-ZM5FOUT4.js.map +1 -0
- package/dist/client.cjs +280 -64
- package/dist/client.cjs.map +1 -1
- package/dist/client.d.cts +56 -13
- package/dist/client.d.ts +56 -13
- package/dist/client.js +275 -62
- package/dist/client.js.map +1 -1
- package/dist/index.cjs +205 -70
- 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 +2 -1
- package/dist/{plugin-BlfpqyX4.d.cts → plugin-DUTUA_t4.d.cts} +73 -19
- package/dist/{plugin-BlfpqyX4.d.ts → plugin-DUTUA_t4.d.ts} +73 -19
- package/dist/style.css +270 -25
- package/dist/vitepress.cjs +341 -138
- package/dist/vitepress.cjs.map +1 -1
- package/dist/vitepress.d.cts +17 -7
- package/dist/vitepress.d.ts +17 -7
- package/dist/vitepress.js +76 -8
- package/dist/vitepress.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-PWNWSYI4.js.map +0 -1
package/dist/client.d.cts
CHANGED
|
@@ -1,29 +1,72 @@
|
|
|
1
|
+
import * as vue from 'vue';
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* The announcement banner: one line across the top of the page, above the
|
|
5
|
+
* nav bar, with the newest announcement linked to its post. With more than
|
|
6
|
+
* one not yet dismissed, a badge counts them and links to the blog page
|
|
7
|
+
* filtered to announcements. Its close button dismisses every current one;
|
|
8
|
+
* a newer announcement brings it back. Put it in the theme's `layout-top` slot:
|
|
9
|
+
*
|
|
10
|
+
* ```ts
|
|
11
|
+
* // .vitepress/theme/index.ts
|
|
12
|
+
* import { h } from 'vue';
|
|
13
|
+
* import DefaultTheme from 'vitepress/theme';
|
|
14
|
+
* import { BlogAnnouncementBanner } from '@binarynoir/vitepress-blog/client';
|
|
15
|
+
*
|
|
16
|
+
* export default {
|
|
17
|
+
* extends: DefaultTheme,
|
|
18
|
+
* Layout: () => h(DefaultTheme.Layout, null, { 'layout-top': () => h(BlogAnnouncementBanner) }),
|
|
19
|
+
* };
|
|
20
|
+
* ```
|
|
21
|
+
*
|
|
22
|
+
* It reads what `blogVitePlugin` (or `withBlog`) puts on the site data, and
|
|
23
|
+
* renders nothing without it.
|
|
24
|
+
*/
|
|
25
|
+
declare const BlogAnnouncementBanner: vue.DefineComponent<{}, () => vue.VNode<vue.RendererNode, vue.RendererElement, {
|
|
26
|
+
[key: string]: any;
|
|
27
|
+
}> | null, {}, {}, {}, vue.ComponentOptionsMixin, vue.ComponentOptionsMixin, {}, string, vue.PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, vue.ComponentProvideOptions, true, {}, any>;
|
|
28
|
+
|
|
29
|
+
/**
|
|
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.
|
|
36
|
+
*/
|
|
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`).
|
|
6
40
|
*/
|
|
7
|
-
interface
|
|
41
|
+
interface BlogFilter {
|
|
42
|
+
category: string;
|
|
8
43
|
tag: string;
|
|
44
|
+
date: string;
|
|
45
|
+
/** Whether only announcements show (`?announcements`). */
|
|
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 {
|
|
9
51
|
page: number;
|
|
10
52
|
pageCount: number;
|
|
11
53
|
}
|
|
12
54
|
/**
|
|
13
|
-
* Shows the posts of one blog page that `url` asks for: those
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
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.
|
|
17
60
|
*/
|
|
18
61
|
declare function applyBlogView(blog: HTMLElement, url: URL): BlogViewState;
|
|
19
62
|
/**
|
|
20
|
-
* 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
|
|
21
64
|
* blog already on screen changes, it is scrolled back to its top.
|
|
22
65
|
*/
|
|
23
66
|
declare function applyBlogViews(root?: ParentNode, url?: URL): void;
|
|
24
67
|
|
|
25
68
|
/**
|
|
26
|
-
* Turns on
|
|
69
|
+
* Turns on filtering (`?category=`, `?tag=`, `?year=`, `?month=`) and pagination (`?page=`) on blog
|
|
27
70
|
* pages. Call it from your theme's `setup()`:
|
|
28
71
|
*
|
|
29
72
|
* ```ts
|
|
@@ -40,7 +83,7 @@ declare function applyBlogViews(root?: ParentNode, url?: URL): void;
|
|
|
40
83
|
* };
|
|
41
84
|
* ```
|
|
42
85
|
*
|
|
43
|
-
*
|
|
86
|
+
* Filter and page links are ordinary links: VitePress's router follows them
|
|
44
87
|
* without reloading the page, and this re-applies the view whenever the path
|
|
45
88
|
* or query string changes. It runs after the page has hydrated, so the
|
|
46
89
|
* server-rendered HTML (every post) and the first client render agree.
|
|
@@ -48,4 +91,4 @@ declare function applyBlogViews(root?: ParentNode, url?: URL): void;
|
|
|
48
91
|
*/
|
|
49
92
|
declare function useBlog(): void;
|
|
50
93
|
|
|
51
|
-
export { type BlogViewState, applyBlogView, applyBlogViews, useBlog };
|
|
94
|
+
export { BlogAnnouncementBanner, type BlogFilter, type BlogViewState, applyBlogView, applyBlogViews, readFilter, useBlog };
|
package/dist/client.d.ts
CHANGED
|
@@ -1,29 +1,72 @@
|
|
|
1
|
+
import * as vue from 'vue';
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* The announcement banner: one line across the top of the page, above the
|
|
5
|
+
* nav bar, with the newest announcement linked to its post. With more than
|
|
6
|
+
* one not yet dismissed, a badge counts them and links to the blog page
|
|
7
|
+
* filtered to announcements. Its close button dismisses every current one;
|
|
8
|
+
* a newer announcement brings it back. Put it in the theme's `layout-top` slot:
|
|
9
|
+
*
|
|
10
|
+
* ```ts
|
|
11
|
+
* // .vitepress/theme/index.ts
|
|
12
|
+
* import { h } from 'vue';
|
|
13
|
+
* import DefaultTheme from 'vitepress/theme';
|
|
14
|
+
* import { BlogAnnouncementBanner } from '@binarynoir/vitepress-blog/client';
|
|
15
|
+
*
|
|
16
|
+
* export default {
|
|
17
|
+
* extends: DefaultTheme,
|
|
18
|
+
* Layout: () => h(DefaultTheme.Layout, null, { 'layout-top': () => h(BlogAnnouncementBanner) }),
|
|
19
|
+
* };
|
|
20
|
+
* ```
|
|
21
|
+
*
|
|
22
|
+
* It reads what `blogVitePlugin` (or `withBlog`) puts on the site data, and
|
|
23
|
+
* renders nothing without it.
|
|
24
|
+
*/
|
|
25
|
+
declare const BlogAnnouncementBanner: vue.DefineComponent<{}, () => vue.VNode<vue.RendererNode, vue.RendererElement, {
|
|
26
|
+
[key: string]: any;
|
|
27
|
+
}> | null, {}, {}, {}, vue.ComponentOptionsMixin, vue.ComponentOptionsMixin, {}, string, vue.PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, vue.ComponentProvideOptions, true, {}, any>;
|
|
28
|
+
|
|
29
|
+
/**
|
|
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.
|
|
36
|
+
*/
|
|
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`).
|
|
6
40
|
*/
|
|
7
|
-
interface
|
|
41
|
+
interface BlogFilter {
|
|
42
|
+
category: string;
|
|
8
43
|
tag: string;
|
|
44
|
+
date: string;
|
|
45
|
+
/** Whether only announcements show (`?announcements`). */
|
|
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 {
|
|
9
51
|
page: number;
|
|
10
52
|
pageCount: number;
|
|
11
53
|
}
|
|
12
54
|
/**
|
|
13
|
-
* Shows the posts of one blog page that `url` asks for: those
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
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.
|
|
17
60
|
*/
|
|
18
61
|
declare function applyBlogView(blog: HTMLElement, url: URL): BlogViewState;
|
|
19
62
|
/**
|
|
20
|
-
* 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
|
|
21
64
|
* blog already on screen changes, it is scrolled back to its top.
|
|
22
65
|
*/
|
|
23
66
|
declare function applyBlogViews(root?: ParentNode, url?: URL): void;
|
|
24
67
|
|
|
25
68
|
/**
|
|
26
|
-
* Turns on
|
|
69
|
+
* Turns on filtering (`?category=`, `?tag=`, `?year=`, `?month=`) and pagination (`?page=`) on blog
|
|
27
70
|
* pages. Call it from your theme's `setup()`:
|
|
28
71
|
*
|
|
29
72
|
* ```ts
|
|
@@ -40,7 +83,7 @@ declare function applyBlogViews(root?: ParentNode, url?: URL): void;
|
|
|
40
83
|
* };
|
|
41
84
|
* ```
|
|
42
85
|
*
|
|
43
|
-
*
|
|
86
|
+
* Filter and page links are ordinary links: VitePress's router follows them
|
|
44
87
|
* without reloading the page, and this re-applies the view whenever the path
|
|
45
88
|
* or query string changes. It runs after the page has hydrated, so the
|
|
46
89
|
* server-rendered HTML (every post) and the first client render agree.
|
|
@@ -48,4 +91,4 @@ declare function applyBlogViews(root?: ParentNode, url?: URL): void;
|
|
|
48
91
|
*/
|
|
49
92
|
declare function useBlog(): void;
|
|
50
93
|
|
|
51
|
-
export { type BlogViewState, applyBlogView, applyBlogViews, useBlog };
|
|
94
|
+
export { BlogAnnouncementBanner, type BlogFilter, type BlogViewState, applyBlogView, applyBlogViews, readFilter, useBlog };
|
package/dist/client.js
CHANGED
|
@@ -1,139 +1,352 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ANNOUNCEMENTS_KEY,
|
|
3
|
+
DISMISSED_CLASS,
|
|
4
|
+
DISMISSED_STORAGE_KEY
|
|
5
|
+
} from "./chunk-42Z6H7IN.js";
|
|
6
|
+
|
|
1
7
|
// src/client.ts
|
|
2
|
-
import * as
|
|
3
|
-
import { nextTick, onMounted, watch } from "vue";
|
|
8
|
+
import * as vitepress2 from "vitepress";
|
|
9
|
+
import { nextTick, onMounted as onMounted2, watch } from "vue";
|
|
4
10
|
|
|
5
11
|
// src/view.ts
|
|
6
12
|
var ITEM = "[data-vp-blog-item]";
|
|
13
|
+
var FILTER_LINKS = ".vp-blog-facet a, .vp-blog-card__category a, .vp-blog-card__tags a";
|
|
7
14
|
function readLabels(blog) {
|
|
8
15
|
const { dataset } = blog;
|
|
9
16
|
return {
|
|
10
|
-
tagged: dataset.labelTagged ?? "Posts tagged {tag}",
|
|
11
17
|
showAll: dataset.labelShowAll ?? "Show all posts",
|
|
12
|
-
noTagged: dataset.labelNoTagged ?? "No posts tagged {tag}.",
|
|
13
18
|
previous: dataset.labelPrevious ?? "Previous",
|
|
14
19
|
next: dataset.labelNext ?? "Next",
|
|
15
|
-
pages: dataset.labelPages ?? "Pages"
|
|
20
|
+
pages: dataset.labelPages ?? "Pages",
|
|
21
|
+
announcements: dataset.labelAnnouncements ?? "Announcements",
|
|
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
|
+
};
|
|
29
|
+
}
|
|
30
|
+
function readFilter(url) {
|
|
31
|
+
const text = (name) => (url.searchParams.get(name) ?? "").trim().toLowerCase();
|
|
32
|
+
const month = text("month");
|
|
33
|
+
const year = text("year");
|
|
34
|
+
return {
|
|
35
|
+
category: text("category"),
|
|
36
|
+
tag: text("tag"),
|
|
37
|
+
date: /^\d{4}-(?:0[1-9]|1[0-2])$/.test(month) ? month : /^\d{4}$/.test(year) ? year : "",
|
|
38
|
+
announcements: url.searchParams.has("announcements")
|
|
16
39
|
};
|
|
17
40
|
}
|
|
18
41
|
function itemElement(marker) {
|
|
19
42
|
return marker.tagName === "ARTICLE" ? marker : marker.closest("li") ?? marker;
|
|
20
43
|
}
|
|
21
|
-
function
|
|
22
|
-
|
|
44
|
+
function matches(marker, filter) {
|
|
45
|
+
const { category, tag, date, announcements } = filter;
|
|
46
|
+
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.
|
|
47
|
+
(!date || (marker.getAttribute("data-month") ?? "").startsWith(date)) && (!announcements || marker.hasAttribute("data-announcement"));
|
|
23
48
|
}
|
|
24
|
-
function toHref(url,
|
|
49
|
+
function toHref(url, filter, page = 1) {
|
|
25
50
|
const next = new URL(url.href);
|
|
26
51
|
next.hash = "";
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
52
|
+
const set = (name, value) => value ? next.searchParams.set(name, value) : next.searchParams.delete(name);
|
|
53
|
+
set("category", filter.category);
|
|
54
|
+
set("tag", filter.tag);
|
|
55
|
+
set("year", filter.date.length === 4 ? filter.date : "");
|
|
56
|
+
set("month", filter.date.length === 7 ? filter.date : "");
|
|
57
|
+
if (filter.announcements) next.searchParams.set("announcements", "");
|
|
58
|
+
else next.searchParams.delete("announcements");
|
|
59
|
+
set("page", page > 1 ? String(page) : "");
|
|
60
|
+
return next.pathname + next.search.replace(/([?&]announcements)=(?=&|$)/, "$1");
|
|
61
|
+
}
|
|
62
|
+
function readFilterLink(anchor) {
|
|
63
|
+
if (!anchor.dataset.vpBlogFacet) {
|
|
64
|
+
let params;
|
|
65
|
+
try {
|
|
66
|
+
params = new URL(anchor.href, location.href).searchParams;
|
|
67
|
+
} catch {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
const param = ["category", "tag", "month", "year"].find((name) => params.has(name));
|
|
71
|
+
if (!param) return null;
|
|
72
|
+
anchor.dataset.vpBlogFacet = param === "year" || param === "month" ? "date" : param;
|
|
73
|
+
anchor.dataset.vpBlogValue = (params.get(param) ?? "").trim().toLowerCase();
|
|
74
|
+
}
|
|
75
|
+
return { facet: anchor.dataset.vpBlogFacet, value: anchor.dataset.vpBlogValue ?? "" };
|
|
76
|
+
}
|
|
77
|
+
function withValue(filter, { facet, value }) {
|
|
78
|
+
return { ...filter, [facet]: value };
|
|
79
|
+
}
|
|
80
|
+
function isActive({ facet, value }, filter) {
|
|
81
|
+
if (facet !== "date") return filter[facet] === value;
|
|
82
|
+
return value.length === 4 ? filter.date.startsWith(value) : filter.date === value;
|
|
83
|
+
}
|
|
84
|
+
function toggle(filter, link2) {
|
|
85
|
+
if (link2.facet !== "date")
|
|
86
|
+
return withValue(filter, { ...link2, value: filter[link2.facet] === link2.value ? "" : link2.value });
|
|
87
|
+
if (filter.date !== link2.value) return withValue(filter, link2);
|
|
88
|
+
return { ...filter, date: link2.value.length === 7 ? link2.value.slice(0, 4) : "" };
|
|
89
|
+
}
|
|
90
|
+
function markActive(anchor, active) {
|
|
91
|
+
anchor.classList.toggle("is-active", active);
|
|
92
|
+
if (active) anchor.setAttribute("aria-current", "true");
|
|
93
|
+
else anchor.removeAttribute("aria-current");
|
|
94
|
+
}
|
|
95
|
+
function syncBrowse(blog, url, filter, markers) {
|
|
96
|
+
const browse = blog.querySelector(".vp-blog-browse");
|
|
97
|
+
if (!browse) return;
|
|
98
|
+
for (const anchor of browse.querySelectorAll(".vp-blog-facet a")) {
|
|
99
|
+
const link2 = readFilterLink(anchor);
|
|
100
|
+
if (!link2) continue;
|
|
101
|
+
const active = isActive(link2, filter);
|
|
102
|
+
const count = markers.filter((marker) => matches(marker, withValue(filter, link2))).length;
|
|
103
|
+
anchor.href = toHref(url, toggle(filter, link2));
|
|
104
|
+
markActive(anchor, active);
|
|
105
|
+
anchor.classList.toggle("is-empty", count === 0 && !active);
|
|
106
|
+
const counter = anchor.querySelector(".vp-blog-facet__count");
|
|
107
|
+
if (counter) counter.textContent = String(count);
|
|
108
|
+
}
|
|
109
|
+
const months = [...browse.querySelectorAll('[data-vp-blog-facet="month"]')];
|
|
110
|
+
for (const row of months) row.hidden = months.length > 1 && row.dataset.year !== filter.date.slice(0, 4);
|
|
111
|
+
}
|
|
112
|
+
function findName(blog, facet, value) {
|
|
113
|
+
for (const anchor of blog.querySelectorAll(FILTER_LINKS)) {
|
|
114
|
+
const link2 = readFilterLink(anchor);
|
|
115
|
+
if (link2?.facet === facet && link2.value === value) {
|
|
116
|
+
const name = (anchor.querySelector(".vp-blog-facet__name") ?? anchor).textContent?.trim();
|
|
117
|
+
if (name) return name;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return value;
|
|
121
|
+
}
|
|
122
|
+
function formatDate(date, locale) {
|
|
123
|
+
if (date.length === 4) return date;
|
|
124
|
+
const [year, month] = date.split("-").map(Number);
|
|
125
|
+
try {
|
|
126
|
+
return new Intl.DateTimeFormat(locale, { timeZone: "UTC", month: "long", year: "numeric" }).format(
|
|
127
|
+
Date.UTC(year, month - 1, 1, 12)
|
|
128
|
+
);
|
|
129
|
+
} catch {
|
|
130
|
+
return date;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
function element(tag, className, text = "") {
|
|
134
|
+
const node = document.createElement(tag);
|
|
135
|
+
node.className = className;
|
|
136
|
+
node.textContent = text;
|
|
137
|
+
return node;
|
|
38
138
|
}
|
|
39
139
|
function link(text, href, className) {
|
|
40
|
-
const anchor =
|
|
41
|
-
anchor.className = className;
|
|
140
|
+
const anchor = element("a", className, text);
|
|
42
141
|
anchor.href = href;
|
|
43
|
-
anchor.textContent = text;
|
|
44
142
|
return anchor;
|
|
45
143
|
}
|
|
46
|
-
function
|
|
47
|
-
|
|
48
|
-
if (
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
144
|
+
function listActiveFilters(blog, filter, labels) {
|
|
145
|
+
const active = [];
|
|
146
|
+
if (filter.announcements) {
|
|
147
|
+
active.push({ kind: "", name: labels.announcements, without: { ...filter, announcements: false } });
|
|
148
|
+
}
|
|
149
|
+
if (filter.category) {
|
|
150
|
+
const name = findName(blog, "category", filter.category);
|
|
151
|
+
active.push({ kind: labels.category, name, without: { ...filter, category: "" } });
|
|
152
|
+
}
|
|
153
|
+
if (filter.tag) {
|
|
154
|
+
active.push({ kind: labels.tag, name: findName(blog, "tag", filter.tag), without: { ...filter, tag: "" } });
|
|
155
|
+
}
|
|
156
|
+
if (filter.date) {
|
|
157
|
+
const name = formatDate(filter.date, blog.dataset.locale || document.documentElement.lang || "en-US");
|
|
158
|
+
active.push({ kind: "", name, without: { ...filter, date: "" } });
|
|
159
|
+
}
|
|
160
|
+
return active;
|
|
161
|
+
}
|
|
162
|
+
function renderFilter(slot, blog, url, filter, count, labels) {
|
|
163
|
+
const active = listActiveFilters(blog, filter, labels);
|
|
164
|
+
slot.hidden = active.length === 0;
|
|
165
|
+
if (slot.hidden) return slot.replaceChildren();
|
|
166
|
+
const summary = count === 0 ? labels.noMatches : count === 1 ? labels.postCountOne : labels.postCount;
|
|
167
|
+
const parts = [element("span", "vp-blog-filter__count", summary.replace("{n}", String(count)))];
|
|
168
|
+
for (const { kind, name, without } of active) {
|
|
169
|
+
const chip = link("", toHref(url, without), "vp-blog-filter__chip");
|
|
170
|
+
chip.setAttribute("aria-label", labels.removeFilter.replace("{filter}", kind ? `${kind} ${name}` : name));
|
|
171
|
+
if (kind) chip.append(element("span", "vp-blog-filter__kind", kind), " ");
|
|
172
|
+
const remove = element("span", "vp-blog-filter__remove", "\xD7");
|
|
173
|
+
remove.setAttribute("aria-hidden", "true");
|
|
174
|
+
chip.append(name, remove);
|
|
175
|
+
parts.push(chip);
|
|
176
|
+
}
|
|
177
|
+
const clear = { category: "", tag: "", date: "", announcements: false };
|
|
178
|
+
parts.push(link(labels.showAll, toHref(url, clear), "vp-blog-filter__clear"));
|
|
179
|
+
slot.replaceChildren(...parts);
|
|
52
180
|
}
|
|
53
|
-
function renderPagination(slot, url,
|
|
181
|
+
function renderPagination(slot, url, filter, page, pageCount, labels) {
|
|
54
182
|
slot.hidden = pageCount < 2;
|
|
55
183
|
slot.setAttribute("aria-label", labels.pages);
|
|
56
184
|
if (pageCount < 2) return slot.replaceChildren();
|
|
57
185
|
const parts = [];
|
|
58
|
-
if (page > 1) parts.push(link(labels.previous, toHref(url,
|
|
186
|
+
if (page > 1) parts.push(link(labels.previous, toHref(url, filter, page - 1), "vp-blog-pagination__previous"));
|
|
59
187
|
for (let number = 1; number <= pageCount; number++) {
|
|
60
188
|
if (number === page) {
|
|
61
|
-
const current =
|
|
62
|
-
current.className = "vp-blog-pagination__page is-current";
|
|
189
|
+
const current = element("span", "vp-blog-pagination__page is-current", String(number));
|
|
63
190
|
current.setAttribute("aria-current", "page");
|
|
64
|
-
current.textContent = String(number);
|
|
65
191
|
parts.push(current);
|
|
66
192
|
} else {
|
|
67
|
-
parts.push(link(String(number), toHref(url,
|
|
193
|
+
parts.push(link(String(number), toHref(url, filter, number), "vp-blog-pagination__page"));
|
|
68
194
|
}
|
|
69
195
|
}
|
|
70
|
-
if (page < pageCount) parts.push(link(labels.next, toHref(url,
|
|
196
|
+
if (page < pageCount) parts.push(link(labels.next, toHref(url, filter, page + 1), "vp-blog-pagination__next"));
|
|
71
197
|
slot.replaceChildren(...parts);
|
|
72
198
|
}
|
|
73
|
-
function
|
|
74
|
-
for (const anchor of blog.querySelectorAll(".vp-blog-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
linkTag = new URL(anchor.href, location.href).searchParams.get("tag") ?? "";
|
|
78
|
-
} catch {
|
|
79
|
-
}
|
|
80
|
-
const active = !!tag && linkTag.toLowerCase() === tag;
|
|
81
|
-
anchor.classList.toggle("is-active", active);
|
|
82
|
-
if (active) anchor.setAttribute("aria-current", "true");
|
|
83
|
-
else anchor.removeAttribute("aria-current");
|
|
199
|
+
function markCardLinks(blog, filter) {
|
|
200
|
+
for (const anchor of blog.querySelectorAll(".vp-blog-card__category a, .vp-blog-card__tags a")) {
|
|
201
|
+
const link2 = readFilterLink(anchor);
|
|
202
|
+
markActive(anchor, !!link2 && isActive(link2, filter));
|
|
84
203
|
}
|
|
85
204
|
}
|
|
86
205
|
function applyBlogView(blog, url) {
|
|
87
206
|
const labels = readLabels(blog);
|
|
88
|
-
const
|
|
207
|
+
const filter = readFilter(url);
|
|
89
208
|
const pageSize = Math.max(0, Number(blog.dataset.pageSize) || 0);
|
|
90
209
|
const markers = [...blog.querySelectorAll(ITEM)];
|
|
91
|
-
const
|
|
92
|
-
const
|
|
93
|
-
const rest = markers.filter((marker) => !
|
|
210
|
+
const isPinned = (marker) => !!marker.closest("[data-vp-blog-pinned]");
|
|
211
|
+
const pinned = markers.filter((marker) => isPinned(marker) && matches(marker, filter));
|
|
212
|
+
const rest = markers.filter((marker) => !isPinned(marker) && matches(marker, filter));
|
|
94
213
|
const pageCount = pageSize ? Math.max(1, Math.ceil(rest.length / pageSize)) : 1;
|
|
95
214
|
const requested = Number.parseInt(url.searchParams.get("page") ?? "1", 10);
|
|
96
215
|
const page = Math.min(pageCount, Math.max(1, Number.isFinite(requested) ? requested : 1));
|
|
97
216
|
const shown = /* @__PURE__ */ new Set([
|
|
98
|
-
...page === 1 ?
|
|
217
|
+
...page === 1 ? pinned : [],
|
|
99
218
|
...pageSize ? rest.slice((page - 1) * pageSize, page * pageSize) : rest
|
|
100
219
|
]);
|
|
101
220
|
for (const marker of markers) itemElement(marker).hidden = !shown.has(marker);
|
|
102
|
-
for (const group of blog.querySelectorAll("[data-vp-blog-
|
|
221
|
+
for (const group of blog.querySelectorAll("[data-vp-blog-pinned], .vp-blog-group")) {
|
|
103
222
|
group.hidden = ![...group.querySelectorAll(ITEM)].some((marker) => shown.has(marker));
|
|
104
223
|
}
|
|
105
|
-
const
|
|
106
|
-
if (
|
|
224
|
+
const banner = blog.querySelector("[data-vp-blog-filter]");
|
|
225
|
+
if (banner) renderFilter(banner, blog, url, filter, pinned.length + rest.length, labels);
|
|
107
226
|
const pagination = blog.querySelector("[data-vp-blog-pagination]");
|
|
108
|
-
if (pagination) renderPagination(pagination, url,
|
|
109
|
-
|
|
110
|
-
|
|
227
|
+
if (pagination) renderPagination(pagination, url, filter, page, pageCount, labels);
|
|
228
|
+
syncBrowse(blog, url, filter, markers);
|
|
229
|
+
markCardLinks(blog, filter);
|
|
230
|
+
return { ...filter, page, pageCount };
|
|
111
231
|
}
|
|
112
232
|
var lastViews = /* @__PURE__ */ new WeakMap();
|
|
233
|
+
function viewChanged(previous, view) {
|
|
234
|
+
return ["category", "tag", "date", "announcements", "page"].some((key) => previous[key] !== view[key]);
|
|
235
|
+
}
|
|
113
236
|
function applyBlogViews(root = document, url = new URL(location.href)) {
|
|
114
237
|
for (const blog of root.querySelectorAll("[data-vp-blog]")) {
|
|
115
238
|
const previous = lastViews.get(blog);
|
|
116
239
|
const view = applyBlogView(blog, url);
|
|
117
240
|
lastViews.set(blog, view);
|
|
118
|
-
if (previous && (previous
|
|
241
|
+
if (previous && viewChanged(previous, view) && blog.getBoundingClientRect().top < 0) {
|
|
119
242
|
blog.scrollIntoView({ block: "start" });
|
|
120
243
|
}
|
|
121
244
|
}
|
|
122
245
|
}
|
|
123
246
|
|
|
247
|
+
// src/banner.ts
|
|
248
|
+
import * as vitepress from "vitepress";
|
|
249
|
+
import { computed, defineComponent, h, onMounted, ref } from "vue";
|
|
250
|
+
var closeIcon = () => h("svg", { viewBox: "0 0 24 24", width: 16, height: 16, "aria-hidden": "true" }, [
|
|
251
|
+
h("path", {
|
|
252
|
+
fill: "currentColor",
|
|
253
|
+
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"
|
|
254
|
+
})
|
|
255
|
+
]);
|
|
256
|
+
function readDismissed() {
|
|
257
|
+
try {
|
|
258
|
+
const value = JSON.parse(localStorage.getItem(DISMISSED_STORAGE_KEY) ?? "[]");
|
|
259
|
+
return Array.isArray(value) ? value.filter((id) => typeof id === "string") : [];
|
|
260
|
+
} catch {
|
|
261
|
+
return [];
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
function writeDismissed(ids) {
|
|
265
|
+
try {
|
|
266
|
+
localStorage.setItem(DISMISSED_STORAGE_KEY, JSON.stringify(ids));
|
|
267
|
+
} catch {
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
var BlogAnnouncementBanner = defineComponent({
|
|
271
|
+
name: "BlogAnnouncementBanner",
|
|
272
|
+
setup() {
|
|
273
|
+
if (typeof vitepress.useData !== "function") return () => null;
|
|
274
|
+
const { site } = vitepress.useData();
|
|
275
|
+
const data = computed(
|
|
276
|
+
() => site.value[ANNOUNCEMENTS_KEY]
|
|
277
|
+
);
|
|
278
|
+
const dismissed = ref(null);
|
|
279
|
+
onMounted(() => {
|
|
280
|
+
dismissed.value = readDismissed();
|
|
281
|
+
});
|
|
282
|
+
const latest = computed(() => data.value?.items[0]);
|
|
283
|
+
const active = computed(() => (data.value?.items ?? []).filter((item) => !dismissed.value?.includes(item.id)));
|
|
284
|
+
const dismiss = () => {
|
|
285
|
+
const ids = (data.value?.items ?? []).map((item) => item.id);
|
|
286
|
+
dismissed.value = ids;
|
|
287
|
+
writeDismissed(ids);
|
|
288
|
+
document.documentElement.classList.add(DISMISSED_CLASS);
|
|
289
|
+
};
|
|
290
|
+
return () => {
|
|
291
|
+
const announcement = latest.value;
|
|
292
|
+
if (!data.value || !announcement || dismissed.value?.includes(announcement.id)) return null;
|
|
293
|
+
const { labels, listLink } = data.value;
|
|
294
|
+
const count = active.value.length;
|
|
295
|
+
const countLabel = labels.announcementCount.replace("{n}", String(count));
|
|
296
|
+
const badge = count > 1 ? listLink ? h(
|
|
297
|
+
"a",
|
|
298
|
+
{
|
|
299
|
+
class: "vp-blog-announcement__count",
|
|
300
|
+
href: vitepress.withBase(listLink),
|
|
301
|
+
title: countLabel,
|
|
302
|
+
"aria-label": countLabel
|
|
303
|
+
},
|
|
304
|
+
String(count)
|
|
305
|
+
) : h(
|
|
306
|
+
"span",
|
|
307
|
+
{ class: "vp-blog-announcement__count", title: countLabel, "aria-label": countLabel },
|
|
308
|
+
String(count)
|
|
309
|
+
) : null;
|
|
310
|
+
return h("div", { class: "vp-blog-announcement", role: "region", "aria-label": labels.announcement }, [
|
|
311
|
+
h("div", { class: "vp-blog-announcement__content" }, [
|
|
312
|
+
h(
|
|
313
|
+
"a",
|
|
314
|
+
{ class: "vp-blog-announcement__text", href: vitepress.withBase(announcement.link) },
|
|
315
|
+
announcement.text
|
|
316
|
+
),
|
|
317
|
+
badge
|
|
318
|
+
]),
|
|
319
|
+
h(
|
|
320
|
+
"button",
|
|
321
|
+
{
|
|
322
|
+
class: "vp-blog-announcement__dismiss",
|
|
323
|
+
type: "button",
|
|
324
|
+
title: labels.dismiss,
|
|
325
|
+
"aria-label": labels.dismiss,
|
|
326
|
+
onClick: dismiss
|
|
327
|
+
},
|
|
328
|
+
[closeIcon()]
|
|
329
|
+
)
|
|
330
|
+
]);
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
});
|
|
334
|
+
|
|
124
335
|
// src/client.ts
|
|
125
336
|
function useBlog() {
|
|
126
|
-
if (!
|
|
127
|
-
const route =
|
|
337
|
+
if (!vitepress2.inBrowser) return;
|
|
338
|
+
const route = vitepress2.useRoute();
|
|
128
339
|
const apply = () => nextTick(() => applyBlogViews());
|
|
129
|
-
|
|
340
|
+
onMounted2(() => {
|
|
130
341
|
watch(() => [route.path, route.query], apply, { immediate: true });
|
|
131
342
|
});
|
|
132
|
-
|
|
343
|
+
vitepress2.onContentUpdated(apply);
|
|
133
344
|
}
|
|
134
345
|
export {
|
|
346
|
+
BlogAnnouncementBanner,
|
|
135
347
|
applyBlogView,
|
|
136
348
|
applyBlogViews,
|
|
349
|
+
readFilter,
|
|
137
350
|
useBlog
|
|
138
351
|
};
|
|
139
352
|
//# sourceMappingURL=client.js.map
|