UNPKG

myst-cli

Version:
121 lines (120 loc) 4.52 kB
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); }