docs: stuff

This commit is contained in:
Morten Olsen
2025-05-18 22:18:15 +02:00
parent 6b74a28989
commit e0707e74fb
12 changed files with 322 additions and 14 deletions

View File

@@ -1,14 +1,15 @@
import { program } from 'commander';
import { resolve } from 'node:path';
import { marked } from 'marked';
import { Marked } from 'marked';
import { markedTerminal } from 'marked-terminal';
import { execute } from '../execution/execution.js';
import { Context } from '../context/context.js';
import { writeFile } from 'node:fs/promises';
import { Watcher } from '../watcher/watcher.js';
import { UI } from './ui/ui.js';
import { wrapBody } from '../theme/theme.html.js';
marked.use(markedTerminal() as any);
program
.command('dev')
@@ -17,11 +18,15 @@ program
.option('-w, --watch', 'watch for changes')
.option('-i, --input <input...>', 'input variables (-i foo=bar -i baz=qux)')
.action(async (name, options) => {
const marked = new Marked();
marked.use(markedTerminal() as any);
const {
watch = false,
input: i = [],
} = options;
const ui = new UI();
const input = Object.fromEntries(
i.map((item: string) => {
const [key, value] = item.split('=');
@@ -39,7 +44,7 @@ program
});
const markdown = await marked.parse(result.markdown);
console.log(markdown);
ui.content = markdown;
return {
...result,
@@ -49,6 +54,10 @@ program
const result = await build();
ui.screen.key(['r'], () => {
build();
});
if (watch) {
const watcher = new Watcher();
watcher.watchFiles(Array.from(result.context.files));
@@ -66,12 +75,14 @@ program
.argument('<name>', 'http.md file name')
.argument('<output>', 'output file name')
.description('Run a http.md document')
.option('-f, --format <format>', 'output format (html, markdown)')
.option('-w, --watch', 'watch for changes')
.option('-i, --input <input...>', 'input variables (-i foo=bar -i baz=qux)')
.action(async (name, output, options) => {
const {
watch = false,
input: i = [],
format = 'markdown',
} = options;
@@ -91,7 +102,15 @@ program
context,
});
await writeFile(output, result.markdown);
if (format === 'html') {
const marked = new Marked();
const html = await marked.parse(result.markdown);
await writeFile(output, wrapBody(html));
} else if (format === 'markdown') {
await writeFile(output, result.markdown);
} else {
throw new Error('Invalid format');
}
return {
...result,
context,

70
src/cli/ui/ui.ts Normal file
View File

@@ -0,0 +1,70 @@
import blessed from 'blessed';
import chalk from 'chalk';
class UI {
#box: blessed.Widgets.BoxElement;
#screen: blessed.Widgets.Screen;
constructor() {
const screen = blessed.screen({
smartCSR: true,
title: 'Markdown Viewer'
});
const scrollableBox = blessed.box({ // Or blessed.scrollablebox
parent: screen,
top: 0,
left: 0,
width: '100%',
height: '100%',
content: '',
scrollable: true,
alwaysScroll: true,
keys: true,
vi: true, // vi-like keybindings
mouse: true,
scrollbar: {
ch: ' ',
track: {
bg: 'cyan'
},
style: {
inverse: true
}
},
style: {
fg: 'white',
bg: 'black'
}
});
this.#box = scrollableBox;
this.#screen = screen;
screen.key(['escape', 'q', 'C-c'], () => {
return process.exit(0);
});
scrollableBox.focus();
screen.render();
}
public get screen() {
return this.#screen;
}
public set content(content: string) {
const originalLines = content.split('\n');
const maxLineNoDigits = String(originalLines.length).length; // For padding
const linesWithNumbers = originalLines.map((line, index) => {
const lineNumber = String(index + 1).padStart(maxLineNoDigits, ' ');
const styledLineNumber = chalk.dim.yellow(`${lineNumber} | `);
return `${styledLineNumber}${line}`;
});
const contentWithLineNumbers = linesWithNumbers.join('\n');
this.#box.setContent(contentWithLineNumbers);
this.#screen.render();
}
}
export { UI };

View File

@@ -5,18 +5,12 @@ import remarkParse from 'remark-parse'
import remarkRehype from 'remark-rehype'
import remarkDirective from 'remark-directive'
import remarkStringify from 'remark-stringify'
import behead from 'remark-behead';
import { unified } from 'unified'
import { visit } from 'unist-util-visit'
import { Context } from "../context/context.js";
import { handlers } from './handlers/handlers.js';
const parser = unified()
.use(remarkParse)
.use(remarkGfm)
.use(remarkDirective)
.use(remarkStringify)
.use(remarkRehype);
import { handlers, postHandlers } from './handlers/handlers.js';
type BaseNode = {
type: string;
@@ -53,6 +47,7 @@ type ExecutionHandler = (options: {
type ExexutionExecuteOptions = {
context: Context;
behead?: number;
}
const execute = async (file: string, options: ExexutionExecuteOptions) => {
@@ -61,6 +56,16 @@ const execute = async (file: string, options: ExexutionExecuteOptions) => {
const content = await readFile(file, 'utf-8');
const steps: Set<ExecutionStep> = new Set();
const parser = unified()
.use(remarkParse)
.use(remarkGfm)
.use(remarkDirective)
.use(remarkStringify)
.use(remarkRehype)
.use(behead, {
depth: options.behead,
});
const root = parser.parse(content);
visit(root, (node, index, parent) => {
@@ -75,6 +80,18 @@ const execute = async (file: string, options: ExexutionExecuteOptions) => {
});
}
});
visit(root, (node, index, parent) => {
for (const handler of postHandlers) {
handler({
addStep: (step) => steps.add(step),
node: node as BaseNode,
root,
parent: parent as BaseNode | undefined,
index,
file,
});
}
});
for (const step of steps) {
const { node, action } = step;

View File

@@ -23,6 +23,7 @@ const fileHandler: ExecutionHandler = ({
}
const { root: newRoot } = await execute(filePath, {
context,
behead: node.attributes?.behead ? parseInt(node.attributes.behead) : undefined,
});
if (!parent) {
throw new Error('Parent node is required');

View File

@@ -53,6 +53,7 @@ const responseHandler: ExecutionHandler = ({
const codeNode = {
type: 'code',
lang: 'http',
value: responseContent,
};
if (!parent || !('children' in parent) || index === undefined) {

View File

@@ -0,0 +1,29 @@
import { toc } from 'mdast-util-toc';
import { type ExecutionHandler } from '../execution.js';
const tocHandler: ExecutionHandler = ({
addStep,
node,
root,
parent,
index,
}) => {
if (node.type === 'leafDirective' && node.name === 'toc') {
addStep({
type: 'toc',
node,
action: async () => {
const result = toc(root, {
tight: true,
minDepth: 2,
})
if (!parent || !parent.children || index === undefined) {
throw new Error('Parent node is not valid');
}
parent.children.splice(index, 1, result.map as any);
},
})
}
}
export { tocHandler };

View File

@@ -6,6 +6,7 @@ import { rawMdHandler } from "./handlers.raw-md.js";
import { responseHandler } from "./handlers.response.js";
import { textHandler } from "./handlers.text.js";
import { codeHandler } from "./handlers.code.js";
import { tocHandler } from "./handlers.toc.js";
const handlers = [
fileHandler,
@@ -17,4 +18,8 @@ const handlers = [
codeHandler,
] satisfies ExecutionHandler[];
export { handlers };
const postHandlers = [
tocHandler,
] satisfies ExecutionHandler[];
export { handlers, postHandlers };

29
src/theme/theme.html.ts Normal file
View File

@@ -0,0 +1,29 @@
const wrapBody = (body: string) => {
return `<!DOCTYPE html>
<html lang="en">
<head>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/github-markdown-css/5.8.1/github-markdown.min.css" />
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 0;
}
.markdown-body {
max-width: 800px;
padding: 20px;
margin: auto;
}
</style>
</head>
<body>
<article class="markdown-body">
${body}
</article>
</body>
</html>`;
};
export { wrapBody };