qiskit-documentation/scripts/js/commands/checkPatternsIndex.ts

205 lines
5.8 KiB
TypeScript
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// This code is a Qiskit project.
//
// (C) Copyright IBM 2024.
//
// 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 { readFile } from "fs/promises";
import type { TocEntry } from "../lib/api/generateToc.js";
const IGNORED_URLS: string[] = ["/guides/qiskit-addons"];
const INDEX_PAGES = [
"docs/guides/map-problem-to-circuits.mdx",
"docs/guides/optimize-for-hardware.mdx",
"docs/guides/execute-on-hardware.mdx",
"docs/guides/post-process-results.mdx",
"docs/guides/intro-to-patterns.mdx",
];
const TOC_PATH = "docs/guides/_toc.json";
async function getIndexEntries(indexPath: string): Promise<string[]> {
const rawIndex = await readFile(indexPath, "utf-8");
const result: string[] = [];
// The index page has several unordered lists starting with *, each with links to other pages.
// We want to get every slug from those lists. Note that we don't care which list the slugs show
// up in - we only care if the slug shows up at all anywhere.
for (const line of rawIndex.split("\n")) {
if (!line.trimStart().startsWith("* ")) {
continue;
}
const slug = extractPageSlug(line);
if (slug) {
result.push(slug);
}
}
return result;
}
function extractPageSlug(text: string): string | undefined {
const re = /\((.*)\)/gm;
// Ex: '* [Circuit library](./circuit-library)'.
const match = re.exec(text);
if (!match) {
// Nested sections don't have any link
return undefined;
}
const pageSlug = match[1];
if (pageSlug.startsWith("http") || pageSlug.startsWith("/")) {
return pageSlug;
}
const page = pageSlug.split("/").pop();
return `/guides/${page}`;
}
function getTocSectionPageNames(sectionNode: TocEntry): string[] {
let results = [];
if (sectionNode.url) {
results.push(sectionNode.url);
}
if (sectionNode.children) {
for (const child of sectionNode.children) {
results.push(...getTocSectionPageNames(child));
}
}
return results;
}
async function getToolsTocEntriesToCheck(): Promise<string[]> {
const toc = JSON.parse(await readFile(TOC_PATH, "utf-8"));
const toolsNode = toc.children.find(
(child: TocEntry) => child.title == "Tools",
);
const toolsPages = getTocSectionPageNames(toolsNode);
return toolsPages.filter((page) => !IGNORED_URLS.includes(page));
}
async function deduplicateEntries(
filePath: string,
entries: string[],
): Promise<[Set<string>, string[]]> {
const deduplicatedPages: Set<string> = new Set();
const errors: string[] = [];
for (const entry of entries) {
if (deduplicatedPages.has(entry)) {
errors.push(`${filePath}: The entry ${entry} is duplicated`);
} else {
deduplicatedPages.add(entry);
}
}
return [deduplicatedPages, errors];
}
function getExtraIndexPagesErrors(
indexPage: string,
indexEntries: Set<string>,
toolsEntries: Set<string>,
): string[] {
return [...indexEntries]
.filter((page) => !toolsEntries.has(page))
.map(
(page) =>
`${indexPage}: The entry ${page} doesn't appear in the \`Tools\` menu.`,
);
}
function getExtraToolsEntriesErrors(
remainingToolsEntries: Set<string>,
): string[] {
return [...remainingToolsEntries].map(
(page) => `❌ The entry ${page} is not present on any index page`,
);
}
function maybePrintErrorsAndFail(
duplicatesErrors: string[],
extraIndexEntriesErrors: string[],
extraToolsEntriesErrors: string[],
): void {
let allGood = true;
if (duplicatesErrors.length > 0) {
duplicatesErrors.forEach((error) => console.error(error));
console.error(
`\nRemove all duplicated entries on the indices and in the Tools menu, which is set in docs/guides/_toc.json.`,
);
console.error("--------\n");
allGood = false;
}
if (extraIndexEntriesErrors.length > 0) {
extraIndexEntriesErrors.forEach((error) => console.error(error));
console.error(
`\nMake sure all pages have an entry in the Tools menu, which is set in docs/guides/_toc.json.`,
);
console.error("--------\n");
allGood = false;
}
if (extraToolsEntriesErrors.length > 0) {
extraToolsEntriesErrors.forEach((error) => console.error(error));
console.error(
"\nAdd the entries in one of the following index pages, or add the URL to the `IGNORED_URLS` list at the beginning of `/scripts/js/commands/checkPatternsIndex.tsx` if it's not used in Workflow:",
);
INDEX_PAGES.forEach((index) => console.error(`\t➡ ${index}`));
allGood = false;
}
if (!allGood) {
process.exit(1);
}
}
async function main() {
const toolsAllEntries = await getToolsTocEntriesToCheck();
let [toolsEntries, duplicatesErrors] = await deduplicateEntries(
TOC_PATH,
toolsAllEntries,
);
let extraIndexEntriesErrors: string[] = [];
for (const indexPage of INDEX_PAGES) {
const indexAllEntries = await getIndexEntries(indexPage);
let [indexEntries, indexDuplicatedErrors] = await deduplicateEntries(
indexPage,
indexAllEntries,
);
duplicatesErrors.push(...indexDuplicatedErrors);
extraIndexEntriesErrors.push(
...getExtraIndexPagesErrors(indexPage, indexEntries, toolsEntries),
);
toolsEntries.forEach((page) => {
if (indexEntries.has(page)) {
toolsEntries.delete(page);
}
});
}
const extraToolsEntriesErrors = getExtraToolsEntriesErrors(toolsEntries);
maybePrintErrorsAndFail(
duplicatesErrors,
extraIndexEntriesErrors,
extraToolsEntriesErrors,
);
console.log("\n✅ No missing or duplicated pages were found\n");
}
main().then(() => process.exit());