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
` +
- `Version | Changes |
` +
- `${mapChangesIntoTable.reverse().join('\n')}
`;
+ // 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 });