Skip to content

Commit

Permalink
Adapt DocCardList for the Teleport docs site
Browse files Browse the repository at this point in the history
Partially addresses #29

Swizzle the Docusaurus-native `DocCardList` component and adapt it to
suit the Teleport docs site. Since we prefer a more text-oriented
approach to components, edit `DocCardList` to return a plain `ul`,
rather than tiles.

Since the `DocCardList` component is a Docusaurus-native alternative to
our `remark-toc` plugin, edit `remark-toc` to return a `DocCardList`
instead of querying the filesystem to generate a table of contents. Once
we replace all `(!toc!)` expressions with `<DocCardList />` elements, we
can remove `remark-toc` entirely.

Also make `DocCardList` an MDX component so docs authors don't need to
add `import` statements to the top of a docs page.
  • Loading branch information
ptgott committed Jan 3, 2025
1 parent 3f68d6c commit 3960825
Show file tree
Hide file tree
Showing 3 changed files with 77 additions and 77 deletions.
84 changes: 7 additions & 77 deletions server/remark-toc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,76 +8,6 @@ import type { VFile } from "vfile";
import type { Root, Content } from "mdast";
import type { Transformer } from "unified";

// relativePathToFile takes a filepath and returns a path we can use in links
// to the file in a table of contents page. The link path is a relative path
// to the directory where we are placing the table of contents page.
// @param root {string} - the directory path to the table of contents page.
// @param filepath {string} - the path from which to generate a link path.
const relativePathToFile = (root: string, filepath: string) => {
// Return the filepath without the first segment, removing the first
// slash. This is because the TOC file we are generating is located at
// root.
return filepath.slice(root.length).replace(/^\//, "");
};

// getTOC generates a list of links to all files in the same directory as
// filePath except for filePath. The return value is an object with two
// properties:
// - result: a string containing the resulting list of links.
// - error: an error message encountered during processing
export const getTOC = (filePath: string, fs: any = nodeFS) => {
const dirPath = path.dirname(filePath);
if (!fs.existsSync(dirPath)) {
return {
error: `Cannot generate a table of contents for nonexistent directory at ${dirPath}`,
};
}

const { name } = path.parse(filePath);

const files = fs.readdirSync(dirPath, "utf8");
let mdxFiles = new Set();
const dirs = files.reduce((accum, current) => {
// Don't add a TOC entry for the current file.
if (name == path.parse(current).name) {
return accum;
}
const stats = fs.statSync(path.join(dirPath, current));
if (!stats.isDirectory() && current.endsWith(".mdx")) {
mdxFiles.add(path.join(dirPath, current));
return accum;
}
accum.add(path.join(dirPath, current));
return accum;
}, new Set());

// Add rows to the menu page for non-menu pages.
const entries = [];
mdxFiles.forEach((f: string, idx: number) => {
const text = fs.readFileSync(f, "utf8");
let relPath = relativePathToFile(dirPath, f);
const { data } = matter(text);
entries.push(`- [${data.title}](${relPath}): ${data.description}`);
});

// Add rows to the menu page for first-level child menu pages
dirs.forEach((f: string, idx: number) => {
const menuPath = path.join(f, path.parse(f).base + ".mdx");
if (!fs.existsSync(menuPath)) {
return {
error: `there must be a page called ${menuPath} that introduces ${f}`,
};
}
const text = fs.readFileSync(menuPath, "utf8");
let relPath = relativePathToFile(dirPath, menuPath);
const { data } = matter(text);

entries.push(`- [${data.title}](${relPath}): ${data.description}`);
});
entries.sort();
return { result: entries.join("\n") };
};

const tocRegexpPattern = "^\\(!toc!\\)$";

// remarkTOC replaces (!toc!) syntax in a page with a list of docs pages at a
Expand All @@ -104,17 +34,17 @@ export default function remarkTOC(): Transformer {
return;
}

const { result, error } = getTOC(vfile.path);
if (!!error) {
vfile.message(error, node);
return;
}
const tree = fromMarkdown(result, {});
const tree = {
type: "mdxJsxFlowElement",
name: "DocCardList",
attributes: [],
children: [],
};

const grandParent = ancestors[ancestors.length - 2] as Parent;
const parentIndex = grandParent.children.indexOf(parent);

grandParent.children.splice(parentIndex, 1, ...(tree as Root).children);
grandParent.children.splice(parentIndex, 1, tree);
});
};
}
68 changes: 68 additions & 0 deletions src/theme/DocCardList/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import React from "react";
import clsx from "clsx";
import {
useCurrentSidebarCategory,
filterDocCardListItems,
useDocById,
} from "@docusaurus/plugin-content-docs/client";
import DocCard from "@theme/DocCard";
import type { Props } from "@theme/DocCardList";

function DocCardListForCurrentSidebarCategory({ className }: Props) {
const category = useCurrentSidebarCategory();
return <DocCardList items={category.items} className={className} />;
}

// categoryHrefoToDocID returns the Docusaurus page ID that corresponds to the
// given href. Category pages do not have IDs in the items prop, so we generate
// a page ID based on the assumption that category page slugs are the same as
// their containing directory names.
function categoryHrefToDocID(href: string): string {
if (!href) {
return href;
}
const idPrefix = href.replace(new RegExp(`^/ver/[0-9]+\\.x/`), "");
const slugRE = new RegExp(`/([^/]+)/$`);
const slug = slugRE.exec(href);
if (!slug || slug.length != 2) {
return "";
}
return idPrefix + slug[1];
}

export default function DocCardList(props: Props): JSX.Element {
const { items, className } = props;
if (!items) {
return <DocCardListForCurrentSidebarCategory {...props} />;
}
const filteredItems = filterDocCardListItems(items).map((item) => {
const doc = useDocById(item.docId ?? undefined);

if (item.type == "link") {
return {
href: item.href,
label: item.label,
description: doc?.description,
};
}
if (item.type == "category") {
const indexPage = useDocById(categoryHrefToDocID(item.href) ?? undefined);

return {
href: item.href,
label: item.label + " (section)",
description: indexPage?.description,
};
}
});

return (
<ul className={clsx("row", className)}>
{filteredItems.map((item, index) => (
<li key={index}>
<a href={item.href}>{item.label}</a>: {item.description}
</li>
))}
</ul>
);
}
2 changes: 2 additions & 0 deletions src/theme/MDXComponents/index.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import OriginalMDXComponents from "@theme-original/MDXComponents";
import Tabs from "@theme/Tabs";
import TabItem from "@theme/TabItem";
import DocCardList from "@theme/DocCardList";
import Admonition from "@theme/Admonition";
import React, { type ComponentProps } from "react";
import Head from "@docusaurus/Head";
Expand All @@ -21,6 +22,7 @@ import type { MDXComponentsObject } from "@theme/MDXComponents";
const MDXComponents: MDXComponentsObject = {
...OriginalMDXComponents,
Details: MDXDetails,
DocCardList: DocCardList,
Head,
TabItem,
Tabs,
Expand Down

0 comments on commit 3960825

Please sign in to comment.