diff --git a/.vscode/settings.json b/.vscode/settings.json index dddc071a..70f705e7 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,5 +1,5 @@ { - "editor.formatOnSave": false, + "editor.formatOnSave": true, "editor.codeActionsOnSave": ["source.formatDocument", "source.fixAll.eslint"], "search.exclude": { "**/.yarn": true, diff --git a/next.config.js b/next.config.js index c74920ca..bf53b999 100644 --- a/next.config.js +++ b/next.config.js @@ -1,16 +1,14 @@ /* eslint-disable @typescript-eslint/no-var-requires */ // eslint-disable-next-line function-paren-newline -const withMarkdoc = require('@markdoc/next.js')( - /* config: https://markdoc.io/docs/nextjs#options */ { - schemaPath: './src/markdoc', - }) -const withTM = require('next-transpile-modules')(['@pluralsh/design-system', 'honorable', 'honorable-theme-default'], +const withTM = require('next-transpile-modules')( + ['@pluralsh/design-system', 'honorable', 'honorable-theme-default'], { debug: false, - }) + } +) module.exports = () => { - const plugins = [withMarkdoc, withTM] + const plugins = [withTM] return plugins.reduce((acc, next) => next(acc), { reactStrictMode: false, @@ -23,7 +21,14 @@ module.exports = () => { locales: ['en-US'], defaultLocale: 'en-US', }, - pageExtensions: ['js', 'jsx', 'ts', 'tsx', 'md', 'mdoc'], + pageExtensions: ['js', 'jsx', 'ts', 'tsx'], + webpack: (config) => { + config.module.rules.push({ + test: /\.md$/, + use: 'raw-loader', + }) + return config + }, async redirects() { return [ { @@ -53,12 +58,14 @@ module.exports = () => { }, { source: '/getting-started/getting-started-with-runbooks/runbook-yaml', - destination: '/adding-new-application/getting-started-with-runbooks/runbook-yaml', + destination: + '/adding-new-application/getting-started-with-runbooks/runbook-yaml', permanent: true, }, { source: '/basic-setup-and-deployment/setting-up-gitops', - destination: '/getting-started/managing-git-repository/setting-up-gitops', + destination: + '/getting-started/managing-git-repository/setting-up-gitops', permanent: true, }, { @@ -87,7 +94,8 @@ module.exports = () => { permanent: true, }, { - source: '/reference/operator-guides/adding-kubecost-for-cost-analysis', + source: + '/reference/operator-guides/adding-kubecost-for-cost-analysis', destination: '/repositories/kubecost', permanent: true, }, @@ -132,8 +140,10 @@ module.exports = () => { permanent: true, }, { - source: '/advanced-topics/dns-setup/creating-dns-zone-in-your-cloud-provider-console', - destination: '/operations/dns-setup/creating-dns-zone-in-your-cloud-provider-console', + source: + '/advanced-topics/dns-setup/creating-dns-zone-in-your-cloud-provider-console', + destination: + '/operations/dns-setup/creating-dns-zone-in-your-cloud-provider-console', permanent: true, }, { @@ -143,7 +153,8 @@ module.exports = () => { }, { source: '/advanced-topics/security/secret-management', - destination: '/getting-started/manage-git-repositories/sharing-git-repositories', + destination: + '/getting-started/manage-git-repositories/sharing-git-repositories', permanent: true, }, { @@ -172,12 +183,14 @@ module.exports = () => { permanent: true, }, { - source: '/advanced-topics/identity-and-access-management/introduction', + source: + '/advanced-topics/identity-and-access-management/introduction', destination: '/operations/auth-access-control', permanent: true, }, { - source: '/advanced-topics/identity-and-access-management/openid-connect', + source: + '/advanced-topics/identity-and-access-management/openid-connect', destination: '/operations/auth-access-control/openid-connect', permanent: true, }, @@ -187,28 +200,37 @@ module.exports = () => { permanent: true, }, { - source: '/advanced-topics/identity-and-access-management/identity-and-installations', - destination: '/operations/auth-access-control/identity-and-installations', + source: + '/advanced-topics/identity-and-access-management/identity-and-installations', + destination: + '/operations/auth-access-control/identity-and-installations', permanent: true, }, { - source: '/advanced-topics/identity-and-access-management/identity-and-installations/audit-logging', - destination: '/operations/auth-access-control/identity-and-installations/audit-logging', + source: + '/advanced-topics/identity-and-access-management/identity-and-installations/audit-logging', + destination: + '/operations/auth-access-control/identity-and-installations/audit-logging', permanent: true, }, { - source: '/advanced-topics/identity-and-access-management/identity-and-installations/service-accounts', - destination: '/operations/auth-access-control/identity-and-installations/service-accounts', + source: + '/advanced-topics/identity-and-access-management/identity-and-installations/service-accounts', + destination: + '/operations/auth-access-control/identity-and-installations/service-accounts', permanent: true, }, { - source: '/advanced-topics/identity-and-access-management/identity-and-installations/sharing-existing-repos', - destination: '/getting-started/manage-git-repositories/sharing-git-repository', + source: + '/advanced-topics/identity-and-access-management/identity-and-installations/sharing-existing-repos', + destination: + '/getting-started/manage-git-repositories/sharing-git-repository', permanent: true, }, { source: '/reference/workspaces', - destination: '/getting-started/manage-git-repositories/your-plural-workspace', + destination: + '/getting-started/manage-git-repositories/your-plural-workspace', permanent: true, }, ] diff --git a/package.json b/package.json index 82929d4f..3b17a710 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ "graphql": "16.6.0", "honorable": "0.194.0", "honorable-theme-default": "0.77.0", + "htmlparser2": "9.1.0", "immer": "10.0.2", "js-yaml": "4.1.0", "lodash": "4.17.21", diff --git a/pages/[...slug].tsx b/pages/[...slug].tsx new file mode 100644 index 00000000..4de48b84 --- /dev/null +++ b/pages/[...slug].tsx @@ -0,0 +1,98 @@ +import fs from 'fs' +import path from 'path' +import { type ParsedUrlQuery } from 'querystring' + +import { type GetStaticPaths, type GetStaticProps } from 'next' + +import MarkdocComponent from '@src/components/MarkdocContent' +import { readMdFileCached } from '@src/markdoc/mdParser' +import { type MarkdocPage } from '@src/markdoc/mdSchema' + +interface Params extends ParsedUrlQuery { + slug: string[] +} + +export default function MarkdocContent({ + markdoc, +}: { + markdoc: MarkdocPage | null +}) { + return markdoc && +} + +export const getStaticPaths: GetStaticPaths = async () => { + const pagesDirectory = path.join('pages') + + // recursively get all .md files in the 'pages/' directory + function getAllMarkdownFiles(dir: string, files: string[] = []): string[] { + const entries = fs.readdirSync(dir, { withFileTypes: true }) + + for (const entry of entries) { + const fullPath = path.join(dir, entry.name) + + if (entry.isDirectory()) { + getAllMarkdownFiles(fullPath, files) + } else if (entry.isFile() && entry.name.endsWith('.md')) { + files.push(fullPath) + } + } + + return files + } + + const markdownFiles = getAllMarkdownFiles(pagesDirectory) + + const paths = markdownFiles.map((file) => { + const relativePath = path.relative(pagesDirectory, file) + const parsedPath = path.parse(relativePath) + + const dirSegments = parsedPath.dir ? parsedPath.dir.split(path.sep) : [] + + let slug: string[] + + if (parsedPath.name === 'index') slug = dirSegments + else slug = [...dirSegments, parsedPath.name] + + return { + params: { + slug, + }, + } + }) + + return { + paths, + fallback: 'blocking', + } +} + +export const getStaticProps: GetStaticProps< + { markdoc: MarkdocPage | null }, + Params +> = async ({ params }) => { + if (!params?.slug) { + return { notFound: true } + } + + const slugPath = params.slug.join('/') + + // looks for folder/name/index.md first, then folder/name.md + const filePath = + [ + path.join('pages', slugPath, 'index.md'), + path.join('pages', `${slugPath}.md`), + ].find(fs.existsSync) || null + + if (!filePath) { + return { notFound: true } + } + const markdoc = await readMdFileCached(filePath) + + return { + props: { + displayTitle: markdoc?.frontmatter?.title ?? '', + displayDescription: markdoc?.frontmatter?.description ?? '', + markdoc, + }, + } +} diff --git a/pages/how-to/set-up/mgmt-cluster.md b/pages/how-to/set-up/mgmt-cluster.md index 935c1300..7c197777 100644 --- a/pages/how-to/set-up/mgmt-cluster.md +++ b/pages/how-to/set-up/mgmt-cluster.md @@ -46,4 +46,4 @@ There are a few reasons you'd consider using this over Plural Cloud: * Integration - Oftentimes resources needed by Plural are themselves hosted on private networks, for instance Git Repositories. In that case, it's logistically easier to self-host and place it in an integrated network. * Scaling - you want complete control as to how Plural Scales for your enterprise. `dedicated` cloud hosting does this perfectly well too, but some orgs want their own hands on the wheel. -Plural is meant to be architecturally simple and efficient. Most organizations that do chose to self-host are shocked at how streamlined managing it is, especially compared to some more bloated CNCF projects, so it is a surprisingly viable way to manage the software if that is what your organization desires. +Plural is meant to be architecturally simple and efficient. Most organizations that do choose to self-host are shocked at how streamlined managing it is, especially compared to some more bloated CNCF projects, so it is a surprisingly viable way to manage the software if that is what your organization desires. diff --git a/src/markdoc/mdParser.ts b/src/markdoc/mdParser.ts index e6ed96d5..4d2cd6c9 100644 --- a/src/markdoc/mdParser.ts +++ b/src/markdoc/mdParser.ts @@ -2,6 +2,7 @@ import { promises as fs } from 'fs' import path from 'path' import Markdoc from '@markdoc/markdoc' +import { Parser } from 'htmlparser2' import yaml from 'js-yaml' import { config as schemaConfig } from './mdSchema' @@ -13,7 +14,7 @@ const fileCache = new Map() export const readMdFileCached = async ( filePath: string ): Promise => { - if (!filePath.startsWith('/pages')) { + if (!filePath.startsWith('pages')) { return null } function cacheAndReturn(val: MarkdocPage | null) { @@ -36,10 +37,15 @@ export const readMdFileCached = async ( return cacheAndReturn(null) } - const ast = Markdoc.parse(file) + const tokenizer = new Markdoc.Tokenizer({ html: true }) + const tokens = tokenizer.tokenize(file) + const processed = processHtmlTokens(tokens) + const ast = Markdoc.parse(processed) + const frontmatter = ast.attributes.frontmatter ? yaml.load(ast.attributes.frontmatter) : {} + const content = Markdoc.transform(ast, schemaConfig) const ret: MarkdocPage = JSON.parse( @@ -47,7 +53,7 @@ export const readMdFileCached = async ( content, frontmatter, file: { - path: filePath.replace(/^\/pages/g, ''), + path: filePath.replace(/^pages/g, ''), }, }) ) @@ -59,3 +65,52 @@ export const readMdFileCached = async ( return cacheAndReturn(null) } } + +// see https://github.com/markdoc/markdoc/issues/10 +// https://gist.github.com/rpaul-stripe/941eb22c4779ea87b1adf7715d76ca08 +function processHtmlTokens(tokens) { + const output: any[] = [] + + const parser = new Parser({ + onopentag(name, attrs) { + output.push({ + type: 'tag_open', + nesting: 1, + meta: { + tag: 'html-tag', + attributes: [ + { type: 'attribute', name: 'name', value: name }, + { type: 'attribute', name: 'attrs', value: attrs }, + ], + }, + }) + }, + + ontext(content) { + if (typeof content === 'string' && content.trim().length > 0) + output.push({ type: 'text', content }) + }, + + onclosetag() { + output.push({ + type: 'tag_close', + nesting: -1, + meta: { tag: 'html-tag' }, + }) + }, + }) + + for (const token of tokens) { + if (token.type.startsWith('html')) { + parser.write(token.content) + continue + } + + if (token.type === 'inline') + token.children = processHtmlTokens(token.children) + + output.push(token) + } + + return output +} diff --git a/src/markdoc/mdSchema.ts b/src/markdoc/mdSchema.ts index 767af09e..1b0e7a28 100644 --- a/src/markdoc/mdSchema.ts +++ b/src/markdoc/mdSchema.ts @@ -3,8 +3,8 @@ import merge from 'lodash/merge' import * as config from './config' import * as functions from './functions' -import * as nodes from './nodes' -import * as tags from './tags' +import { nodes } from './nodes' +import { tags } from './tags' import type { MarkdocNextJsPageProps } from '@markdoc/next.js' diff --git a/src/markdoc/nodes/index.ts b/src/markdoc/nodes/index.ts index bb542b91..187c6119 100644 --- a/src/markdoc/nodes/index.ts +++ b/src/markdoc/nodes/index.ts @@ -1,2 +1,56 @@ /* Markdoc nodes must be exported from this file to work with markdoc/nextjs plugin */ -export * from '@pluralsh/design-system/dist/markdoc/nodes' + +import { Tag } from '@markdoc/markdoc' +import { Table } from '@pluralsh/design-system/dist/markdoc/components' +import * as designSystemNodes from '@pluralsh/design-system/dist/markdoc/nodes' +import { isTag } from '@pluralsh/design-system/dist/markdoc/types' + +export const nodes = { + ...designSystemNodes, + // slight fork of old DS version + table: { + render: Table, + description: 'Display horizontal tabs in a box', + children: ['tab'], + attributes: {}, + transform(node, config) { + const children = node.transformChildren(config) + + const thead = children + .find( + (child): child is Tag => + isTag(child) && child?.name.toLowerCase() === 'thead' + ) + ?.children.find( + (tr): tr is Tag => isTag(tr) && tr?.name.toLowerCase() === 'tr' + ) + ?.children.filter( + (th): th is Tag => isTag(th) && th?.name.toLowerCase() === 'th' + ) + .map((th) => th.children) + + const tbody = children + .find( + (child): child is Tag => + isTag(child) && child?.name.toLowerCase() === 'tbody' + ) + ?.children.filter( + (tr): tr is Tag => isTag(tr) && tr?.name.toLowerCase() === 'tr' + ) + ?.map((tr) => + tr.children + .filter( + (trChild): trChild is Tag => + isTag(trChild) && trChild?.name.toLowerCase() === 'td' + ) + .map((td) => td.children) + ) + + return new Tag( + this.render as any, + { thead, tbody, children }, + node.transformChildren(config) + ) + }, + }, +} diff --git a/src/markdoc/tags/htmlTag.markdoc.js b/src/markdoc/tags/htmlTag.markdoc.js new file mode 100644 index 00000000..6e284fd4 --- /dev/null +++ b/src/markdoc/tags/htmlTag.markdoc.js @@ -0,0 +1,14 @@ +import Markdoc from '@markdoc/markdoc' + +export const htmlTag = { + attributes: { + name: { type: String, required: true }, + attrs: { type: Object }, + }, + transform(node, config) { + const { name, attrs } = node.attributes + const children = node.transformChildren(config) + + return new Markdoc.Tag(name, attrs, children) + }, +} diff --git a/src/markdoc/tags/index.ts b/src/markdoc/tags/index.ts index 1a3c70c4..ea6a351f 100644 --- a/src/markdoc/tags/index.ts +++ b/src/markdoc/tags/index.ts @@ -1,5 +1,13 @@ -/* Markdoc tags must be exported from this file to work with markdoc/nextjs plugin */ -/* Use this file to export your markdoc tags */ +import * as designSystemTags from '@pluralsh/design-system/dist/markdoc/tags' -export * from '@pluralsh/design-system/dist/markdoc/tags' -export { comment, head, script, link } from './nextjs.markdoc' +import { htmlTag } from './htmlTag.markdoc' +import { comment, head, link, script } from './nextjs.markdoc' + +export const tags = { + ...designSystemTags, + comment, + head, + script, + link, + 'html-tag': htmlTag, +} diff --git a/yarn.lock b/yarn.lock index 05021be5..d010752f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6950,6 +6950,44 @@ __metadata: languageName: node linkType: hard +"dom-serializer@npm:^2.0.0": + version: 2.0.0 + resolution: "dom-serializer@npm:2.0.0" + dependencies: + domelementtype: ^2.3.0 + domhandler: ^5.0.2 + entities: ^4.2.0 + checksum: cd1810544fd8cdfbd51fa2c0c1128ec3a13ba92f14e61b7650b5de421b88205fd2e3f0cc6ace82f13334114addb90ed1c2f23074a51770a8e9c1273acbc7f3e6 + languageName: node + linkType: hard + +"domelementtype@npm:^2.3.0": + version: 2.3.0 + resolution: "domelementtype@npm:2.3.0" + checksum: ee837a318ff702622f383409d1f5b25dd1024b692ef64d3096ff702e26339f8e345820f29a68bcdcea8cfee3531776b3382651232fbeae95612d6f0a75efb4f6 + languageName: node + linkType: hard + +"domhandler@npm:^5.0.2, domhandler@npm:^5.0.3": + version: 5.0.3 + resolution: "domhandler@npm:5.0.3" + dependencies: + domelementtype: ^2.3.0 + checksum: 0f58f4a6af63e6f3a4320aa446d28b5790a009018707bce2859dcb1d21144c7876482b5188395a188dfa974238c019e0a1e610d2fc269a12b2c192ea2b0b131c + languageName: node + linkType: hard + +"domutils@npm:^3.1.0": + version: 3.1.0 + resolution: "domutils@npm:3.1.0" + dependencies: + dom-serializer: ^2.0.0 + domelementtype: ^2.3.0 + domhandler: ^5.0.3 + checksum: e5757456ddd173caa411cfc02c2bb64133c65546d2c4081381a3bafc8a57411a41eed70494551aa58030be9e58574fcc489828bebd673863d39924fb4878f416 + languageName: node + linkType: hard + "dot-case@npm:^3.0.4": version: 3.0.4 resolution: "dot-case@npm:3.0.4" @@ -7037,6 +7075,13 @@ __metadata: languageName: node linkType: hard +"entities@npm:^4.2.0, entities@npm:^4.5.0": + version: 4.5.0 + resolution: "entities@npm:4.5.0" + checksum: 853f8ebd5b425d350bffa97dd6958143179a5938352ccae092c62d1267c4e392a039be1bae7d51b6e4ffad25f51f9617531fedf5237f15df302ccfb452cbf2d7 + languageName: node + linkType: hard + "env-paths@npm:^2.2.0": version: 2.2.1 resolution: "env-paths@npm:2.2.1" @@ -8539,6 +8584,18 @@ __metadata: languageName: node linkType: hard +"htmlparser2@npm:9.1.0": + version: 9.1.0 + resolution: "htmlparser2@npm:9.1.0" + dependencies: + domelementtype: ^2.3.0 + domhandler: ^5.0.3 + domutils: ^3.1.0 + entities: ^4.5.0 + checksum: e5f8d5193967e4a500226f37bdf2c0f858cecb39dde14d0439f24bf2c461a4342778740d988fbaba652b0e4cb6052f7f2e99e69fc1a329a86c629032bb76e7c8 + languageName: node + linkType: hard + "http-cache-semantics@npm:^4.1.0": version: 4.1.0 resolution: "http-cache-semantics@npm:4.1.0" @@ -12069,6 +12126,7 @@ __metadata: graphql: 16.6.0 honorable: 0.194.0 honorable-theme-default: 0.77.0 + htmlparser2: 9.1.0 husky: 8.0.3 immer: 10.0.2 js-yaml: 4.1.0