|
|
|
// Functions for loading the AO Manual, a hierarchy of markdown files
|
|
|
|
import chalk from 'chalk'
|
|
|
|
import inquirer from 'inquirer'
|
|
|
|
import { loadYamlMarkdownFile, lsFolder, isFolder } from './files.js'
|
|
|
|
import { repeatString, centerLines } from './strings.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'
|
|
|
|
|
|
|
|
// 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
|
|
|
|
}
|
|
|
|
console.log(`\n${headerStyle(menuTitle)}`)
|
|
|
|
const answer = await inquirer.prompt({
|
|
|
|
name: 'manual_menu',
|
|
|
|
type: 'rawlist',
|
|
|
|
message: 'Choose a topic:',
|
|
|
|
choices: menuChoices,
|
|
|
|
pageSize: menuChoices.length,
|
|
|
|
default: previousMenuChoice
|
|
|
|
})
|
|
|
|
const chosenMenuIndex = menuChoices.indexOf(answer.manual_menu)
|
|
|
|
if(answer.manual_menu === backOption) {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
const chosenPath = path + Object.values(menuItems.find(menuItem => Object.keys(menuItem)[0] === answer.manual_menu))[0]
|
|
|
|
await printManualPage(chosenPath, answer.manual_menu)
|
|
|
|
const newBackOption = backOption === 'Back to Main Menu' ? 'Back to Table of Contents' : 'Back to ' + menuTitle
|
|
|
|
let previousChoice = 0
|
|
|
|
do {
|
|
|
|
previousChoice = await manualFolderAsMenu(chosenPath, answer.manual_menu, newBackOption, previousChoice + 1)
|
|
|
|
}
|
|
|
|
while(previousChoice !== false)
|
|
|
|
return chosenMenuIndex
|
|
|
|
}
|