From e0707e74fba83dd27e3c34c07047e8c1fe8e0a60 Mon Sep 17 00:00:00 2001 From: Morten Olsen Date: Sun, 18 May 2025 22:18:15 +0200 Subject: [PATCH] docs: stuff --- .gitignore | 1 + docs/README.md | 6 +- package.json | 4 + pnpm-lock.yaml | 128 ++++++++++++++++++++ src/cli/cli.ts | 27 ++++- src/cli/ui/ui.ts | 70 +++++++++++ src/execution/execution.ts | 33 +++-- src/execution/handlers/handlers.md.ts | 1 + src/execution/handlers/handlers.response.ts | 1 + src/execution/handlers/handlers.toc.ts | 29 +++++ src/execution/handlers/handlers.ts | 7 +- src/theme/theme.html.ts | 29 +++++ 12 files changed, 322 insertions(+), 14 deletions(-) create mode 100644 src/cli/ui/ui.ts create mode 100644 src/execution/handlers/handlers.toc.ts create mode 100644 src/theme/theme.html.ts diff --git a/.gitignore b/.gitignore index b0a5c34..df8612d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ /node_modules/ /dist/ +/*.html diff --git a/docs/README.md b/docs/README.md index 738be72..cd2fccb 100644 --- a/docs/README.md +++ b/docs/README.md @@ -21,6 +21,10 @@ It allows developers to create API documentation that is always accurate and up- - **Tutorials & Guides:** Build step-by-step guides where each HTTP interaction is shown with its real output. - **Rapid Prototyping:** Quickly experiment with APIs and document your findings. +## Content + +::toc + ## Installation Install `http.md` globally using npm: @@ -85,7 +89,7 @@ _(Note: Actual headers and some response fields might vary.)_ HTTP requests are defined in fenced code blocks annotated with `http`. The syntax is similar to the raw HTTP format: -``` +```http disable : ... diff --git a/package.json b/package.json index 7e9872a..908e852 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "build": "pnpm run build:lib && pnpm run build:readme", "build:lib": "tsc --build", "build:readme": "pnpm run cli build docs/README.md README.md", + "build:readme-html": "pnpm run cli build docs/README.md README.html -f html", "dev:readme": "pnpm run cli dev docs/README.md --watch", "test": "echo \"Error: no test specified\" && exit 1" }, @@ -35,6 +36,7 @@ }, "dependencies": { "blessed": "^0.1.81", + "chalk": "^5.4.1", "commander": "^14.0.0", "dotenv": "^16.5.0", "eventemitter3": "^5.0.1", @@ -44,7 +46,9 @@ "marked-terminal": "^7.3.0", "mdast-util-to-markdown": "^2.1.2", "mdast-util-to-string": "^4.0.0", + "mdast-util-toc": "^7.1.0", "rehype-stringify": "^10.0.1", + "remark-behead": "^3.1.0", "remark-directive": "^4.0.0", "remark-gfm": "^4.0.1", "remark-parse": "^11.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index db8b01d..de29f63 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,6 +11,9 @@ importers: blessed: specifier: ^0.1.81 version: 0.1.81 + chalk: + specifier: ^5.4.1 + version: 5.4.1 commander: specifier: ^14.0.0 version: 14.0.0 @@ -38,9 +41,15 @@ importers: mdast-util-to-string: specifier: ^4.0.0 version: 4.0.0 + mdast-util-toc: + specifier: ^7.1.0 + version: 7.1.0 rehype-stringify: specifier: ^10.0.1 version: 10.0.1 + remark-behead: + specifier: ^3.1.0 + version: 3.1.0 remark-directive: specifier: ^4.0.0 version: 4.0.0 @@ -455,6 +464,9 @@ packages: '@types/terminal-kit@2.5.7': resolution: {integrity: sha512-IpbCBFSb3OqCEZBZlk368tGftqss88eNQaJdD9msEShRbksEiVahEqroONi60ppUt9/arLM6IDrHMx9jpzzCOw==} + '@types/ungap__structured-clone@1.2.0': + resolution: {integrity: sha512-ZoaihZNLeZSxESbk9PUAPZOlSpcKx81I1+4emtULDVmBLkYutTcMlCj2K9VNlf9EWODxdO6gkAqEaLorXwZQVA==} + '@types/unist@2.0.11': resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==} @@ -740,6 +752,9 @@ packages: get-tsconfig@4.10.0: resolution: {integrity: sha512-kGzZ3LWWQcGIAmg6iWvXn0ei6WDtV26wzHRMwDSzmAbcXrTEXxHy6IehI6/4eT6VRKyMP1eF1VqwrVUmE/LR7A==} + github-slugger@2.0.0: + resolution: {integrity: sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==} + glob-parent@5.1.2: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} @@ -890,6 +905,9 @@ packages: resolution: {integrity: sha512-gUD/epcRms75Cw8RT1pUdHugZYM5ce64ucs2GEISABwkRsOQr0q2wm/MV2TKThycIe5e0ytRweW2RZxclogCdQ==} engines: {node: '>=8'} + lodash.iteratee@4.7.0: + resolution: {integrity: sha512-yv3cSQZmfpbIKo4Yo45B1taEvxjNvcpF1CEOc0Y6dEyvhPIfEJE3twDwPgWTPQubcSgXyBwBKG6wpQvWMDOf6Q==} + longest-streak@3.1.0: resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} @@ -959,6 +977,9 @@ packages: mdast-util-to-string@4.0.0: resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==} + mdast-util-toc@7.1.0: + resolution: {integrity: sha512-2TVKotOQzqdY7THOdn2gGzS9d1Sdd66bvxUyw3aNpWfcPXCLYSJCCgfPy30sEtuzkDraJgqF35dzgmz6xlvH/w==} + mem@8.1.1: resolution: {integrity: sha512-qFCFUDs7U3b8mBDPyz5EToEKoAkgCzqquIgi9nkkR9bixxOVOre+09lbuH7+9Kn2NFpm56M3GUWVbU2hQgdACA==} engines: {node: '>=10'} @@ -1227,6 +1248,10 @@ packages: rehype-stringify@10.0.1: resolution: {integrity: sha512-k9ecfXHmIPuFVI61B9DeLPN0qFHfawM6RsuX48hoqlaKSF61RskNjSm1lI8PhBEM0MRdLxVVm4WmTqJQccH9mA==} + remark-behead@3.1.0: + resolution: {integrity: sha512-rKns7st91lgppaD5YaH58O4ECFVXTVnkyYQBuCw4ISRE2TFK/iVySMaKbvV2pVbUVIjAaDciugrTI/tyuPOlWQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + remark-directive@4.0.0: resolution: {integrity: sha512-7sxn4RfF1o3izevPV1DheyGDD6X4c9hrGpfdUpm7uC++dqrnJxIZVkk7CoKqcLm0VUMAuOol7Mno3m6g8cfMuA==} @@ -1431,6 +1456,25 @@ packages: resolution: {integrity: sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==} engines: {node: '>=8'} + unist-util-find-all-after@4.0.1: + resolution: {integrity: sha512-AO8++e6HJfwNoTrqkV7xSeW65e6uSsLRQST/9LWi8FmFSz1gS7TBd+DkL/CYiElsSZIQgT4J5U54v5/kJX5Nqg==} + + unist-util-find-all-before@4.0.1: + resolution: {integrity: sha512-xg4UHtZ6VbcjQbfDtmLZch6kQYQFF3nfaW05Ie3+t2UectzeqSx/iqLmh/wWogwU+YDWnD40PjZKK7ORmCma+g==} + + unist-util-find-all-between@2.1.0: + resolution: {integrity: sha512-OCCUtDD8UHKeODw3TPXyFDxPCbpgBzbGTTaDpR68nvxkwiVcawBqMVrokfBMvUi7ij2F5q7S4s4Jq5dvkcBt+w==} + engines: {node: '>=10'} + + unist-util-find@1.0.4: + resolution: {integrity: sha512-T5vI7IkhroDj7KxAIy057VbIeGnCXfso4d4GoUsjbAmDLQUkzAeszlBtzx1+KHgdsYYBygaqUBvrbYCfePedZw==} + + unist-util-is@4.1.0: + resolution: {integrity: sha512-ZOQSsnce92GrxSqlnEEseX0gi7GH9zTJZ0p9dtu87WRb/37mMPO2Ilx1s/t9vBHrFhbgweUwb+t7cIn5dxPhZg==} + + unist-util-is@5.2.1: + resolution: {integrity: sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw==} + unist-util-is@6.0.0: resolution: {integrity: sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==} @@ -1440,9 +1484,21 @@ packages: unist-util-stringify-position@4.0.0: resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==} + unist-util-visit-parents@3.1.1: + resolution: {integrity: sha512-1KROIZWo6bcMrZEwiH2UrXDyalAa0uqzWCxCJj6lPOvTve2WkfgCytoDTPaMnodXh1WrXOq0haVYHj99ynJlsg==} + + unist-util-visit-parents@5.1.3: + resolution: {integrity: sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg==} + unist-util-visit-parents@6.0.1: resolution: {integrity: sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==} + unist-util-visit@2.0.3: + resolution: {integrity: sha512-iJ4/RczbJMkD0712mGktuGpm/U4By4FfDonL7N/9tATGIF4imikjOuagyMY53tnZq3NP6BcmlrHhEKAfGWjh7Q==} + + unist-util-visit@4.1.2: + resolution: {integrity: sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg==} + unist-util-visit@5.0.0: resolution: {integrity: sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==} @@ -1895,6 +1951,8 @@ snapshots: dependencies: '@types/nextgen-events': 1.1.4 + '@types/ungap__structured-clone@1.2.0': {} + '@types/unist@2.0.11': {} '@types/unist@3.0.3': {} @@ -2182,6 +2240,8 @@ snapshots: dependencies: resolve-pkg-maps: 1.0.0 + github-slugger@2.0.0: {} + glob-parent@5.1.2: dependencies: is-glob: 4.0.3 @@ -2313,6 +2373,8 @@ snapshots: strip-bom: 4.0.0 type-fest: 0.6.0 + lodash.iteratee@4.7.0: {} + longest-streak@3.1.0: {} map-age-cleaner@0.1.3: @@ -2466,6 +2528,16 @@ snapshots: dependencies: '@types/mdast': 4.0.4 + mdast-util-toc@7.1.0: + dependencies: + '@types/mdast': 4.0.4 + '@types/ungap__structured-clone': 1.2.0 + '@ungap/structured-clone': 1.3.0 + github-slugger: 2.0.0 + mdast-util-to-string: 4.0.0 + unist-util-is: 6.0.0 + unist-util-visit: 5.0.0 + mem@8.1.1: dependencies: map-age-cleaner: 0.1.3 @@ -2833,6 +2905,14 @@ snapshots: hast-util-to-html: 9.0.5 unified: 11.0.5 + remark-behead@3.1.0: + dependencies: + unist-util-find: 1.0.4 + unist-util-find-all-after: 4.0.1 + unist-util-find-all-before: 4.0.1 + unist-util-find-all-between: 2.1.0 + unist-util-visit: 4.1.2 + remark-directive@4.0.0: dependencies: '@types/mdast': 4.0.4 @@ -3051,6 +3131,32 @@ snapshots: dependencies: crypto-random-string: 2.0.0 + unist-util-find-all-after@4.0.1: + dependencies: + '@types/unist': 2.0.11 + unist-util-is: 5.2.1 + + unist-util-find-all-before@4.0.1: + dependencies: + '@types/unist': 2.0.11 + unist-util-is: 5.2.1 + + unist-util-find-all-between@2.1.0: + dependencies: + unist-util-find: 1.0.4 + unist-util-is: 4.1.0 + + unist-util-find@1.0.4: + dependencies: + lodash.iteratee: 4.7.0 + unist-util-visit: 2.0.3 + + unist-util-is@4.1.0: {} + + unist-util-is@5.2.1: + dependencies: + '@types/unist': 2.0.11 + unist-util-is@6.0.0: dependencies: '@types/unist': 3.0.3 @@ -3063,11 +3169,33 @@ snapshots: dependencies: '@types/unist': 3.0.3 + unist-util-visit-parents@3.1.1: + dependencies: + '@types/unist': 2.0.11 + unist-util-is: 4.1.0 + + unist-util-visit-parents@5.1.3: + dependencies: + '@types/unist': 2.0.11 + unist-util-is: 5.2.1 + unist-util-visit-parents@6.0.1: dependencies: '@types/unist': 3.0.3 unist-util-is: 6.0.0 + unist-util-visit@2.0.3: + dependencies: + '@types/unist': 2.0.11 + unist-util-is: 4.1.0 + unist-util-visit-parents: 3.1.1 + + unist-util-visit@4.1.2: + dependencies: + '@types/unist': 2.0.11 + unist-util-is: 5.2.1 + unist-util-visit-parents: 5.1.3 + unist-util-visit@5.0.0: dependencies: '@types/unist': 3.0.3 diff --git a/src/cli/cli.ts b/src/cli/cli.ts index 20218d9..0e47fe0 100644 --- a/src/cli/cli.ts +++ b/src/cli/cli.ts @@ -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 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('', 'http.md file name') .argument('', 'output file name') .description('Run a http.md document') + .option('-f, --format ', 'output format (html, markdown)') .option('-w, --watch', 'watch for changes') .option('-i, --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, diff --git a/src/cli/ui/ui.ts b/src/cli/ui/ui.ts new file mode 100644 index 0000000..a52231f --- /dev/null +++ b/src/cli/ui/ui.ts @@ -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 }; diff --git a/src/execution/execution.ts b/src/execution/execution.ts index ff4139f..aca909c 100644 --- a/src/execution/execution.ts +++ b/src/execution/execution.ts @@ -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 = 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; diff --git a/src/execution/handlers/handlers.md.ts b/src/execution/handlers/handlers.md.ts index ef0f2ac..ccf68b9 100644 --- a/src/execution/handlers/handlers.md.ts +++ b/src/execution/handlers/handlers.md.ts @@ -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'); diff --git a/src/execution/handlers/handlers.response.ts b/src/execution/handlers/handlers.response.ts index 8ef9c19..a17acd9 100644 --- a/src/execution/handlers/handlers.response.ts +++ b/src/execution/handlers/handlers.response.ts @@ -53,6 +53,7 @@ const responseHandler: ExecutionHandler = ({ const codeNode = { type: 'code', + lang: 'http', value: responseContent, }; if (!parent || !('children' in parent) || index === undefined) { diff --git a/src/execution/handlers/handlers.toc.ts b/src/execution/handlers/handlers.toc.ts new file mode 100644 index 0000000..e1f0dac --- /dev/null +++ b/src/execution/handlers/handlers.toc.ts @@ -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 }; diff --git a/src/execution/handlers/handlers.ts b/src/execution/handlers/handlers.ts index 4c19e9d..1abd7eb 100644 --- a/src/execution/handlers/handlers.ts +++ b/src/execution/handlers/handlers.ts @@ -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 }; diff --git a/src/theme/theme.html.ts b/src/theme/theme.html.ts new file mode 100644 index 0000000..52f81d7 --- /dev/null +++ b/src/theme/theme.html.ts @@ -0,0 +1,29 @@ +const wrapBody = (body: string) => { + return ` + + + + + + Document + + + +
+ ${body} +
+ + `; +}; + +export { wrapBody };