@binarynoir/vitepress-blog 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +238 -88
- package/dist/chunk-42Z6H7IN.js +52 -0
- package/dist/chunk-42Z6H7IN.js.map +1 -0
- package/dist/{chunk-PWNWSYI4.js → chunk-ZM5FOUT4.js} +229 -106
- package/dist/chunk-ZM5FOUT4.js.map +1 -0
- package/dist/client.cjs +280 -64
- package/dist/client.cjs.map +1 -1
- package/dist/client.d.cts +56 -13
- package/dist/client.d.ts +56 -13
- package/dist/client.js +275 -62
- package/dist/client.js.map +1 -1
- package/dist/index.cjs +205 -70
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +2 -1
- package/dist/{plugin-BlfpqyX4.d.cts → plugin-DUTUA_t4.d.cts} +73 -19
- package/dist/{plugin-BlfpqyX4.d.ts → plugin-DUTUA_t4.d.ts} +73 -19
- package/dist/style.css +270 -25
- package/dist/vitepress.cjs +341 -138
- package/dist/vitepress.cjs.map +1 -1
- package/dist/vitepress.d.cts +17 -7
- package/dist/vitepress.d.ts +17 -7
- package/dist/vitepress.js +76 -8
- package/dist/vitepress.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-PWNWSYI4.js.map +0 -1
package/dist/client.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/client.ts","../src/view.ts"],"sourcesContent":["// A namespace import, not named ones: during the server-side build Node loads\n// VitePress's node entry, which has no `inBrowser`, `useRoute` or\n// `onContentUpdated`. Named imports would fail to link there; on the\n// namespace they are just `undefined`.\nimport * as vitepress from 'vitepress';\nimport { nextTick, onMounted, watch } from 'vue';\nimport { applyBlogViews } from './view.js';\n\n/**\n * Turns on keyword filtering (`?tag=`) and pagination (`?page=`) on blog\n * pages. Call it from your theme's `setup()`:\n *\n * ```ts\n * // .vitepress/theme/index.ts\n * import DefaultTheme from 'vitepress/theme';\n * import { useBlog } from '@binarynoir/vitepress-blog/client';\n * import '@binarynoir/vitepress-blog/style.css';\n *\n * export default {\n * extends: DefaultTheme,\n * setup() {\n * useBlog();\n * },\n * };\n * ```\n *\n * Keyword and page links are ordinary links: VitePress's router follows them\n * without reloading the page, and this re-applies the view whenever the path\n * or query string changes. It runs after the page has hydrated, so the\n * server-rendered HTML (every post) and the first client render agree.\n * Does nothing during the server-side build.\n */\nexport function useBlog(): void {\n if (!vitepress.inBrowser) return;\n const route = vitepress.useRoute();\n const apply = () => nextTick(() => applyBlogViews());\n onMounted(() => {\n watch(() => [route.path, route.query], apply, { immediate: true });\n });\n // `vitepress dev` swaps a page's content in place when it is edited.\n vitepress.onContentUpdated(apply);\n}\n\nexport { applyBlogView, applyBlogViews, type BlogViewState } from './view.js';\n","/**\n * Keyword filtering and pagination for blog pages, in the browser. The build\n * writes every post into the page (see `renderPostList`); this hides the ones\n * the URL's `?tag=` and `?page=` leave out, and fills in the filter banner and\n * the page links. Without it, every post simply shows.\n */\n\nconst ITEM = '[data-vp-blog-item]';\n\ninterface BlogLabels {\n tagged: string;\n showAll: string;\n noTagged: string;\n previous: string;\n next: string;\n pages: string;\n}\n\nfunction readLabels(blog: HTMLElement): BlogLabels {\n const { dataset } = blog;\n return {\n tagged: dataset.labelTagged ?? 'Posts tagged {tag}',\n showAll: dataset.labelShowAll ?? 'Show all posts',\n noTagged: dataset.labelNoTagged ?? 'No posts tagged {tag}.',\n previous: dataset.labelPrevious ?? 'Previous',\n next: dataset.labelNext ?? 'Next',\n pages: dataset.labelPages ?? 'Pages',\n };\n}\n\n/** The element a marker stands for: a card is its own marker; a compact list item holds one. */\nfunction itemElement(marker: Element): HTMLElement {\n return (marker.tagName === 'ARTICLE' ? marker : (marker.closest('li') ?? marker)) as HTMLElement;\n}\n\nfunction hasTag(marker: Element, tag: string): boolean {\n return !tag || (marker.getAttribute('data-tags') ?? '').split('|').includes(tag);\n}\n\n/** `url` with `tag` and `page` set (or removed when empty or page 1), as a path the router can follow. */\nfunction toHref(url: URL, tag: string, page: number): string {\n const next = new URL(url.href);\n next.hash = '';\n if (tag) next.searchParams.set('tag', tag);\n else next.searchParams.delete('tag');\n if (page > 1) next.searchParams.set('page', String(page));\n else next.searchParams.delete('page');\n return next.pathname + next.search;\n}\n\n/** `template` with `{tag}` replaced by a `<strong>` holding the tag, as text: the tag comes from the URL. */\nfunction fillWithTag(element: HTMLElement, template: string, tag: string): void {\n const [before, after = ''] = template.split('{tag}');\n const strong = document.createElement('strong');\n strong.textContent = tag;\n element.replaceChildren(document.createTextNode(before), ...(template.includes('{tag}') ? [strong] : []), after);\n}\n\nfunction link(text: string, href: string, className: string): HTMLAnchorElement {\n const anchor = document.createElement('a');\n anchor.className = className;\n anchor.href = href;\n anchor.textContent = text;\n return anchor;\n}\n\nfunction renderFilter(slot: HTMLElement, url: URL, tag: string, matches: number, labels: BlogLabels): void {\n slot.hidden = !tag;\n if (!tag) return slot.replaceChildren();\n const message = document.createElement('span');\n fillWithTag(message, matches ? labels.tagged : labels.noTagged, tag);\n slot.replaceChildren(message, ' ', link(labels.showAll, toHref(url, '', 1), 'vp-blog-filter__clear'));\n}\n\nfunction renderPagination(\n slot: HTMLElement,\n url: URL,\n tag: string,\n page: number,\n pageCount: number,\n labels: BlogLabels,\n): void {\n slot.hidden = pageCount < 2;\n slot.setAttribute('aria-label', labels.pages);\n if (pageCount < 2) return slot.replaceChildren();\n\n const parts: Node[] = [];\n if (page > 1) parts.push(link(labels.previous, toHref(url, tag, page - 1), 'vp-blog-pagination__previous'));\n for (let number = 1; number <= pageCount; number++) {\n if (number === page) {\n const current = document.createElement('span');\n current.className = 'vp-blog-pagination__page is-current';\n current.setAttribute('aria-current', 'page');\n current.textContent = String(number);\n parts.push(current);\n } else {\n parts.push(link(String(number), toHref(url, tag, number), 'vp-blog-pagination__page'));\n }\n }\n if (page < pageCount) parts.push(link(labels.next, toHref(url, tag, page + 1), 'vp-blog-pagination__next'));\n slot.replaceChildren(...parts);\n}\n\n/** Marks the keyword links (in the keyword list and on cards) that match the active keyword. */\nfunction markActiveTags(blog: HTMLElement, tag: string): void {\n for (const anchor of blog.querySelectorAll<HTMLAnchorElement>('.vp-blog-tags a, .vp-blog-card__tags a')) {\n let linkTag = '';\n try {\n linkTag = new URL(anchor.href, location.href).searchParams.get('tag') ?? '';\n } catch {\n // Leave a link that isn't a URL unmarked.\n }\n const active = !!tag && linkTag.toLowerCase() === tag;\n anchor.classList.toggle('is-active', active);\n if (active) anchor.setAttribute('aria-current', 'true');\n else anchor.removeAttribute('aria-current');\n }\n}\n\nexport interface BlogViewState {\n tag: string;\n page: number;\n pageCount: number;\n}\n\n/**\n * Shows the posts of one blog page that `url` asks for: those with its\n * `?tag=` keyword (any case), and of those, the ones on its `?page=`. Pinned\n * sticky posts only show on the first page and aren't counted in the page\n * size. Returns what is showing.\n */\nexport function applyBlogView(blog: HTMLElement, url: URL): BlogViewState {\n const labels = readLabels(blog);\n const tag = (url.searchParams.get('tag') ?? '').trim().toLowerCase();\n const pageSize = Math.max(0, Number(blog.dataset.pageSize) || 0);\n\n const markers = [...blog.querySelectorAll(ITEM)];\n const isSticky = (marker: Element) => !!marker.closest('[data-vp-blog-sticky]');\n const sticky = markers.filter((marker) => isSticky(marker) && hasTag(marker, tag));\n const rest = markers.filter((marker) => !isSticky(marker) && hasTag(marker, tag));\n\n const pageCount = pageSize ? Math.max(1, Math.ceil(rest.length / pageSize)) : 1;\n const requested = Number.parseInt(url.searchParams.get('page') ?? '1', 10);\n const page = Math.min(pageCount, Math.max(1, Number.isFinite(requested) ? requested : 1));\n\n const shown = new Set<Element>([\n ...(page === 1 ? sticky : []),\n ...(pageSize ? rest.slice((page - 1) * pageSize, page * pageSize) : rest),\n ]);\n for (const marker of markers) itemElement(marker).hidden = !shown.has(marker);\n\n // A sticky section or year with nothing showing is hidden, heading and all.\n for (const group of blog.querySelectorAll<HTMLElement>('[data-vp-blog-sticky], .vp-blog-year')) {\n group.hidden = ![...group.querySelectorAll(ITEM)].some((marker) => shown.has(marker));\n }\n\n const filter = blog.querySelector<HTMLElement>('[data-vp-blog-filter]');\n if (filter) renderFilter(filter, url, tag, sticky.length + rest.length, labels);\n const pagination = blog.querySelector<HTMLElement>('[data-vp-blog-pagination]');\n if (pagination) renderPagination(pagination, url, tag, page, pageCount, labels);\n markActiveTags(blog, tag);\n\n return { tag, page, pageCount };\n}\n\nconst lastViews = new WeakMap<HTMLElement, BlogViewState>();\n\n/**\n * Applies `url` to every blog page in `root`. When the page or keyword of a\n * blog already on screen changes, it is scrolled back to its top.\n */\nexport function applyBlogViews(root: ParentNode = document, url: URL = new URL(location.href)): void {\n for (const blog of root.querySelectorAll<HTMLElement>('[data-vp-blog]')) {\n const previous = lastViews.get(blog);\n const view = applyBlogView(blog, url);\n lastViews.set(blog, view);\n if (\n previous &&\n (previous.page !== view.page || previous.tag !== view.tag) &&\n blog.getBoundingClientRect().top < 0\n ) {\n blog.scrollIntoView({ block: 'start' });\n }\n }\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/client.ts","../src/view.ts","../src/banner.ts"],"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"]}
|
package/dist/index.cjs
CHANGED
|
@@ -63,6 +63,11 @@ function hasAnyTag(post, tags) {
|
|
|
63
63
|
const wanted = new Set(tags.map((tag) => tag.toLowerCase()));
|
|
64
64
|
return post.post.tags.some((tag) => wanted.has(tag.toLowerCase()));
|
|
65
65
|
}
|
|
66
|
+
function inAnyCategory(post, categories) {
|
|
67
|
+
if (categories.length === 0) return true;
|
|
68
|
+
const category = post.post.category.toLowerCase();
|
|
69
|
+
return !!category && categories.some((wanted) => wanted.toLowerCase() === category);
|
|
70
|
+
}
|
|
66
71
|
function blogCoversPost(index, post, sections) {
|
|
67
72
|
if (index.index.scope === "section") return isInFolders(post.relativePath, index.index.folders);
|
|
68
73
|
if (!isInFolders(post.relativePath, index.index.folders)) return false;
|
|
@@ -76,22 +81,22 @@ function compareDates(a, b) {
|
|
|
76
81
|
function compareByDate(a, b) {
|
|
77
82
|
return compareDates(a.post.date, b.post.date) || a.post.title.localeCompare(b.post.title);
|
|
78
83
|
}
|
|
79
|
-
function
|
|
80
|
-
const
|
|
81
|
-
const pinned = new Set(limit ?
|
|
84
|
+
function pinPosts(posts, limit) {
|
|
85
|
+
const candidates = posts.filter((post) => post.post.pinned);
|
|
86
|
+
const pinned = new Set(limit ? candidates.slice(0, limit) : candidates);
|
|
82
87
|
return { pinned: [...pinned], rest: posts.filter((post) => !pinned.has(post)) };
|
|
83
88
|
}
|
|
84
89
|
function listBlogPosts(pages, index, options) {
|
|
85
90
|
const sections = pages.filter(isIndex).filter((page) => page.index.scope === "section");
|
|
86
|
-
let posts = pages.filter(isPost).filter((post) => options.drafts || !post.post.draft).filter((post) => blogCoversPost(index, post, sections) && hasAnyTag(post, index.index.tags)).sort(compareByDate);
|
|
87
|
-
if (options.
|
|
88
|
-
const { pinned, rest } =
|
|
91
|
+
let posts = pages.filter(isPost).filter((post) => options.drafts || !post.post.draft).filter((post) => blogCoversPost(index, post, sections) && hasAnyTag(post, index.index.tags)).filter((post) => inAnyCategory(post, index.index.categories)).sort(compareByDate);
|
|
92
|
+
if (options.pinnedFirst) {
|
|
93
|
+
const { pinned, rest } = pinPosts(posts, index.index.pinLimit);
|
|
89
94
|
posts = [...pinned, ...rest];
|
|
90
95
|
}
|
|
91
96
|
return options.applyLimit && index.index.limit ? posts.slice(0, index.index.limit) : posts;
|
|
92
97
|
}
|
|
93
98
|
function findHomeBlog(pages, post, drafts) {
|
|
94
|
-
const listing = pages.filter(isIndex).filter((index) => listBlogPosts(pages, index, { drafts,
|
|
99
|
+
const listing = pages.filter(isIndex).filter((index) => listBlogPosts(pages, index, { drafts, pinnedFirst: false, applyLimit: false }).includes(post));
|
|
95
100
|
const sections = listing.filter((index) => index.index.scope === "section");
|
|
96
101
|
if (sections.length) return deepestFirst(sections, post.relativePath)[0];
|
|
97
102
|
return listing[0] ?? null;
|
|
@@ -207,6 +212,13 @@ function toDateTimeAttribute({ date, hasTime }, timeZone) {
|
|
|
207
212
|
function yearIn(date, timeZone) {
|
|
208
213
|
return Number(toZonedIsoDate(date, timeZone).slice(0, 4));
|
|
209
214
|
}
|
|
215
|
+
function monthIn(date, timeZone) {
|
|
216
|
+
return toZonedIsoDate(date, timeZone).slice(0, 7);
|
|
217
|
+
}
|
|
218
|
+
function formatMonth(month, locale, format) {
|
|
219
|
+
const [year, number] = month.split("-").map(Number);
|
|
220
|
+
return new Intl.DateTimeFormat(locale, { timeZone: "UTC", ...format }).format(Date.UTC(year, number - 1, 1, 12));
|
|
221
|
+
}
|
|
210
222
|
|
|
211
223
|
// src/feed.ts
|
|
212
224
|
var import_node_crypto = require("crypto");
|
|
@@ -258,13 +270,24 @@ var DEFAULT_LABELS = {
|
|
|
258
270
|
older: "Older post",
|
|
259
271
|
allPosts: "All posts in {blog}",
|
|
260
272
|
draft: "Draft",
|
|
261
|
-
|
|
273
|
+
pinned: "Pinned",
|
|
274
|
+
announcement: "Announcement",
|
|
275
|
+
announcements: "Announcements",
|
|
276
|
+
announcementCount: "{n} announcements",
|
|
277
|
+
dismiss: "Dismiss",
|
|
262
278
|
noPosts: "No posts yet.",
|
|
263
279
|
undated: "Undated",
|
|
280
|
+
browse: "Browse posts",
|
|
281
|
+
categories: "Categories",
|
|
264
282
|
tags: "Keywords",
|
|
265
|
-
|
|
283
|
+
archive: "Archive",
|
|
284
|
+
category: "Category",
|
|
285
|
+
tag: "Keyword",
|
|
286
|
+
postCount: "{n} posts",
|
|
287
|
+
postCountOne: "{n} post",
|
|
288
|
+
noMatches: "No posts match these filters.",
|
|
289
|
+
removeFilter: "Remove filter: {filter}",
|
|
266
290
|
showAll: "Show all posts",
|
|
267
|
-
noTagged: "No posts tagged {tag}.",
|
|
268
291
|
previous: "Previous",
|
|
269
292
|
next: "Next",
|
|
270
293
|
pages: "Pages",
|
|
@@ -326,25 +349,41 @@ function renderAuthors(authors, context) {
|
|
|
326
349
|
function renderBadge(kind, context) {
|
|
327
350
|
return `<span class="vp-blog-badge vp-blog-badge--${kind}">${escapeTemplateText(context.labels[kind])}</span>`;
|
|
328
351
|
}
|
|
329
|
-
function
|
|
330
|
-
const
|
|
352
|
+
function toFilterUrl(blogRoutePath, param, value) {
|
|
353
|
+
const encoded = encodeURIComponent(value.toLowerCase()).replace(/[()]/g, (c) => c === "(" ? "%28" : "%29");
|
|
331
354
|
const page = blogRoutePath.replace(/(^|\/)index\.md$/, "$1");
|
|
332
|
-
return `${toMarkdownUrl(`/${page}`)}
|
|
355
|
+
return `${toMarkdownUrl(`/${page}`)}?${param}=${encoded}`;
|
|
356
|
+
}
|
|
357
|
+
function renderFilterLink(text, param, blogRoutePath, className) {
|
|
358
|
+
return blogRoutePath ? `[${escapeMarkdown(text)}](${toFilterUrl(blogRoutePath, param, text)})` : `<span class="${className}">${escapeTemplateText(text)}</span>`;
|
|
333
359
|
}
|
|
334
360
|
function renderTags(tags, blogRoutePath) {
|
|
335
|
-
return tags.map(
|
|
336
|
-
(tag) => blogRoutePath ? `[${escapeMarkdown(tag)}](${toTagUrl(blogRoutePath, tag)})` : `<span class="vp-blog-tag">${escapeTemplateText(tag)}</span>`
|
|
337
|
-
).join(" ");
|
|
361
|
+
return tags.map((tag) => renderFilterLink(tag, "tag", blogRoutePath, "vp-blog-tag")).join(" ");
|
|
338
362
|
}
|
|
339
363
|
function toTagsAttribute(post) {
|
|
340
364
|
return escapeHtml(post.post.tags.map((tag) => tag.toLowerCase()).join("|"));
|
|
341
365
|
}
|
|
366
|
+
function toItemAttributes(post, context) {
|
|
367
|
+
const { category, date, announcement } = post.post;
|
|
368
|
+
const attributes = [`data-vp-blog-item data-tags="${toTagsAttribute(post)}"`];
|
|
369
|
+
if (category) attributes.push(`data-category="${escapeHtml(category.toLowerCase())}"`);
|
|
370
|
+
if (date) attributes.push(`data-month="${monthIn(date, context.dates.timeZone)}"`);
|
|
371
|
+
if (announcement) attributes.push("data-announcement");
|
|
372
|
+
return attributes.join(" ");
|
|
373
|
+
}
|
|
374
|
+
function renderBadges(post, context) {
|
|
375
|
+
const { draft, pinned, announcement } = post.post;
|
|
376
|
+
return [
|
|
377
|
+
draft && renderBadge("draft", context),
|
|
378
|
+
pinned && renderBadge("pinned", context),
|
|
379
|
+
announcement && renderBadge("announcement", context)
|
|
380
|
+
].filter(Boolean).join(" ");
|
|
381
|
+
}
|
|
342
382
|
function renderMeta(post, context, include) {
|
|
343
|
-
const { date, dateHasTime, updated, updatedHasTime, readingMinutes, authors
|
|
383
|
+
const { date, dateHasTime, updated, updatedHasTime, readingMinutes, authors } = post.post;
|
|
344
384
|
const parts = [];
|
|
345
385
|
if (include.badges) {
|
|
346
|
-
const
|
|
347
|
-
const shown = badges.filter(Boolean).join(" ");
|
|
386
|
+
const shown = renderBadges(post, context);
|
|
348
387
|
if (shown) parts.push(shown);
|
|
349
388
|
}
|
|
350
389
|
if (date) {
|
|
@@ -371,16 +410,18 @@ function renderCard(post, context, index) {
|
|
|
371
410
|
const link = `/${post.routePath}`;
|
|
372
411
|
const image = toImageSource(post, context.pageDirectory);
|
|
373
412
|
const modifiers = [
|
|
374
|
-
post.post.
|
|
413
|
+
post.post.pinned && "pinned",
|
|
375
414
|
post.post.draft && "draft",
|
|
415
|
+
post.post.announcement && "announcement",
|
|
376
416
|
image && "with-image",
|
|
377
417
|
image && post.post.imageFit === "contain" && "contain"
|
|
378
418
|
].filter(Boolean).map((modifier) => ` vp-blog-card--${modifier}`).join("");
|
|
379
419
|
const alt = escapeMarkdown(post.post.imageAlt);
|
|
380
420
|
return [
|
|
381
|
-
`<article class="vp-blog-card${modifiers}"${imageStyle(post)}
|
|
421
|
+
`<article class="vp-blog-card${modifiers}"${imageStyle(post)} ${toItemAttributes(post, context)}>`,
|
|
382
422
|
...image ? block("vp-blog-card__image", `[})](${toMarkdownUrl(link)})`) : [],
|
|
383
423
|
'<div class="vp-blog-card__body">',
|
|
424
|
+
...index.index.showCategory && post.post.category ? block("vp-blog-card__category", renderFilterLink(post.post.category, "category", index.routePath, "")) : [],
|
|
384
425
|
...block("vp-blog-card__title", toPageLink(post.post.title, post.routePath)),
|
|
385
426
|
...block("vp-blog-card__meta", renderMeta(post, context, { authors: true, updated: false, badges: true })),
|
|
386
427
|
...block("vp-blog-card__summary", escapeMarkdown(post.post.summary)),
|
|
@@ -391,8 +432,8 @@ function renderCard(post, context, index) {
|
|
|
391
432
|
}
|
|
392
433
|
function renderCompactItem(post, context) {
|
|
393
434
|
const date = post.post.date ? `${renderTime(post.post.date, post.post.dateHasTime, context)} ` : "";
|
|
394
|
-
const badges =
|
|
395
|
-
const marker = `<span hidden
|
|
435
|
+
const badges = renderBadges(post, context);
|
|
436
|
+
const marker = `<span hidden ${toItemAttributes(post, context)}></span>`;
|
|
396
437
|
return `- ${marker}${date}${toPageLink(post.post.title, post.routePath)}${badges ? ` ${badges}` : ""}`;
|
|
397
438
|
}
|
|
398
439
|
function renderGroup(posts, index, context) {
|
|
@@ -412,18 +453,23 @@ function renderGroup(posts, index, context) {
|
|
|
412
453
|
"</div>"
|
|
413
454
|
];
|
|
414
455
|
}
|
|
415
|
-
function
|
|
416
|
-
const
|
|
456
|
+
function renderGroups(posts, index, context) {
|
|
457
|
+
const { groupBy } = index.index;
|
|
458
|
+
if (!groupBy) return renderGroup(posts, index, context);
|
|
459
|
+
const { timeZone, locale } = context.dates;
|
|
460
|
+
const groups = /* @__PURE__ */ new Map();
|
|
417
461
|
for (const post of posts) {
|
|
418
|
-
const
|
|
419
|
-
|
|
462
|
+
const { date } = post.post;
|
|
463
|
+
const key = !date ? "" : groupBy === "year" ? String(yearIn(date, timeZone)) : monthIn(date, timeZone);
|
|
464
|
+
groups.set(key, [...groups.get(key) ?? [], post]);
|
|
420
465
|
}
|
|
421
466
|
const lines = [];
|
|
422
|
-
for (const [
|
|
467
|
+
for (const [key, group] of groups) {
|
|
468
|
+
const heading = !key ? context.labels.undated : groupBy === "year" ? key : formatMonth(key, locale, { month: "long", year: "numeric" });
|
|
423
469
|
lines.push(
|
|
424
|
-
|
|
470
|
+
`<section class="vp-blog-group vp-blog-${groupBy}">`,
|
|
425
471
|
"",
|
|
426
|
-
`## ${escapeMarkdown(
|
|
472
|
+
`## ${escapeMarkdown(heading)}`,
|
|
427
473
|
"",
|
|
428
474
|
...renderGroup(group, index, context),
|
|
429
475
|
"",
|
|
@@ -432,42 +478,114 @@ function renderYears(posts, index, context) {
|
|
|
432
478
|
}
|
|
433
479
|
return lines;
|
|
434
480
|
}
|
|
435
|
-
function
|
|
436
|
-
const
|
|
481
|
+
function collectValues(posts, pick) {
|
|
482
|
+
const values = /* @__PURE__ */ new Map();
|
|
437
483
|
for (const post of posts) {
|
|
438
|
-
for (const
|
|
439
|
-
const entry =
|
|
484
|
+
for (const value of new Set(pick(post))) {
|
|
485
|
+
const entry = values.get(value.toLowerCase()) ?? { value, count: 0 };
|
|
440
486
|
entry.count++;
|
|
441
|
-
|
|
487
|
+
values.set(value.toLowerCase(), entry);
|
|
442
488
|
}
|
|
443
489
|
}
|
|
444
|
-
return [...
|
|
490
|
+
return [...values.values()].sort((a, b) => a.value.localeCompare(b.value, void 0, { sensitivity: "base" }));
|
|
445
491
|
}
|
|
446
|
-
function
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
492
|
+
function collectTags(posts) {
|
|
493
|
+
return collectValues(posts, (post) => post.post.tags).map(({ value, count }) => ({ tag: value, count }));
|
|
494
|
+
}
|
|
495
|
+
function collectCategories(posts) {
|
|
496
|
+
return collectValues(posts, (post) => post.post.category ? [post.post.category] : []);
|
|
497
|
+
}
|
|
498
|
+
function collectArchive(posts, timeZone) {
|
|
499
|
+
const years = /* @__PURE__ */ new Map();
|
|
500
|
+
for (const post of posts) {
|
|
501
|
+
if (!post.post.date) continue;
|
|
502
|
+
const month = monthIn(post.post.date, timeZone);
|
|
503
|
+
const year = years.get(month.slice(0, 4)) ?? { value: month.slice(0, 4), count: 0, months: [] };
|
|
504
|
+
year.count++;
|
|
505
|
+
const entry = year.months.find((candidate) => candidate.value === month);
|
|
506
|
+
if (entry) entry.count++;
|
|
507
|
+
else year.months.push({ value: month, count: 1 });
|
|
508
|
+
years.set(year.value, year);
|
|
509
|
+
}
|
|
510
|
+
const newestFirst = (a, b) => b.value.localeCompare(a.value);
|
|
511
|
+
return [...years.values()].sort(newestFirst).map((year) => ({ ...year, months: [...year.months].sort(newestFirst) }));
|
|
512
|
+
}
|
|
513
|
+
function renderChip(name, count, url, title = "") {
|
|
514
|
+
const tooltip = title ? ` "${title.replace(/"/g, """)}"` : "";
|
|
515
|
+
return `[<span class="vp-blog-facet__name">${escapeMarkdown(name)}</span> <span class="vp-blog-facet__count">${count}</span>](${url}${tooltip})`;
|
|
516
|
+
}
|
|
517
|
+
function renderFacet(facet, label, chips, ariaLabel = label, attributes = "") {
|
|
453
518
|
return [
|
|
454
|
-
`<
|
|
519
|
+
`<div class="vp-blog-facet vp-blog-facet--${facet}" role="group" aria-label="${escapeHtml(ariaLabel)}" data-vp-blog-facet="${facet}"${attributes}>`,
|
|
520
|
+
`<span class="vp-blog-facet__label" aria-hidden="true">${escapeTemplateText(label)}</span>`,
|
|
521
|
+
'<div class="vp-blog-facet__chips">',
|
|
455
522
|
"",
|
|
456
|
-
|
|
523
|
+
chips.join(" "),
|
|
457
524
|
"",
|
|
458
|
-
"</
|
|
525
|
+
"</div>",
|
|
526
|
+
"</div>"
|
|
459
527
|
];
|
|
460
528
|
}
|
|
529
|
+
function renderBrowse(posts, index, context) {
|
|
530
|
+
const { labels } = context;
|
|
531
|
+
const { locale, timeZone } = context.dates;
|
|
532
|
+
const blog = index.routePath;
|
|
533
|
+
const rows = [];
|
|
534
|
+
const categories = index.index.categoryList ? collectCategories(posts) : [];
|
|
535
|
+
if (categories.length) {
|
|
536
|
+
const chips = categories.map(({ value, count }) => renderChip(value, count, toFilterUrl(blog, "category", value)));
|
|
537
|
+
rows.push(...renderFacet("category", labels.categories, chips));
|
|
538
|
+
}
|
|
539
|
+
const tags = index.index.tagList ? collectTags(posts) : [];
|
|
540
|
+
if (tags.length) {
|
|
541
|
+
const chips = tags.map(({ tag, count }) => renderChip(tag, count, toFilterUrl(blog, "tag", tag)));
|
|
542
|
+
rows.push(...renderFacet("tag", labels.tags, chips));
|
|
543
|
+
}
|
|
544
|
+
const archive = index.index.archive ? collectArchive(posts, timeZone) : [];
|
|
545
|
+
if (archive.reduce((months, year) => months + year.months.length, 0) > 1) {
|
|
546
|
+
const years = archive.map(({ value, count }) => renderChip(value, count, toFilterUrl(blog, "year", value)));
|
|
547
|
+
rows.push(...renderFacet("year", labels.archive, years));
|
|
548
|
+
for (const year of archive) {
|
|
549
|
+
const months = year.months.map(
|
|
550
|
+
({ value, count }) => renderChip(
|
|
551
|
+
formatMonth(value, locale, { month: "short" }),
|
|
552
|
+
count,
|
|
553
|
+
toFilterUrl(blog, "month", value),
|
|
554
|
+
formatMonth(value, locale, { month: "long", year: "numeric" })
|
|
555
|
+
)
|
|
556
|
+
);
|
|
557
|
+
const shown = archive.length === 1 ? "" : " hidden";
|
|
558
|
+
rows.push(
|
|
559
|
+
...renderFacet(
|
|
560
|
+
"month",
|
|
561
|
+
year.value,
|
|
562
|
+
months,
|
|
563
|
+
`${labels.archive} ${year.value}`,
|
|
564
|
+
` data-year="${year.value}"${shown}`
|
|
565
|
+
)
|
|
566
|
+
);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
if (rows.length === 0) return [];
|
|
570
|
+
return [`<nav class="vp-blog-browse" aria-label="${escapeHtml(labels.browse)}">`, ...rows, "</nav>"];
|
|
571
|
+
}
|
|
461
572
|
function renderClientAttributes(index, context) {
|
|
462
573
|
const { labels } = context;
|
|
463
574
|
const attributes = {
|
|
464
575
|
"data-page-size": String(index.index.pageSize),
|
|
465
|
-
"
|
|
576
|
+
// For writing a `?month=` filter's name: "October 2026".
|
|
577
|
+
"data-locale": context.locale,
|
|
466
578
|
"data-label-show-all": labels.showAll,
|
|
467
|
-
"data-label-no-tagged": labels.noTagged,
|
|
468
579
|
"data-label-previous": labels.previous,
|
|
469
580
|
"data-label-next": labels.next,
|
|
470
|
-
"data-label-pages": labels.pages
|
|
581
|
+
"data-label-pages": labels.pages,
|
|
582
|
+
"data-label-announcements": labels.announcements,
|
|
583
|
+
"data-label-category": labels.category,
|
|
584
|
+
"data-label-tag": labels.tag,
|
|
585
|
+
"data-label-post-count": labels.postCount,
|
|
586
|
+
"data-label-post-count-one": labels.postCountOne,
|
|
587
|
+
"data-label-no-matches": labels.noMatches,
|
|
588
|
+
"data-label-remove-filter": labels.removeFilter
|
|
471
589
|
};
|
|
472
590
|
return Object.entries(attributes).map(([name, value]) => `${name}="${escapeHtml(value)}"`).join(" ");
|
|
473
591
|
}
|
|
@@ -478,17 +596,16 @@ function renderPostList(pinned, rest, index, context) {
|
|
|
478
596
|
const lines = [
|
|
479
597
|
`<div class="vp-blog" data-vp-blog ${renderClientAttributes(index, context)}>`,
|
|
480
598
|
...block("vp-blog-subscribe", renderRssLink(index, context)),
|
|
481
|
-
...
|
|
482
|
-
'<div class="vp-blog-filter" data-vp-blog-filter hidden></div>'
|
|
599
|
+
...renderBrowse(all, index, context),
|
|
600
|
+
'<div class="vp-blog-filter" data-vp-blog-filter role="status" hidden></div>'
|
|
483
601
|
];
|
|
484
602
|
if (pinned.length)
|
|
485
603
|
lines.push(
|
|
486
|
-
'<section class="vp-blog-
|
|
604
|
+
'<section class="vp-blog-pinned" data-vp-blog-pinned>',
|
|
487
605
|
...renderGroup(pinned, index, context),
|
|
488
606
|
"</section>"
|
|
489
607
|
);
|
|
490
|
-
if (rest.length)
|
|
491
|
-
lines.push(...index.index.groupByYear ? renderYears(rest, index, context) : renderGroup(rest, index, context));
|
|
608
|
+
if (rest.length) lines.push(...renderGroups(rest, index, context));
|
|
492
609
|
lines.push(`<nav class="vp-blog-pagination" data-vp-blog-pagination hidden></nav>`, "</div>");
|
|
493
610
|
return `${lines.join("\n")}
|
|
494
611
|
`;
|
|
@@ -497,8 +614,12 @@ function renderPostHeader(post, context, show, blogRoutePath = null) {
|
|
|
497
614
|
const lines = [];
|
|
498
615
|
if (show.byline) {
|
|
499
616
|
const meta = renderMeta(post, context, { authors: true, updated: true, badges: true });
|
|
500
|
-
const
|
|
501
|
-
|
|
617
|
+
const { category, tags } = post.post;
|
|
618
|
+
const chips = [
|
|
619
|
+
category && `<span class="vp-blog-byline__category">${renderFilterLink(category, "category", blogRoutePath, "vp-blog-category")}</span>`,
|
|
620
|
+
tags.length && `<span class="vp-blog-byline__tags">${renderTags(tags, blogRoutePath)}</span>`
|
|
621
|
+
].filter(Boolean).join(" ");
|
|
622
|
+
lines.push(...block("vp-blog-byline", [meta, chips].filter(Boolean).join("\n\n")));
|
|
502
623
|
}
|
|
503
624
|
if (show.cover && post.post.image) {
|
|
504
625
|
const image = /^[./@~]/.test(post.post.image) || isExternalUrl(post.post.image) ? post.post.image : `./${post.post.image}`;
|
|
@@ -638,6 +759,7 @@ function resolvePostData(data, content, fallbackTitle, options = {}) {
|
|
|
638
759
|
const timeZone = resolveTimeZone(options.timeZone);
|
|
639
760
|
const date = parsePostDate(pick("date"), timeZone);
|
|
640
761
|
const updated = parsePostDate(pick("updated"), timeZone);
|
|
762
|
+
const announcementText = typeof post.announcement === "string" ? clean(post.announcement.trim()) : "";
|
|
641
763
|
return {
|
|
642
764
|
title: clean(toText(post.title) || toText(data.title) || readHeading(content)) || fallbackTitle,
|
|
643
765
|
summary: clean(toText(post.summary) || toText(data.description) || readExcerpt(content)),
|
|
@@ -653,8 +775,11 @@ function resolvePostData(data, content, fallbackTitle, options = {}) {
|
|
|
653
775
|
tags: toStringList(pick("tags")),
|
|
654
776
|
category: toText(pick("category")),
|
|
655
777
|
scope: toScope(post.scope),
|
|
656
|
-
|
|
778
|
+
// `sticky` is this setting's old name.
|
|
779
|
+
pinned: (post.pinned ?? post.sticky) === true,
|
|
657
780
|
draft: post.draft === true,
|
|
781
|
+
announcement: post.announcement === true || !!announcementText,
|
|
782
|
+
announcementText,
|
|
658
783
|
byline: toBoolean(post.byline),
|
|
659
784
|
cover: toBoolean(post.cover),
|
|
660
785
|
postNav: toBoolean(post.postNav),
|
|
@@ -664,6 +789,9 @@ function resolvePostData(data, content, fallbackTitle, options = {}) {
|
|
|
664
789
|
function toLayout(value) {
|
|
665
790
|
return value === "grid" || value === "compact" ? value : "list";
|
|
666
791
|
}
|
|
792
|
+
function toGroupBy(value) {
|
|
793
|
+
return value === "year" || value === "month" ? value : "";
|
|
794
|
+
}
|
|
667
795
|
function defaultFeedPath(relativePath) {
|
|
668
796
|
const name = import_node_path3.default.posix.basename(relativePath, ".md");
|
|
669
797
|
return name === "index" ? "feed.xml" : `${name}-feed.xml`;
|
|
@@ -697,13 +825,17 @@ function resolveIndexConfig(data, relativePath, defaults = {}) {
|
|
|
697
825
|
scope,
|
|
698
826
|
folders: folders.length ? resolveFolders(folders, relativePath) : scope === "section" ? [ownFolder] : [],
|
|
699
827
|
tags: toStringList(options.tags),
|
|
828
|
+
categories: toStringList(options.categories),
|
|
700
829
|
limit: typeof options.limit === "number" && options.limit > 0 ? Math.floor(options.limit) : 0,
|
|
701
830
|
layout: toLayout(options.layout),
|
|
702
|
-
|
|
831
|
+
groupBy: toGroupBy(options.groupBy),
|
|
703
832
|
showTags: options.showTags !== false,
|
|
833
|
+
showCategory: options.showCategory !== false,
|
|
834
|
+
categoryList: options.categoryList !== false,
|
|
704
835
|
tagList: options.tagList !== false,
|
|
836
|
+
archive: options.archive !== false,
|
|
705
837
|
pageSize: toCount(options.pageSize, toCount(defaults.pageSize, 10)),
|
|
706
|
-
|
|
838
|
+
pinLimit: toCount(options.pinLimit ?? options.stickyLimit, toCount(defaults.pinLimit ?? defaults.stickyLimit, 0)),
|
|
707
839
|
rssLink: typeof options.rssLink === "boolean" ? options.rssLink : defaults.rssLink ?? true,
|
|
708
840
|
feed: toFeed(options.feed, relativePath)
|
|
709
841
|
};
|
|
@@ -770,7 +902,7 @@ function toOptionsKey(options) {
|
|
|
770
902
|
options.wordsPerMinute,
|
|
771
903
|
options.stripTags,
|
|
772
904
|
options.pageSize,
|
|
773
|
-
options.stickyLimit,
|
|
905
|
+
options.pinLimit ?? options.stickyLimit,
|
|
774
906
|
options.rssLink,
|
|
775
907
|
options.timeZone
|
|
776
908
|
]);
|
|
@@ -850,10 +982,13 @@ function resolveWidgetConfig(body) {
|
|
|
850
982
|
config: {
|
|
851
983
|
blog: typeof data.blog === "string" ? data.blog.trim() : "",
|
|
852
984
|
limit: toCount2(data.limit) ?? DEFAULT_LIMIT,
|
|
853
|
-
stickyLimit
|
|
985
|
+
// `stickyLimit` is this setting's old name.
|
|
986
|
+
pinLimit: toCount2(data.pinLimit ?? data.stickyLimit),
|
|
854
987
|
layout,
|
|
855
988
|
showTags: typeof data.showTags === "boolean" ? data.showTags : void 0,
|
|
989
|
+
showCategory: typeof data.showCategory === "boolean" ? data.showCategory : void 0,
|
|
856
990
|
tags: toStringList(data.tags),
|
|
991
|
+
categories: toStringList(data.categories),
|
|
857
992
|
more: data.more === false ? null : typeof data.more === "string" && data.more.trim() ? data.more.trim() : "",
|
|
858
993
|
rssLink: typeof data.rssLink === "boolean" ? data.rssLink : void 0
|
|
859
994
|
},
|
|
@@ -869,19 +1004,19 @@ function renderWidget(body, environment) {
|
|
|
869
1004
|
warn(config.blog ? `no blog page found at "${config.blog}"` : 'no blog covers this page: name one with "blog:"');
|
|
870
1005
|
return "";
|
|
871
1006
|
}
|
|
872
|
-
const
|
|
873
|
-
const
|
|
874
|
-
|
|
875
|
-
(post) => wanted.size === 0 || post.post.tags.some((tag) => wanted.has(tag.toLowerCase()))
|
|
1007
|
+
const pinLimit = config.pinLimit ?? blog.index.pinLimit;
|
|
1008
|
+
const posts = listBlogPosts(pages, blog, { drafts, pinnedFirst: false, applyLimit: false }).filter(
|
|
1009
|
+
(post) => hasAnyTag(post, config.tags) && inAnyCategory(post, config.categories)
|
|
876
1010
|
);
|
|
877
|
-
const { pinned, rest } =
|
|
1011
|
+
const { pinned, rest } = pinPosts(posts, pinLimit);
|
|
878
1012
|
const shown = [...pinned, ...rest].slice(0, config.limit || void 0);
|
|
879
1013
|
const view = {
|
|
880
1014
|
...blog,
|
|
881
1015
|
index: {
|
|
882
1016
|
...blog.index,
|
|
883
1017
|
layout: config.layout ?? blog.index.layout,
|
|
884
|
-
showTags: config.showTags ?? blog.index.showTags
|
|
1018
|
+
showTags: config.showTags ?? blog.index.showTags,
|
|
1019
|
+
showCategory: config.showCategory ?? blog.index.showCategory
|
|
885
1020
|
}
|
|
886
1021
|
};
|
|
887
1022
|
const lines = [`<div class="vp-blog-widget">`];
|
|
@@ -954,8 +1089,8 @@ function hasFeeds(options) {
|
|
|
954
1089
|
}
|
|
955
1090
|
function withPostList(source, page, pages, options) {
|
|
956
1091
|
if (!isIndex(page)) return source;
|
|
957
|
-
const posts = listBlogPosts(pages, page, { drafts: options.drafts ?? false,
|
|
958
|
-
const { pinned, rest } =
|
|
1092
|
+
const posts = listBlogPosts(pages, page, { drafts: options.drafts ?? false, pinnedFirst: true, applyLimit: true });
|
|
1093
|
+
const { pinned, rest } = pinPosts(posts, page.index.pinLimit);
|
|
959
1094
|
const context = createRenderContext(options, import_node_path6.default.dirname(page.absolutePath), hasFeeds(options));
|
|
960
1095
|
const generated = renderPostList(pinned, rest, page, context);
|
|
961
1096
|
return source.includes(BLOG_POSTS_PLACEHOLDER) ? source.replace(BLOG_POSTS_PLACEHOLDER, () => `
|
|
@@ -978,7 +1113,7 @@ function withPostDecorations(source, page, pages, options) {
|
|
|
978
1113
|
let result = header ? insertAfterTitle(source, header) : source;
|
|
979
1114
|
if (page.post.postNav ?? options.postNav ?? true) {
|
|
980
1115
|
if (blog) {
|
|
981
|
-
const posts = listBlogPosts(pages, blog, { drafts,
|
|
1116
|
+
const posts = listBlogPosts(pages, blog, { drafts, pinnedFirst: false, applyLimit: false });
|
|
982
1117
|
result = `${result.replace(/\s*$/, "")}
|
|
983
1118
|
|
|
984
1119
|
${renderPostNav(page, posts, blog, context)}
|