react-map-gl/website/write-heading-ids.ts
2023-05-25 18:19:51 -07:00

145 lines
5.2 KiB
TypeScript

/**
* This script adds custom heading ids to all files in the docs directory.
* The headings in our API reference may have format such as
+ ### `opacity` (Number, optional)
+ ### `opacity?: number`
+ ### `getPosition` ([Function](../../developer-guide/using-layers.md#accessors), optional) ![transition-enabled](https://img.shields.io/badge/transition-enabled-green.svg?style=flat-square")
+ ### `needsRedraw(options?: {clearRedrawFlags?: boolean}): string | false`
* The default generated hash id by Docusaurus will be something like:
+ #-opacity-number-optional
+ #-opacity-number-
+ #-getposition-functiondeveloper-guideusing-layersmdaccessors-optional-transition-enabledhttpsimgshieldsiobadgetransition-enabled-greensvgstyleflat-square
+ #-needsredraw-options-clearredrawflags-boolean-string-false-
* They are long, difficult to read, subject to how the documentation is written, and when the call signature changes.
* This script appends a custom id tag to the end of such headings, so that their hash ids will look like:
+ #opacity
+ #opacity
+ #getposition
+ #needsredraw
*/
const fs = require('fs/promises');
const path = require('path');
const docsDir: string = path.resolve(__dirname, '../docs');
/** Should match if the line is a header */
const headerTest = /^(#+)\s+(?<headerContent>.*?)\s*(?<customId>\{#[\w\-]+\})?$/;
/** Should match if the header describes an API */
const apiTest = /^`((?<code>\w+)[^`]*)`(\s*\(.*?\)|:.*?|$)/;
test();
main();
/** Test that getCustomId handles different formats correctly */
function test() {
expect(
getCustomId(
`The line width of each object, in units specified by widthUnits (default pixels). `
)?.[2],
undefined,
'not header'
);
expect(getCustomId(`## Learning deck.gl`)?.[2], undefined, 'does not contain code');
expect(getCustomId(`## Changes to \`TileLayer\``)?.[2], undefined, 'is not api');
expect(getCustomId(`## \`pickObjects\``)?.[2], '{#pickobjects}', 'single word api');
expect(getCustomId(`## \`@deck.gl/extensions\``)?.[2], undefined, 'Package name');
expect(
getCustomId(`## \`strokeOpacity\` (Number)`)?.[2],
'{#strokeopacity}',
'with type annotation 1'
);
expect(
getCustomId(`## \`backgroundPadding:number[]\``)?.[2],
'{#backgroundpadding}',
'with type annotation 2'
);
expect(
getCustomId(`## \`onBeforeRender(gl: WebGLRenderingContext)\``)?.[2],
'{#onbeforerender}',
'with call signature'
);
expect(
getCustomId(
`##### \`getWidth\` ([Function](../../developer-guide/using-layers.md#accessors)|Number, optional) ![transition-enabled](https://img.shields.io/badge/transition-enabled-green.svg?style=flat-square")`
)?.[2],
'{#getwidth}',
'with extra flag'
);
console.log('All tests pass!\n');
}
/** Traverse all files in the docs directory, append custom ids if necessary */
async function main() {
const files = await listFiles(docsDir, '.md');
for (const f of files) {
await processFile(f);
}
}
/** Parse a single line of text in a .md file.
* @returns new content in 3 parts if custom id is needed, null otherwise.
*/
function getCustomId(line: string): [hash: string, headerContent: string, customId: string] | null {
const m = line.trim().match(headerTest);
if (!m) {
return null;
}
const m1 = m.groups!.headerContent.match(apiTest);
if (!m1) {
return null;
}
const customId = m1.groups!.code.toLowerCase();
return [m[1], m[2], `{#${customId}}`];
}
/** Process a md file. Rewrites the file content if custom ids are needed. */
async function processFile(path: string): Promise<void> {
const context = await fs.readFile(path, {encoding: 'utf-8'});
let changed = false;
const lines = context.split('\n').map(line => {
const customId = getCustomId(line);
if (customId) {
changed = true;
return customId.join(' ');
}
return line;
});
if (changed) {
console.log(path);
await fs.writeFile(path, lines.join('\n'));
}
}
/** Recursively get all files within the given directory. */
async function listFiles(path: string, extension: string): Promise<string[]> {
const result: string[] = [];
const items = await fs.readdir(path);
for (const item of items) {
const itemPath = `${path}/${item}`;
const info = await fs.lstat(itemPath);
if (item.endsWith(extension) && info.isFile()) {
result.push(itemPath);
} else if (info.isDirectory()) {
const files = await listFiles(itemPath, extension);
result.push(...files);
}
}
return result;
}
/* Test utils */
function expect(value1: any, value2: any, message: string) {
if (value1 === value2) {
console.log('Ok', message, value2 || '');
} else {
console.log('Not ok', message);
throw new Error(`Expect ${value2}, got ${value1}`);
}
}