vitepress-velonor 0.1.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 ADDED
@@ -0,0 +1,9 @@
1
+ # vitepress-velonor
2
+
3
+ Lightweight blog micro-engine for VitePress themes.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm i vitepress-velonor
9
+ ```
@@ -0,0 +1,31 @@
1
+ "use strict";
2
+ //#region rolldown:runtime
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __copyProps = (to, from, except, desc) => {
10
+ if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
11
+ key = keys[i];
12
+ if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
13
+ get: ((k) => from[k]).bind(null, key),
14
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
15
+ });
16
+ }
17
+ return to;
18
+ };
19
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
20
+ value: mod,
21
+ enumerable: true
22
+ }) : target, mod));
23
+
24
+ //#endregion
25
+
26
+ Object.defineProperty(exports, '__toESM', {
27
+ enumerable: true,
28
+ get: function () {
29
+ return __toESM;
30
+ }
31
+ });
@@ -0,0 +1,261 @@
1
+ "use strict";
2
+ const require_chunk = require('./chunk-BCwAaXi7.cjs');
3
+ const require_constants = require('./constants-CnBvTOWr.cjs');
4
+ const require_date = require('./date-ygOc7hIU.cjs');
5
+ const vue = require_chunk.__toESM(require("vue"));
6
+ const vitepress_client = require_chunk.__toESM(require("vitepress/client"));
7
+
8
+ //#region src/utils/url.ts
9
+ const isClient = typeof window !== "undefined";
10
+ const getQueryParam = (key) => {
11
+ if (!isClient) return null;
12
+ const url = new URL(window.location.href);
13
+ return url.searchParams.get(key);
14
+ };
15
+ const setQueryParam = (key, value, options) => {
16
+ if (!isClient) return;
17
+ const url = new URL(window.location.href);
18
+ if (value === null || value === "") url.searchParams.delete(key);
19
+ else url.searchParams.set(key, value);
20
+ const replace = options?.replace ?? true;
21
+ if (replace) window.history.replaceState(null, "", url.toString());
22
+ else window.history.pushState(null, "", url.toString());
23
+ };
24
+
25
+ //#endregion
26
+ //#region src/composables/useQueryParam.ts
27
+ const useSyncedQueryParam = (options) => {
28
+ const { key, defaultValue, parse, serialize } = options;
29
+ const route = (0, vitepress_client.useRoute)();
30
+ const state = (0, vue.ref)(defaultValue);
31
+ const read = () => {
32
+ const raw = getQueryParam(key);
33
+ const next = parse ? parse(raw) : raw ?? defaultValue;
34
+ state.value = next;
35
+ };
36
+ const write = (value) => {
37
+ const current = getQueryParam(key);
38
+ const next = serialize ? serialize(value) : value === defaultValue ? null : String(value);
39
+ if (next === current || next === null && current === null) return;
40
+ setQueryParam(key, next, { replace: true });
41
+ };
42
+ const canUseDom = typeof window !== "undefined";
43
+ if (canUseDom) read();
44
+ (0, vue.watch)(() => route.path, () => {
45
+ if (canUseDom) read();
46
+ });
47
+ (0, vue.watch)(state, (value) => {
48
+ if (canUseDom) write(value);
49
+ }, { deep: false });
50
+ (0, vue.onMounted)(() => {
51
+ if (!canUseDom) return;
52
+ window.addEventListener("popstate", read, { passive: true });
53
+ });
54
+ (0, vue.onUnmounted)(() => {
55
+ if (!canUseDom) return;
56
+ window.removeEventListener("popstate", read);
57
+ });
58
+ return state;
59
+ };
60
+
61
+ //#endregion
62
+ //#region src/composables/useTags.ts
63
+ const createTagsState = (posts) => {
64
+ const activeTag = useSyncedQueryParam({
65
+ key: "tag",
66
+ defaultValue: "",
67
+ parse: (raw) => raw || "",
68
+ serialize: (value) => value ? value : null
69
+ });
70
+ const tagsMap = (0, vue.computed)(() => {
71
+ const map = { "": posts.length };
72
+ posts.forEach((post) => {
73
+ const tags = post.frontmatter.tags;
74
+ if (tags) tags.forEach((tag) => {
75
+ map[tag] = (map[tag] || 0) + 1;
76
+ });
77
+ });
78
+ return map;
79
+ });
80
+ const getTagArray = () => {
81
+ const arr = Object.entries(tagsMap.value);
82
+ arr.sort((a, b) => b[1] - a[1]);
83
+ return arr;
84
+ };
85
+ const uniqueTagCount = (0, vue.computed)(() => {
86
+ const set = new Set();
87
+ Object.keys(tagsMap.value).forEach((k) => {
88
+ if (k) set.add(k);
89
+ });
90
+ return set.size;
91
+ });
92
+ const filterPostsByActiveTag = (tag) => {
93
+ const t = tag ?? activeTag.value;
94
+ if (!t) return posts;
95
+ return posts.filter((item) => item.frontmatter.tags && item.frontmatter.tags.includes(t));
96
+ };
97
+ return {
98
+ tagsMap,
99
+ activeTag,
100
+ getTagArray,
101
+ uniqueTagCount,
102
+ filterPostsByActiveTag
103
+ };
104
+ };
105
+ const createTagsStore = (posts) => {
106
+ let sharedState = null;
107
+ return () => {
108
+ if (!sharedState) sharedState = createTagsState(posts);
109
+ return sharedState;
110
+ };
111
+ };
112
+
113
+ //#endregion
114
+ //#region src/composables/useCategories.ts
115
+ const extractCategoryFromUrl = (url, otherLabel) => {
116
+ const match = url.match(/^\/posts\/([^/]+)\//);
117
+ if (match && match[1]) return match[1];
118
+ if (url.startsWith("/posts/") && /^\/posts\/[^/]+(\.html)?$/.test(url)) return otherLabel;
119
+ return "";
120
+ };
121
+ const createCategoriesState = (posts, otherLabel) => {
122
+ const otherLabelRef = (0, vue.ref)(otherLabel);
123
+ const activeCategory = useSyncedQueryParam({
124
+ key: "category",
125
+ defaultValue: "",
126
+ parse: (raw) => raw || "",
127
+ serialize: (value) => value ? value : null
128
+ });
129
+ const getPostCategory = (post) => {
130
+ const fmCategory = post.frontmatter?.category;
131
+ if (fmCategory) return fmCategory;
132
+ return extractCategoryFromUrl(post.url, otherLabelRef.value);
133
+ };
134
+ const categoriesMap = (0, vue.computed)(() => {
135
+ const map = { "": posts.length };
136
+ posts.forEach((post) => {
137
+ const category = getPostCategory(post);
138
+ if (category) map[category] = (map[category] || 0) + 1;
139
+ });
140
+ return map;
141
+ });
142
+ const getCategoryArray = () => {
143
+ const arr = Object.entries(categoriesMap.value);
144
+ arr.sort((a, b) => {
145
+ if (a[0] === "") return -1;
146
+ if (b[0] === "") return 1;
147
+ return b[1] - a[1];
148
+ });
149
+ return arr;
150
+ };
151
+ const uniqueCategoryCount = (0, vue.computed)(() => {
152
+ const set = new Set();
153
+ Object.keys(categoriesMap.value).forEach((k) => {
154
+ if (k) set.add(k);
155
+ });
156
+ return set.size;
157
+ });
158
+ const filterPostsByActiveCategory = (category) => {
159
+ const c = category ?? activeCategory.value;
160
+ if (!c) return posts;
161
+ return posts.filter((item) => getPostCategory(item) === c);
162
+ };
163
+ return {
164
+ categoriesMap,
165
+ activeCategory,
166
+ getCategoryArray,
167
+ uniqueCategoryCount,
168
+ filterPostsByActiveCategory,
169
+ getCategoryFromUrl: (url) => extractCategoryFromUrl(url, otherLabelRef.value),
170
+ updateOtherLabel: (label) => {
171
+ otherLabelRef.value = label;
172
+ }
173
+ };
174
+ };
175
+ const createCategoriesStore = (posts) => {
176
+ let sharedState = null;
177
+ return (options) => {
178
+ const otherLabel = options?.otherLabel || "Other";
179
+ if (!sharedState) sharedState = createCategoriesState(posts, otherLabel);
180
+ else if (options?.otherLabel) sharedState.updateOtherLabel(options.otherLabel);
181
+ return sharedState;
182
+ };
183
+ };
184
+
185
+ //#endregion
186
+ //#region src/composables/usePagination.ts
187
+ const usePagination = (items, options) => {
188
+ const pageParam = options.pageParam ?? "page";
189
+ const pageSize = (0, vue.computed)(() => typeof options.pageSize === "number" ? options.pageSize : options.pageSize.value);
190
+ const currentPage = useSyncedQueryParam({
191
+ key: pageParam,
192
+ defaultValue: 1,
193
+ parse: (raw) => {
194
+ const n = Number.parseInt(raw || "1", 10);
195
+ return Number.isFinite(n) && n > 0 ? n : 1;
196
+ },
197
+ serialize: (value) => value <= 1 ? null : String(value)
198
+ });
199
+ const totalPages = (0, vue.computed)(() => {
200
+ if (!items.value.length) return 0;
201
+ return Math.ceil(items.value.length / pageSize.value);
202
+ });
203
+ (0, vue.watch)(totalPages, (count) => {
204
+ if (count === 0) currentPage.value = 1;
205
+ else if (currentPage.value > count) currentPage.value = count;
206
+ }, { immediate: true });
207
+ const paginatedItems = (0, vue.computed)(() => {
208
+ const start = (currentPage.value - 1) * pageSize.value;
209
+ const end = start + pageSize.value;
210
+ return items.value.slice(start, end);
211
+ });
212
+ const pageRange = (0, vue.computed)(() => {
213
+ const count = totalPages.value;
214
+ if (count <= 1) return [];
215
+ const groupSize = Math.max(3, options.pageGroupSize || require_constants.DEFAULT_PAGE_GROUP_SIZE);
216
+ const half = Math.floor(groupSize / 2);
217
+ let start = Math.max(1, currentPage.value - half);
218
+ let end = Math.min(count, start + groupSize - 1);
219
+ start = Math.max(1, end - groupSize + 1);
220
+ const pages = [];
221
+ for (let i = start; i <= end; i++) pages.push(i);
222
+ return pages;
223
+ });
224
+ const setPage = (page) => {
225
+ const count = totalPages.value || 1;
226
+ const target = Math.min(Math.max(page, 1), count);
227
+ currentPage.value = target;
228
+ };
229
+ const nextPage = () => setPage(currentPage.value + 1);
230
+ const prevPage = () => setPage(currentPage.value - 1);
231
+ const jumpInput = (0, vue.ref)("");
232
+ const jumpToInput = () => {
233
+ const n = Number.parseInt(jumpInput.value, 10);
234
+ if (!Number.isFinite(n)) return;
235
+ setPage(n);
236
+ };
237
+ return {
238
+ currentPage,
239
+ totalPages,
240
+ pageRange,
241
+ paginatedItems,
242
+ setPage,
243
+ nextPage,
244
+ prevPage,
245
+ jumpInput,
246
+ jumpToInput
247
+ };
248
+ };
249
+
250
+ //#endregion
251
+ exports.DEFAULT_PAGE_GROUP_SIZE = require_constants.DEFAULT_PAGE_GROUP_SIZE
252
+ exports.DEFAULT_PAGE_SIZE = require_constants.DEFAULT_PAGE_SIZE
253
+ exports.LOCALIZED_STRINGS = require_constants.LOCALIZED_STRINGS
254
+ exports.MAX_DISPLAYED_TAGS = require_constants.MAX_DISPLAYED_TAGS
255
+ exports.createCategoriesStore = createCategoriesStore
256
+ exports.createTagsStore = createTagsStore
257
+ exports.formatDate = require_date.formatDate
258
+ exports.getLocalizedString = require_constants.getLocalizedString
259
+ exports.parseDateValue = require_date.parseDateValue
260
+ exports.usePagination = usePagination
261
+ exports.useSyncedQueryParam = useSyncedQueryParam
@@ -0,0 +1,72 @@
1
+ import { BlogFrontmatter, BlogPost, DEFAULT_PAGE_GROUP_SIZE, DEFAULT_PAGE_SIZE, LOCALIZED_STRINGS, MAX_DISPLAYED_TAGS, SupportedLanguage, formatDate, getLocalizedString, parseDateValue } from "./date-CCP55OAZ.cjs";
2
+ import * as vue2 from "vue";
3
+ import * as vue3 from "vue";
4
+ import * as vue4 from "vue";
5
+ import * as vue5 from "vue";
6
+ import * as vue1 from "vue";
7
+ import { ComputedRef } from "vue";
8
+
9
+ //#region src/composables/useQueryParam.d.ts
10
+ type Parser<T> = (raw: string | null) => T;
11
+ type Serializer<T> = (value: T) => string | null;
12
+ interface SyncedQueryParamOptions<T> {
13
+ key: string;
14
+ defaultValue: T;
15
+ parse?: Parser<T>;
16
+ serialize?: Serializer<T>;
17
+ }
18
+ declare const useSyncedQueryParam: <T>(options: SyncedQueryParamOptions<T>) => {
19
+ value: T;
20
+ };
21
+
22
+ //#endregion
23
+ //#region src/composables/useTags.d.ts
24
+ declare const createTagsStore: (posts: BlogPost[]) => () => {
25
+ readonly tagsMap: vue2.ComputedRef<Record<string, number>>;
26
+ readonly activeTag: {
27
+ value: string;
28
+ };
29
+ readonly getTagArray: () => [string, number][];
30
+ readonly uniqueTagCount: vue3.ComputedRef<number>;
31
+ readonly filterPostsByActiveTag: (tag?: string) => BlogPost[];
32
+ };
33
+
34
+ //#endregion
35
+ //#region src/composables/useCategories.d.ts
36
+ declare const createCategoriesStore: (posts: BlogPost[]) => (options?: {
37
+ otherLabel?: string;
38
+ }) => {
39
+ readonly categoriesMap: vue4.ComputedRef<Record<string, number>>;
40
+ readonly activeCategory: {
41
+ value: string;
42
+ };
43
+ readonly getCategoryArray: () => [string, number][];
44
+ readonly uniqueCategoryCount: vue5.ComputedRef<number>;
45
+ readonly filterPostsByActiveCategory: (category?: string) => BlogPost[];
46
+ readonly getCategoryFromUrl: (url: string) => string;
47
+ readonly updateOtherLabel: (label: string) => void;
48
+ };
49
+
50
+ //#endregion
51
+ //#region src/composables/usePagination.d.ts
52
+ interface PaginationOptions {
53
+ pageSize: number | ComputedRef<number>;
54
+ pageGroupSize?: number;
55
+ pageParam?: string;
56
+ }
57
+ declare const usePagination: <T>(items: ComputedRef<T[]>, options: PaginationOptions) => {
58
+ readonly currentPage: {
59
+ value: number;
60
+ };
61
+ readonly totalPages: ComputedRef<number>;
62
+ readonly pageRange: ComputedRef<number[]>;
63
+ readonly paginatedItems: ComputedRef<T[]>;
64
+ readonly setPage: (page: number) => void;
65
+ readonly nextPage: () => void;
66
+ readonly prevPage: () => void;
67
+ readonly jumpInput: vue1.Ref<string, string>;
68
+ readonly jumpToInput: () => void;
69
+ };
70
+
71
+ //#endregion
72
+ export { BlogFrontmatter, BlogPost, DEFAULT_PAGE_GROUP_SIZE, DEFAULT_PAGE_SIZE, LOCALIZED_STRINGS, MAX_DISPLAYED_TAGS, SupportedLanguage, createCategoriesStore, createTagsStore, formatDate, getLocalizedString, parseDateValue, usePagination, useSyncedQueryParam };
@@ -0,0 +1,72 @@
1
+ import { BlogFrontmatter, BlogPost, DEFAULT_PAGE_GROUP_SIZE$1 as DEFAULT_PAGE_GROUP_SIZE, DEFAULT_PAGE_SIZE$1 as DEFAULT_PAGE_SIZE, LOCALIZED_STRINGS$1 as LOCALIZED_STRINGS, MAX_DISPLAYED_TAGS$1 as MAX_DISPLAYED_TAGS, SupportedLanguage, formatDate$1 as formatDate, getLocalizedString$1 as getLocalizedString, parseDateValue$1 as parseDateValue } from "./date-BySfVUhd.js";
2
+ import * as vue1 from "vue";
3
+ import * as vue2 from "vue";
4
+ import * as vue4 from "vue";
5
+ import * as vue5 from "vue";
6
+ import * as vue3 from "vue";
7
+ import { ComputedRef } from "vue";
8
+
9
+ //#region src/composables/useQueryParam.d.ts
10
+ type Parser<T> = (raw: string | null) => T;
11
+ type Serializer<T> = (value: T) => string | null;
12
+ interface SyncedQueryParamOptions<T> {
13
+ key: string;
14
+ defaultValue: T;
15
+ parse?: Parser<T>;
16
+ serialize?: Serializer<T>;
17
+ }
18
+ declare const useSyncedQueryParam: <T>(options: SyncedQueryParamOptions<T>) => {
19
+ value: T;
20
+ };
21
+
22
+ //#endregion
23
+ //#region src/composables/useTags.d.ts
24
+ declare const createTagsStore: (posts: BlogPost[]) => () => {
25
+ readonly tagsMap: vue1.ComputedRef<Record<string, number>>;
26
+ readonly activeTag: {
27
+ value: string;
28
+ };
29
+ readonly getTagArray: () => [string, number][];
30
+ readonly uniqueTagCount: vue2.ComputedRef<number>;
31
+ readonly filterPostsByActiveTag: (tag?: string) => BlogPost[];
32
+ };
33
+
34
+ //#endregion
35
+ //#region src/composables/useCategories.d.ts
36
+ declare const createCategoriesStore: (posts: BlogPost[]) => (options?: {
37
+ otherLabel?: string;
38
+ }) => {
39
+ readonly categoriesMap: vue4.ComputedRef<Record<string, number>>;
40
+ readonly activeCategory: {
41
+ value: string;
42
+ };
43
+ readonly getCategoryArray: () => [string, number][];
44
+ readonly uniqueCategoryCount: vue5.ComputedRef<number>;
45
+ readonly filterPostsByActiveCategory: (category?: string) => BlogPost[];
46
+ readonly getCategoryFromUrl: (url: string) => string;
47
+ readonly updateOtherLabel: (label: string) => void;
48
+ };
49
+
50
+ //#endregion
51
+ //#region src/composables/usePagination.d.ts
52
+ interface PaginationOptions {
53
+ pageSize: number | ComputedRef<number>;
54
+ pageGroupSize?: number;
55
+ pageParam?: string;
56
+ }
57
+ declare const usePagination: <T>(items: ComputedRef<T[]>, options: PaginationOptions) => {
58
+ readonly currentPage: {
59
+ value: number;
60
+ };
61
+ readonly totalPages: ComputedRef<number>;
62
+ readonly pageRange: ComputedRef<number[]>;
63
+ readonly paginatedItems: ComputedRef<T[]>;
64
+ readonly setPage: (page: number) => void;
65
+ readonly nextPage: () => void;
66
+ readonly prevPage: () => void;
67
+ readonly jumpInput: vue3.Ref<string, string>;
68
+ readonly jumpToInput: () => void;
69
+ };
70
+
71
+ //#endregion
72
+ export { BlogFrontmatter, BlogPost, DEFAULT_PAGE_GROUP_SIZE, DEFAULT_PAGE_SIZE, LOCALIZED_STRINGS, MAX_DISPLAYED_TAGS, SupportedLanguage, createCategoriesStore, createTagsStore, formatDate, getLocalizedString, parseDateValue, usePagination, useSyncedQueryParam };
package/dist/client.js ADDED
@@ -0,0 +1,249 @@
1
+ import { DEFAULT_PAGE_GROUP_SIZE, DEFAULT_PAGE_SIZE, LOCALIZED_STRINGS, MAX_DISPLAYED_TAGS, getLocalizedString } from "./constants-Dc4Co5gF.js";
2
+ import { formatDate, parseDateValue } from "./date-DAUKykKr.js";
3
+ import { computed, onMounted, onUnmounted, ref, watch } from "vue";
4
+ import { useRoute } from "vitepress/client";
5
+
6
+ //#region src/utils/url.ts
7
+ const isClient = typeof window !== "undefined";
8
+ const getQueryParam = (key) => {
9
+ if (!isClient) return null;
10
+ const url = new URL(window.location.href);
11
+ return url.searchParams.get(key);
12
+ };
13
+ const setQueryParam = (key, value, options) => {
14
+ if (!isClient) return;
15
+ const url = new URL(window.location.href);
16
+ if (value === null || value === "") url.searchParams.delete(key);
17
+ else url.searchParams.set(key, value);
18
+ const replace = options?.replace ?? true;
19
+ if (replace) window.history.replaceState(null, "", url.toString());
20
+ else window.history.pushState(null, "", url.toString());
21
+ };
22
+
23
+ //#endregion
24
+ //#region src/composables/useQueryParam.ts
25
+ const useSyncedQueryParam = (options) => {
26
+ const { key, defaultValue, parse, serialize } = options;
27
+ const route = useRoute();
28
+ const state = ref(defaultValue);
29
+ const read = () => {
30
+ const raw = getQueryParam(key);
31
+ const next = parse ? parse(raw) : raw ?? defaultValue;
32
+ state.value = next;
33
+ };
34
+ const write = (value) => {
35
+ const current = getQueryParam(key);
36
+ const next = serialize ? serialize(value) : value === defaultValue ? null : String(value);
37
+ if (next === current || next === null && current === null) return;
38
+ setQueryParam(key, next, { replace: true });
39
+ };
40
+ const canUseDom = typeof window !== "undefined";
41
+ if (canUseDom) read();
42
+ watch(() => route.path, () => {
43
+ if (canUseDom) read();
44
+ });
45
+ watch(state, (value) => {
46
+ if (canUseDom) write(value);
47
+ }, { deep: false });
48
+ onMounted(() => {
49
+ if (!canUseDom) return;
50
+ window.addEventListener("popstate", read, { passive: true });
51
+ });
52
+ onUnmounted(() => {
53
+ if (!canUseDom) return;
54
+ window.removeEventListener("popstate", read);
55
+ });
56
+ return state;
57
+ };
58
+
59
+ //#endregion
60
+ //#region src/composables/useTags.ts
61
+ const createTagsState = (posts) => {
62
+ const activeTag = useSyncedQueryParam({
63
+ key: "tag",
64
+ defaultValue: "",
65
+ parse: (raw) => raw || "",
66
+ serialize: (value) => value ? value : null
67
+ });
68
+ const tagsMap = computed(() => {
69
+ const map = { "": posts.length };
70
+ posts.forEach((post) => {
71
+ const tags = post.frontmatter.tags;
72
+ if (tags) tags.forEach((tag) => {
73
+ map[tag] = (map[tag] || 0) + 1;
74
+ });
75
+ });
76
+ return map;
77
+ });
78
+ const getTagArray = () => {
79
+ const arr = Object.entries(tagsMap.value);
80
+ arr.sort((a, b) => b[1] - a[1]);
81
+ return arr;
82
+ };
83
+ const uniqueTagCount = computed(() => {
84
+ const set = new Set();
85
+ Object.keys(tagsMap.value).forEach((k) => {
86
+ if (k) set.add(k);
87
+ });
88
+ return set.size;
89
+ });
90
+ const filterPostsByActiveTag = (tag) => {
91
+ const t = tag ?? activeTag.value;
92
+ if (!t) return posts;
93
+ return posts.filter((item) => item.frontmatter.tags && item.frontmatter.tags.includes(t));
94
+ };
95
+ return {
96
+ tagsMap,
97
+ activeTag,
98
+ getTagArray,
99
+ uniqueTagCount,
100
+ filterPostsByActiveTag
101
+ };
102
+ };
103
+ const createTagsStore = (posts) => {
104
+ let sharedState = null;
105
+ return () => {
106
+ if (!sharedState) sharedState = createTagsState(posts);
107
+ return sharedState;
108
+ };
109
+ };
110
+
111
+ //#endregion
112
+ //#region src/composables/useCategories.ts
113
+ const extractCategoryFromUrl = (url, otherLabel) => {
114
+ const match = url.match(/^\/posts\/([^/]+)\//);
115
+ if (match && match[1]) return match[1];
116
+ if (url.startsWith("/posts/") && /^\/posts\/[^/]+(\.html)?$/.test(url)) return otherLabel;
117
+ return "";
118
+ };
119
+ const createCategoriesState = (posts, otherLabel) => {
120
+ const otherLabelRef = ref(otherLabel);
121
+ const activeCategory = useSyncedQueryParam({
122
+ key: "category",
123
+ defaultValue: "",
124
+ parse: (raw) => raw || "",
125
+ serialize: (value) => value ? value : null
126
+ });
127
+ const getPostCategory = (post) => {
128
+ const fmCategory = post.frontmatter?.category;
129
+ if (fmCategory) return fmCategory;
130
+ return extractCategoryFromUrl(post.url, otherLabelRef.value);
131
+ };
132
+ const categoriesMap = computed(() => {
133
+ const map = { "": posts.length };
134
+ posts.forEach((post) => {
135
+ const category = getPostCategory(post);
136
+ if (category) map[category] = (map[category] || 0) + 1;
137
+ });
138
+ return map;
139
+ });
140
+ const getCategoryArray = () => {
141
+ const arr = Object.entries(categoriesMap.value);
142
+ arr.sort((a, b) => {
143
+ if (a[0] === "") return -1;
144
+ if (b[0] === "") return 1;
145
+ return b[1] - a[1];
146
+ });
147
+ return arr;
148
+ };
149
+ const uniqueCategoryCount = computed(() => {
150
+ const set = new Set();
151
+ Object.keys(categoriesMap.value).forEach((k) => {
152
+ if (k) set.add(k);
153
+ });
154
+ return set.size;
155
+ });
156
+ const filterPostsByActiveCategory = (category) => {
157
+ const c = category ?? activeCategory.value;
158
+ if (!c) return posts;
159
+ return posts.filter((item) => getPostCategory(item) === c);
160
+ };
161
+ return {
162
+ categoriesMap,
163
+ activeCategory,
164
+ getCategoryArray,
165
+ uniqueCategoryCount,
166
+ filterPostsByActiveCategory,
167
+ getCategoryFromUrl: (url) => extractCategoryFromUrl(url, otherLabelRef.value),
168
+ updateOtherLabel: (label) => {
169
+ otherLabelRef.value = label;
170
+ }
171
+ };
172
+ };
173
+ const createCategoriesStore = (posts) => {
174
+ let sharedState = null;
175
+ return (options) => {
176
+ const otherLabel = options?.otherLabel || "Other";
177
+ if (!sharedState) sharedState = createCategoriesState(posts, otherLabel);
178
+ else if (options?.otherLabel) sharedState.updateOtherLabel(options.otherLabel);
179
+ return sharedState;
180
+ };
181
+ };
182
+
183
+ //#endregion
184
+ //#region src/composables/usePagination.ts
185
+ const usePagination = (items, options) => {
186
+ const pageParam = options.pageParam ?? "page";
187
+ const pageSize = computed(() => typeof options.pageSize === "number" ? options.pageSize : options.pageSize.value);
188
+ const currentPage = useSyncedQueryParam({
189
+ key: pageParam,
190
+ defaultValue: 1,
191
+ parse: (raw) => {
192
+ const n = Number.parseInt(raw || "1", 10);
193
+ return Number.isFinite(n) && n > 0 ? n : 1;
194
+ },
195
+ serialize: (value) => value <= 1 ? null : String(value)
196
+ });
197
+ const totalPages = computed(() => {
198
+ if (!items.value.length) return 0;
199
+ return Math.ceil(items.value.length / pageSize.value);
200
+ });
201
+ watch(totalPages, (count) => {
202
+ if (count === 0) currentPage.value = 1;
203
+ else if (currentPage.value > count) currentPage.value = count;
204
+ }, { immediate: true });
205
+ const paginatedItems = computed(() => {
206
+ const start = (currentPage.value - 1) * pageSize.value;
207
+ const end = start + pageSize.value;
208
+ return items.value.slice(start, end);
209
+ });
210
+ const pageRange = computed(() => {
211
+ const count = totalPages.value;
212
+ if (count <= 1) return [];
213
+ const groupSize = Math.max(3, options.pageGroupSize || DEFAULT_PAGE_GROUP_SIZE);
214
+ const half = Math.floor(groupSize / 2);
215
+ let start = Math.max(1, currentPage.value - half);
216
+ let end = Math.min(count, start + groupSize - 1);
217
+ start = Math.max(1, end - groupSize + 1);
218
+ const pages = [];
219
+ for (let i = start; i <= end; i++) pages.push(i);
220
+ return pages;
221
+ });
222
+ const setPage = (page) => {
223
+ const count = totalPages.value || 1;
224
+ const target = Math.min(Math.max(page, 1), count);
225
+ currentPage.value = target;
226
+ };
227
+ const nextPage = () => setPage(currentPage.value + 1);
228
+ const prevPage = () => setPage(currentPage.value - 1);
229
+ const jumpInput = ref("");
230
+ const jumpToInput = () => {
231
+ const n = Number.parseInt(jumpInput.value, 10);
232
+ if (!Number.isFinite(n)) return;
233
+ setPage(n);
234
+ };
235
+ return {
236
+ currentPage,
237
+ totalPages,
238
+ pageRange,
239
+ paginatedItems,
240
+ setPage,
241
+ nextPage,
242
+ prevPage,
243
+ jumpInput,
244
+ jumpToInput
245
+ };
246
+ };
247
+
248
+ //#endregion
249
+ export { DEFAULT_PAGE_GROUP_SIZE, DEFAULT_PAGE_SIZE, LOCALIZED_STRINGS, MAX_DISPLAYED_TAGS, createCategoriesStore, createTagsStore, formatDate, getLocalizedString, parseDateValue, usePagination, useSyncedQueryParam };