From 71c39f71d5e4fb1bd64ef3d179f5b2e9c3644296 Mon Sep 17 00:00:00 2001 From: Claudio Wunder Date: Sat, 10 Aug 2024 16:04:54 +0200 Subject: [PATCH] chore: more code refactors and cleanups --- package-lock.json | 24 ++- package.json | 5 +- shiki.config.mjs | 12 +- src/generators/legacy-html/assets/style.css | 2 +- .../legacy-html/utils/buildContent.mjs | 162 +++++++++--------- src/loader.mjs | 4 +- src/parser.mjs | 19 +- src/queries.mjs | 35 ++++ src/utils/parser.mjs | 8 +- src/utils/remark.mjs | 11 +- 10 files changed, 172 insertions(+), 110 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0465b20..5a9d702 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "commander": "^12.1.0", "github-slugger": "^2.0.0", "glob": "^11.0.0", + "hastscript": "^9.0.0", "html-minifier-terser": "^7.2.0", "rehype-stringify": "^10.0.0", "remark": "^15.0.1", @@ -1370,6 +1371,23 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/hast-util-from-parse5/node_modules/hastscript": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-8.0.0.tgz", + "integrity": "sha512-dMOtzCEd3ABUeSIISmrETiKuyydk1w0pa+gE/uormcTpSYuaNJPbX1NU3JLyscSLjwAQM8bWMhhIlnCqnRvDTw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-parse-selector": "^4.0.0", + "property-information": "^6.0.0", + "space-separated-tokens": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/hast-util-parse-selector": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-4.0.0.tgz", @@ -1478,9 +1496,9 @@ } }, "node_modules/hastscript": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-8.0.0.tgz", - "integrity": "sha512-dMOtzCEd3ABUeSIISmrETiKuyydk1w0pa+gE/uormcTpSYuaNJPbX1NU3JLyscSLjwAQM8bWMhhIlnCqnRvDTw==", + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-9.0.0.tgz", + "integrity": "sha512-jzaLBGavEDKHrc5EfFImKN7nZKKBdSLIdGvCwDZ9TfzbF2ffXiov8CKE445L2Z1Ek2t/m4SKQ2j6Ipv7NyUolw==", "license": "MIT", "dependencies": { "@types/hast": "^3.0.0", diff --git a/package.json b/package.json index 71f0154..dcdb911 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,9 @@ "test": "node --test", "test:watch": "node --test --watch", "test:coverage": "node --experimental-test-coverage --test", - "prepare": "husky" + "prepare": "husky", + "run": "node bin/cli.mjs", + "watch": "node --watch bin/cli.mjs" }, "bin": { "api-docs-tooling": "./bin/cli.mjs" @@ -28,6 +30,7 @@ "commander": "^12.1.0", "github-slugger": "^2.0.0", "glob": "^11.0.0", + "hastscript": "^9.0.0", "html-minifier-terser": "^7.2.0", "rehype-stringify": "^10.0.0", "remark": "^15.0.1", diff --git a/shiki.config.mjs b/shiki.config.mjs index 3d288e4..b992804 100644 --- a/shiki.config.mjs +++ b/shiki.config.mjs @@ -1,6 +1,6 @@ 'use strict'; -import { u as createTree } from 'unist-builder'; +import { h as createElement } from 'hastscript'; import { getWasmInstance } from '@shikijs/core/wasm-inlined'; @@ -21,11 +21,11 @@ import shikiNordTheme from 'shiki/themes/nord.mjs'; // Creates a static button element which is used for the "copy" button // within codeboxes for copying the code to the clipboard -const copyButtonElement = createTree('element', { - tagName: 'button', - properties: { class: 'copy-button' }, - children: [createTree('text', 'copy')], -}); +const copyButtonElement = createElement( + 'button', + { class: 'copy-button' }, + createElement('text', 'copy') +); /** * @TODO: Use `shiki.config.mjs` from nodejs/nodejs.org diff --git a/src/generators/legacy-html/assets/style.css b/src/generators/legacy-html/assets/style.css index 06fe461..e86e7a4 100644 --- a/src/generators/legacy-html/assets/style.css +++ b/src/generators/legacy-html/assets/style.css @@ -668,7 +668,7 @@ tt, } .api_stability code { - background-color: rgb(0 0 0 / 10%); + background-color: rgb(0 0 0 / 10%) !important; } a code { diff --git a/src/generators/legacy-html/utils/buildContent.mjs b/src/generators/legacy-html/utils/buildContent.mjs index dd2a608..4ae6be2 100644 --- a/src/generators/legacy-html/utils/buildContent.mjs +++ b/src/generators/legacy-html/utils/buildContent.mjs @@ -1,29 +1,30 @@ 'use strict'; +import { h as createElement } from 'hastscript'; import { u as createTree } from 'unist-builder'; -import { - DOC_API_STABILITY_SECTION_REF_URL, - DOC_NODE_BLOB_BASE_URL, -} from '../../../constants.mjs'; - -// The regular expression to match the Stability Index prefix -const STABILITY_INDEX_PREFIX_REGEX = /Stability: ([0-5])/; +import { DOC_NODE_BLOB_BASE_URL } from '../../../constants.mjs'; /** * Builds a Markdown heading for a given node * * @param {ApiDocMetadataEntry} node The node to build the Markdown heading for + * @returns {import('unist').Parent} The HTML AST tree of the heading content */ -const buildNodeHeading = node => { +const buildHeadingElement = node => { const [, headingId] = node.slug.split('#'); - // Creates the anchor element for the heading (floats on the right side) - const anchor = `#`; + // Creates the element that references the link to the heading + const headingLinkElement = createElement( + 'span', + createElement('a.mark#headingId', { href: `#${headingId}` }, '#') + ); - // The extra `\n` (new lines) are done to ensure that remark parses Markdown within HTML - // as it would normally do, as it would not parse it if it were in the same line - return `\n\n${node.heading.text} ${anchor}\n\n`; + // Creates the heading element with the heading text and the link to the heading + return createElement(`h${node.heading.depth + 1}`, [ + node.heading.text, + headingLinkElement, + ]); }; /** @@ -31,79 +32,78 @@ const buildNodeHeading = node => { * * @param {ApiDocMetadataEntry} node The node to build the Markdown Stability Index for * @param {import('remark').remark} remark The Remark instance to be used to process + * @returns {import('unist').Parent} The AST tree of the Stability Index content */ -const buildStabilityIndex = (node, remark) => { - // Iterates through the Stability Indexes to build the content - return node.stability.children.reduce((acc, stabilityNode) => { - // Transforms the Stability Index AST nodes into a HTML string - let stabilityIndex = remark.stringify(remark.runSync(stabilityNode)); - - // If the current node being processed is not the documentation node, we - // define a link to the Stability Index section in the documentation - // as they are originally referenced within the `documentation` node. - if (node.api !== 'documentation') { - stabilityIndex = stabilityIndex.replace( - STABILITY_INDEX_PREFIX_REGEX, - match => `${match}` - ); - } - - // Wraps the inner content with an HTML div to apply the CSS styles - acc += `
${stabilityIndex}
`; - - return acc; - }, ''); +const buildStabilityIndexes = (node, remark) => { + // Iterates over each stability index to create a `div` element with the stability index class + const parsedStabilityIndexes = node.stability.children.map(stabilityNode => + createElement( + // Creates the `div` element with the class `api_stability` and the stability index class + `div.api_stability.api_stability_${stabilityNode.data.index}`, + // Processed the Markdown nodes into HTML nodes + remark.runSync(stabilityNode).children + ) + ); + + // Creates a tree to surround the Stability Indexes + return createTree('root', parsedStabilityIndexes); }; /** * Builds the Metadata Properties into content * * @param {ApiDocMetadataEntry} node The node to build to build the properties from + * @returns {import('unist').Parent} The HTML AST tree of the properties content */ -const buildProperties = node => { - let metadataSection = '
'; +const buildMetadataElement = node => { + const metadataElement = createElement('div.api_metadata'); // We use a `span` element to display the source link as a clickable link to the source within Node.js if (node.sourceLink && node.sourceLink.length) { - metadataSection += - `Source Code: ` + - `${node.sourceLink}` + - ``; + // Creates the source link URL with the base URL and the source link + const sourceLink = `${DOC_NODE_BLOB_BASE_URL}${node.sourceLink}`; + + // Creates the source link element with the source link and the source link text + const sourceLinkElement = createElement('span', [ + createElement('b', 'Source Code: '), + createElement('a', { href: sourceLink }, node.sourceLink), + ]); + + // Appends the source link element to the metadata element + metadataElement.children.push(sourceLinkElement); } // If there are changes, we create a `details` element with a `table` element to display the changes // Differently from the old API docs, on this version we always enforce a table to display the changes if (node.changes && node.changes.length) { - const mapChangesIntoTable = node.changes.map( - ({ version, description }) => - // We use double `\n` to ensure that the Markdown is correctly parsed - `${version.join(', ')}\n\n${description}\n\n` + // Maps the changes into a `tr` element with the version and the description + const mappedHistoryEntries = node.changes.map(({ version, description }) => + createElement('tr', [ + createElement('td', version.join(', ')), + createElement('td', description), + ]) ); - metadataSection += - `
History` + - `` + - `${mapChangesIntoTable.reverse().join('\n')}
VersionChanges
`; + // Creates the history details element with a summary and a table with the changes + const historyDetailsElement = createElement('details.changelog', [ + createElement('summary', 'History'), + createElement('table', [ + createElement('thead', [ + createElement('tr', [ + createElement('th', 'Version'), + createElement('th', 'Changes'), + ]), + ]), + createElement('tbody', mappedHistoryEntries), + ]), + ]); + + // Appends the history details element to the metadata element + metadataElement.children.push(historyDetailsElement); } - metadataSection += '
'; - - return metadataSection; -}; - -/** - * Builds the aggregated Markdown metadata content for a given node - * - * @param {ApiDocMetadataEntry} node Builds the content of a given node - * @param {import('remark').remark} remark The Remark instance to be used to process - */ -const buildMetadata = (node, remark) => { - const heading = buildNodeHeading(node); - const stabilityIndeex = buildStabilityIndex(node, remark); - const properties = buildProperties(node); - - // Concatenates all the strings and parses with remark into an AST tree - return remark.parse(`${heading}${stabilityIndeex}${properties}`); + // Parses and processes the mixed Markdonw/HTML content into an HTML AST tree + return metadataElement; }; /** @@ -116,23 +116,23 @@ export default (nodes, remark) => { 'root', // Parses the metadata pieces of each node and the content nodes.map(node => { - // Parses the metadata into AST, since it they are strings - const parsedMetadata = buildMetadata(node, remark); - - // aggregate the two AST trees to then be parsed by runSync - return createTree('root', [parsedMetadata, node.content]); + const headingElement = buildHeadingElement(node); + const metadataElement = buildMetadataElement(node); + const stabilityIndexes = buildStabilityIndexes(node, remark); + + // Processes the Markdown AST tree into an HTML AST tree + const processedContent = remark.runSync(node.content); + + // Concatenates all the strings and parses with remark into an AST tree + return createElement('section', [ + headingElement, + metadataElement, + ...stabilityIndexes.children, + ...processedContent.children, + ]); }) ); - // Processes the nodes to ensure that the Markdown is correctly parsed - const processedNodes = remark.runSync(parsedNodes); - - // We transform the outer `div` elements into `section` elements - // This is done to ensure that each section is an actual section in the HTML - processedNodes.children.forEach(node => { - node.tagName = node.tagName === 'div' ? 'section' : node.tagName; - }); - // Stringifies the processed nodes to return the final Markdown content - return remark.stringify(processedNodes); + return remark.stringify(parsedNodes); }; diff --git a/src/loader.mjs b/src/loader.mjs index 8dcd12f..b3ae300 100644 --- a/src/loader.mjs +++ b/src/loader.mjs @@ -27,9 +27,9 @@ const createLoader = () => { ); return resolvedFiles.map(async filePath => { - const fileBuffer = await readFile(filePath); + const fileContents = await readFile(filePath, 'utf-8'); - return new VFile({ path: filePath, value: fileBuffer }); + return new VFile({ path: filePath, value: fileContents }); }); }; diff --git a/src/parser.mjs b/src/parser.mjs index 3deab4d..91ded03 100644 --- a/src/parser.mjs +++ b/src/parser.mjs @@ -9,7 +9,6 @@ import { SKIP, visit } from 'unist-util-visit'; import createMetadata from './metadata.mjs'; import createQueries from './queries.mjs'; -import { transformTypeToReferenceLink } from './utils/parser.mjs'; import { getRemark } from './utils/remark.mjs'; import { createNodeSlugger } from './utils/slugger.mjs'; @@ -27,6 +26,8 @@ const createParser = () => { addYAMLMetadata, addHeadingMetadata, addStabilityIndexMetadata, + updateTypesToMarkdownLinks, + updateStailityPrefixToMarkdownLinks, } = createQueries(); /** @@ -47,22 +48,18 @@ const createParser = () => { */ const metadataCollection = []; + // Creates a new Slugger instance for the current API doc file + const nodeSlugger = createNodeSlugger(); + // We allow the API doc VFile to be a Promise of a VFile also, // hence we want to ensure that it first resolves before we pass it to the parser const resolvedApiDoc = await Promise.resolve(apiDoc); // Normalizes all the types in the API doc file to be reference links - // which needs to be done before the actual processing is done - // since we're replacing raw text within the Markdown - // @TODO: This could be moved to another place responsible for handling - // text substitutions at the beginning of the parsing process (as dependencies) - resolvedApiDoc.value = String(resolvedApiDoc.value).replaceAll( - createQueries.QUERIES.normalizeTypes, - transformTypeToReferenceLink - ); + updateTypesToMarkdownLinks(resolvedApiDoc); - // Creates a new Slugger instance for the current API doc file - const nodeSlugger = createNodeSlugger(); + // Normalizes all the Stability Index prefixes with Markdown links + updateStailityPrefixToMarkdownLinks(resolvedApiDoc); // Parses the API doc into an AST tree using `unified` and `remark` const apiDocTree = remarkProcessor.parse(resolvedApiDoc); diff --git a/src/queries.mjs b/src/queries.mjs index 1c7c7d2..064110c 100644 --- a/src/queries.mjs +++ b/src/queries.mjs @@ -1,5 +1,7 @@ 'use strict'; +import { DOC_API_STABILITY_SECTION_REF_URL } from './constants.mjs'; + import * as parserUtils from './utils/parser.mjs'; import { transformNodesToString } from './utils/unist.mjs'; @@ -104,12 +106,43 @@ const createQueries = () => { } }; + /** + * Updates type links `{types}` into Markdown links referencing to the correct + * API docs (either MDN or other sources) for the types + * + * @param {import('vfile').VFile} vfile The source Markdown file before any modifications + */ + const updateTypesToMarkdownLinks = vfile => { + // The `vfile` value is a String (check `loaders.mjs`) + vfile.value = vfile.value.replaceAll( + createQueries.QUERIES.normalizeTypes, + parserUtils.transformTypeToReferenceLink + ); + }; + + /** + * Updates the Stability Index Prefixes to be Markdown Links + * to the API documentation + * + * @param {import('vfile').VFile} vfile The source Markdown file before any modifications + */ + const updateStailityPrefixToMarkdownLinks = vfile => { + if (vfile.basename !== 'documentation.md') { + vfile.value = vfile.value.replaceAll( + createQueries.QUERIES.stabilityIndexPrefix, + match => `[${match}](${DOC_API_STABILITY_SECTION_REF_URL})` + ); + } + }; + return { addYAMLMetadata, addHeadingMetadata, updateMarkdownLink, updateLinkReference, addStabilityIndexMetadata, + updateTypesToMarkdownLinks, + updateStailityPrefixToMarkdownLinks, }; }; @@ -122,6 +155,8 @@ createQueries.QUERIES = { normalizeTypes: /(\{|<)(?! )[a-z0-9.| \n\[\]\\]+(?! )(\}|>)/gim, // ReGeX for handling Stability Indexes Metadata stabilityIndex: /^Stability: ([0-5])(?:\s*-\s*)?(.*)$/s, + // ReGeX for handling the Stability Index Prefix + stabilityIndexPrefix: /Stability: ([0-5])/gi, // ReGeX for retrieving the inner content from a YAML block yamlInnerContent: /^/, }; diff --git a/src/utils/parser.mjs b/src/utils/parser.mjs index 75d63c7..ca31416 100644 --- a/src/utils/parser.mjs +++ b/src/utils/parser.mjs @@ -55,7 +55,7 @@ export const transformTypeToReferenceLink = type => { return DOC_TYPES_MAPPING_NODE_MODULES[lookupPiece]; } - return undefined; + return ''; }; const typePieces = typeInput.split('|').map(piece => { @@ -65,7 +65,11 @@ export const transformTypeToReferenceLink = type => { // This is what we will compare against the API types mappings const result = transformType(trimmedPiece.replace(/(?:\[])+$/, '')); - return result && `[\`<${trimmedPiece}>\`](${result})`; + if (trimmedPiece.length && result.length) { + return `[\`<${trimmedPiece}>\`](${result})`; + } + + return ''; }); // Filter out pieces that we failed to map and then join the valid ones diff --git a/src/utils/remark.mjs b/src/utils/remark.mjs index bf1792c..dab1d60 100644 --- a/src/utils/remark.mjs +++ b/src/utils/remark.mjs @@ -1,20 +1,25 @@ 'use strict'; -import { remark } from 'remark'; +import { unified } from 'unified'; + import remarkGfm from 'remark-gfm'; +import remarkParse from 'remark-parse'; import remarkRehype from 'remark-rehype'; +import remarkStringify from 'remark-stringify'; import rehypeStringify from 'rehype-stringify'; import syntaxHighlighter from './highlighter.mjs'; // Retrieves an instance of Remark configured to parse GFM (GitHub Flavored Markdown) -export const getRemark = () => remark().use(remarkGfm); +export const getRemark = () => + unified().use(remarkParse).use(remarkGfm).use(remarkStringify); // Retrieves an instance of Remark configured to output stringified HTML code // including parsing Code Boxes with syntax highlighting export const getRemarkRehype = () => - getRemark() + unified() + .use(remarkParse) .use(remarkRehype, { allowDangerousHtml: true }) .use(syntaxHighlighter) .use(rehypeStringify, { allowDangerousHtml: true });