You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
184 lines
6.6 KiB
184 lines
6.6 KiB
// 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 |
|
}
|
|
|