myst-cli
Version:
Command line tools for MyST
320 lines (319 loc) • 9.82 kB
JavaScript
import { z } from 'zod';
import { resolveExtension } from '../../utils/resolveExtension.js';
import { join, relative } from 'node:path';
import { cwd } from 'node:process';
const TOCTreeOptions = z
.object({
caption: z.string(),
hidden: z.boolean(),
maxdepth: z.number(),
numbered: z.boolean(),
reversed: z.boolean(),
titlesonly: z.boolean(),
})
.partial();
const FileEntry = z.object({
file: z.string(),
title: z.string().optional(),
});
const URLEntry = z.object({
url: z.string(),
title: z.string().optional(),
});
const GlobEntry = z.object({
glob: z.string(),
});
const NoFormatSubtree = TOCTreeOptions.extend({
entries: z.lazy(() => NoFormatEntry.array()),
});
const NoFormatShorthandSubtree = z.object({
entries: z.lazy(() => NoFormatEntry.array()),
options: TOCTreeOptions.optional(),
});
const NoFormatHasSubtrees = z.object({
subtrees: NoFormatSubtree.array(),
});
const NoFormatEntry = z.union([
FileEntry.and(NoFormatShorthandSubtree),
FileEntry.merge(NoFormatHasSubtrees),
FileEntry,
URLEntry,
GlobEntry,
]);
const NoFormatTOCBase = z.object({
root: z.string(),
defaults: TOCTreeOptions.optional(),
});
const NoFormatTOC = z.union([
NoFormatTOCBase.and(NoFormatShorthandSubtree),
NoFormatTOCBase.merge(NoFormatHasSubtrees).strict(),
NoFormatTOCBase.strict(),
]);
const ArticleSubtree = TOCTreeOptions.extend({
sections: z.lazy(() => ArticleEntry.array()),
});
const ArticleShorthandSubtree = z.object({
sections: z.lazy(() => ArticleEntry.array()),
options: TOCTreeOptions.optional(),
});
const ArticleHasSubtrees = z.object({
subtrees: ArticleSubtree.array(),
});
const ArticleEntry = z.union([
FileEntry.and(ArticleShorthandSubtree),
FileEntry.merge(ArticleHasSubtrees),
FileEntry,
URLEntry,
GlobEntry,
]);
const ArticleTOCBase = z.object({
root: z.string(),
format: z.literal('jb-article'),
defaults: TOCTreeOptions.optional(),
});
const ArticleTOC = z.union([
ArticleTOCBase.and(ArticleShorthandSubtree),
ArticleTOCBase.merge(ArticleHasSubtrees).strict(),
ArticleTOCBase.strict(),
]);
const BookOuterSubtree = TOCTreeOptions.extend({
chapters: z.lazy(() => BookEntry.array()),
});
const BookInnerSubtree = TOCTreeOptions.extend({
sections: z.lazy(() => BookEntry.array()),
});
const BookShorthandOuterSubtree = z.object({
chapters: z.lazy(() => BookEntry.array()),
options: TOCTreeOptions.optional(),
});
const BookShorthandInnerSubtree = z.object({
sections: z.lazy(() => BookEntry.array()),
options: TOCTreeOptions.optional(),
});
const BookHasOuterSubtrees = z.object({
parts: BookOuterSubtree.array(),
});
const BookHasInnerSubtrees = z.object({
subtrees: BookInnerSubtree.array(),
});
const BookEntry = z.union([
FileEntry.and(BookShorthandInnerSubtree),
FileEntry.merge(BookHasInnerSubtrees),
FileEntry,
URLEntry,
GlobEntry,
]);
const BookTOCBase = z.object({
root: z.string(),
format: z.literal('jb-book'),
defaults: TOCTreeOptions.optional(),
});
const BookTOC = z.union([
BookTOCBase.and(BookShorthandOuterSubtree),
BookTOCBase.merge(BookHasOuterSubtrees).strict(),
BookTOCBase.strict(),
]);
/** TOC **/
const SphinxExternalTOC = z.union([ArticleTOC, BookTOC, NoFormatTOC]);
export function validateSphinxExternalTOC(toc) {
const result = SphinxExternalTOC.safeParse(toc);
if (!result.success) {
const errors = result.error.errors.map((issue) => `${issue.path.join('.')}: ${issue.message} (${issue.code})`);
throw new Error(`Error(s) in parsing Jupyter Book TOC:\n${errors}`);
}
else {
return result.data;
}
}
/**
* Helper function throwing a compile error if the branch is reachable
*/
function assertNever() {
throw new Error('unreachable code');
}
function maybeResolveDocument(dir, name, session) {
const resolved = resolveExtension(join(dir, name));
if (resolved) {
return relative(dir, resolved);
}
session.log.error(`Could not find a file named ${name} (declared in table of contents)`);
return name;
}
/**
* Convert a no-format TOC to a MyST TOC
*
* @param session - session with logging
* @param dir - directory in which the _toc.yml lives
* @param data - validated TOC
*/
function convertNoFormat(session, dir, data) {
const rootEntry = { file: maybeResolveDocument(dir, data.root, session) };
const convertEntry = (item) => {
let entry;
if ('file' in item) {
entry = {
file: maybeResolveDocument(dir, item.file, session),
title: item.title,
};
}
else if ('url' in item) {
entry = {
url: item.url,
title: item.title,
};
}
else if ('glob' in item) {
entry = {
pattern: item.glob,
};
}
else {
assertNever();
}
if ('subtrees' in item || 'entries' in item) {
const children = convertHasOrIsSubtree(item);
entry = { ...entry, children: children };
}
return entry;
};
const convertSubtree = (item, options, index) => {
var _a;
return {
title: (_a = options === null || options === void 0 ? void 0 : options.caption) !== null && _a !== void 0 ? _a : `Subtree ${index}`,
children: item.entries.map(convertEntry),
};
};
const convertHasOrIsSubtree = (item) => {
if ('subtrees' in item) {
return item.subtrees.map((subtree, i) => convertSubtree(subtree, subtree, i));
}
else {
// Convert the subtree
const subtree = convertSubtree(item, item.options, 0);
// Lift the children (erasing the shorthand subtree)
return subtree.children;
}
};
const entries = [rootEntry];
if ('subtrees' in data || 'entries' in data) {
entries.push(...convertHasOrIsSubtree(data));
}
return entries;
}
/**
* Convert a Book TOC into a no-format TOC
*
* @param data - validated TOC
*/
function convertBookToNoFormat(data) {
const convertEntry = (item) => {
// Drop subtrees and sections
// eslint-disable-next-line prefer-const, @typescript-eslint/no-unused-vars
let { sections, subtrees, ...result } = item;
if ('sections' in item || 'subtrees' in item) {
result = { ...result, ...convertHasOrIsInnerSubtree(item) };
}
return result;
};
const convertInnerSubtree = (item) => {
const { sections, ...result } = item;
return { ...result, entries: sections.map(convertEntry) };
};
const convertHasOrIsInnerSubtree = (item) => {
if ('subtrees' in item) {
const { subtrees, ...rest } = item;
return { ...rest, subtrees: subtrees.map(convertInnerSubtree) };
}
else {
const { options, ...rest } = item;
return { options, ...convertInnerSubtree(rest) };
}
};
const convertOuterSubtree = (item) => {
const { chapters, ...rest } = item;
return { ...rest, entries: chapters.map(convertEntry) };
};
const convertHasOrIsOuterSubtree = (item) => {
if ('parts' in item) {
const { parts, ...rest } = item;
return { ...rest, subtrees: parts.map(convertOuterSubtree) };
}
else {
const { options, ...rest } = item;
return { options, ...convertOuterSubtree(rest) };
}
};
const { root, defaults, format: _, ...rest } = data;
let result = {
root,
defaults,
};
if ('chapters' in rest || 'parts' in rest) {
result = { ...result, ...convertHasOrIsOuterSubtree(rest) };
}
return result;
}
/**
* Convert a Article TOC into a no-format TOC
*
* @param data - validated TOC
*/
function convertArticleToNoFormat(data) {
const convertEntry = (item) => {
// Drop subtrees and sections
// eslint-disable-next-line prefer-const, @typescript-eslint/no-unused-vars
let { sections, subtrees, ...result } = item;
if ('sections' in item || 'subtrees' in item) {
result = { ...result, ...convertHasOrIsSubtree(item) };
}
return result;
};
const convertSubtree = (item) => {
const { sections, ...result } = item;
return { ...result, entries: sections.map(convertEntry) };
};
const convertHasOrIsSubtree = (item) => {
if ('subtrees' in item) {
const { subtrees, ...rest } = item;
return { ...rest, subtrees: subtrees.map(convertSubtree) };
}
else {
const { options, ...rest } = item;
return { options, ...convertSubtree(rest) };
}
};
const { root, defaults, format: _, ...rest } = data;
let result = {
root,
defaults,
};
if ('sections' in rest || 'subtrees' in rest) {
result = { ...result, ...convertHasOrIsSubtree(rest) };
}
return result;
}
/**
* Upgrade a sphinx-external-toc TOC into a MyST TOC
*/
export function upgradeTOC(session, data) {
const dir = cwd();
let dataNoFormat;
if ('format' in data) {
switch (data.format) {
case 'jb-book':
{
dataNoFormat = convertBookToNoFormat(data);
}
break;
case 'jb-article':
{
dataNoFormat = convertArticleToNoFormat(data);
}
break;
}
}
else {
dataNoFormat = data;
}
return convertNoFormat(session, dir, dataNoFormat);
}