mirror of
https://github.com/quartz-community/folder-page.git
synced 2026-07-22 02:50:24 +00:00
feat: exclude unlisted pages from folder discovery and listings
Respect the file.data.unlisted convention in both the folder discovery loop (virtual folder index generation in pageType.generate) and the folder listing itself (pagesFromAllFiles and pagesFromTrie in FolderContent). Unlisted pages no longer contribute folders or subfolders, and no longer appear as entries inside any folder page listing. The foldersWithIndex detection loop also skips unlisted pages so a user-authored unlisted index file does not bypass the filter. Exports pagesFromAllFiles so its filter logic can be tested directly. Adds 5 unit tests covering unlisted exclusion, explicit unlisted: false passthrough, subfolder grouping, and folder-index self-exclusion.
This commit is contained in:
parent
8dcc63d340
commit
1c2dce604c
8 changed files with 86 additions and 10 deletions
|
|
@ -9,4 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
|
||||
### Added
|
||||
|
||||
- Respect the `file.data.unlisted` convention in both folder discovery (virtual page generation) and folder listings (the page-list inside a folder index). Unlisted pages are never shown in folder listings and never contribute folder/subfolder entries.
|
||||
- Exported `pagesFromAllFiles` helper for reuse and testing.
|
||||
- Unit tests for `pagesFromAllFiles` covering the unlisted-filter behavior.
|
||||
|
||||
- Initial Quartz community plugin template.
|
||||
|
|
|
|||
6
dist/components/index.js
vendored
6
dist/components/index.js
vendored
|
|
@ -2667,7 +2667,10 @@ function concatenateResources(...resources) {
|
|||
function pagesFromTrie(folder, showSubfolders) {
|
||||
return folder.children.map((node) => {
|
||||
const nodeData = node.data;
|
||||
if (nodeData) return nodeData;
|
||||
if (nodeData) {
|
||||
if (nodeData.unlisted === true) return void 0;
|
||||
return nodeData;
|
||||
}
|
||||
if (node.isFolder && showSubfolders) {
|
||||
return {
|
||||
slug: node.slug,
|
||||
|
|
@ -2683,6 +2686,7 @@ function pagesFromAllFiles(allFiles, folderSlug, showSubfolders) {
|
|||
const directChildren = [];
|
||||
const subfolderFiles = /* @__PURE__ */ new Map();
|
||||
for (const file of allFiles) {
|
||||
if (file.unlisted === true) continue;
|
||||
const fileSlug = file.slug;
|
||||
if (!fileSlug || !fileSlug.startsWith(folderPrefix)) continue;
|
||||
const relativePath = fileSlug.slice(folderPrefix.length);
|
||||
|
|
|
|||
2
dist/components/index.js.map
vendored
2
dist/components/index.js.map
vendored
File diff suppressed because one or more lines are too long
12
dist/index.js
vendored
12
dist/index.js
vendored
|
|
@ -2652,7 +2652,10 @@ function concatenateResources(...resources) {
|
|||
function pagesFromTrie(folder, showSubfolders) {
|
||||
return folder.children.map((node) => {
|
||||
const nodeData = node.data;
|
||||
if (nodeData) return nodeData;
|
||||
if (nodeData) {
|
||||
if (nodeData.unlisted === true) return void 0;
|
||||
return nodeData;
|
||||
}
|
||||
if (node.isFolder && showSubfolders) {
|
||||
return {
|
||||
slug: node.slug,
|
||||
|
|
@ -2668,6 +2671,7 @@ function pagesFromAllFiles(allFiles, folderSlug, showSubfolders) {
|
|||
const directChildren = [];
|
||||
const subfolderFiles = /* @__PURE__ */ new Map();
|
||||
for (const file of allFiles) {
|
||||
if (file.unlisted === true) continue;
|
||||
const fileSlug = file.slug;
|
||||
if (!fileSlug || !fileSlug.startsWith(folderPrefix)) continue;
|
||||
const relativePath = fileSlug.slice(folderPrefix.length);
|
||||
|
|
@ -2787,7 +2791,7 @@ var FolderPage = (opts) => {
|
|||
priority: 10,
|
||||
match: folderMatcher,
|
||||
generate({ content, cfg }) {
|
||||
const allFiles = content.map((c) => c[1].data);
|
||||
const allFiles = content.map((c) => c[1].data).filter((d) => d?.unlisted !== true);
|
||||
const locale = cfg?.locale ?? "en-US";
|
||||
const folders = /* @__PURE__ */ new Set();
|
||||
const folderDisplayNames = /* @__PURE__ */ new Map();
|
||||
|
|
@ -2813,7 +2817,9 @@ var FolderPage = (opts) => {
|
|||
}
|
||||
const foldersWithIndex = /* @__PURE__ */ new Set();
|
||||
for (const [, file] of content) {
|
||||
const slug2 = file.data?.slug;
|
||||
const data = file.data;
|
||||
if (data?.unlisted === true) continue;
|
||||
const slug2 = data?.slug;
|
||||
if (slug2 && slug2.endsWith("/index")) {
|
||||
const folder = slug2.slice(0, -"/index".length);
|
||||
foldersWithIndex.add(folder);
|
||||
|
|
|
|||
2
dist/index.js.map
vendored
2
dist/index.js.map
vendored
File diff suppressed because one or more lines are too long
|
|
@ -33,6 +33,7 @@ interface TrieNode {
|
|||
|
||||
type PageEntry = {
|
||||
slug?: string;
|
||||
unlisted?: boolean;
|
||||
dates?: { created: Date; modified: Date; published: Date };
|
||||
frontmatter?: { title?: string; tags?: string[] };
|
||||
};
|
||||
|
|
@ -48,7 +49,10 @@ function pagesFromTrie(folder: TrieNode, showSubfolders: boolean): PageEntry[] {
|
|||
return folder.children
|
||||
.map((node) => {
|
||||
const nodeData = node.data as PageEntry | null;
|
||||
if (nodeData) return nodeData;
|
||||
if (nodeData) {
|
||||
if (nodeData.unlisted === true) return undefined;
|
||||
return nodeData;
|
||||
}
|
||||
|
||||
if (node.isFolder && showSubfolders) {
|
||||
return {
|
||||
|
|
@ -62,7 +66,7 @@ function pagesFromTrie(folder: TrieNode, showSubfolders: boolean): PageEntry[] {
|
|||
.filter((page): page is PageEntry => page !== undefined);
|
||||
}
|
||||
|
||||
function pagesFromAllFiles(
|
||||
export function pagesFromAllFiles(
|
||||
allFiles: unknown[],
|
||||
folderSlug: string,
|
||||
showSubfolders: boolean,
|
||||
|
|
@ -77,6 +81,7 @@ function pagesFromAllFiles(
|
|||
const subfolderFiles = new Map<string, PageEntry[]>();
|
||||
|
||||
for (const file of allFiles as PageEntry[]) {
|
||||
if (file.unlisted === true) continue;
|
||||
const fileSlug = file.slug;
|
||||
if (!fileSlug || !fileSlug.startsWith(folderPrefix)) continue;
|
||||
|
||||
|
|
|
|||
|
|
@ -41,7 +41,9 @@ export const FolderPage: QuartzPageTypePlugin<FolderPageOptions> = (opts) => {
|
|||
priority: 10,
|
||||
match: folderMatcher,
|
||||
generate({ content, cfg }) {
|
||||
const allFiles = content.map((c) => c[1].data);
|
||||
const allFiles = content
|
||||
.map((c) => c[1].data)
|
||||
.filter((d) => (d as { unlisted?: unknown } | undefined)?.unlisted !== true);
|
||||
const locale = (cfg as { locale?: string } | undefined)?.locale ?? "en-US";
|
||||
|
||||
const folders = new Set<string>();
|
||||
|
|
@ -76,7 +78,9 @@ export const FolderPage: QuartzPageTypePlugin<FolderPageOptions> = (opts) => {
|
|||
|
||||
const foldersWithIndex = new Set<string>();
|
||||
for (const [, file] of content) {
|
||||
const slug = (file.data as { slug?: string } | undefined)?.slug;
|
||||
const data = file.data as { slug?: string; unlisted?: unknown } | undefined;
|
||||
if (data?.unlisted === true) continue;
|
||||
const slug = data?.slug;
|
||||
if (slug && slug.endsWith("/index")) {
|
||||
const folder = slug.slice(0, -"/index".length);
|
||||
foldersWithIndex.add(folder);
|
||||
|
|
|
|||
53
test/folderContent.test.ts
Normal file
53
test/folderContent.test.ts
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import { pagesFromAllFiles } from "../src/components/FolderContent";
|
||||
|
||||
describe("pagesFromAllFiles", () => {
|
||||
it("returns direct children of the requested folder", () => {
|
||||
const allFiles = [
|
||||
{ slug: "notes/a", frontmatter: { title: "A" } },
|
||||
{ slug: "notes/b", frontmatter: { title: "B" } },
|
||||
{ slug: "other/c", frontmatter: { title: "C" } },
|
||||
];
|
||||
const result = pagesFromAllFiles(allFiles, "notes/index", true);
|
||||
expect(result.map((r) => r.slug).sort()).toEqual(["notes/a", "notes/b"]);
|
||||
});
|
||||
|
||||
it("excludes unlisted pages from folder listings", () => {
|
||||
const allFiles = [
|
||||
{ slug: "notes/public", frontmatter: { title: "Public" } },
|
||||
{ slug: "notes/secret", frontmatter: { title: "Secret" }, unlisted: true },
|
||||
{ slug: "notes/another", frontmatter: { title: "Another" } },
|
||||
];
|
||||
const result = pagesFromAllFiles(allFiles, "notes/index", true);
|
||||
const slugs = result.map((r) => r.slug);
|
||||
expect(slugs).toContain("notes/public");
|
||||
expect(slugs).toContain("notes/another");
|
||||
expect(slugs).not.toContain("notes/secret");
|
||||
});
|
||||
|
||||
it("includes explicit unlisted: false pages", () => {
|
||||
const allFiles = [{ slug: "notes/forced", frontmatter: { title: "Forced" }, unlisted: false }];
|
||||
const result = pagesFromAllFiles(allFiles, "notes/index", true);
|
||||
expect(result.map((r) => r.slug)).toEqual(["notes/forced"]);
|
||||
});
|
||||
|
||||
it("groups subfolders when showSubfolders is true", () => {
|
||||
const allFiles = [
|
||||
{ slug: "notes/top", frontmatter: { title: "Top" } },
|
||||
{ slug: "notes/sub/page", frontmatter: { title: "SubPage" } },
|
||||
];
|
||||
const result = pagesFromAllFiles(allFiles, "notes/index", true);
|
||||
const slugs = result.map((r) => r.slug).sort();
|
||||
expect(slugs).toContain("notes/top");
|
||||
expect(slugs.some((s) => s?.startsWith("notes/sub"))).toBe(true);
|
||||
});
|
||||
|
||||
it("does not include the folder index itself", () => {
|
||||
const allFiles = [
|
||||
{ slug: "notes/index", frontmatter: { title: "Index" } },
|
||||
{ slug: "notes/page", frontmatter: { title: "Page" } },
|
||||
];
|
||||
const result = pagesFromAllFiles(allFiles, "notes/index", true);
|
||||
expect(result.map((r) => r.slug)).toEqual(["notes/page"]);
|
||||
});
|
||||
});
|
||||
Loading…
Reference in a new issue