@binarynoir/vitepress-blog 0.4.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.
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/client.ts","../src/view.ts","../src/banner.ts","../src/util.ts"],"sourcesContent":["// A namespace import, not named ones: during the server-side build Node loads\n// VitePress's node entry, which has no `inBrowser`, `useRoute` or\n// `onContentUpdated`. Named imports would fail to link there; on the\n// namespace they are just `undefined`.\nimport * as vitepress from 'vitepress';\nimport { nextTick, onMounted, watch } from 'vue';\nimport { applyBlogViews } from './view.js';\n\n/**\n * Turns on filtering (`?category=`, `?tag=`, `?year=`, `?month=`) and pagination (`?page=`) on blog\n * pages. Call it from your theme's `setup()`:\n *\n * ```ts\n * // .vitepress/theme/index.ts\n * import DefaultTheme from 'vitepress/theme';\n * import { useBlog } from '@binarynoir/vitepress-blog/client';\n * import '@binarynoir/vitepress-blog/style.css';\n *\n * export default {\n * extends: DefaultTheme,\n * setup() {\n * useBlog();\n * },\n * };\n * ```\n *\n * Filter and page links are ordinary links: VitePress's router follows them\n * without reloading the page, and this re-applies the view whenever the path\n * or query string changes. It runs after the page has hydrated, so the\n * server-rendered HTML (every post) and the first client render agree.\n * Does nothing during the server-side build.\n */\nexport function useBlog(): void {\n if (!vitepress.inBrowser) return;\n const route = vitepress.useRoute();\n const apply = () => nextTick(() => applyBlogViews());\n onMounted(() => {\n watch(() => [route.path, route.query], apply, { immediate: true });\n });\n // `vitepress dev` swaps a page's content in place when it is edited.\n vitepress.onContentUpdated(apply);\n}\n\nexport { BlogAnnouncementBanner } from './banner.js';\nexport { applyBlogView, applyBlogViews, readFilter, type BlogFilter, type BlogViewState } from './view.js';\n","/**\n * Filtering and pagination for blog pages, in the browser. The build writes\n * every post into the page (see `renderPostList`); this hides the ones the\n * URL's `?category=`, `?tag=`, `?year=`, `?month=`, `?announcements` and\n * `?page=` leave out, keeps the browse panel's chips and counts in step with\n * them, and fills in the active filters and the page links. Without it, every\n * post simply shows.\n */\n\nconst ITEM = '[data-vp-blog-item]';\n/** Links that filter the blog: the browse panel's chips, and the category and keyword links on cards. */\nconst FILTER_LINKS = '.vp-blog-facet a, .vp-blog-card__category a, .vp-blog-card__tags a';\n\ninterface BlogLabels {\n showAll: string;\n previous: string;\n next: string;\n pages: string;\n announcements: string;\n category: string;\n tag: string;\n postCount: string;\n postCountOne: string;\n noMatches: string;\n removeFilter: string;\n}\n\nfunction readLabels(blog: HTMLElement): BlogLabels {\n const { dataset } = blog;\n return {\n showAll: dataset.labelShowAll ?? 'Show all posts',\n previous: dataset.labelPrevious ?? 'Previous',\n next: dataset.labelNext ?? 'Next',\n pages: dataset.labelPages ?? 'Pages',\n announcements: dataset.labelAnnouncements ?? 'Announcements',\n category: dataset.labelCategory ?? 'Category',\n tag: dataset.labelTag ?? 'Keyword',\n postCount: dataset.labelPostCount ?? '{n} posts',\n postCountOne: dataset.labelPostCountOne ?? '{n} post',\n noMatches: dataset.labelNoMatches ?? 'No posts match these filters.',\n removeFilter: dataset.labelRemoveFilter ?? 'Remove filter: {filter}',\n };\n}\n\n/**\n * What the URL filters by. Every filter must match. The category and keyword\n * are lowercased; `date` is `''`, a year (`2026`) or a month (`2026-10`).\n */\nexport interface BlogFilter {\n category: string;\n tag: string;\n date: string;\n /** Whether only announcements show (`?announcements`). */\n announcements: boolean;\n}\n\ntype Facet = 'category' | 'tag' | 'date';\n\n/** Reads the filter from `?category=`, `?tag=`, `?year=` and `?month=`. A month names its year too, so it wins over `?year=`. */\nexport function readFilter(url: URL): BlogFilter {\n const text = (name: string) => (url.searchParams.get(name) ?? '').trim().toLowerCase();\n const month = text('month');\n const year = text('year');\n return {\n category: text('category'),\n tag: text('tag'),\n date: /^\\d{4}-(?:0[1-9]|1[0-2])$/.test(month) ? month : /^\\d{4}$/.test(year) ? year : '',\n announcements: url.searchParams.has('announcements'),\n };\n}\n\n/** The element a marker stands for: a card is its own marker; a compact list item holds one. */\nfunction itemElement(marker: Element): HTMLElement {\n return (marker.tagName === 'ARTICLE' ? marker : (marker.closest('li') ?? marker)) as HTMLElement;\n}\n\nfunction matches(marker: Element, filter: BlogFilter): boolean {\n const { category, tag, date, announcements } = filter;\n return (\n (!tag || (marker.getAttribute('data-tags') ?? '').split('|').includes(tag)) &&\n (!category || marker.getAttribute('data-category') === category) &&\n // `2026` matches every month of 2026; `2026-10` only October.\n (!date || (marker.getAttribute('data-month') ?? '').startsWith(date)) &&\n (!announcements || marker.hasAttribute('data-announcement'))\n );\n}\n\n/** `url` with the filter and `page` set (or removed when empty or page 1), as a path the router can follow. */\nfunction toHref(url: URL, filter: BlogFilter, page = 1): string {\n const next = new URL(url.href);\n next.hash = '';\n const set = (name: string, value: string) =>\n value ? next.searchParams.set(name, value) : next.searchParams.delete(name);\n set('category', filter.category);\n set('tag', filter.tag);\n set('year', filter.date.length === 4 ? filter.date : '');\n set('month', filter.date.length === 7 ? filter.date : '');\n if (filter.announcements) next.searchParams.set('announcements', '');\n else next.searchParams.delete('announcements');\n set('page', page > 1 ? String(page) : '');\n // A bare `?announcements`, not `?announcements=`.\n return next.pathname + next.search.replace(/([?&]announcements)=(?=&|$)/, '$1');\n}\n\n/** The filter a link applies, as the build wrote it: `?category=x`, `?tag=x`, `?year=2026` or `?month=2026-10`. */\ninterface FilterLink {\n facet: Facet;\n value: string;\n}\n\n/**\n * The filter a link stands for. It is read from the link's URL the first time\n * and remembered, because the browse panel's links are then rewritten to\n * combine it with the other active filters.\n */\nfunction readFilterLink(anchor: HTMLAnchorElement): FilterLink | null {\n if (!anchor.dataset.vpBlogFacet) {\n let params: URLSearchParams;\n try {\n params = new URL(anchor.href, location.href).searchParams;\n } catch {\n return null;\n }\n const param = (['category', 'tag', 'month', 'year'] as const).find((name) => params.has(name));\n if (!param) return null;\n anchor.dataset.vpBlogFacet = param === 'year' || param === 'month' ? 'date' : param;\n anchor.dataset.vpBlogValue = (params.get(param) ?? '').trim().toLowerCase();\n }\n return { facet: anchor.dataset.vpBlogFacet as Facet, value: anchor.dataset.vpBlogValue ?? '' };\n}\n\nfunction withValue(filter: BlogFilter, { facet, value }: FilterLink): BlogFilter {\n return { ...filter, [facet]: value };\n}\n\n/** Whether the link's filter is applied. A year is also active while one of its months is. */\nfunction isActive({ facet, value }: FilterLink, filter: BlogFilter): boolean {\n if (facet !== 'date') return filter[facet] === value;\n return value.length === 4 ? filter.date.startsWith(value) : filter.date === value;\n}\n\n/**\n * Where a browse chip leads: its filter added to the active ones (replacing\n * any other value of the same kind), or, when it is the active one, taken off\n * again. An active month steps back out to its year.\n */\nfunction toggle(filter: BlogFilter, link: FilterLink): BlogFilter {\n if (link.facet !== 'date')\n return withValue(filter, { ...link, value: filter[link.facet] === link.value ? '' : link.value });\n if (filter.date !== link.value) return withValue(filter, link);\n return { ...filter, date: link.value.length === 7 ? link.value.slice(0, 4) : '' };\n}\n\nfunction markActive(anchor: HTMLAnchorElement, active: boolean): void {\n anchor.classList.toggle('is-active', active);\n if (active) anchor.setAttribute('aria-current', 'true');\n else anchor.removeAttribute('aria-current');\n}\n\n/**\n * Brings the browse panel in step with the filter. Each chip links to the\n * view with it toggled, and counts the posts it would show with the other\n * filters kept, so a reader can see where every next step leads; chips that\n * would show nothing are dimmed. The months of the year in view show below\n * the years.\n */\nfunction syncBrowse(blog: HTMLElement, url: URL, filter: BlogFilter, markers: Element[]): void {\n const browse = blog.querySelector<HTMLElement>('.vp-blog-browse');\n if (!browse) return;\n for (const anchor of browse.querySelectorAll<HTMLAnchorElement>('.vp-blog-facet a')) {\n const link = readFilterLink(anchor);\n if (!link) continue;\n const active = isActive(link, filter);\n const count = markers.filter((marker) => matches(marker, withValue(filter, link))).length;\n anchor.href = toHref(url, toggle(filter, link));\n markActive(anchor, active);\n anchor.classList.toggle('is-empty', count === 0 && !active);\n const counter = anchor.querySelector('.vp-blog-facet__count');\n if (counter) counter.textContent = String(count);\n }\n // With only one year, its months always show.\n const months = [...browse.querySelectorAll<HTMLElement>('[data-vp-blog-facet=\"month\"]')];\n for (const row of months) row.hidden = months.length > 1 && row.dataset.year !== filter.date.slice(0, 4);\n}\n\n/** The name a filter value is shown with: as a chip or card link spells it, else as the URL gave it. */\nfunction findName(blog: HTMLElement, facet: 'category' | 'tag', value: string): string {\n for (const anchor of blog.querySelectorAll<HTMLAnchorElement>(FILTER_LINKS)) {\n const link = readFilterLink(anchor);\n if (link?.facet === facet && link.value === value) {\n const name = (anchor.querySelector('.vp-blog-facet__name') ?? anchor).textContent?.trim();\n if (name) return name;\n }\n }\n return value;\n}\n\n/** A year as written, or a month as \"October 2026\" in the blog's locale. */\nfunction formatDate(date: string, locale: string): string {\n if (date.length === 4) return date;\n const [year, month] = date.split('-').map(Number);\n try {\n return new Intl.DateTimeFormat(locale, { timeZone: 'UTC', month: 'long', year: 'numeric' }).format(\n Date.UTC(year, month - 1, 1, 12),\n );\n } catch {\n return date;\n }\n}\n\nfunction element(tag: string, className: string, text = ''): HTMLElement {\n const node = document.createElement(tag);\n node.className = className;\n node.textContent = text;\n return node;\n}\n\nfunction link(text: string, href: string, className: string): HTMLAnchorElement {\n const anchor = element('a', className, text) as HTMLAnchorElement;\n anchor.href = href;\n return anchor;\n}\n\n/** One active filter: what kind it is (`''` for a date or announcements), its name, and the view without it. */\ninterface ActiveFilter {\n kind: string;\n name: string;\n without: BlogFilter;\n}\n\nfunction listActiveFilters(blog: HTMLElement, filter: BlogFilter, labels: BlogLabels): ActiveFilter[] {\n const active: ActiveFilter[] = [];\n if (filter.announcements) {\n active.push({ kind: '', name: labels.announcements, without: { ...filter, announcements: false } });\n }\n if (filter.category) {\n const name = findName(blog, 'category', filter.category);\n active.push({ kind: labels.category, name, without: { ...filter, category: '' } });\n }\n if (filter.tag) {\n active.push({ kind: labels.tag, name: findName(blog, 'tag', filter.tag), without: { ...filter, tag: '' } });\n }\n if (filter.date) {\n const name = formatDate(filter.date, blog.dataset.locale || document.documentElement.lang || 'en-US');\n active.push({ kind: '', name, without: { ...filter, date: '' } });\n }\n return active;\n}\n\n/**\n * The active filters, above the posts: how many posts match, a chip for each\n * filter that removes just that one, and a link that clears them all. All of\n * it is written as text: the values come from the URL.\n */\nfunction renderFilter(\n slot: HTMLElement,\n blog: HTMLElement,\n url: URL,\n filter: BlogFilter,\n count: number,\n labels: BlogLabels,\n): void {\n const active = listActiveFilters(blog, filter, labels);\n slot.hidden = active.length === 0;\n if (slot.hidden) return slot.replaceChildren();\n\n const summary = count === 0 ? labels.noMatches : count === 1 ? labels.postCountOne : labels.postCount;\n const parts: Node[] = [element('span', 'vp-blog-filter__count', summary.replace('{n}', String(count)))];\n for (const { kind, name, without } of active) {\n const chip = link('', toHref(url, without), 'vp-blog-filter__chip');\n chip.setAttribute('aria-label', labels.removeFilter.replace('{filter}', kind ? `${kind} ${name}` : name));\n if (kind) chip.append(element('span', 'vp-blog-filter__kind', kind), ' ');\n const remove = element('span', 'vp-blog-filter__remove', '×');\n remove.setAttribute('aria-hidden', 'true');\n chip.append(name, remove);\n parts.push(chip);\n }\n const clear = { category: '', tag: '', date: '', announcements: false };\n parts.push(link(labels.showAll, toHref(url, clear), 'vp-blog-filter__clear'));\n slot.replaceChildren(...parts);\n}\n\nfunction renderPagination(\n slot: HTMLElement,\n url: URL,\n filter: BlogFilter,\n page: number,\n pageCount: number,\n labels: BlogLabels,\n): void {\n slot.hidden = pageCount < 2;\n slot.setAttribute('aria-label', labels.pages);\n if (pageCount < 2) return slot.replaceChildren();\n\n const parts: Node[] = [];\n if (page > 1) parts.push(link(labels.previous, toHref(url, filter, page - 1), 'vp-blog-pagination__previous'));\n for (let number = 1; number <= pageCount; number++) {\n if (number === page) {\n const current = element('span', 'vp-blog-pagination__page is-current', String(number));\n current.setAttribute('aria-current', 'page');\n parts.push(current);\n } else {\n parts.push(link(String(number), toHref(url, filter, number), 'vp-blog-pagination__page'));\n }\n }\n if (page < pageCount) parts.push(link(labels.next, toHref(url, filter, page + 1), 'vp-blog-pagination__next'));\n slot.replaceChildren(...parts);\n}\n\n/** Marks the category and keyword links on cards that match the active filter. */\nfunction markCardLinks(blog: HTMLElement, filter: BlogFilter): void {\n for (const anchor of blog.querySelectorAll<HTMLAnchorElement>('.vp-blog-card__category a, .vp-blog-card__tags a')) {\n const link = readFilterLink(anchor);\n markActive(anchor, !!link && isActive(link, filter));\n }\n}\n\nexport interface BlogViewState extends BlogFilter {\n page: number;\n pageCount: number;\n}\n\n/**\n * Shows the posts of one blog page that `url` asks for: those matching every\n * filter it gives (category and keyword in any case, a year or a month, only\n * announcements), and of those, the ones on its `?page=`. Pinned posts\n * only show on the first page and aren't counted in the page size. Returns\n * what is showing.\n */\nexport function applyBlogView(blog: HTMLElement, url: URL): BlogViewState {\n const labels = readLabels(blog);\n const filter = readFilter(url);\n const pageSize = Math.max(0, Number(blog.dataset.pageSize) || 0);\n\n const markers = [...blog.querySelectorAll(ITEM)];\n const isPinned = (marker: Element) => !!marker.closest('[data-vp-blog-pinned]');\n const pinned = markers.filter((marker) => isPinned(marker) && matches(marker, filter));\n const rest = markers.filter((marker) => !isPinned(marker) && matches(marker, filter));\n\n const pageCount = pageSize ? Math.max(1, Math.ceil(rest.length / pageSize)) : 1;\n const requested = Number.parseInt(url.searchParams.get('page') ?? '1', 10);\n const page = Math.min(pageCount, Math.max(1, Number.isFinite(requested) ? requested : 1));\n\n const shown = new Set<Element>([\n ...(page === 1 ? pinned : []),\n ...(pageSize ? rest.slice((page - 1) * pageSize, page * pageSize) : rest),\n ]);\n for (const marker of markers) itemElement(marker).hidden = !shown.has(marker);\n\n // A pinned section, year or month with nothing showing is hidden, heading and all.\n for (const group of blog.querySelectorAll<HTMLElement>('[data-vp-blog-pinned], .vp-blog-group')) {\n group.hidden = ![...group.querySelectorAll(ITEM)].some((marker) => shown.has(marker));\n }\n\n const banner = blog.querySelector<HTMLElement>('[data-vp-blog-filter]');\n if (banner) renderFilter(banner, blog, url, filter, pinned.length + rest.length, labels);\n const pagination = blog.querySelector<HTMLElement>('[data-vp-blog-pagination]');\n if (pagination) renderPagination(pagination, url, filter, page, pageCount, labels);\n syncBrowse(blog, url, filter, markers);\n markCardLinks(blog, filter);\n\n return { ...filter, page, pageCount };\n}\n\nconst lastViews = new WeakMap<HTMLElement, BlogViewState>();\n\nfunction viewChanged(previous: BlogViewState, view: BlogViewState): boolean {\n return (['category', 'tag', 'date', 'announcements', 'page'] as const).some((key) => previous[key] !== view[key]);\n}\n\n/**\n * Applies `url` to every blog page in `root`. When the page or a filter of a\n * blog already on screen changes, it is scrolled back to its top.\n */\nexport function applyBlogViews(root: ParentNode = document, url: URL = new URL(location.href)): void {\n for (const blog of root.querySelectorAll<HTMLElement>('[data-vp-blog]')) {\n const previous = lastViews.get(blog);\n const view = applyBlogView(blog, url);\n lastViews.set(blog, view);\n if (previous && viewChanged(previous, view) && blog.getBoundingClientRect().top < 0) {\n blog.scrollIntoView({ block: 'start' });\n }\n }\n}\n","// A namespace import, as in `client.ts`: loaded by Node without the Vite\n// plugin, VitePress's node entry has no `useData` or `withBase`.\nimport * as vitepress from 'vitepress';\nimport { computed, defineComponent, h, onMounted, ref } from 'vue';\nimport type { BlogAnnouncementData } from './types.js';\nimport { ANNOUNCEMENTS_KEY, DISMISSED_CLASS, DISMISSED_STORAGE_KEY } from './util.js';\n\nconst closeIcon = () =>\n h('svg', { viewBox: '0 0 24 24', width: 16, height: 16, 'aria-hidden': 'true' }, [\n h('path', {\n fill: 'currentColor',\n d: 'M18.3 5.71a1 1 0 0 0-1.41 0L12 10.59 7.11 5.7A1 1 0 0 0 5.7 7.11L10.59 12 5.7 16.89a1 1 0 1 0 1.41 1.41L12 13.41l4.89 4.89a1 1 0 0 0 1.41-1.41L13.41 12l4.89-4.89a1 1 0 0 0 0-1.4z',\n }),\n ]);\n\nfunction readDismissed(): string[] {\n try {\n const value: unknown = JSON.parse(localStorage.getItem(DISMISSED_STORAGE_KEY) ?? '[]');\n return Array.isArray(value) ? value.filter((id): id is string => typeof id === 'string') : [];\n } catch {\n return [];\n }\n}\n\nfunction writeDismissed(ids: string[]): void {\n try {\n localStorage.setItem(DISMISSED_STORAGE_KEY, JSON.stringify(ids));\n } catch {\n // Storage blocked (a private window, say): dismissed until the next page load.\n }\n}\n\n/**\n * The announcement banner: one line across the top of the page, above the\n * nav bar, with the newest announcement linked to its post. With more than\n * one not yet dismissed, a badge counts them and links to the blog page\n * filtered to announcements. Its close button dismisses every current one;\n * a newer announcement brings it back. Put it in the theme's `layout-top` slot:\n *\n * ```ts\n * // .vitepress/theme/index.ts\n * import { h } from 'vue';\n * import DefaultTheme from 'vitepress/theme';\n * import { BlogAnnouncementBanner } from '@binarynoir/vitepress-blog/client';\n *\n * export default {\n * extends: DefaultTheme,\n * Layout: () => h(DefaultTheme.Layout, null, { 'layout-top': () => h(BlogAnnouncementBanner) }),\n * };\n * ```\n *\n * It reads what `blogVitePlugin` (or `withBlog`) puts on the site data, and\n * renders nothing without it.\n */\nexport const BlogAnnouncementBanner = defineComponent({\n name: 'BlogAnnouncementBanner',\n setup() {\n if (typeof vitepress.useData !== 'function') return () => null;\n const { site } = vitepress.useData();\n const data = computed(\n () => (site.value as unknown as Record<string, unknown>)[ANNOUNCEMENTS_KEY] as BlogAnnouncementData | undefined,\n );\n // Unknown until mounted: the server can't read storage, so the first render\n // matches its HTML, and the `<head>` script has already hidden it if dismissed.\n const dismissed = ref<string[] | null>(null);\n onMounted(() => {\n dismissed.value = readDismissed();\n });\n\n const latest = computed(() => data.value?.items[0]);\n const active = computed(() => (data.value?.items ?? []).filter((item) => !dismissed.value?.includes(item.id)));\n\n const dismiss = () => {\n const ids = (data.value?.items ?? []).map((item) => item.id);\n dismissed.value = ids;\n writeDismissed(ids);\n document.documentElement.classList.add(DISMISSED_CLASS);\n };\n\n return () => {\n const announcement = latest.value;\n if (!data.value || !announcement || dismissed.value?.includes(announcement.id)) return null;\n const { labels, listLink } = data.value;\n const count = active.value.length;\n const countLabel = labels.announcementCount.replace('{n}', String(count));\n const badge =\n count > 1\n ? listLink\n ? h(\n 'a',\n {\n class: 'vp-blog-announcement__count',\n href: vitepress.withBase(listLink),\n title: countLabel,\n 'aria-label': countLabel,\n },\n String(count),\n )\n : h(\n 'span',\n { class: 'vp-blog-announcement__count', title: countLabel, 'aria-label': countLabel },\n String(count),\n )\n : null;\n return h('div', { class: 'vp-blog-announcement', role: 'region', 'aria-label': labels.announcement }, [\n h('div', { class: 'vp-blog-announcement__content' }, [\n h(\n 'a',\n { class: 'vp-blog-announcement__text', href: vitepress.withBase(announcement.link) },\n announcement.text,\n ),\n badge,\n ]),\n h(\n 'button',\n {\n class: 'vp-blog-announcement__dismiss',\n type: 'button',\n title: labels.dismiss,\n 'aria-label': labels.dismiss,\n onClick: dismiss,\n },\n [closeIcon()],\n ),\n ]);\n };\n },\n});\n","const HTML_ESCAPES: Record<string, string> = {\n '&': '&amp;',\n '<': '&lt;',\n '>': '&gt;',\n '\"': '&quot;',\n \"'\": '&#39;',\n};\n\n/** Escapes the characters that are special in HTML text and attribute values. */\nexport function escapeHtml(value: string): string {\n return value.replace(/[&<>\"']/g, (character) => HTML_ESCAPES[character]);\n}\n\n/**\n * Escapes text so it stays literal in Markdown and in VitePress's Vue\n * templates. Text with braces goes in a `v-pre` span, since Vue would otherwise\n * evaluate `{{ }}` even when the Markdown escaped it.\n */\nexport function escapeMarkdown(text: string): string {\n const escaped = text\n .replace(/[\\\\`*_[\\]#|~]/g, '\\\\$&')\n .replace(/&/g, '&amp;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;');\n return /[{}]/.test(escaped) ? `<span v-pre>${escaped}</span>` : escaped;\n}\n\n/** HTML text that Vue leaves alone: braces go in a `v-pre` span. */\nexport function escapeTemplateText(text: string): string {\n const escaped = escapeHtml(text);\n return /[{}]/.test(escaped) ? `<span v-pre>${escaped}</span>` : escaped;\n}\n\n/** A URL with a scheme (`https:`, `mailto:`) or protocol-relative (`//cdn`). */\nexport function isExternalUrl(url: string): boolean {\n return /^(?:[a-z][a-z\\d+.-]*:|\\/\\/)/i.test(url);\n}\n\n/** Encodes a URL for a Markdown link destination: spaces and parentheses would end it early. */\nexport function toMarkdownUrl(url: string): string {\n return encodeURI(url).replace(/[()]/g, (character) => (character === '(' ? '%28' : '%29'));\n}\n\n/** A Markdown link to a page by its served path, which VitePress then checks and gives the site's base. */\nexport function toPageLink(text: string, routePath: string): string {\n return `[${escapeMarkdown(text)}](${toMarkdownUrl(`/${routePath}`)})`;\n}\n\n/** A page's URL path, without the site's base: `news/index.md` is `/news/`, `news/launch.md` is `/news/launch`. */\nexport function toUrlPath(routePath: string, cleanUrls: boolean): string {\n return `/${routePath.replace(/(^|\\/)index\\.md$/, '$1').replace(/\\.md$/, cleanUrls ? '' : '.html')}`;\n}\n\n/** `base` and `urlPath` joined with exactly one slash between them. */\nexport function joinUrl(base: string, urlPath: string): string {\n return `${base.replace(/\\/+$/, '')}/${urlPath.replace(/^\\/+/, '')}`;\n}\n\n/** Where the announcement banner's data sits on VitePress's site data: `useData().site.value.blogAnnouncements`. */\nexport const ANNOUNCEMENTS_KEY = 'blogAnnouncements';\n\n/** The `localStorage` key holding the ids of the announcements a visitor dismissed, as a JSON array. */\nexport const DISMISSED_STORAGE_KEY = 'vp-blog-announcements-dismissed';\n\n/** The class on `<html>` that hides a dismissed banner before the page hydrates. */\nexport const DISMISSED_CLASS = 'vp-blog-announcement-dismissed';\n"],"mappings":"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","names":["vitepress","import_vue","link"]}
1
+ {"version":3,"sources":["../src/client.ts","../src/view.ts","../src/banner.ts","../src/util.ts"],"sourcesContent":["// A namespace import, not named ones: during the server-side build Node loads\n// VitePress's node entry, which has no `inBrowser`, `useRoute` or\n// `onContentUpdated`. Named imports would fail to link there; on the\n// namespace they are just `undefined`.\nimport * as vitepress from 'vitepress';\nimport { nextTick, onMounted, watch } from 'vue';\nimport { applyBlogViews } from './view.js';\n\n/**\n * Turns on filtering (`?category=`, `?tag=`, `?year=`, `?month=`) and pagination (`?page=`) on blog\n * pages. Call it from your theme's `setup()`:\n *\n * ```ts\n * // .vitepress/theme/index.ts\n * import DefaultTheme from 'vitepress/theme';\n * import { useBlog } from '@binarynoir/vitepress-blog/client';\n * import '@binarynoir/vitepress-blog/style.css';\n *\n * export default {\n * extends: DefaultTheme,\n * setup() {\n * useBlog();\n * },\n * };\n * ```\n *\n * Filter and page links are ordinary links: VitePress's router follows them\n * without reloading the page, and this re-applies the view whenever the path\n * or query string changes. It runs after the page has hydrated, so the\n * server-rendered HTML (every post) and the first client render agree.\n * Does nothing during the server-side build.\n */\nexport function useBlog(): void {\n if (!vitepress.inBrowser) return;\n const route = vitepress.useRoute();\n const apply = () => nextTick(() => applyBlogViews());\n onMounted(() => {\n watch(() => [route.path, route.query], apply, { immediate: true });\n });\n // `vitepress dev` swaps a page's content in place when it is edited.\n vitepress.onContentUpdated(apply);\n}\n\nexport { BlogAnnouncementBanner } from './banner.js';\nexport { applyBlogView, applyBlogViews, readFilter, type BlogFilter, type BlogViewState } from './view.js';\n","/**\n * Filtering and pagination for blog pages, in the browser. The build writes\n * every post into the page (see `renderPostList`); this hides the ones the\n * URL's `?category=`, `?tag=`, `?year=`, `?month=`, `?announcements` and\n * `?page=` leave out, keeps the browse panel's chips and counts in step with\n * them, and fills in the active filters and the page links. Without it, every\n * post simply shows.\n */\n\nconst ITEM = '[data-vp-blog-item]';\n/** Links that filter the blog: the browse panel's chips, and the category and keyword links on cards. */\nconst FILTER_LINKS = '.vp-blog-facet__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","const HTML_ESCAPES: Record<string, string> = {\n '&': '&amp;',\n '<': '&lt;',\n '>': '&gt;',\n '\"': '&quot;',\n \"'\": '&#39;',\n};\n\n/** Escapes the characters that are special in HTML text and attribute values. */\nexport function escapeHtml(value: string): string {\n return value.replace(/[&<>\"']/g, (character) => HTML_ESCAPES[character]);\n}\n\n/**\n * Escapes text so it stays literal in Markdown and in VitePress's Vue\n * templates. Text with braces goes in a `v-pre` span, since Vue would otherwise\n * evaluate `{{ }}` even when the Markdown escaped it.\n */\nexport function escapeMarkdown(text: string): string {\n const escaped = text\n .replace(/[\\\\`*_[\\]#|~]/g, '\\\\$&')\n .replace(/&/g, '&amp;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;');\n return /[{}]/.test(escaped) ? `<span v-pre>${escaped}</span>` : escaped;\n}\n\n/** HTML text that Vue leaves alone: braces go in a `v-pre` span. */\nexport function escapeTemplateText(text: string): string {\n const escaped = escapeHtml(text);\n return /[{}]/.test(escaped) ? `<span v-pre>${escaped}</span>` : escaped;\n}\n\n/** A URL with a scheme (`https:`, `mailto:`) or protocol-relative (`//cdn`). */\nexport function isExternalUrl(url: string): boolean {\n return /^(?:[a-z][a-z\\d+.-]*:|\\/\\/)/i.test(url);\n}\n\n/** Encodes a URL for a Markdown link destination: spaces and parentheses would end it early. */\nexport function toMarkdownUrl(url: string): string {\n return encodeURI(url).replace(/[()]/g, (character) => (character === '(' ? '%28' : '%29'));\n}\n\n/** A Markdown link to a page by its served path, which VitePress then checks and gives the site's base. */\nexport function toPageLink(text: string, routePath: string): string {\n return `[${escapeMarkdown(text)}](${toMarkdownUrl(`/${routePath}`)})`;\n}\n\n/** A page's URL path, without the site's base: `news/index.md` is `/news/`, `news/launch.md` is `/news/launch`. */\nexport function toUrlPath(routePath: string, cleanUrls: boolean): string {\n return `/${routePath.replace(/(^|\\/)index\\.md$/, '$1').replace(/\\.md$/, cleanUrls ? '' : '.html')}`;\n}\n\n/** `base` and `urlPath` joined with exactly one slash between them. */\nexport function joinUrl(base: string, urlPath: string): string {\n return `${base.replace(/\\/+$/, '')}/${urlPath.replace(/^\\/+/, '')}`;\n}\n\n/** Where the announcement banner's data sits on VitePress's site data: `useData().site.value.blogAnnouncements`. */\nexport const ANNOUNCEMENTS_KEY = 'blogAnnouncements';\n\n/** The `localStorage` key holding the ids of the announcements a visitor dismissed, as a JSON array. */\nexport const DISMISSED_STORAGE_KEY = 'vp-blog-announcements-dismissed';\n\n/** The class on `<html>` that hides a dismissed banner before the page hydrates. */\nexport const DISMISSED_CLASS = 'vp-blog-announcement-dismissed';\n"],"mappings":"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","names":["vitepress","import_vue","link"]}
package/dist/client.js CHANGED
@@ -10,7 +10,7 @@ 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 a, .vp-blog-card__category a, .vp-blog-card__tags a";
13
+ var FILTER_LINKS = ".vp-blog-facet__chips a, .vp-blog-card__category a, .vp-blog-card__tags a";
14
14
  function readLabels(blog) {
15
15
  const { dataset } = blog;
16
16
  return {
@@ -24,7 +24,11 @@ function readLabels(blog) {
24
24
  postCount: dataset.labelPostCount ?? "{n} posts",
25
25
  postCountOne: dataset.labelPostCountOne ?? "{n} post",
26
26
  noMatches: dataset.labelNoMatches ?? "No posts match these filters.",
27
- removeFilter: dataset.labelRemoveFilter ?? "Remove filter: {filter}"
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."
28
32
  };
29
33
  }
30
34
  function readFilter(url) {
@@ -92,10 +96,69 @@ function markActive(anchor, active) {
92
96
  if (active) anchor.setAttribute("aria-current", "true");
93
97
  else anchor.removeAttribute("aria-current");
94
98
  }
95
- function syncBrowse(blog, url, filter, markers) {
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) {
96
159
  const browse = blog.querySelector(".vp-blog-browse");
97
160
  if (!browse) return;
98
- for (const anchor of browse.querySelectorAll(".vp-blog-facet a")) {
161
+ for (const anchor of browse.querySelectorAll(".vp-blog-facet__chips a")) {
99
162
  const link2 = readFilterLink(anchor);
100
163
  if (!link2) continue;
101
164
  const active = isActive(link2, filter);
@@ -108,6 +171,11 @@ function syncBrowse(blog, url, filter, markers) {
108
171
  }
109
172
  const months = [...browse.querySelectorAll('[data-vp-blog-facet="month"]')];
110
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
+ }
111
179
  }
112
180
  function findName(blog, facet, value) {
113
181
  for (const anchor of blog.querySelectorAll(FILTER_LINKS)) {
@@ -225,7 +293,7 @@ function applyBlogView(blog, url) {
225
293
  if (banner) renderFilter(banner, blog, url, filter, pinned.length + rest.length, labels);
226
294
  const pagination = blog.querySelector("[data-vp-blog-pagination]");
227
295
  if (pagination) renderPagination(pagination, url, filter, page, pageCount, labels);
228
- syncBrowse(blog, url, filter, markers);
296
+ syncBrowse(blog, url, filter, markers, labels);
229
297
  markCardLinks(blog, filter);
230
298
  return { ...filter, page, pageCount };
231
299
  }
@@ -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 filtering (`?category=`, `?tag=`, `?year=`, `?month=`) and pagination (`?page=`) on blog\n * pages. Call it from your theme's `setup()`:\n *\n * ```ts\n * // .vitepress/theme/index.ts\n * import DefaultTheme from 'vitepress/theme';\n * import { useBlog } from '@binarynoir/vitepress-blog/client';\n * import '@binarynoir/vitepress-blog/style.css';\n *\n * export default {\n * extends: DefaultTheme,\n * setup() {\n * useBlog();\n * },\n * };\n * ```\n *\n * Filter and page links are ordinary links: VitePress's router follows them\n * without reloading the page, and this re-applies the view whenever the path\n * or query string changes. It runs after the page has hydrated, so the\n * server-rendered HTML (every post) and the first client render agree.\n * Does nothing during the server-side build.\n */\nexport function useBlog(): void {\n if (!vitepress.inBrowser) return;\n const route = vitepress.useRoute();\n const apply = () => nextTick(() => applyBlogViews());\n onMounted(() => {\n watch(() => [route.path, route.query], apply, { immediate: true });\n });\n // `vitepress dev` swaps a page's content in place when it is edited.\n vitepress.onContentUpdated(apply);\n}\n\nexport { BlogAnnouncementBanner } from './banner.js';\nexport { applyBlogView, applyBlogViews, readFilter, type BlogFilter, type BlogViewState } from './view.js';\n","/**\n * Filtering and pagination for blog pages, in the browser. The build writes\n * every post into the page (see `renderPostList`); this hides the ones the\n * URL's `?category=`, `?tag=`, `?year=`, `?month=`, `?announcements` and\n * `?page=` leave out, keeps the browse panel's chips and counts in step with\n * them, and fills in the active filters and the page links. Without it, every\n * post simply shows.\n */\n\nconst ITEM = '[data-vp-blog-item]';\n/** Links that filter the blog: the browse panel's chips, and the category and keyword links on cards. */\nconst FILTER_LINKS = '.vp-blog-facet a, .vp-blog-card__category a, .vp-blog-card__tags a';\n\ninterface BlogLabels {\n showAll: string;\n previous: string;\n next: string;\n pages: string;\n announcements: string;\n category: string;\n tag: string;\n postCount: string;\n postCountOne: string;\n noMatches: string;\n removeFilter: string;\n}\n\nfunction readLabels(blog: HTMLElement): BlogLabels {\n const { dataset } = blog;\n return {\n showAll: dataset.labelShowAll ?? 'Show all posts',\n previous: dataset.labelPrevious ?? 'Previous',\n next: dataset.labelNext ?? 'Next',\n pages: dataset.labelPages ?? 'Pages',\n announcements: dataset.labelAnnouncements ?? 'Announcements',\n category: dataset.labelCategory ?? 'Category',\n tag: dataset.labelTag ?? 'Keyword',\n postCount: dataset.labelPostCount ?? '{n} posts',\n postCountOne: dataset.labelPostCountOne ?? '{n} post',\n noMatches: dataset.labelNoMatches ?? 'No posts match these filters.',\n removeFilter: dataset.labelRemoveFilter ?? 'Remove filter: {filter}',\n };\n}\n\n/**\n * What the URL filters by. Every filter must match. The category and keyword\n * are lowercased; `date` is `''`, a year (`2026`) or a month (`2026-10`).\n */\nexport interface BlogFilter {\n category: string;\n tag: string;\n date: string;\n /** Whether only announcements show (`?announcements`). */\n announcements: boolean;\n}\n\ntype Facet = 'category' | 'tag' | 'date';\n\n/** Reads the filter from `?category=`, `?tag=`, `?year=` and `?month=`. A month names its year too, so it wins over `?year=`. */\nexport function readFilter(url: URL): BlogFilter {\n const text = (name: string) => (url.searchParams.get(name) ?? '').trim().toLowerCase();\n const month = text('month');\n const year = text('year');\n return {\n category: text('category'),\n tag: text('tag'),\n date: /^\\d{4}-(?:0[1-9]|1[0-2])$/.test(month) ? month : /^\\d{4}$/.test(year) ? year : '',\n announcements: url.searchParams.has('announcements'),\n };\n}\n\n/** The element a marker stands for: a card is its own marker; a compact list item holds one. */\nfunction itemElement(marker: Element): HTMLElement {\n return (marker.tagName === 'ARTICLE' ? marker : (marker.closest('li') ?? marker)) as HTMLElement;\n}\n\nfunction matches(marker: Element, filter: BlogFilter): boolean {\n const { category, tag, date, announcements } = filter;\n return (\n (!tag || (marker.getAttribute('data-tags') ?? '').split('|').includes(tag)) &&\n (!category || marker.getAttribute('data-category') === category) &&\n // `2026` matches every month of 2026; `2026-10` only October.\n (!date || (marker.getAttribute('data-month') ?? '').startsWith(date)) &&\n (!announcements || marker.hasAttribute('data-announcement'))\n );\n}\n\n/** `url` with the filter and `page` set (or removed when empty or page 1), as a path the router can follow. */\nfunction toHref(url: URL, filter: BlogFilter, page = 1): string {\n const next = new URL(url.href);\n next.hash = '';\n const set = (name: string, value: string) =>\n value ? next.searchParams.set(name, value) : next.searchParams.delete(name);\n set('category', filter.category);\n set('tag', filter.tag);\n set('year', filter.date.length === 4 ? filter.date : '');\n set('month', filter.date.length === 7 ? filter.date : '');\n if (filter.announcements) next.searchParams.set('announcements', '');\n else next.searchParams.delete('announcements');\n set('page', page > 1 ? String(page) : '');\n // A bare `?announcements`, not `?announcements=`.\n return next.pathname + next.search.replace(/([?&]announcements)=(?=&|$)/, '$1');\n}\n\n/** The filter a link applies, as the build wrote it: `?category=x`, `?tag=x`, `?year=2026` or `?month=2026-10`. */\ninterface FilterLink {\n facet: Facet;\n value: string;\n}\n\n/**\n * The filter a link stands for. It is read from the link's URL the first time\n * and remembered, because the browse panel's links are then rewritten to\n * combine it with the other active filters.\n */\nfunction readFilterLink(anchor: HTMLAnchorElement): FilterLink | null {\n if (!anchor.dataset.vpBlogFacet) {\n let params: URLSearchParams;\n try {\n params = new URL(anchor.href, location.href).searchParams;\n } catch {\n return null;\n }\n const param = (['category', 'tag', 'month', 'year'] as const).find((name) => params.has(name));\n if (!param) return null;\n anchor.dataset.vpBlogFacet = param === 'year' || param === 'month' ? 'date' : param;\n anchor.dataset.vpBlogValue = (params.get(param) ?? '').trim().toLowerCase();\n }\n return { facet: anchor.dataset.vpBlogFacet as Facet, value: anchor.dataset.vpBlogValue ?? '' };\n}\n\nfunction withValue(filter: BlogFilter, { facet, value }: FilterLink): BlogFilter {\n return { ...filter, [facet]: value };\n}\n\n/** Whether the link's filter is applied. A year is also active while one of its months is. */\nfunction isActive({ facet, value }: FilterLink, filter: BlogFilter): boolean {\n if (facet !== 'date') return filter[facet] === value;\n return value.length === 4 ? filter.date.startsWith(value) : filter.date === value;\n}\n\n/**\n * Where a browse chip leads: its filter added to the active ones (replacing\n * any other value of the same kind), or, when it is the active one, taken off\n * again. An active month steps back out to its year.\n */\nfunction toggle(filter: BlogFilter, link: FilterLink): BlogFilter {\n if (link.facet !== 'date')\n return withValue(filter, { ...link, value: filter[link.facet] === link.value ? '' : link.value });\n if (filter.date !== link.value) return withValue(filter, link);\n return { ...filter, date: link.value.length === 7 ? link.value.slice(0, 4) : '' };\n}\n\nfunction markActive(anchor: HTMLAnchorElement, active: boolean): void {\n anchor.classList.toggle('is-active', active);\n if (active) anchor.setAttribute('aria-current', 'true');\n else anchor.removeAttribute('aria-current');\n}\n\n/**\n * Brings the browse panel in step with the filter. Each chip links to the\n * view with it toggled, and counts the posts it would show with the other\n * filters kept, so a reader can see where every next step leads; chips that\n * would show nothing are dimmed. The months of the year in view show below\n * the years.\n */\nfunction syncBrowse(blog: HTMLElement, url: URL, filter: BlogFilter, markers: Element[]): void {\n const browse = blog.querySelector<HTMLElement>('.vp-blog-browse');\n if (!browse) return;\n for (const anchor of browse.querySelectorAll<HTMLAnchorElement>('.vp-blog-facet a')) {\n const link = readFilterLink(anchor);\n if (!link) continue;\n const active = isActive(link, filter);\n const count = markers.filter((marker) => matches(marker, withValue(filter, link))).length;\n anchor.href = toHref(url, toggle(filter, link));\n markActive(anchor, active);\n anchor.classList.toggle('is-empty', count === 0 && !active);\n const counter = anchor.querySelector('.vp-blog-facet__count');\n if (counter) counter.textContent = String(count);\n }\n // With only one year, its months always show.\n const months = [...browse.querySelectorAll<HTMLElement>('[data-vp-blog-facet=\"month\"]')];\n for (const row of months) row.hidden = months.length > 1 && row.dataset.year !== filter.date.slice(0, 4);\n}\n\n/** The name a filter value is shown with: as a chip or card link spells it, else as the URL gave it. */\nfunction findName(blog: HTMLElement, facet: 'category' | 'tag', value: string): string {\n for (const anchor of blog.querySelectorAll<HTMLAnchorElement>(FILTER_LINKS)) {\n const link = readFilterLink(anchor);\n if (link?.facet === facet && link.value === value) {\n const name = (anchor.querySelector('.vp-blog-facet__name') ?? anchor).textContent?.trim();\n if (name) return name;\n }\n }\n return value;\n}\n\n/** A year as written, or a month as \"October 2026\" in the blog's locale. */\nfunction formatDate(date: string, locale: string): string {\n if (date.length === 4) return date;\n const [year, month] = date.split('-').map(Number);\n try {\n return new Intl.DateTimeFormat(locale, { timeZone: 'UTC', month: 'long', year: 'numeric' }).format(\n Date.UTC(year, month - 1, 1, 12),\n );\n } catch {\n return date;\n }\n}\n\nfunction element(tag: string, className: string, text = ''): HTMLElement {\n const node = document.createElement(tag);\n node.className = className;\n node.textContent = text;\n return node;\n}\n\nfunction link(text: string, href: string, className: string): HTMLAnchorElement {\n const anchor = element('a', className, text) as HTMLAnchorElement;\n anchor.href = href;\n return anchor;\n}\n\n/** One active filter: what kind it is (`''` for a date or announcements), its name, and the view without it. */\ninterface ActiveFilter {\n kind: string;\n name: string;\n without: BlogFilter;\n}\n\nfunction listActiveFilters(blog: HTMLElement, filter: BlogFilter, labels: BlogLabels): ActiveFilter[] {\n const active: ActiveFilter[] = [];\n if (filter.announcements) {\n active.push({ kind: '', name: labels.announcements, without: { ...filter, announcements: false } });\n }\n if (filter.category) {\n const name = findName(blog, 'category', filter.category);\n active.push({ kind: labels.category, name, without: { ...filter, category: '' } });\n }\n if (filter.tag) {\n active.push({ kind: labels.tag, name: findName(blog, 'tag', filter.tag), without: { ...filter, tag: '' } });\n }\n if (filter.date) {\n const name = formatDate(filter.date, blog.dataset.locale || document.documentElement.lang || 'en-US');\n active.push({ kind: '', name, without: { ...filter, date: '' } });\n }\n return active;\n}\n\n/**\n * The active filters, above the posts: how many posts match, a chip for each\n * filter that removes just that one, and a link that clears them all. All of\n * it is written as text: the values come from the URL.\n */\nfunction renderFilter(\n slot: HTMLElement,\n blog: HTMLElement,\n url: URL,\n filter: BlogFilter,\n count: number,\n labels: BlogLabels,\n): void {\n const active = listActiveFilters(blog, filter, labels);\n slot.hidden = active.length === 0;\n if (slot.hidden) return slot.replaceChildren();\n\n const summary = count === 0 ? labels.noMatches : count === 1 ? labels.postCountOne : labels.postCount;\n const parts: Node[] = [element('span', 'vp-blog-filter__count', summary.replace('{n}', String(count)))];\n for (const { kind, name, without } of active) {\n const chip = link('', toHref(url, without), 'vp-blog-filter__chip');\n chip.setAttribute('aria-label', labels.removeFilter.replace('{filter}', kind ? `${kind} ${name}` : name));\n if (kind) chip.append(element('span', 'vp-blog-filter__kind', kind), ' ');\n const remove = element('span', 'vp-blog-filter__remove', '×');\n remove.setAttribute('aria-hidden', 'true');\n chip.append(name, remove);\n parts.push(chip);\n }\n const clear = { category: '', tag: '', date: '', announcements: false };\n parts.push(link(labels.showAll, toHref(url, clear), 'vp-blog-filter__clear'));\n slot.replaceChildren(...parts);\n}\n\nfunction renderPagination(\n slot: HTMLElement,\n url: URL,\n filter: BlogFilter,\n page: number,\n pageCount: number,\n labels: BlogLabels,\n): void {\n slot.hidden = pageCount < 2;\n slot.setAttribute('aria-label', labels.pages);\n if (pageCount < 2) return slot.replaceChildren();\n\n const parts: Node[] = [];\n if (page > 1) parts.push(link(labels.previous, toHref(url, filter, page - 1), 'vp-blog-pagination__previous'));\n for (let number = 1; number <= pageCount; number++) {\n if (number === page) {\n const current = element('span', 'vp-blog-pagination__page is-current', String(number));\n current.setAttribute('aria-current', 'page');\n parts.push(current);\n } else {\n parts.push(link(String(number), toHref(url, filter, number), 'vp-blog-pagination__page'));\n }\n }\n if (page < pageCount) parts.push(link(labels.next, toHref(url, filter, page + 1), 'vp-blog-pagination__next'));\n slot.replaceChildren(...parts);\n}\n\n/** Marks the category and keyword links on cards that match the active filter. */\nfunction markCardLinks(blog: HTMLElement, filter: BlogFilter): void {\n for (const anchor of blog.querySelectorAll<HTMLAnchorElement>('.vp-blog-card__category a, .vp-blog-card__tags a')) {\n const link = readFilterLink(anchor);\n markActive(anchor, !!link && isActive(link, filter));\n }\n}\n\nexport interface BlogViewState extends BlogFilter {\n page: number;\n pageCount: number;\n}\n\n/**\n * Shows the posts of one blog page that `url` asks for: those matching every\n * filter it gives (category and keyword in any case, a year or a month, only\n * announcements), and of those, the ones on its `?page=`. Pinned posts\n * only show on the first page and aren't counted in the page size. Returns\n * what is showing.\n */\nexport function applyBlogView(blog: HTMLElement, url: URL): BlogViewState {\n const labels = readLabels(blog);\n const filter = readFilter(url);\n const pageSize = Math.max(0, Number(blog.dataset.pageSize) || 0);\n\n const markers = [...blog.querySelectorAll(ITEM)];\n const isPinned = (marker: Element) => !!marker.closest('[data-vp-blog-pinned]');\n const pinned = markers.filter((marker) => isPinned(marker) && matches(marker, filter));\n const rest = markers.filter((marker) => !isPinned(marker) && matches(marker, filter));\n\n const pageCount = pageSize ? Math.max(1, Math.ceil(rest.length / pageSize)) : 1;\n const requested = Number.parseInt(url.searchParams.get('page') ?? '1', 10);\n const page = Math.min(pageCount, Math.max(1, Number.isFinite(requested) ? requested : 1));\n\n const shown = new Set<Element>([\n ...(page === 1 ? pinned : []),\n ...(pageSize ? rest.slice((page - 1) * pageSize, page * pageSize) : rest),\n ]);\n for (const marker of markers) itemElement(marker).hidden = !shown.has(marker);\n\n // A pinned section, year or month with nothing showing is hidden, heading and all.\n for (const group of blog.querySelectorAll<HTMLElement>('[data-vp-blog-pinned], .vp-blog-group')) {\n group.hidden = ![...group.querySelectorAll(ITEM)].some((marker) => shown.has(marker));\n }\n\n const banner = blog.querySelector<HTMLElement>('[data-vp-blog-filter]');\n if (banner) renderFilter(banner, blog, url, filter, pinned.length + rest.length, labels);\n const pagination = blog.querySelector<HTMLElement>('[data-vp-blog-pagination]');\n if (pagination) renderPagination(pagination, url, filter, page, pageCount, labels);\n syncBrowse(blog, url, filter, markers);\n markCardLinks(blog, filter);\n\n return { ...filter, page, pageCount };\n}\n\nconst lastViews = new WeakMap<HTMLElement, BlogViewState>();\n\nfunction viewChanged(previous: BlogViewState, view: BlogViewState): boolean {\n return (['category', 'tag', 'date', 'announcements', 'page'] as const).some((key) => previous[key] !== view[key]);\n}\n\n/**\n * Applies `url` to every blog page in `root`. When the page or a filter of a\n * blog already on screen changes, it is scrolled back to its top.\n */\nexport function applyBlogViews(root: ParentNode = document, url: URL = new URL(location.href)): void {\n for (const blog of root.querySelectorAll<HTMLElement>('[data-vp-blog]')) {\n const previous = lastViews.get(blog);\n const view = applyBlogView(blog, url);\n lastViews.set(blog, view);\n if (previous && viewChanged(previous, view) && blog.getBoundingClientRect().top < 0) {\n blog.scrollIntoView({ block: 'start' });\n }\n }\n}\n","// A namespace import, as in `client.ts`: loaded by Node without the Vite\n// plugin, VitePress's node entry has no `useData` or `withBase`.\nimport * as vitepress from 'vitepress';\nimport { computed, defineComponent, h, onMounted, ref } from 'vue';\nimport type { BlogAnnouncementData } from './types.js';\nimport { ANNOUNCEMENTS_KEY, DISMISSED_CLASS, DISMISSED_STORAGE_KEY } from './util.js';\n\nconst closeIcon = () =>\n h('svg', { viewBox: '0 0 24 24', width: 16, height: 16, 'aria-hidden': 'true' }, [\n h('path', {\n fill: 'currentColor',\n d: 'M18.3 5.71a1 1 0 0 0-1.41 0L12 10.59 7.11 5.7A1 1 0 0 0 5.7 7.11L10.59 12 5.7 16.89a1 1 0 1 0 1.41 1.41L12 13.41l4.89 4.89a1 1 0 0 0 1.41-1.41L13.41 12l4.89-4.89a1 1 0 0 0 0-1.4z',\n }),\n ]);\n\nfunction readDismissed(): string[] {\n try {\n const value: unknown = JSON.parse(localStorage.getItem(DISMISSED_STORAGE_KEY) ?? '[]');\n return Array.isArray(value) ? value.filter((id): id is string => typeof id === 'string') : [];\n } catch {\n return [];\n }\n}\n\nfunction writeDismissed(ids: string[]): void {\n try {\n localStorage.setItem(DISMISSED_STORAGE_KEY, JSON.stringify(ids));\n } catch {\n // Storage blocked (a private window, say): dismissed until the next page load.\n }\n}\n\n/**\n * The announcement banner: one line across the top of the page, above the\n * nav bar, with the newest announcement linked to its post. With more than\n * one not yet dismissed, a badge counts them and links to the blog page\n * filtered to announcements. Its close button dismisses every current one;\n * a newer announcement brings it back. Put it in the theme's `layout-top` slot:\n *\n * ```ts\n * // .vitepress/theme/index.ts\n * import { h } from 'vue';\n * import DefaultTheme from 'vitepress/theme';\n * import { BlogAnnouncementBanner } from '@binarynoir/vitepress-blog/client';\n *\n * export default {\n * extends: DefaultTheme,\n * Layout: () => h(DefaultTheme.Layout, null, { 'layout-top': () => h(BlogAnnouncementBanner) }),\n * };\n * ```\n *\n * It reads what `blogVitePlugin` (or `withBlog`) puts on the site data, and\n * renders nothing without it.\n */\nexport const BlogAnnouncementBanner = defineComponent({\n name: 'BlogAnnouncementBanner',\n setup() {\n if (typeof vitepress.useData !== 'function') return () => null;\n const { site } = vitepress.useData();\n const data = computed(\n () => (site.value as unknown as Record<string, unknown>)[ANNOUNCEMENTS_KEY] as BlogAnnouncementData | undefined,\n );\n // Unknown until mounted: the server can't read storage, so the first render\n // matches its HTML, and the `<head>` script has already hidden it if dismissed.\n const dismissed = ref<string[] | null>(null);\n onMounted(() => {\n dismissed.value = readDismissed();\n });\n\n const latest = computed(() => data.value?.items[0]);\n const active = computed(() => (data.value?.items ?? []).filter((item) => !dismissed.value?.includes(item.id)));\n\n const dismiss = () => {\n const ids = (data.value?.items ?? []).map((item) => item.id);\n dismissed.value = ids;\n writeDismissed(ids);\n document.documentElement.classList.add(DISMISSED_CLASS);\n };\n\n return () => {\n const announcement = latest.value;\n if (!data.value || !announcement || dismissed.value?.includes(announcement.id)) return null;\n const { labels, listLink } = data.value;\n const count = active.value.length;\n const countLabel = labels.announcementCount.replace('{n}', String(count));\n const badge =\n count > 1\n ? listLink\n ? h(\n 'a',\n {\n class: 'vp-blog-announcement__count',\n href: vitepress.withBase(listLink),\n title: countLabel,\n 'aria-label': countLabel,\n },\n String(count),\n )\n : h(\n 'span',\n { class: 'vp-blog-announcement__count', title: countLabel, 'aria-label': countLabel },\n String(count),\n )\n : null;\n return h('div', { class: 'vp-blog-announcement', role: 'region', 'aria-label': labels.announcement }, [\n h('div', { class: 'vp-blog-announcement__content' }, [\n h(\n 'a',\n { class: 'vp-blog-announcement__text', href: vitepress.withBase(announcement.link) },\n announcement.text,\n ),\n badge,\n ]),\n h(\n 'button',\n {\n class: 'vp-blog-announcement__dismiss',\n type: 'button',\n title: labels.dismiss,\n 'aria-label': labels.dismiss,\n onClick: dismiss,\n },\n [closeIcon()],\n ),\n ]);\n };\n },\n});\n"],"mappings":"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","names":["vitepress","onMounted","link","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"]}