myst-cli
Version:
Command line tools for MyST
121 lines (120 loc) • 4.52 kB
JavaScript
import fs from 'node:fs';
import { findYAMLSection, yamlLineIndent } from './utils.js';
import yaml from 'js-yaml';
import { relative, parse } from 'node:path';
function getRelativeDocumentLink(file, path) {
const relativePath = path === '.' ? file : relative(path, file);
// Normalize path separators to forward slashes for cross-platform compatibility
return relativePath.replace(/\\/g, '/');
}
function tocFromPages(pages, path) {
const levels = pages.map((page) => page.level);
const currentLevel = Math.min(...levels);
const currentLevelIndices = levels.reduce((inds, val, i) => {
if (val === currentLevel) {
inds.push(i);
}
return inds;
}, []);
return currentLevelIndices.map((index, i) => {
let nextPages;
if (currentLevelIndices[i + 1]) {
nextPages = pages.slice(index + 1, currentLevelIndices[i + 1]);
}
else {
nextPages = pages.slice(index + 1);
}
if ('file' in pages[index]) {
const page = pages[index];
if (nextPages.length) {
return {
file: getRelativeDocumentLink(page.file, path),
children: tocFromPages(nextPages, path),
};
}
else {
return {
file: getRelativeDocumentLink(page.file, path),
};
}
}
else if ('title' in pages[index]) {
const page = pages[index];
return { title: page.title, children: tocFromPages(nextPages, path) };
}
else {
return undefined; // This is never hit
}
});
}
export function tocFromProject(project, dir = '.') {
return [
{ file: getRelativeDocumentLink(project.file, dir) },
...tocFromPages(project.pages, dir),
];
}
const DEFAULT_INDENT = 2;
export async function writeTOCToConfigFile(project, srcPath, dstPath) {
// Read the existing configuration file
let lines;
if (fs.existsSync(srcPath)) {
lines = fs.readFileSync(srcPath, 'utf8').split(/\r\n|\r|\n/);
}
else {
lines = [];
}
// Identify the first non-empty line's indentation
const indent = lines.map(yamlLineIndent).find((item) => item !== undefined) ?? 0;
// Find `project` section
let projectInfo = findYAMLSection('project', indent, lines);
// Or, create an empty project section
if (!projectInfo) {
const prefix = ' '.repeat(indent);
lines.push(`${prefix}project:`);
projectInfo = { start: lines.length, stop: lines.length, indent: indent + DEFAULT_INDENT };
}
// Identify the child indentation of the project section
const projectIndent = projectInfo.indent ?? indent + DEFAULT_INDENT;
// Find `toc` section
let tocInfo = findYAMLSection('toc', projectIndent, lines, projectInfo.start);
// Or create an empty toc section
if (!tocInfo) {
const prefix = ' '.repeat(projectIndent);
lines.splice(projectInfo.stop, 0, `${prefix}toc:`);
// Append project info
tocInfo = {
start: projectInfo.stop + 1,
stop: projectInfo.stop + 1,
indent: projectIndent + DEFAULT_INDENT,
};
}
// Determine indentation of section
const tocIndent = tocInfo.indent ?? projectIndent + DEFAULT_INDENT;
const tocPrefix = ' '.repeat(tocIndent);
// Build the new `toc` section
const { dir } = parse(srcPath);
const toc = tocFromProject(project, dir);
const tocLines = [
`${tocPrefix}# Auto-generated by \`myst init --write-toc\``,
...yaml
.dump(toc)
.split(/\r\n|\r|\n/)
.map((line) => `${tocPrefix}${line}`),
];
// Splice these `toc` lines into the proper location
lines.splice(tocInfo.start, tocInfo.stop - tocInfo.start, ...tocLines);
// Write the new configuration
const newConfigContent = lines.join('\n');
// Ensure the hand-rolled YAML is valid
try {
yaml.load(newConfigContent);
}
catch (err) {
const errorMessage = lines.map((line) => ` ${line}`).join('\n');
throw new Error(`Invalid YAML was generated when attempting to write the table-of-contents to ${dstPath}.
This should not happenm please file a bug report at https://github.com/jupyter-book/mystmd.
The invalid TOC contents are as follows:
${errorMessage}`);
}
fs.writeFileSync(dstPath, newConfigContent);
}