// Functions for downloading, updating, and displaying the AO Manual, a hierarchy of markdown files import chalk from 'chalk' import { execSync, exec } from 'child_process' import { loadYamlMarkdownFile, lsFolder, isFolder } from '../files.js' import { repeatString, centerLines } from '../strings.js' import { promptMenu } from '../welcome.js' import { headerStyle, manualTitleStyle } from '../styles.js' import { basename } from 'path' import { marked } from 'marked' import TerminalRenderer from 'marked-terminal' export const AO_MANUAL_PATH = process.env.HOME + '/.ao/manual' export function manualStatus() { // There are at least eighteen items in the manual if(lsFolder(AO_MANUAL_PATH).length >= 18) { return 'installed' } return 'off' } // Downloads the ao-manual repo to ~/.ao/manual/. Returns false if it fails, which usually means the folder already exists (update instead). export function downloadManual() { try { execSync('git clone http://git.coalitionofinvisiblecolleges.org:3009/autonomousorganization/ao-manual.git ' + AO_MANUAL_PATH + ' 2>&1') } catch(err) { switch(err.code) { case 128: return false } } return true } export async function updateManual() { exec('cd ' + process.env.HOME + '/.ao/manual && git pull origin main 2>&1', (error, stdout, stderr) => { //console.log('error:', error, 'stdout:', stdout, 'stderr:', stderr) if(error) { console.log('git pull failed with error:', error) } if(stdout.includes('Already up to date.')) { return } console.log('/nAO User Manual was updated.') }) } // Removes numbered prefix such as 12_ and .md suffix, replaces underscores with spaces, and adds titlecase function formatManualTitleString(title) { // Remove .md suffix if(/\.md$/.exec(title)) { title = title.substring(0, title.length - 3) } // Remove numbered prefix e.g., 12_ if(/^\d*_/.exec(title)) { title = title.split('_').slice(1).join('_') } // Replace underscores with spaces title = title.replaceAll('_', ' ') return title.toTitleCase() } marked.setOptions({ renderer: new TerminalRenderer({ showSectionPrefix: false, }) }) // Given a path and file/folder name, it returns the appropriate manual title // If it's a folder and there is an index.js inside that has a title: field, that overrides the title // Otherwise it's the filename or foldername, minus anything before the first underscore (_), in titlecase async function loadManualTitle(path, fileOrFolder) { // If it's a .md file, check inside for a title: field if(/\.md$/.exec(fileOrFolder)) { const indexTitle = (await loadYamlMarkdownFile(path + fileOrFolder))?.meta?.title if(indexTitle) { return indexTitle } } // If it's a folder, check for a title: field in index.md and return if exists if(isFolder(path + fileOrFolder)) { const indexPath = path + fileOrFolder + '/index.md' const indexTitle = (await loadYamlMarkdownFile(indexPath))?.meta?.title if(indexTitle) { return indexTitle } } // Fall back to using the file/folder name as the title return formatManualTitleString(fileOrFolder) } // Prints the specified manual page to the screen export async function printManualPage(path, injectedTitle = '') { if(isFolder(path)) { path += '/index.md' } const dict = await loadYamlMarkdownFile(path) const title = injectedTitle || dict?.meta?.title || formatManualTitleString(basename(path)) const formattedTitle = manualTitleStyle(title).centerInLine(title.length).centerInConsole() console.log('\n' + formattedTitle + '\n') const renderedMarkdown = marked(dict?.tail).wordWrap().centerInConsole() console.log(renderedMarkdown) } // Render the manual folder or a subfolder as a menu // First the index.js is listed using the folder name or title loaded from inside the file as the menu item title // Next, any other files not starting with a number are loaded and displayed in discovered/arbitrary order // Next, items starting with 0_, then 1_, and so on are displayed in order. You can mix files and folders. // Selecting a menu item renders it. For .md files it renders it and shows the same level Manual menu again. // For folders, it goes into that folder and renders it as a manual menu folder // This allows arbitrarily nested Manual menu folders to be explored in a standardized menu system export async function manualFolderAsMenu(path, menuTitle, backOption, previousMenuChoice = 0) { if(!isFolder(path)) { return false } if(path[path.length - 1] != '/') { path += '/' } let menuItems = [] const folderContents = lsFolder(path) if(folderContents.some(fileOrFolder => fileOrFolder === 'index.md')) { const indexTitle = await loadManualTitle(path, 'index.md') let indexMenuItem = {} indexMenuItem[indexTitle] = 'index.md' menuItems.push(indexMenuItem) } let unNumberedItems = [] let numberedItems = [] const sortedFolderContents = folderContents.sort((a, b) => a.localeCompare(b, undefined, { numeric: true })) for(let i = 0; i < sortedFolderContents.length; i++) { const fileOrFolder = sortedFolderContents[i] if(fileOrFolder === 'index.md') { continue } const potentialNumber = fileOrFolder.split('_')[0] const initialNumber = parseInt(potentialNumber) const title = await loadManualTitle(path, fileOrFolder) const menuItem = {} menuItem[title] = fileOrFolder if(isNaN(initialNumber)) { unNumberedItems.push(menuItem) } else { numberedItems.push(menuItem) } } menuItems = menuItems.concat(unNumberedItems, numberedItems) const menuChoices = menuItems.map(menuItem => Object.keys(menuItem)[0]) menuChoices.push(backOption) if(previousMenuChoice >= menuChoices.length) { previousMenuChoice = 0 } const answer = await promptMenu(menuChoices, menuTitle, 'choose a topic', previousMenuChoice, undefined, true) const chosenMenuIndex = menuChoices.indexOf(answer) if(answer === backOption) { return false } const chosenPath = path + Object.values(menuItems.find(menuItem => Object.keys(menuItem)[0] === answer))[0] await printManualPage(chosenPath, answer) const newBackOption = backOption === 'Back to Main Menu' ? 'Back to Table of Contents' : 'Back to ' + menuTitle let previousChoice = 0 do { previousChoice = await manualFolderAsMenu(chosenPath, answer, newBackOption, previousChoice + 1) } while(previousChoice !== false) return chosenMenuIndex } export default { name: 'Manual', description: 'AO user manual', status: manualStatus, install: downloadManual, isInstalled: () => manualStatus() === 'installed', update: updateManual }