From 5564865cefe6bf255196f24ab3090f6dc14ba960 Mon Sep 17 00:00:00 2001 From: Benny Powers Date: Sun, 26 Jan 2025 15:44:10 +0200 Subject: [PATCH 1/2] refactor: element docs pages data flow --- docs/_includes/layouts/pages/element.11ty.ts | 20 +- docs/_plugins/element-docs.ts | 286 ++++++++----------- 2 files changed, 135 insertions(+), 171 deletions(-) diff --git a/docs/_includes/layouts/pages/element.11ty.ts b/docs/_includes/layouts/pages/element.11ty.ts index 405679ccdb..0c53876643 100644 --- a/docs/_includes/layouts/pages/element.11ty.ts +++ b/docs/_includes/layouts/pages/element.11ty.ts @@ -6,7 +6,8 @@ import { tokens } from '@rhds/tokens/meta.js'; import { join } from 'node:path'; import { fileURLToPath } from 'node:url'; import { readFile } from 'node:fs/promises'; -import { copyCell, dedent, getTokenHref } from '#11ty-plugins/tokensHelpers.js'; +import { capitalize, copyCell, dedent, getTokenHref } from '#11ty-plugins/tokensHelpers.js'; +import { getPfeConfig } from '@patternfly/pfe-tools/config.js'; import { Generator } from '@jspm/generator'; import { AssetCache } from '@11ty/eleventy-fetch'; import { Renderer } from '#eleventy.config'; @@ -15,6 +16,7 @@ import type { ImportMap } from '#11ty-plugins/importMap.js'; type FileEntry = [string, FileOptions & { inline: string }]; const html = String.raw; // for editor highlighting +const pfeconfig = getPfeConfig(); const { version: packageVersion } = JSON.parse(await readFile( fileURLToPath(import.meta.resolve('@rhds/elements')).replace('elements.js', 'package.json'), @@ -52,23 +54,17 @@ export default class ElementsPage extends Renderer { layout: 'layouts/pages/has-toc.njk', permalink: ({ doc }: Context) => doc.permalink, eleventyComputed: { - title: ({ doc }: Context) => doc.pageTitle, + title: ({ doc }: Context) => `${doc.pageTitle} | ${pfeconfig.aliases[doc.tagName] ?? capitalize(doc.tagName.replace('rh-', '').replaceAll('-', ' '))}`, tagName: ({ doc }: Context) => doc.tagName, }, }; } async render(ctx: Context) { - const { doc } = ctx; - const { - fileExists, - filePath, - isCodePage, - isDemoPage, - isOverviewPage, - tagName, - planned, - } = doc; + const { fileExists, filePath, pageSlug, planned, tagName } = ctx.doc; + const isCodePage = pageSlug === 'code'; + const isDemoPage = pageSlug === 'demos'; + const isOverviewPage = pageSlug === 'overview'; const content = fileExists ? await this.renderFile(filePath, ctx) : ''; const stylesheets = [ '/assets/packages/@rhds/elements/elements/rh-table/rh-table-lightdom.css', diff --git a/docs/_plugins/element-docs.ts b/docs/_plugins/element-docs.ts index acc66bdab8..9a7fd81c82 100644 --- a/docs/_plugins/element-docs.ts +++ b/docs/_plugins/element-docs.ts @@ -3,7 +3,7 @@ import type { UserConfig } from '@11ty/eleventy'; import slugify from 'slugify'; import { basename, dirname, join, sep } from 'node:path'; import { glob, readFile, readdir, stat } from 'node:fs/promises'; -import { deslugify, getPfeConfig } from '@patternfly/pfe-tools/config.js'; +import { getPfeConfig } from '@patternfly/pfe-tools/config.js'; import { getAllManifests } from '@patternfly/pfe-tools/custom-elements-manifest/custom-elements-manifest.js'; import { capitalize } from '#11ty-plugins/tokensHelpers.js'; import { DocsPage } from '@patternfly/pfe-tools/11ty/DocsPage.js'; @@ -14,21 +14,16 @@ interface ElementDocsPageTabData { url: string; /** element name slug e.g. 'call-to-action' or 'footer' */ slug: string; - /** e.g. `Code` or `Guidelines` */ - pageTitle: string; - /** e.g. 'code' or 'guidelines' */ - pageSlug: string; /** e.g. `/elements/call-to-action/code/index.html` */ permalink: string; + /** e.g. 'code' or 'guidelines' */ + pageSlug: string; filePath: string; tagName: string; } interface ElementDocsPageBasicData extends ElementDocsPageTabData { description?: string; - isCodePage: boolean; - isDemoPage: boolean; - isOverviewPage: boolean; absPath: string; /** configured alias for this element e.g. `Call to Action` for `rh-cta` */ alias?: string; @@ -84,70 +79,7 @@ function getTagNameSlug(tagName: string) { }); } -function getTabData(filePath: string): ElementDocsPageTabData { - const parts = filePath.split(sep); - const tagName = parts.find(x => x.startsWith('rh-'))!; - const pageSlug = filePath.split(sep).pop()?.split('.').shift()?.replace(/^(\d+|rh)-/, '') ?? ''; - const slug = getTagNameSlug(tagName); - const permalink = - pageSlug === 'overview' ? `/elements/${slug}/index.html` - : `/elements/${slug}/${pageSlug}/index.html`; - const pageTitle = capitalize(deslugify(pageSlug).toLowerCase().replace('rh-', '')); - return { - tagName, - filePath, - permalink, - slug, - url: permalink.replace('index.html', ''), - pageTitle, - pageSlug, - }; -} - -function getBasicData(filePath: string): ElementDocsPageBasicData { - const tabProps = getTabData(filePath); - const status = repoStatus.find(x => x.tagName === tabProps.tagName); - return { ...tabProps, - isCodePage: tabProps.pageSlug === 'code', - isDemoPage: tabProps.pageSlug === 'demos', - isOverviewPage: tabProps.pageSlug === 'overview', - absPath: join(cwd, filePath), - description: status?.description, - alias: pfeconfig.aliases?.[tabProps.tagName], - screenshotPath: `/elements/${tabProps.slug}/screenshot.png`, - overviewHref: `/elements/${tabProps.slug}/`, - }; -} - -async function getFSData(props: ElementDocsPageBasicData): Promise { - const elDir = join(cwd, 'elements', props.tagName); - const docsDir = join(elDir, 'docs'); - const demoPath = join(elDir, 'demo', `${props.tagName}.html`); - const allTSFiles = await Array.fromAsync(glob(`elements/${props.tagName}/*.ts`, { cwd })); - const hasDocs = await exists(docsDir); - const docsDirLs = hasDocs ? await readdir(docsDir) : null; - const _overviewImageHref = docsDirLs?.find(x => x.match(/overview.(png|svg)/)); - const overviewImageHref = - _overviewImageHref && await exists(join(docsDir, _overviewImageHref)) ? - _overviewImageHref : undefined; - return { - ...props, - fileExists: await exists(props.absPath), - planned: await isPlanned(props.tagName), - hidden: await isHidden(props.tagName), - hasLightdom: await exists(join(elDir, `${props.tagName}-lightdom.css`)), - hasLightdomShim: await exists(join(elDir, `${props.tagName}-lightdom-shim.css`)), - mainDemoContent: await exists(demoPath) ? await readFile(demoPath, 'utf8') : '', - overviewImageHref, - siblingElements: allTSFiles - .map(x => basename(x)) - .filter(isElementSource) - .map(stripExtension) - .filter((x): x is string => !!x && x !== props.tagName), - }; -} - -async function isPlanned(tagName: string) { +function isPlanned(tagName: string) { for (const manifest of getAllManifests()) { if (manifest.declarations.has(tagName)) { return false; @@ -157,100 +89,136 @@ async function isPlanned(tagName: string) { return element?.libraries.rhds === 'planned'; } -async function isHidden(tagName: string) { +function isHidden(tagName: string) { const element = repoStatus.find(element => element.tagName === tagName); return element?.type === 'hidden'; } -const isDocFor = (tagName: string) => - (x: string) => - x.split('/docs/').at(0) === `elements/${tagName}`; - -async function getDocsPageData( - data: ElementDocsPageFileSystemData, - docsPages: DocsPage[] | (() => Promise), - docFilePaths: string[], - getPrettyElementName: (tagName: string) => string, -): Promise { - const [manifest] = getAllManifests(); - - if (typeof docsPages === 'function') { - docsPages = await docsPages(); - } - - const docsPage = docsPages.find(x => x.tagName === data.tagName) - ?? new DocsPage(manifest); - - let tabs = docFilePaths - .filter(isDocFor(data.tagName)) - .sort() - .map(x => getTabData(x)); - - if (!data.planned) { - // Add 'virtual' demos page, it's generated by demos.11ty.cjs - const tab = tabs.find(x => x.pageSlug === 'demo'); - if (tab) { - tab.url = `/elements/${data.slug}/demos/`; - tab.pageTitle = getPrettyElementName(data.tagName); - tab.pageSlug = 'demos'; - tab.permalink = `/elements/${data.slug}/demos/index.html`; - tab.filePath = `${dirname(data.filePath)}/40-demos.md`; - tab.slug = data.slug; - tab.tagName = data.tagName; - } - } - - if (data.planned) { - tabs = tabs.filter(x => x.pageSlug !== 'code' && x.pageSlug !== 'demos'); - } - - const collection = `${data.tagName}-tabs`; - - const pageSlug = getTagNameSlug(data.tagName); - const pageTitle = capitalize(deslugify(pageSlug).toLowerCase().replace('rh-', '')); - return { - ...data, - tags: [collection], - layout: 'layouts/pages/element.11ty.ts', - subnav: { - collection, - }, - pageSlug, - pageTitle, - docsPage, - tabs, - }; -} - export default function(eleventyConfig: UserConfig) { eleventyConfig.addCollection('elementDocs', async function() { try { - const docFilePaths = await Array.fromAsync(glob(`elements/*/docs/*.md`, { cwd })); - // TODO: adding the code file in the next line is a temporary hack to add in a virtual - // `XX-code.md` and demo if one doesn't already exist. At a later date, this entire function - // (elementDocs) should be refactored, and the elements/*/docs/*.md files should be used - // only for markdown content, but the code and demos tabs should be fully generated, with - // the XX-code.md content interjected, if any. - const fakeCodeDocsPaths = await Array.fromAsync(glob('elements/*', { cwd }), x => - x.includes('.') ? [] : [ - `${x}/docs/30-code.md`, - `${x}/docs/90-demos.md`, - ]); - const filePaths = Array.from(new Set([...docFilePaths, ...fakeCodeDocsPaths.flat()])) - .sort(function alphabetically(a: string, b: string) { - return ( a < b ? -1 : a > b ? 1 : 0); - }); - const { elements } = eleventyConfig.globalData; - const getPrettyElementName = - eleventyConfig.getFilter('getPrettyElementName') as (tagname: string) => string; - const elementDocs = await Promise.all(filePaths.map(async filePath => - getDocsPageData( - await getFSData(getBasicData(filePath)), - elements as DocsPage[] | (() => Promise), - filePaths, - getPrettyElementName - ))); - return elementDocs; + const [manifest] = getAllManifests(); + + const _docsPages = + eleventyConfig.globalData.elements as DocsPage[] | (() => Promise); + + const docsPages = typeof _docsPages === 'function' ? await _docsPages() : _docsPages; + + // 1. compile a list of all sibling element names by scanning the filesystem + // 2. compile a list of all output files + // 3. assign helpful data to each page entry + // 4. compile a list of tabs for each page's subnav. + // this step depends on the full list from step 2. + // 5. assign data which relies on the filesystem (async) + + const siblingElementsByTagName = new Map(); + for await (const path of glob(`elements/*/*.ts`, { cwd })) { + const tagName = path.replace('elements/', '').split('/').shift()!; + const filebasename = basename(path); + const siblingTagName = stripExtension(filebasename); + if (siblingTagName && isElementSource(filebasename) && siblingTagName !== tagName) { + if (!siblingElementsByTagName.has(tagName)) { + siblingElementsByTagName.set(tagName, []); + } + siblingElementsByTagName.get(tagName)!.push(siblingTagName); + } + } + + return (await Promise.all( + Array.from(new Set([ + // docs file paths that exist on disk + ...await Array.fromAsync(glob(`elements/*/docs/*.md`, { cwd })), + // ensure that code and demos pages are generated, should there not be any content for them + // in elements/*/docs/*-code.md or elements/*/docs/*-demos.md. Duplicates are avoided with + // the new Set constructor + ...(await Array.fromAsync(glob('elements/*', { cwd }), x => + x.includes('.') ? [] : [ + `${x}/docs/30-code.md`, + `${x}/docs/90-demos.md`, + ])).flat(), + ])) + .sort() + .map(filePath => { + const tagName = filePath + .split(sep) + .find(x => x.startsWith('rh-'))!; + const pageSlug = filePath + .split(sep) + .pop() + ?.split('.') + ?.shift() + ?.replace(/^(\d+|rh)-/, '') ?? ''; + const slug = getTagNameSlug(tagName); + const collection = `${tagName}-tabs`; + const permalink = + pageSlug === 'overview' ? `/elements/${slug}/index.html` + : `/elements/${slug}/${pageSlug}/index.html`; + return { + filePath, + tagName, + pageSlug, + layout: 'layouts/pages/element.11ty.ts', + planned: isPlanned(tagName), + hidden: isHidden(tagName), + subnav: { collection }, + slug, + pageTitle: capitalize(pageSlug), + docsPage: docsPages.find(x => x.tagName === tagName) ?? new DocsPage(manifest), + permalink, + tags: [collection], + url: permalink.replace('index.html', ''), + absPath: join(cwd, filePath), + description: repoStatus.find(x => x.tagName === tagName)?.description, + alias: pfeconfig.aliases?.[tagName], + screenshotPath: `/elements/${slug}/screenshot.png`, + overviewHref: `/elements/${slug}/`, + }; + }) + .map((data, _, a) => ({ + ...data, + tabs: a + .filter(x => + x.tagName === data.tagName + && ( + !data.planned + || ( + x.pageSlug !== 'code' + && x.pageSlug !== 'demos' + ) + )) + .map(tab => !data.planned || tab.pageSlug !== 'demo' ? tab : ({ + // Add 'virtual' demos page, it's generated by demos.11ty.cjs + ...tab, + url: `/elements/${data.slug}/demos/`, + pageSlug: 'demos', + permalink: `/elements/${data.slug}/demos/index.html`, + filePath: `${dirname(data.filePath)}/40-demos.md`, + slug: data.slug, + tagName: data.tagName, + })) + .sort((a, b) => a.filePath > b.filePath ? 1 : -1), + })) + .map(async data => { + const elDir = join(cwd, 'elements', data.tagName); + const docsDir = join(elDir, 'docs'); + const demoPath = join(elDir, 'demo', `${data.tagName}.html`); + const hasDocs = await exists(docsDir); + const docsDirLs = hasDocs ? await readdir(docsDir) : null; + const _overviewImageHref = docsDirLs?.find(x => x.match(/overview.(png|svg)/)); + const overviewImageHref = + _overviewImageHref && await exists(join(docsDir, _overviewImageHref)) ? + _overviewImageHref : undefined; + return { + ...data, + fileExists: await exists(data.absPath), + hasLightdom: await exists(join(elDir, `${data.tagName}-lightdom.css`)), + hasLightdomShim: await exists(join(elDir, `${data.tagName}-lightdom-shim.css`)), + mainDemoContent: await exists(demoPath) ? await readFile(demoPath, 'utf8') : '', + overviewImageHref, + siblingElements: siblingElementsByTagName.get(data.tagName) ?? [], + }; + }) + )); } catch (e) { // it's important to surface this // eslint-disable-next-line no-console From 6c711978d035680ddd613ee1da5d3ddeaa083cf8 Mon Sep 17 00:00:00 2001 From: Benny Powers Date: Mon, 27 Jan 2025 08:00:04 +0200 Subject: [PATCH 2/2] refactor: mostly whitespace changes --- docs/_plugins/element-docs.ts | 58 +++++++++++++++++------------------ 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/docs/_plugins/element-docs.ts b/docs/_plugins/element-docs.ts index 9a7fd81c82..2f374f1977 100644 --- a/docs/_plugins/element-docs.ts +++ b/docs/_plugins/element-docs.ts @@ -96,21 +96,14 @@ function isHidden(tagName: string) { export default function(eleventyConfig: UserConfig) { eleventyConfig.addCollection('elementDocs', async function() { - try { - const [manifest] = getAllManifests(); - - const _docsPages = - eleventyConfig.globalData.elements as DocsPage[] | (() => Promise); - - const docsPages = typeof _docsPages === 'function' ? await _docsPages() : _docsPages; - - // 1. compile a list of all sibling element names by scanning the filesystem - // 2. compile a list of all output files - // 3. assign helpful data to each page entry - // 4. compile a list of tabs for each page's subnav. - // this step depends on the full list from step 2. - // 5. assign data which relies on the filesystem (async) + // 1. compile a list of all sibling element names by scanning the filesystem + // 2. compile a list of all output files + // 3. assign helpful data to each page entry + // 4. compile a list of tabs for each page's subnav. + // this step depends on the full list from step 2. + // 5. assign data which relies on the filesystem (async) + try { const siblingElementsByTagName = new Map(); for await (const path of glob(`elements/*/*.ts`, { cwd })) { const tagName = path.replace('elements/', '').split('/').shift()!; @@ -124,26 +117,33 @@ export default function(eleventyConfig: UserConfig) { } } - return (await Promise.all( - Array.from(new Set([ - // docs file paths that exist on disk - ...await Array.fromAsync(glob(`elements/*/docs/*.md`, { cwd })), - // ensure that code and demos pages are generated, should there not be any content for them - // in elements/*/docs/*-code.md or elements/*/docs/*-demos.md. Duplicates are avoided with - // the new Set constructor - ...(await Array.fromAsync(glob('elements/*', { cwd }), x => + const [manifest] = getAllManifests(); + + const _docsPages = + eleventyConfig.globalData.elements as DocsPage[] | (() => Promise); + + const docsPages = typeof _docsPages === 'function' ? await _docsPages() : _docsPages; + + const allFiles = Array.from(new Set([ + // docs file paths that exist on disk + ...await Array.fromAsync(glob(`elements/*/docs/*.md`, { cwd })), + // ensure that code and demos pages are generated, should there not be any content for them + // in elements/*/docs/*-code.md or elements/*/docs/*-demos.md. Duplicates are avoided with + // the new Set constructor + ...(await Array.fromAsync(glob('elements/*', { cwd }), x => x.includes('.') ? [] : [ `${x}/docs/30-code.md`, `${x}/docs/90-demos.md`, ])).flat(), - ])) + ])); + + return (await Promise.all( + allFiles .sort() .map(filePath => { - const tagName = filePath - .split(sep) - .find(x => x.startsWith('rh-'))!; - const pageSlug = filePath - .split(sep) + const pathParts = filePath.split(sep); + const tagName = pathParts.find(x => x.startsWith('rh-'))!; + const pageSlug = pathParts .pop() ?.split('.') ?.shift() @@ -217,7 +217,7 @@ export default function(eleventyConfig: UserConfig) { overviewImageHref, siblingElements: siblingElementsByTagName.get(data.tagName) ?? [], }; - }) + }) satisfies Promise[] )); } catch (e) { // it's important to surface this