Skip to content
This repository has been archived by the owner on Jan 8, 2025. It is now read-only.

Improve the sidebar generator #481

Merged
merged 1 commit into from
Jul 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
108 changes: 85 additions & 23 deletions server/pages-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,42 @@ const getEntryForPath = (fs, filePath) => {
};
};

// sortByTitle takes two navigation entries, a and b, and sorts them in
// alphabetically ascending order by their "title" field. If either title
// includes the substring "introduction", sortByTitle sorts that entry first.
const sortByTitle = (a, b) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you add a comment here describing the algorithm chosen for sorting? "if it includes this, sort alphabetically, otherwise go by title" or whatever

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added in 52328f0

switch (true) {
case a.title.toLowerCase().includes("introduction"):
return -1;
break;
case b.title.toLowerCase().includes("introduction"):
return 1;
break;
default:
return a.title < b.title ? -1 : 1;
}
};

// categoryPagePathForDir looks for a category page at the same directory level
// as its associated directory OR within the associated directory. Throws an
// error if there is no category page for the directory.
const categoryPagePathForDir = (fs, dirPath) => {
const { name } = parse(dirPath);

const outerCategoryPage = join(dirname(dirPath), name + ".mdx");
const innerCategoryPage = join(dirPath, name + ".mdx");

if (fs.existsSync(outerCategoryPage)) {
return outerCategoryPage;
}
if (fs.existsSync(innerCategoryPage)) {
return innerCategoryPage;
}
throw new Error(
`subdirectory in generated sidebar section ${dirPath} has no category page ${innerCategoryPage} or ${outerCategoryPage}`
);
};

export const generateNavPaths = (fs, dirPath) => {
const firstLvl = fs.readdirSync(dirPath, "utf8");
let result = [];
Expand All @@ -86,53 +122,79 @@ export const generateNavPaths = (fs, dirPath) => {
}
firstLvlFiles.add(fullPath);
});
let sectionIntros = new Set();
firstLvlDirs.forEach((d: string) => {
const { name } = parse(d);
const asFile = join(d, name + ".mdx");

if (!fs.existsSync(asFile)) {
throw `subdirectory in generated sidebar section ${d} has no category page ${asFile}`;
}
sectionIntros.add(asFile);
return;
// Map category pages to the directories they introduce so we can can add a
// sidebar entry for the category page, then traverse the directory.
let sectionIntros = new Map();
firstLvlDirs.forEach((d: string) => {
sectionIntros.set(categoryPagePathForDir(fs, d), d);
});

// Add files with no corresponding directory to the navigation first. Section
// introductions, by convention, have a filename that corresponds to the
// subdirectory containing pages in the section, or have the name
// "introduction.mdx".
firstLvlFiles.forEach((f) => {
firstLvlFiles.forEach((f: string) => {
// Handle section intros separately
if (sectionIntros.has(f)) {
return;
}
if (!f.endsWith(".mdx")) {
return;
}
result.push(getEntryForPath(fs, f));
});

sectionIntros.forEach((si: string) => {
const { slug, title } = getEntryForPath(fs, si);
sectionIntros.forEach((dirPath, categoryPagePath) => {
const { slug, title } = getEntryForPath(fs, categoryPagePath);
const section = {
title: title,
slug: slug,
entries: [],
};
const sectionDir = dirname(si);
const secondLvl = fs.readdirSync(sectionDir, "utf8");
secondLvl.forEach((f2) => {
const { name } = parse(f2);

// The directory name is the same as the filename, meaning that we have
// already used this as a category page.
if (sectionDir.endsWith(name)) {
const secondLvl = new Set(fs.readdirSync(dirPath, "utf8"));

// Find all second-level category pages first so we don't
// repeat them in the sidebar.
secondLvl.forEach((f2: string) => {
let fullPath2 = join(dirPath, f2);
const stat = fs.statSync(fullPath2);

// List category pages on the second level, but not their contents.
if (!stat.isDirectory()) {
return;
}
const catPath = categoryPagePathForDir(fs, fullPath2);
fullPath2 = catPath;
secondLvl.delete(f2);

const fullPath2 = join(sectionDir, f2);
const stat = fs.statSync(fullPath2);
if (stat.isDirectory()) {
// Delete the category page from the set so we don't add it again
// when we add individual files.
secondLvl.delete(parse(catPath).base);
section.entries.push(getEntryForPath(fs, fullPath2));
});

secondLvl.forEach((f2: string) => {
// Only add entries for MDX files here
if (!f2.endsWith(".mdx")) {
return;
}

let fullPath2 = join(dirPath, f2);

// This is a first-level category page that happens to exist on the second
// level.
if (sectionIntros.has(fullPath2)) {
return;
}

const stat = fs.statSync(fullPath2);
section.entries.push(getEntryForPath(fs, fullPath2));
});

section.entries.sort(sortByTitle);
result.push(section);
});
result.sort(sortByTitle);
return result;
};
204 changes: 196 additions & 8 deletions uvu-tests/config-docs.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,10 +139,6 @@ title: Database RBAC Reference
};

const expected = [
{
title: "Protect Databases with Teleport",
slug: "/database-access/introduction/",
},
{
title: "Database Access Guides",
slug: "/database-access/guides/guides/",
Expand All @@ -161,16 +157,20 @@ title: Database RBAC Reference
title: "Database Access RBAC",
slug: "/database-access/rbac/rbac/",
entries: [
{
title: "Get Started with DB RBAC",
slug: "/database-access/rbac/get-started/",
},
{
title: "Database RBAC Reference",
slug: "/database-access/rbac/reference/",
},
{
title: "Get Started with DB RBAC",
slug: "/database-access/rbac/get-started/",
},
],
},
{
title: "Protect Databases with Teleport",
slug: "/database-access/introduction/",
},
];

const vol = Volume.fromJSON(files);
Expand All @@ -179,6 +179,96 @@ title: Database RBAC Reference
assert.equal(actual, expected);
});

Suite(
"generateNavPaths alphabetizes second-level links except 'Introduction'",
() => {
const files = {
"/docs/pages/database-access/mongodb.mdx": `---
title: MongoDB
---`,
"/docs/pages/database-access/azure-dbs.mdx": `---
title: Azure
---`,
"/docs/pages/database-access/introduction.mdx": `---
title: Introduction to Database Access
---`,
};

const expected = [
{
title: "Introduction to Database Access",
slug: "/database-access/introduction/",
},
{
title: "Azure",
slug: "/database-access/azure-dbs/",
},
{
title: "MongoDB",
slug: "/database-access/mongodb/",
},
];

const vol = Volume.fromJSON(files);
const fs = createFsFromVolume(vol);
const actual = generateNavPaths(fs, "/docs/pages/database-access");
assert.equal(actual, expected);
}
);

Suite(
"generateNavPaths alphabetizes third-level links except 'Introduction'",
() => {
const files = {
"/docs/pages/database-access/guides/guides.mdx": `---
title: Database Access Guides
---`,
"/docs/pages/database-access/guides/postgres.mdx": `---
title: Postgres Guide
---`,
"/docs/pages/database-access/guides/mysql.mdx": `---
title: MySQL Guide
---`,
"/docs/pages/database-access/guides/get-started.mdx": `---
title: Introduction to Database RBAC
---`,
"/docs/pages/database-access/guides/reference.mdx": `---
title: Database RBAC Reference
---`,
};

const expected = [
{
title: "Database Access Guides",
slug: "/database-access/guides/guides/",
entries: [
{
title: "Introduction to Database RBAC",
slug: "/database-access/guides/get-started/",
},
{
title: "Database RBAC Reference",
slug: "/database-access/guides/reference/",
},
{
title: "MySQL Guide",
slug: "/database-access/guides/mysql/",
},
{
title: "Postgres Guide",
slug: "/database-access/guides/postgres/",
},
],
},
];

const vol = Volume.fromJSON(files);
const fs = createFsFromVolume(vol);
const actual = generateNavPaths(fs, "/docs/pages/database-access");
assert.equal(actual, expected);
}
);

Suite(
"generateNavPaths throws if there is no category page in a subdirectory",
() => {
Expand All @@ -199,4 +289,102 @@ title: MySQL Guide
}
);

Suite(
"generateNavPaths shows third-level category pages on the sidebar",
() => {
const files = {
"/docs/pages/database-access/guides/guides.mdx": `---
title: Database Access Guides
---`,
"/docs/pages/database-access/guides/postgres.mdx": `---
title: Postgres Guide
---`,
"/docs/pages/database-access/guides/mysql.mdx": `---
title: MySQL Guide
---`,
"/docs/pages/database-access/guides/rbac/rbac.mdx": `---
title: Database Access RBAC
---`,
"/docs/pages/database-access/guides/rbac/get-started.mdx": `---
title: Get Started with DB RBAC
---`,
};

const expected = [
{
title: "Database Access Guides",
slug: "/database-access/guides/guides/",
entries: [
{
title: "Database Access RBAC",
slug: "/database-access/guides/rbac/rbac/",
},
{
title: "MySQL Guide",
slug: "/database-access/guides/mysql/",
},
{
title: "Postgres Guide",
slug: "/database-access/guides/postgres/",
},
],
},
];

const vol = Volume.fromJSON(files);
const fs = createFsFromVolume(vol);
const actual = generateNavPaths(fs, "/docs/pages/database-access");
assert.equal(actual, expected);
}
);

Suite(
"allows category pages in the same directory as the associated subdirectory",
() => {
const files = {
"/docs/pages/database-access/guides.mdx": `---
title: Database Access Guides
---`,
"/docs/pages/database-access/guides/postgres.mdx": `---
title: Postgres Guide
---`,
"/docs/pages/database-access/guides/mysql.mdx": `---
title: MySQL Guide
---`,
"/docs/pages/database-access/guides/rbac.mdx": `---
title: Database Access RBAC
---`,
"/docs/pages/database-access/guides/rbac/get-started.mdx": `---
title: Get Started with DB RBAC
---`,
};

const expected = [
{
title: "Database Access Guides",
slug: "/database-access/guides/",
entries: [
{
title: "Database Access RBAC",
slug: "/database-access/guides/rbac/",
},
{
title: "MySQL Guide",
slug: "/database-access/guides/mysql/",
},
{
title: "Postgres Guide",
slug: "/database-access/guides/postgres/",
},
],
},
];

const vol = Volume.fromJSON(files);
const fs = createFsFromVolume(vol);
let actual = generateNavPaths(fs, "/docs/pages/database-access");
assert.equal(actual, expected);
}
);

Suite.run();
Loading