@binarynoir/vitepress-blog 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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 pinStickyPosts(posts, limit) {
80
- const sticky = posts.filter((post) => post.post.sticky);
81
- const pinned = new Set(limit ? sticky.slice(0, limit) : sticky);
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.stickyFirst) {
88
- const { pinned, rest } = pinStickyPosts(posts, index.index.stickyLimit);
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, stickyFirst: false, applyLimit: false }).includes(post));
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,18 +270,28 @@ var DEFAULT_LABELS = {
258
270
  older: "Older post",
259
271
  allPosts: "All posts in {blog}",
260
272
  draft: "Draft",
261
- sticky: "Pinned",
273
+ pinned: "Pinned",
262
274
  announcement: "Announcement",
263
275
  announcements: "Announcements",
264
- noAnnouncements: "No announcements.",
265
276
  announcementCount: "{n} announcements",
266
277
  dismiss: "Dismiss",
267
278
  noPosts: "No posts yet.",
268
279
  undated: "Undated",
280
+ browse: "Browse posts",
281
+ categories: "Categories",
269
282
  tags: "Keywords",
270
- tagged: "Posts tagged {tag}",
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}",
271
290
  showAll: "Show all posts",
272
- noTagged: "No posts tagged {tag}.",
291
+ more: "+{n} more",
292
+ less: "Show fewer",
293
+ filter: "Filter {row}",
294
+ noFilterMatches: "Nothing matches.",
273
295
  previous: "Previous",
274
296
  next: "Next",
275
297
  pages: "Pages",
@@ -301,7 +323,8 @@ function renderRssLink(index, context, show = index.index.rssLink) {
301
323
  if (!show || !context.feeds || !index.index.feed) return "";
302
324
  const title = fill(context.labels.rssFeed, { blog: index.title }).replace(/"/g, """);
303
325
  const url = toMarkdownUrl(`/${toFeedPath(index)}`);
304
- return `<span class="vp-blog-rss">[${RSS_ICON} ${escapeMarkdown(context.labels.subscribe)}](${url} "${title}")</span>`;
326
+ const text = `<span class="vp-blog-rss__text">${escapeMarkdown(context.labels.subscribe)}</span>`;
327
+ return `<span class="vp-blog-rss">[${RSS_ICON} ${text}](${url} "${title}")</span>`;
305
328
  }
306
329
  function fill(template, values) {
307
330
  return template.replace(/\{(\w+)\}/g, (match, key) => values[key] ?? match);
@@ -331,27 +354,33 @@ function renderAuthors(authors, context) {
331
354
  function renderBadge(kind, context) {
332
355
  return `<span class="vp-blog-badge vp-blog-badge--${kind}">${escapeTemplateText(context.labels[kind])}</span>`;
333
356
  }
334
- function toTagUrl(blogRoutePath, tag) {
335
- const value = encodeURIComponent(tag.toLowerCase()).replace(/[()]/g, (c) => c === "(" ? "%28" : "%29");
357
+ function toFilterUrl(blogRoutePath, param, value) {
358
+ const encoded = encodeURIComponent(value.toLowerCase()).replace(/[()]/g, (c) => c === "(" ? "%28" : "%29");
336
359
  const page = blogRoutePath.replace(/(^|\/)index\.md$/, "$1");
337
- return `${toMarkdownUrl(`/${page}`)}?tag=${value}`;
360
+ return `${toMarkdownUrl(`/${page}`)}?${param}=${encoded}`;
361
+ }
362
+ function renderFilterLink(text, param, blogRoutePath, className) {
363
+ return blogRoutePath ? `[${escapeMarkdown(text)}](${toFilterUrl(blogRoutePath, param, text)})` : `<span class="${className}">${escapeTemplateText(text)}</span>`;
338
364
  }
339
365
  function renderTags(tags, blogRoutePath) {
340
- return tags.map(
341
- (tag) => blogRoutePath ? `[${escapeMarkdown(tag)}](${toTagUrl(blogRoutePath, tag)})` : `<span class="vp-blog-tag">${escapeTemplateText(tag)}</span>`
342
- ).join(" ");
366
+ return tags.map((tag) => renderFilterLink(tag, "tag", blogRoutePath, "vp-blog-tag")).join(" ");
343
367
  }
344
368
  function toTagsAttribute(post) {
345
369
  return escapeHtml(post.post.tags.map((tag) => tag.toLowerCase()).join("|"));
346
370
  }
347
- function toItemAttributes(post) {
348
- return `data-vp-blog-item data-tags="${toTagsAttribute(post)}"${post.post.announcement ? " data-announcement" : ""}`;
371
+ function toItemAttributes(post, context) {
372
+ const { category, date, announcement } = post.post;
373
+ const attributes = [`data-vp-blog-item data-tags="${toTagsAttribute(post)}"`];
374
+ if (category) attributes.push(`data-category="${escapeHtml(category.toLowerCase())}"`);
375
+ if (date) attributes.push(`data-month="${monthIn(date, context.dates.timeZone)}"`);
376
+ if (announcement) attributes.push("data-announcement");
377
+ return attributes.join(" ");
349
378
  }
350
379
  function renderBadges(post, context) {
351
- const { draft, sticky, announcement } = post.post;
380
+ const { draft, pinned, announcement } = post.post;
352
381
  return [
353
382
  draft && renderBadge("draft", context),
354
- sticky && renderBadge("sticky", context),
383
+ pinned && renderBadge("pinned", context),
355
384
  announcement && renderBadge("announcement", context)
356
385
  ].filter(Boolean).join(" ");
357
386
  }
@@ -386,7 +415,7 @@ function renderCard(post, context, index) {
386
415
  const link = `/${post.routePath}`;
387
416
  const image = toImageSource(post, context.pageDirectory);
388
417
  const modifiers = [
389
- post.post.sticky && "sticky",
418
+ post.post.pinned && "pinned",
390
419
  post.post.draft && "draft",
391
420
  post.post.announcement && "announcement",
392
421
  image && "with-image",
@@ -394,9 +423,10 @@ function renderCard(post, context, index) {
394
423
  ].filter(Boolean).map((modifier) => ` vp-blog-card--${modifier}`).join("");
395
424
  const alt = escapeMarkdown(post.post.imageAlt);
396
425
  return [
397
- `<article class="vp-blog-card${modifiers}"${imageStyle(post)} ${toItemAttributes(post)}>`,
426
+ `<article class="vp-blog-card${modifiers}"${imageStyle(post)} ${toItemAttributes(post, context)}>`,
398
427
  ...image ? block("vp-blog-card__image", `[![${alt}](${toMarkdownUrl(image)})](${toMarkdownUrl(link)})`) : [],
399
428
  '<div class="vp-blog-card__body">',
429
+ ...index.index.showCategory && post.post.category ? block("vp-blog-card__category", renderFilterLink(post.post.category, "category", index.routePath, "")) : [],
400
430
  ...block("vp-blog-card__title", toPageLink(post.post.title, post.routePath)),
401
431
  ...block("vp-blog-card__meta", renderMeta(post, context, { authors: true, updated: false, badges: true })),
402
432
  ...block("vp-blog-card__summary", escapeMarkdown(post.post.summary)),
@@ -408,7 +438,7 @@ function renderCard(post, context, index) {
408
438
  function renderCompactItem(post, context) {
409
439
  const date = post.post.date ? `${renderTime(post.post.date, post.post.dateHasTime, context)} ` : "";
410
440
  const badges = renderBadges(post, context);
411
- const marker = `<span hidden ${toItemAttributes(post)}></span>`;
441
+ const marker = `<span hidden ${toItemAttributes(post, context)}></span>`;
412
442
  return `- ${marker}${date}${toPageLink(post.post.title, post.routePath)}${badges ? ` ${badges}` : ""}`;
413
443
  }
414
444
  function renderGroup(posts, index, context) {
@@ -428,18 +458,23 @@ function renderGroup(posts, index, context) {
428
458
  "</div>"
429
459
  ];
430
460
  }
431
- function renderYears(posts, index, context) {
432
- const years = /* @__PURE__ */ new Map();
461
+ function renderGroups(posts, index, context) {
462
+ const { groupBy } = index.index;
463
+ if (!groupBy) return renderGroup(posts, index, context);
464
+ const { timeZone, locale } = context.dates;
465
+ const groups = /* @__PURE__ */ new Map();
433
466
  for (const post of posts) {
434
- const year = post.post.date ? String(yearIn(post.post.date, context.dates.timeZone)) : context.labels.undated;
435
- years.set(year, [...years.get(year) ?? [], post]);
467
+ const { date } = post.post;
468
+ const key = !date ? "" : groupBy === "year" ? String(yearIn(date, timeZone)) : monthIn(date, timeZone);
469
+ groups.set(key, [...groups.get(key) ?? [], post]);
436
470
  }
437
471
  const lines = [];
438
- for (const [year, group] of years) {
472
+ for (const [key, group] of groups) {
473
+ const heading = !key ? context.labels.undated : groupBy === "year" ? key : formatMonth(key, locale, { month: "long", year: "numeric" });
439
474
  lines.push(
440
- '<section class="vp-blog-year">',
475
+ `<section class="vp-blog-group vp-blog-${groupBy}">`,
441
476
  "",
442
- `## ${escapeMarkdown(year)}`,
477
+ `## ${escapeMarkdown(heading)}`,
443
478
  "",
444
479
  ...renderGroup(group, index, context),
445
480
  "",
@@ -448,29 +483,107 @@ function renderYears(posts, index, context) {
448
483
  }
449
484
  return lines;
450
485
  }
451
- function collectTags(posts) {
452
- const tags = /* @__PURE__ */ new Map();
486
+ function collectValues(posts, pick) {
487
+ const values = /* @__PURE__ */ new Map();
453
488
  for (const post of posts) {
454
- for (const tag of new Set(post.post.tags)) {
455
- const entry = tags.get(tag.toLowerCase()) ?? { tag, count: 0 };
489
+ for (const value of new Set(pick(post))) {
490
+ const entry = values.get(value.toLowerCase()) ?? { value, count: 0 };
456
491
  entry.count++;
457
- tags.set(tag.toLowerCase(), entry);
492
+ values.set(value.toLowerCase(), entry);
458
493
  }
459
494
  }
460
- return [...tags.values()].sort((a, b) => a.tag.localeCompare(b.tag, void 0, { sensitivity: "base" }));
495
+ return [...values.values()].sort((a, b) => a.value.localeCompare(b.value, void 0, { sensitivity: "base" }));
461
496
  }
462
- function renderTagList(posts, index, context) {
463
- const tags = collectTags(posts);
464
- if (!index.index.tagList || tags.length === 0) return [];
465
- const links = tags.map(
466
- ({ tag, count }) => `[${escapeMarkdown(tag)} <span class="vp-blog-tags__count">${count}</span>](${toTagUrl(index.routePath, tag)})`
467
- );
468
- const label = `<span class="vp-blog-tags__label">${escapeTemplateText(context.labels.tags)}</span>`;
497
+ function collectTags(posts) {
498
+ return collectValues(posts, (post) => post.post.tags).map(({ value, count }) => ({ tag: value, count }));
499
+ }
500
+ function collectCategories(posts) {
501
+ return collectValues(posts, (post) => post.post.category ? [post.post.category] : []);
502
+ }
503
+ function collectArchive(posts, timeZone) {
504
+ const years = /* @__PURE__ */ new Map();
505
+ for (const post of posts) {
506
+ if (!post.post.date) continue;
507
+ const month = monthIn(post.post.date, timeZone);
508
+ const year = years.get(month.slice(0, 4)) ?? { value: month.slice(0, 4), count: 0, months: [] };
509
+ year.count++;
510
+ const entry = year.months.find((candidate) => candidate.value === month);
511
+ if (entry) entry.count++;
512
+ else year.months.push({ value: month, count: 1 });
513
+ years.set(year.value, year);
514
+ }
515
+ const newestFirst = (a, b) => b.value.localeCompare(a.value);
516
+ return [...years.values()].sort(newestFirst).map((year) => ({ ...year, months: [...year.months].sort(newestFirst) }));
517
+ }
518
+ function renderChip(name, count, url, title = "") {
519
+ const tooltip = title ? ` "${title.replace(/"/g, "&quot;")}"` : "";
520
+ return `[<span class="vp-blog-facet__name">${escapeMarkdown(name)}</span> <span class="vp-blog-facet__count">${count}</span>](${url}${tooltip})`;
521
+ }
522
+ function renderFacet(row) {
523
+ const { facet, label, chips, ariaLabel = label, limit = 0, attributes = "", aside = "" } = row;
524
+ const limited = limit && chips.length > limit ? ` data-limit="${limit}"` : "";
469
525
  return [
470
- `<nav class="vp-blog-tags" aria-label="${escapeHtml(context.labels.tags)}">`,
526
+ `<div class="vp-blog-facet vp-blog-facet--${facet}" role="group" aria-label="${escapeHtml(ariaLabel)}" data-vp-blog-facet="${facet}"${limited}${attributes}>`,
527
+ `<span class="vp-blog-facet__label" aria-hidden="true">${escapeTemplateText(label)}</span>`,
528
+ '<div class="vp-blog-facet__chips">',
471
529
  "",
472
- `${label} ${links.join(" ")}`,
530
+ chips.join(" "),
473
531
  "",
532
+ "</div>",
533
+ ...block("vp-blog-facet__aside", aside),
534
+ "</div>"
535
+ ];
536
+ }
537
+ function canNarrow(values, total) {
538
+ return values.length > 1 || values.length === 1 && values[0].count < total;
539
+ }
540
+ function byUse(a, b) {
541
+ return b.count - a.count || a.value.localeCompare(b.value, void 0, { sensitivity: "base" });
542
+ }
543
+ function renderBrowse(posts, index, context) {
544
+ const { labels } = context;
545
+ const { locale, timeZone } = context.dates;
546
+ const { categoryList, categoryLimit, tagList, tagLimit, tagMinCount, archive: showArchive } = index.index;
547
+ const blog = index.routePath;
548
+ const rows = [];
549
+ const categories = categoryList ? collectCategories(posts).sort(byUse) : [];
550
+ if (canNarrow(categories, posts.length)) {
551
+ const chips = categories.map(({ value, count }) => renderChip(value, count, toFilterUrl(blog, "category", value)));
552
+ rows.push({ facet: "category", label: labels.categories, chips, limit: categoryLimit });
553
+ }
554
+ const tags = tagList ? collectTags(posts).map(({ tag, count }) => ({ value: tag, count })).filter(({ count }) => count >= tagMinCount).sort(byUse) : [];
555
+ if (canNarrow(tags, posts.length)) {
556
+ const chips = tags.map(({ value, count }) => renderChip(value, count, toFilterUrl(blog, "tag", value)));
557
+ rows.push({ facet: "tag", label: labels.tags, chips, limit: tagLimit });
558
+ }
559
+ const archive = showArchive ? collectArchive(posts, timeZone) : [];
560
+ const monthChips = (months) => months.map(
561
+ ({ value, count }) => renderChip(
562
+ formatMonth(value, locale, { month: "short" }),
563
+ count,
564
+ toFilterUrl(blog, "month", value),
565
+ formatMonth(value, locale, { month: "long", year: "numeric" })
566
+ )
567
+ );
568
+ if (archive.length === 1 && canNarrow(archive[0].months, posts.length)) {
569
+ const [year] = archive;
570
+ const ariaLabel = `${labels.archive} ${year.value}`;
571
+ const attributes = ` data-year="${year.value}"`;
572
+ rows.push({ facet: "month", label: labels.archive, chips: monthChips(year.months), ariaLabel, attributes });
573
+ } else if (archive.length > 1) {
574
+ const years = archive.map(({ value, count }) => renderChip(value, count, toFilterUrl(blog, "year", value)));
575
+ rows.push({ facet: "year", label: labels.archive, chips: years });
576
+ for (const year of archive) {
577
+ const ariaLabel = `${labels.archive} ${year.value}`;
578
+ const attributes = ` data-year="${year.value}" hidden`;
579
+ rows.push({ facet: "month", label: year.value, chips: monthChips(year.months), ariaLabel, attributes });
580
+ }
581
+ }
582
+ if (rows.length === 0) return [];
583
+ rows[0].aside = renderRssLink(index, context);
584
+ return [
585
+ `<nav class="vp-blog-browse" aria-label="${escapeHtml(labels.browse)}">`,
586
+ ...rows.flatMap(renderFacet),
474
587
  "</nav>"
475
588
  ];
476
589
  }
@@ -478,14 +591,23 @@ function renderClientAttributes(index, context) {
478
591
  const { labels } = context;
479
592
  const attributes = {
480
593
  "data-page-size": String(index.index.pageSize),
481
- "data-label-tagged": labels.tagged,
594
+ // For writing a `?month=` filter's name: "October 2026".
595
+ "data-locale": context.locale,
482
596
  "data-label-show-all": labels.showAll,
483
- "data-label-no-tagged": labels.noTagged,
484
597
  "data-label-previous": labels.previous,
485
598
  "data-label-next": labels.next,
486
599
  "data-label-pages": labels.pages,
487
600
  "data-label-announcements": labels.announcements,
488
- "data-label-no-announcements": labels.noAnnouncements
601
+ "data-label-category": labels.category,
602
+ "data-label-tag": labels.tag,
603
+ "data-label-post-count": labels.postCount,
604
+ "data-label-post-count-one": labels.postCountOne,
605
+ "data-label-no-matches": labels.noMatches,
606
+ "data-label-remove-filter": labels.removeFilter,
607
+ "data-label-more": labels.more,
608
+ "data-label-less": labels.less,
609
+ "data-label-filter": labels.filter,
610
+ "data-label-no-filter-matches": labels.noFilterMatches
489
611
  };
490
612
  return Object.entries(attributes).map(([name, value]) => `${name}="${escapeHtml(value)}"`).join(" ");
491
613
  }
@@ -493,20 +615,19 @@ function renderPostList(pinned, rest, index, context) {
493
615
  const all = [...pinned, ...rest];
494
616
  if (all.length === 0) return `<p class="vp-blog-empty">${escapeTemplateText(context.labels.noPosts)}</p>
495
617
  `;
618
+ const browse = renderBrowse(all, index, context);
496
619
  const lines = [
497
620
  `<div class="vp-blog" data-vp-blog ${renderClientAttributes(index, context)}>`,
498
- ...block("vp-blog-subscribe", renderRssLink(index, context)),
499
- ...renderTagList(all, index, context),
500
- '<div class="vp-blog-filter" data-vp-blog-filter hidden></div>'
621
+ ...browse.length ? browse : block("vp-blog-subscribe", renderRssLink(index, context)),
622
+ '<div class="vp-blog-filter" data-vp-blog-filter role="status" hidden></div>'
501
623
  ];
502
624
  if (pinned.length)
503
625
  lines.push(
504
- '<section class="vp-blog-sticky" data-vp-blog-sticky>',
626
+ '<section class="vp-blog-pinned" data-vp-blog-pinned>',
505
627
  ...renderGroup(pinned, index, context),
506
628
  "</section>"
507
629
  );
508
- if (rest.length)
509
- lines.push(...index.index.groupByYear ? renderYears(rest, index, context) : renderGroup(rest, index, context));
630
+ if (rest.length) lines.push(...renderGroups(rest, index, context));
510
631
  lines.push(`<nav class="vp-blog-pagination" data-vp-blog-pagination hidden></nav>`, "</div>");
511
632
  return `${lines.join("\n")}
512
633
  `;
@@ -515,8 +636,12 @@ function renderPostHeader(post, context, show, blogRoutePath = null) {
515
636
  const lines = [];
516
637
  if (show.byline) {
517
638
  const meta = renderMeta(post, context, { authors: true, updated: true, badges: true });
518
- const tags = post.post.tags.length ? `<span class="vp-blog-byline__tags">${renderTags(post.post.tags, blogRoutePath)}</span>` : "";
519
- lines.push(...block("vp-blog-byline", [meta, tags].filter(Boolean).join("\n\n")));
639
+ const { category, tags } = post.post;
640
+ const chips = [
641
+ category && `<span class="vp-blog-byline__category">${renderFilterLink(category, "category", blogRoutePath, "vp-blog-category")}</span>`,
642
+ tags.length && `<span class="vp-blog-byline__tags">${renderTags(tags, blogRoutePath)}</span>`
643
+ ].filter(Boolean).join(" ");
644
+ lines.push(...block("vp-blog-byline", [meta, chips].filter(Boolean).join("\n\n")));
520
645
  }
521
646
  if (show.cover && post.post.image) {
522
647
  const image = /^[./@~]/.test(post.post.image) || isExternalUrl(post.post.image) ? post.post.image : `./${post.post.image}`;
@@ -559,6 +684,8 @@ var FENCED_CODE = /^[ \t]*(`{3,}|~{3,})[^\n]*\n[\s\S]*?(?:^[ \t]*\1[`~]*[ \t]*$|
559
684
  var HEADING_1 = /^ {0,3}#[ \t]+(.*?)(?:[ \t]+#+)?[ \t]*$/m;
560
685
  var TAG_SYNTAX = /\(\(<?tag([|/])[^|/)\r\n]*(?:\1[^|/)\r\n]*){0,2}\)\)/g;
561
686
  var MAX_SUMMARY_LENGTH = 200;
687
+ var DEFAULT_CATEGORY_LIMIT = 8;
688
+ var DEFAULT_TAG_LIMIT = 12;
562
689
  function readFrontmatter(raw) {
563
690
  const source = raw.replace(/^/, "");
564
691
  const match = FRONTMATTER.exec(source);
@@ -672,7 +799,8 @@ function resolvePostData(data, content, fallbackTitle, options = {}) {
672
799
  tags: toStringList(pick("tags")),
673
800
  category: toText(pick("category")),
674
801
  scope: toScope(post.scope),
675
- sticky: post.sticky === true,
802
+ // `sticky` is this setting's old name.
803
+ pinned: (post.pinned ?? post.sticky) === true,
676
804
  draft: post.draft === true,
677
805
  announcement: post.announcement === true || !!announcementText,
678
806
  announcementText,
@@ -685,6 +813,9 @@ function resolvePostData(data, content, fallbackTitle, options = {}) {
685
813
  function toLayout(value) {
686
814
  return value === "grid" || value === "compact" ? value : "list";
687
815
  }
816
+ function toGroupBy(value) {
817
+ return value === "year" || value === "month" ? value : "";
818
+ }
688
819
  function defaultFeedPath(relativePath) {
689
820
  const name = import_node_path3.default.posix.basename(relativePath, ".md");
690
821
  return name === "index" ? "feed.xml" : `${name}-feed.xml`;
@@ -718,13 +849,20 @@ function resolveIndexConfig(data, relativePath, defaults = {}) {
718
849
  scope,
719
850
  folders: folders.length ? resolveFolders(folders, relativePath) : scope === "section" ? [ownFolder] : [],
720
851
  tags: toStringList(options.tags),
852
+ categories: toStringList(options.categories),
721
853
  limit: typeof options.limit === "number" && options.limit > 0 ? Math.floor(options.limit) : 0,
722
854
  layout: toLayout(options.layout),
723
- groupByYear: options.groupBy === "year",
855
+ groupBy: toGroupBy(options.groupBy),
724
856
  showTags: options.showTags !== false,
857
+ showCategory: options.showCategory !== false,
858
+ categoryList: options.categoryList !== false,
859
+ categoryLimit: toCount(options.categoryLimit, DEFAULT_CATEGORY_LIMIT),
725
860
  tagList: options.tagList !== false,
861
+ tagLimit: toCount(options.tagLimit, DEFAULT_TAG_LIMIT),
862
+ tagMinCount: toCount(options.tagMinCount, 1),
863
+ archive: options.archive !== false,
726
864
  pageSize: toCount(options.pageSize, toCount(defaults.pageSize, 10)),
727
- stickyLimit: toCount(options.stickyLimit, toCount(defaults.stickyLimit, 0)),
865
+ pinLimit: toCount(options.pinLimit ?? options.stickyLimit, toCount(defaults.pinLimit ?? defaults.stickyLimit, 0)),
728
866
  rssLink: typeof options.rssLink === "boolean" ? options.rssLink : defaults.rssLink ?? true,
729
867
  feed: toFeed(options.feed, relativePath)
730
868
  };
@@ -791,7 +929,7 @@ function toOptionsKey(options) {
791
929
  options.wordsPerMinute,
792
930
  options.stripTags,
793
931
  options.pageSize,
794
- options.stickyLimit,
932
+ options.pinLimit ?? options.stickyLimit,
795
933
  options.rssLink,
796
934
  options.timeZone
797
935
  ]);
@@ -871,10 +1009,13 @@ function resolveWidgetConfig(body) {
871
1009
  config: {
872
1010
  blog: typeof data.blog === "string" ? data.blog.trim() : "",
873
1011
  limit: toCount2(data.limit) ?? DEFAULT_LIMIT,
874
- stickyLimit: toCount2(data.stickyLimit),
1012
+ // `stickyLimit` is this setting's old name.
1013
+ pinLimit: toCount2(data.pinLimit ?? data.stickyLimit),
875
1014
  layout,
876
1015
  showTags: typeof data.showTags === "boolean" ? data.showTags : void 0,
1016
+ showCategory: typeof data.showCategory === "boolean" ? data.showCategory : void 0,
877
1017
  tags: toStringList(data.tags),
1018
+ categories: toStringList(data.categories),
878
1019
  more: data.more === false ? null : typeof data.more === "string" && data.more.trim() ? data.more.trim() : "",
879
1020
  rssLink: typeof data.rssLink === "boolean" ? data.rssLink : void 0
880
1021
  },
@@ -890,19 +1031,19 @@ function renderWidget(body, environment) {
890
1031
  warn(config.blog ? `no blog page found at "${config.blog}"` : 'no blog covers this page: name one with "blog:"');
891
1032
  return "";
892
1033
  }
893
- const stickyLimit = config.stickyLimit ?? blog.index.stickyLimit;
894
- const wanted = new Set(config.tags.map((tag) => tag.toLowerCase()));
895
- const posts = listBlogPosts(pages, blog, { drafts, stickyFirst: false, applyLimit: false }).filter(
896
- (post) => wanted.size === 0 || post.post.tags.some((tag) => wanted.has(tag.toLowerCase()))
1034
+ const pinLimit = config.pinLimit ?? blog.index.pinLimit;
1035
+ const posts = listBlogPosts(pages, blog, { drafts, pinnedFirst: false, applyLimit: false }).filter(
1036
+ (post) => hasAnyTag(post, config.tags) && inAnyCategory(post, config.categories)
897
1037
  );
898
- const { pinned, rest } = pinStickyPosts(posts, stickyLimit);
1038
+ const { pinned, rest } = pinPosts(posts, pinLimit);
899
1039
  const shown = [...pinned, ...rest].slice(0, config.limit || void 0);
900
1040
  const view = {
901
1041
  ...blog,
902
1042
  index: {
903
1043
  ...blog.index,
904
1044
  layout: config.layout ?? blog.index.layout,
905
- showTags: config.showTags ?? blog.index.showTags
1045
+ showTags: config.showTags ?? blog.index.showTags,
1046
+ showCategory: config.showCategory ?? blog.index.showCategory
906
1047
  }
907
1048
  };
908
1049
  const lines = [`<div class="vp-blog-widget">`];
@@ -975,8 +1116,8 @@ function hasFeeds(options) {
975
1116
  }
976
1117
  function withPostList(source, page, pages, options) {
977
1118
  if (!isIndex(page)) return source;
978
- const posts = listBlogPosts(pages, page, { drafts: options.drafts ?? false, stickyFirst: true, applyLimit: true });
979
- const { pinned, rest } = pinStickyPosts(posts, page.index.stickyLimit);
1119
+ const posts = listBlogPosts(pages, page, { drafts: options.drafts ?? false, pinnedFirst: true, applyLimit: true });
1120
+ const { pinned, rest } = pinPosts(posts, page.index.pinLimit);
980
1121
  const context = createRenderContext(options, import_node_path6.default.dirname(page.absolutePath), hasFeeds(options));
981
1122
  const generated = renderPostList(pinned, rest, page, context);
982
1123
  return source.includes(BLOG_POSTS_PLACEHOLDER) ? source.replace(BLOG_POSTS_PLACEHOLDER, () => `
@@ -999,7 +1140,7 @@ function withPostDecorations(source, page, pages, options) {
999
1140
  let result = header ? insertAfterTitle(source, header) : source;
1000
1141
  if (page.post.postNav ?? options.postNav ?? true) {
1001
1142
  if (blog) {
1002
- const posts = listBlogPosts(pages, blog, { drafts, stickyFirst: false, applyLimit: false });
1143
+ const posts = listBlogPosts(pages, blog, { drafts, pinnedFirst: false, applyLimit: false });
1003
1144
  result = `${result.replace(/\s*$/, "")}
1004
1145
 
1005
1146
  ${renderPostNav(page, posts, blog, context)}