qiskit-documentation/scripts/lib/api/generateToc.ts

254 lines
7.6 KiB
TypeScript

// This code is a Qiskit project.
//
// (C) Copyright IBM 2023.
//
// This code is licensed under the Apache License, Version 2.0. You may
// obtain a copy of this license in the LICENSE file in the root directory
// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
//
// Any modifications or derivative works of this code must retain this
// copyright notice, and modified files need to carry a notice indicating
// that they have been altered from the originals.
import { isEmpty, orderBy } from "lodash";
import { getLastPartFromFullIdentifier } from "../stringUtils";
import { HtmlToMdResultWithUrl } from "./HtmlToMdResult";
import { Pkg } from "./Pkg";
import type { TocGrouping } from "./TocGrouping";
export type TocEntry = {
title: string;
url?: string;
children?: TocEntry[];
};
type Toc = {
title: string;
subtitle?: string;
children: TocEntry[];
collapsed: boolean;
};
export function generateToc(pkg: Pkg, results: HtmlToMdResultWithUrl[]): Toc {
const [modules, items] = getModulesAndItems(results);
const tocModules = generateTocModules(modules);
const tocModulesByTitle = new Map(
tocModules.map((entry) => [entry.title, entry]),
);
const tocModuleTitles = Array.from(tocModulesByTitle.keys());
addItemsToModules(items, tocModulesByTitle, tocModuleTitles);
const orderedEntries = pkg.tocGrouping
? groupAndSortModules(pkg.tocGrouping, tocModulesByTitle)
: orderEntriesByTitle(tocModules);
generateOverviewPage(tocModules);
const maybeIndexPage = ensureIndexPage(pkg, orderedEntries);
if (maybeIndexPage) {
orderedEntries.unshift(maybeIndexPage);
}
const maybeReleaseNotes = generateReleaseNotesEntry(pkg);
if (maybeReleaseNotes) {
orderedEntries.push(maybeReleaseNotes);
}
return {
title: pkg.title,
children: orderedEntries,
collapsed: true,
};
}
function getModulesAndItems(
results: HtmlToMdResultWithUrl[],
): [HtmlToMdResultWithUrl[], HtmlToMdResultWithUrl[]] {
const resultsWithName = results.filter(
(result) => !isEmpty(result.meta.apiName),
);
const modules = resultsWithName.filter(
(result) => result.meta.apiType === "module",
);
const items = resultsWithName.filter(
(result) =>
result.meta.apiType === "class" ||
result.meta.apiType === "function" ||
result.meta.apiType === "exception",
);
return [modules, items];
}
function generateTocModules(modules: HtmlToMdResultWithUrl[]): TocEntry[] {
return modules.map(
(module): TocEntry => ({
title: module.meta.apiName!,
// Remove the final /index from the url
url: module.url.replace(/\/index$/, ""),
}),
);
}
function addItemsToModules(
items: HtmlToMdResultWithUrl[],
tocModulesByTitle: Map<string, TocEntry>,
tocModuleTitles: string[],
) {
for (const item of items) {
if (!item.meta.apiName) continue;
const itemModuleTitle = findClosestParentModules(
item.meta.apiName,
tocModuleTitles,
);
if (itemModuleTitle) {
const itemModule = tocModulesByTitle.get(itemModuleTitle) as TocEntry;
if (!itemModule.children) itemModule.children = [];
const itemTocEntry: TocEntry = {
title: getLastPartFromFullIdentifier(item.meta.apiName!),
url: item.url,
};
itemModule.children.push(itemTocEntry);
}
}
}
/** Group the modules into the user-defined tocGrouping, which defines the order
* of the top-level entries in the ToC.
*
* Within each TocGrouping.Section, this function will also sort the modules alphabetically.
*/
function groupAndSortModules(
tocGrouping: TocGrouping,
tocModulesByTitle: Map<string, TocEntry>,
): TocEntry[] {
// First, record every valid section and top-level module.
const topLevelModuleIds = new Set<string>();
const sectionsToModules = new Map<string, TocEntry[]>();
tocGrouping.entries.forEach((entry) => {
if (entry.kind === "module") {
topLevelModuleIds.add(entry.moduleId);
} else {
sectionsToModules.set(entry.name, []);
}
});
// Go through each module in use and ensure it is either a top-level module
// or assign it to its section.
for (const tocModule of tocModulesByTitle.values()) {
if (topLevelModuleIds.has(tocModule.title)) continue;
const section = tocGrouping.moduleToSection(tocModule.title);
if (!section) {
throw new Error(
`Unrecognized module '${tocModule.title}'. It must either be listed as a module in TocGrouping.entries or be matched in TocGrouping.moduleToSection().`,
);
}
const sectionModules = sectionsToModules.get(section);
if (!sectionModules) {
throw new Error(
`Unknown section '${section}' set for the module '${tocModule.title}'. This means TocGrouping.moduleToSection() is not aligned with TocGrouping.entries`,
);
}
sectionModules.push(tocModule);
}
// Finally, create the ToC by using the ordering from moduleGrouping.entries.
// Note that moduleGrouping.entries might be a superset of the modules/sections
// actually in use for the API version, so we sometimes skip adding individual
// entries to the final result.
const result: TocEntry[] = [];
tocGrouping.entries.forEach((entry) => {
if (entry.kind === "module") {
const module = tocModulesByTitle.get(entry.moduleId);
if (!module) return;
module.title = entry.title;
result.push(module);
} else {
const modules = sectionsToModules.get(entry.name);
if (!modules || modules.length === 0) return;
result.push({
title: entry.name,
// Within a section, sort alphabetically.
children: orderEntriesByTitle(modules),
});
}
});
return result;
}
/**
* Create a new TocEntry pointing to the index page if is not already there.
*
* Certain APIs like Runtime and Provider do not have the index page included,
* whereas Qiskit SDK already does.
*/
function ensureIndexPage(
pkg: Pkg,
tocModules: TocEntry[],
): TocEntry | undefined {
const docsFolder = pkg.outputDir("/");
return tocModules.some((entry) => entry.url === docsFolder)
? undefined
: {
title: "API index",
url: docsFolder,
};
}
function generateOverviewPage(tocModules: TocEntry[]): void {
for (const tocModule of tocModules) {
if (tocModule.children && tocModule.children.length > 0) {
tocModule.children = [
{ title: "Module overview", url: tocModule.url },
...orderEntriesByChildrenAndTitle(tocModule.children),
];
delete tocModule.url;
}
}
}
function generateReleaseNotesEntry(pkg: Pkg): TocEntry | undefined {
if (!pkg.releaseNotesConfig.enabled) return;
const releaseNotesUrl = `/api/${pkg.name}/release-notes`;
const releaseNotesEntry: TocEntry = {
title: "Release notes",
};
if (pkg.hasSeparateReleaseNotes()) {
releaseNotesEntry.children =
pkg.releaseNotesConfig.separatePagesVersions.map((vers) => ({
title: vers,
url: `${releaseNotesUrl}/${vers}`,
}));
} else {
releaseNotesEntry.url = releaseNotesUrl;
}
return releaseNotesEntry;
}
function findClosestParentModules(
id: string,
possibleParents: string[],
): string | undefined {
const idParts = id.split(".");
for (let i = idParts.length - 1; i > 0; i--) {
const testId = idParts.slice(0, i).join(".");
if (possibleParents.includes(testId)) {
return testId;
}
}
}
function orderEntriesByTitle(items: TocEntry[]): TocEntry[] {
return orderBy(items, [(item) => item.title.toLowerCase()]);
}
function orderEntriesByChildrenAndTitle(items: TocEntry[]): TocEntry[] {
return orderBy(items, [
(item) => (isEmpty(item.children) ? 0 : 1),
(item) => item.title.toLowerCase(),
]);
}