-
Notifications
You must be signed in to change notification settings - Fork 13
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add a remark plugin for generating a TOC menu
As we reorganize the documentation, it becomes cumbersome to manually change lists of links to pages within the documentation, e.g., in table of contents pages for subsections of the docs. This change adds a remark plugin for generating a list of links to pages in the current directory. The plugin works similarly to `remark-includes`, and accesses the local filesystem during the docs build. It replaces any lines consisting of `(!toc!)` with a list of links to pages in the current directory. The assumption is that a category page within a directory can use this to list contents.
- Loading branch information
Showing
9 changed files
with
349 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
--- | ||
title: Protect Databases with Teleport | ||
description: Guides to protecting databases with Teleport. | ||
--- |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
--- | ||
title: Protect MySQL with Teleport | ||
description: How to enroll your MySQL database with Teleport | ||
--- |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
--- | ||
title: Protect Postgres with Teleport | ||
description: How to enroll Postgres with your Teleport cluster | ||
--- |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
## Header | ||
|
||
Here is an intro. | ||
|
||
(!toc!) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
## Header | ||
|
||
Here is an intro. | ||
|
||
* [Protect Databases with Teleport](database-access.mdx): Guides to protecting databases with Teleport. | ||
* [Protect MySQL with Teleport](mysql.mdx): How to enroll your MySQL database with Teleport | ||
* [Protect Postgres with Teleport](postgres.mdx): How to enroll Postgres with your Teleport cluster |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,120 @@ | ||
import * as nodeFS from "fs"; | ||
import path from "path"; | ||
import matter from "gray-matter"; | ||
import { visitParents } from "unist-util-visit-parents"; | ||
import { fromMarkdown } from "mdast-util-from-markdown"; | ||
import type { Parent } from "unist"; | ||
import type { VFile } from "vfile"; | ||
import type { 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 = 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. | ||
let 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 | ||
// given directory location. | ||
export default function remarkTOC(): Transformer { | ||
return (root: Content, vfile: VFile) => { | ||
const lastErrorIndex = vfile.messages.length; | ||
|
||
visitParents(root, (node, ancestors: Parent[]) => { | ||
if (node.type !== "text") { | ||
return; | ||
} | ||
const parent = ancestors[ancestors.length - 1]; | ||
|
||
if (parent.type !== "paragraph") { | ||
return; | ||
} | ||
if (!parent.children || parent.children.length !== 1) { | ||
return; | ||
} | ||
|
||
const tocExpr = node.value.trim().match(tocRegexpPattern); | ||
if (!tocExpr) { | ||
return; | ||
} | ||
|
||
const { result, error } = getTOC(vfile.path); | ||
if (!!error) { | ||
vfile.message(error, node); | ||
return; | ||
} | ||
const tree = fromMarkdown(result, {}); | ||
|
||
const grandParent = ancestors[ancestors.length - 2] as Parent; | ||
const parentIndex = grandParent.children.indexOf(parent); | ||
|
||
grandParent.children.splice(parentIndex, 1, ...tree.children); | ||
}); | ||
}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,185 @@ | ||
import { Volume, createFsFromVolume } from "memfs"; | ||
import { default as remarkTOC, getTOC } from "../server/remark-toc"; | ||
import { readFileSync } from "fs"; | ||
import { resolve } from "path"; | ||
import { suite } from "uvu"; | ||
import * as assert from "uvu/assert"; | ||
import { VFile, VFileOptions } from "vfile"; | ||
import remarkMdx from "remark-mdx"; | ||
import remarkGFM from "remark-gfm"; | ||
import { remark } from "remark"; | ||
|
||
const Suite = suite("server/remark-toc"); | ||
|
||
const testFilesTwoSections = { | ||
"/docs/docs.mdx": `--- | ||
title: "Documentation Home" | ||
description: "Guides to setting up the product." | ||
--- | ||
Guides to setting up the product. | ||
`, | ||
"/docs/database-access/database-access.mdx": `--- | ||
title: "Database Access" | ||
description: Guides related to Database Access. | ||
--- | ||
Guides related to Database Access. | ||
`, | ||
"/docs/database-access/page1.mdx": `--- | ||
title: "Database Access Page 1" | ||
description: "Protecting DB 1 with Teleport" | ||
---`, | ||
"/docs/database-access/page2.mdx": `--- | ||
title: "Database Access Page 2" | ||
description: "Protecting DB 2 with Teleport" | ||
---`, | ||
"/docs/application-access/application-access.mdx": `--- | ||
title: "Application Access" | ||
description: "Guides related to Application Access" | ||
--- | ||
Guides related to Application Access. | ||
`, | ||
"/docs/application-access/page1.mdx": `--- | ||
title: "Application Access Page 1" | ||
description: "Protecting App 1 with Teleport" | ||
---`, | ||
"/docs/application-access/page2.mdx": `--- | ||
title: "Application Access Page 2" | ||
description: "Protecting App 2 with Teleport" | ||
---`, | ||
}; | ||
|
||
Suite("getTOC with one link to a directory", () => { | ||
const expected = `- [Application Access](application-access/application-access.mdx): Guides related to Application Access`; | ||
|
||
const vol = Volume.fromJSON({ | ||
"/docs/docs.mdx": `--- | ||
title: Documentation Home | ||
description: Guides for setting up the product. | ||
--- | ||
Guides for setting up the product. | ||
`, | ||
"/docs/application-access/application-access.mdx": `--- | ||
title: "Application Access" | ||
description: "Guides related to Application Access" | ||
--- | ||
`, | ||
"/docs/application-access/page1.mdx": `--- | ||
title: "Application Access Page 1" | ||
description: "Protecting App 1 with Teleport" | ||
---`, | ||
"/docs/application-access/page2.mdx": `--- | ||
title: "Application Access Page 2" | ||
description: "Protecting App 2 with Teleport" | ||
---`, | ||
}); | ||
const fs = createFsFromVolume(vol); | ||
const actual = getTOC("/docs/docs.mdx", fs); | ||
assert.equal(actual.result, expected); | ||
}); | ||
|
||
Suite("getTOC with multiple links to directories", () => { | ||
const expected = `- [Application Access](application-access/application-access.mdx): Guides related to Application Access | ||
- [Database Access](database-access/database-access.mdx): Guides related to Database Access.`; | ||
|
||
const vol = Volume.fromJSON(testFilesTwoSections); | ||
const fs = createFsFromVolume(vol); | ||
const actual = getTOC("/docs/docs.mdx", fs); | ||
assert.equal(actual.result, expected); | ||
}); | ||
|
||
Suite("getTOC orders sections correctly", () => { | ||
const expected = `- [API Usage](api.mdx): Using the API. | ||
- [Application Access](application-access/application-access.mdx): Guides related to Application Access | ||
- [Desktop Access](desktop-access/desktop-access.mdx): Guides related to Desktop Access | ||
- [Initial Setup](initial-setup.mdx): How to set up the product for the first time. | ||
- [Kubernetes](kubernetes.mdx): A guide related to Kubernetes.`; | ||
|
||
const vol = Volume.fromJSON({ | ||
"/docs/docs.mdx": `--- | ||
title: Documentation Home | ||
description: Guides to setting up the product. | ||
--- | ||
Guides to setting up the product. | ||
`, | ||
"/docs/desktop-access/desktop-access.mdx": `--- | ||
title: "Desktop Access" | ||
description: "Guides related to Desktop Access" | ||
--- | ||
`, | ||
|
||
"/docs/application-access/application-access.mdx": `--- | ||
title: "Application Access" | ||
description: "Guides related to Application Access" | ||
--- | ||
`, | ||
"/docs/desktop-access/get-started.mdx": `--- | ||
title: "Get Started" | ||
description: "Get started with desktop access." | ||
---`, | ||
"/docs/application-access/page1.mdx": `--- | ||
title: "Application Access Page 1" | ||
description: "Protecting App 1 with Teleport" | ||
---`, | ||
"/docs/kubernetes.mdx": `--- | ||
title: "Kubernetes" | ||
description: "A guide related to Kubernetes." | ||
---`, | ||
|
||
"/docs/initial-setup.mdx": `--- | ||
title: "Initial Setup" | ||
description: "How to set up the product for the first time." | ||
---`, | ||
"/docs/api.mdx": `--- | ||
title: "API Usage" | ||
description: "Using the API." | ||
---`, | ||
}); | ||
const fs = createFsFromVolume(vol); | ||
const actual = getTOC("/docs/docs.mdx", fs); | ||
assert.equal(actual.result, expected); | ||
}); | ||
|
||
const transformer = (vfileOptions: VFileOptions) => { | ||
const file = new VFile(vfileOptions); | ||
|
||
return remark() | ||
.use(remarkMdx) | ||
.use(remarkGFM) | ||
.use(remarkTOC) | ||
.processSync(file); | ||
}; | ||
|
||
Suite("replaces inclusion expressions", () => { | ||
const sourcePath = "server/fixtures/toc/database-access/source.mdx"; | ||
const value = readFileSync(resolve(sourcePath), "utf-8"); | ||
|
||
const result = transformer({ | ||
value, | ||
path: sourcePath, | ||
}); | ||
|
||
const actual = result.toString(); | ||
|
||
const expected = readFileSync( | ||
resolve("server/fixtures/toc/expected.mdx"), | ||
"utf-8" | ||
); | ||
|
||
assert.equal(result.messages, []); | ||
assert.equal(actual, expected); | ||
}); | ||
|
||
Suite.run(); |