Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add man-page generator #125

Merged
merged 2 commits into from
Oct 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/codespell.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,4 @@ jobs:
with:
ignore_words_list: crate,raison
exclude_file: .gitignore
skip: package-lock.json
skip: package-lock.json, ./src/generators/mandoc/template.1
6 changes: 2 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,7 @@ Options:
-i, --input [patterns...] Specify input file patterns using glob syntax
-o, --output <path> Specify the relative or absolute output directory
-v, --version <semver> Specify the target version of Node.js, semver compliant (default: "v22.6.0")
-c, --changelog <url> Specify the path (file: or https://) to the CHANGELOG.md file (default:
"https://raw.githubusercontent.com/nodejs/node/HEAD/CHANGELOG.md")
-t, --target [mode...] Set the processing target modes (choices: "json-simple", "legacy-html",
"legacy-html-all")
-c, --changelog <url> Specify the path (file: or https://) to the CHANGELOG.md file (default: "https://raw.githubusercontent.com/nodejs/node/HEAD/CHANGELOG.md")
-t, --target [mode...] Set the processing target modes (choices: "json-simple", "legacy-html", "legacy-html-all", "man-page")
-h, --help display help for command
```
2 changes: 2 additions & 0 deletions src/generators/index.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@
import jsonSimple from './json-simple/index.mjs';
import legacyHtml from './legacy-html/index.mjs';
import legacyHtmlAll from './legacy-html-all/index.mjs';
import manPage from './man-page/index.mjs';

export default {
'json-simple': jsonSimple,
'legacy-html': legacyHtml,
'legacy-html-all': legacyHtmlAll,
'man-page': manPage,
};
81 changes: 81 additions & 0 deletions src/generators/man-page/index.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
'use strict';

import { writeFile, readFile } from 'node:fs/promises';
import { join } from 'node:path';

import {
convertOptionToMandoc,
convertEnvVarToMandoc,
} from './utils/converter.mjs';

/**
* This generator generates a man page version of the CLI.md file.
* See https://man.openbsd.org/mdoc.7 for the formatting.
*
* @typedef {Array<ApiDocMetadataEntry>} Input
*
* @type {import('../types.d.ts').GeneratorMetadata<Input, string>}
*/
export default {
name: 'man-page',

version: '1.0.0',

description: 'Generates the Node.js man-page.',

dependsOn: 'ast',

async generate(input, options) {
// Filter to only 'cli'.
const components = input.filter(({ api }) => api === 'cli');
if (!components.length) {
throw new Error('CLI.md not found');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could have been a more laid-out error message

}

// Find the appropriate headers
const optionsStart = components.findIndex(({ slug }) => slug === 'options');
const environmentStart = components.findIndex(
({ slug }) => slug === 'environment-variables-1'
Copy link
Member

@ovflowd ovflowd Oct 21, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should probably be a constant (including the options one above) and reference where from the CLI.md source we are looking for this.

If that file ever gets updated with this content removed, we're screwed.

);
// The first header that is <3 in depth after environmentStart
const environmentEnd = components.findIndex(
({ heading }, index) => heading.depth < 3 && index > environmentStart
);

const output = {
// Extract the CLI options.
options: extractMandoc(
components,
optionsStart + 1,
environmentStart,
convertOptionToMandoc
),
// Extract the environment variables.
env: extractMandoc(
components,
environmentStart + 1,
environmentEnd,
convertEnvVarToMandoc
),
};

const template = await readFile(
join(import.meta.dirname, 'template.1'),
'utf-8'
);

const filledTemplate = template
.replace('__OPTIONS__', output.options)
.replace('__ENVIRONMENT__', output.env);

await writeFile(options.output, filledTemplate);
},
};

function extractMandoc(components, start, end, convert) {
return components
.slice(start, end)
.filter(({ heading }) => heading.depth === 3)
.map(convert)
.join('');
}
74 changes: 74 additions & 0 deletions src/generators/man-page/template.1
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
.\"
.\" This file is automatically generated by api-docs-tooling.
.\" Do not edit this file directly. Please make changes to CLI.md
.\" and then regenerate this file.
.\"
.\" For generation instructions using api-docs-tooling, see:
.\" https://github.com/nodejs/api-docs-tooling
.\"
.\"======================================================================
.Dd $Mdocdate$
.Dt NODE 1
.
.Sh NAME
.Nm node
.Nd server-side JavaScript runtime
.
.Sh SYNOPSIS
.Nm node
.Op Ar options
.Op Ar v8 options
.Op Ar <program-entry-point> | Fl e Ar string | Fl -
.Op Ar arguments ...
.
.Nm node
.Cm inspect,
.Op Ar <program-entry-point> | Fl e Ar string | Ar <host>:<port>
.Ar ...
.
.Nm node
.Op Fl -v8-options
.
.Sh DESCRIPTION
Node.js is a set of libraries for JavaScript which allows it to be used outside of the browser.
It is primarily focused on creating simple, easy-to-build network clients and servers.
.Pp
Execute
.Nm
without arguments to start a REPL.
.
.Sh OPTIONS
.Bl -tag -width 6n
__OPTIONS__
.El
.
.Sh ENVIRONMENT
.Bl -tag -width 6n
__ENVIRONMENT__
.El
.
.Sh BUGS
Bugs are tracked in GitHub Issues:
.Sy https://github.com/nodejs/node/issues
.
.Sh COPYRIGHT
Copyright Node.js contributors.
Node.js is available under the MIT license.
.
.Pp
Node.js also includes external libraries that are available under a variety of licenses.
See
.Sy https://github.com/nodejs/node/blob/HEAD/LICENSE
for the full license text.
.
.Sh SEE ALSO
Website:
.Sy https://nodejs.org/
.
.Pp
Documentation:
.Sy https://nodejs.org/api/
.
.Pp
GitHub repository and issue tracker:
.Sy https://github.com/nodejs/node
145 changes: 145 additions & 0 deletions src/generators/man-page/utils/converter.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
/**
* Converts an Abstract Syntax Tree (AST) node to the Mandoc format for Unix manual pages.
* This function processes the node recursively, converting each supported node type
* to its corresponding Mandoc markup representation. Unsupported node types will be ignored.
*
* @param {import("mdast").Node} node - The AST node to be converted to Mandoc format.
* @param {boolean} [isListItem=false] - Indicates if the current node is a list item.
* This parameter is used to correctly format list elements in Mandoc.
* @returns {string} The Mandoc formatted string representing the given node and its children.
*/
export function convertNodeToMandoc(node, isListItem = false) {
const convertChildren = (sep = '', ili = false) =>
node.children.map(child => convertNodeToMandoc(child, ili)).join(sep);
const escapeText = () => node.value.replace(/\\/g, '\\\\');

switch (node.type) {
case 'root':
// Process the root node by converting all children and separating them with new lines.
return convertChildren('\n');

case 'heading':
// Convert to a Mandoc heading section (.Sh).
return `.Sh ${convertChildren()}`;

case 'link':
case 'paragraph':
case 'listItem':
// Convert to Mandoc paragraph or list item.
// .It denotes a list item in Mandoc, added only if the node is a list item.
return `${isListItem && node.type === 'listItem' ? '.It\n' : ''}${convertChildren()}`;

case 'text':
// Escape any special characters in plain text content.
return escapeText();

case 'inlineCode':
// Format inline code using Mandoc's bold markup (\\fB ... \\fR).
return `\\fB${escapeText()}\\fR`;

case 'strong':
// Format inline code + strong using Mandoc's bold markup (\\fB ... \\fR).
return `\\fB${convertChildren()}\\fR`;

case 'code':
// Format code blocks as literal text using .Bd -literal and .Ed for start and end.
return `.Bd -literal\n${escapeText()}\n.Ed`;

case 'list':
// Convert to a bullet list in Mandoc, starting with .Bl -bullet and ending with .El.
return `.Bl -bullet\n${convertChildren('\n', true)}\n.El`;

case 'emphasis':
// Format emphasized text in Mandoc using italic markup (\\fI ... \\fR).
return `\\fI${convertChildren()}\\fR`;

default:
// Ignore `html`, `blockquote`, etc.
return '';
}
}

/**
* Converts a command-line flag to its Mandoc representation.
* This function splits the flag into its name and optional value (if present),
* formatting them appropriately for Mandoc manual pages.
*
* @param {string} flag - The command-line flag to be formatted. It may include a value
* specified with either an equals sign (=) or a space.
* @returns {string} The Mandoc formatted representation of the flag and its value.
*/
export function flagValueToMandoc(flag) {
// The separator is '=' or ' '.
let sep = flag.match(/[= ]/)?.[0];

if (sep == null) {
// This flag does not have a default value.
return '';
}

// Split the flag into the name and value based on the separator ('=' or space).
const value = flag.split(sep)[1];

// If there is no value, return an empty string.
if (!value) {
return '';
}

// Determine the prefix based on the separator type.
const prefix = sep === ' ' ? '' : ' Ns = Ns';

// Combine prefix and formatted value.
return `${prefix} Ar ${value.replace(/\]$/, '')}`;
}

const formatFlag = flag =>
// 'Fl' denotes a flag, followed by an optional 'Ar' (argument).
`Fl ${flag.split(/[= ]/)[0].slice(1)}${flagValueToMandoc(flag)}`;

/**
* Converts an API option metadata entry into the Mandoc format.
* This function formats command-line options, including flags and descriptions,
* for display in Unix manual pages using Mandoc.
*
* @param {ApiDocMetadataEntry} element - The metadata entry containing details about the API option.
* @returns {string} The Mandoc formatted string representing the API option, including flags and content.
*/
export function convertOptionToMandoc(element) {
// Format the option flags by splitting them, removing backticks, and converting each flag.
const formattedFlags = element.heading.data.text
.replace(/`/g, '')
.split(', ')
.map(formatFlag)
.join(' , ')
.trim();

// Remove the header itself.
element.content.children.shift();

// Return the formatted flags and content, separated by Mandoc markers.
RedYetiDev marked this conversation as resolved.
Show resolved Hide resolved
return `.It ${formattedFlags}\n${convertNodeToMandoc(element.content).trim()}\n.\n`;
}

/**
* Converts an API environment variable metadata entry into the Mandoc format.
* This function formats environment variables for Unix manual pages, converting
* the variable name and value, along with any associated descriptions, into Mandoc.
*
* @param {ApiDocMetadataEntry} element - The metadata entry containing details about the environment variable.
* @returns {string} The Mandoc formatted representation of the environment variable and its content.
*/
export function convertEnvVarToMandoc(element) {
// Split the environment variable into name and optional value.
const [varName, varValue] = element.heading.data.text
.replace(/`/g, '')
.split('=');

// Format the variable value if present.
RedYetiDev marked this conversation as resolved.
Show resolved Hide resolved
const formattedValue = varValue ? ` Ar ${varValue}` : '';

// Remove the header itself.
element.content.children.shift();

// Return the formatted environment variable and content, using Mandoc's .It (List item) and .Ev (Env Var) macros.
RedYetiDev marked this conversation as resolved.
Show resolved Hide resolved
return `.It Ev ${varName}${formattedValue}\n${convertNodeToMandoc(element.content)}\n.\n`;
}
Loading
Loading