3 Commits

Author SHA1 Message Date
Morten Olsen
ba56b66222 fix: yaml parsing 2025-05-18 22:29:50 +02:00
Morten Olsen
e0707e74fb docs: stuff 2025-05-18 22:18:15 +02:00
Morten Olsen
6b74a28989 docs: improved docs 2025-05-18 21:26:05 +02:00
16 changed files with 341 additions and 23 deletions

1
.gitignore vendored
View File

@@ -1,2 +1,3 @@
/node_modules/ /node_modules/
/dist/ /dist/
/*.html

View File

@@ -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. - **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. - **Rapid Prototyping:** Quickly experiment with APIs and document your findings.
## Content
::toc
## Installation ## Installation
Install `http.md` globally using npm: Install `http.md` globally using npm:
@@ -128,10 +132,17 @@ You can assign a unique ID to an `http` request block. This allows you to:
1. Reference its specific response in a `::response` directive. 1. Reference its specific response in a `::response` directive.
2. Access its request and response data in [Templating](https://www.google.com/search?q=%23templating-with-handlebars) via the `requests` and `responses` dictionaries. 2. Access its request and response data in [Templating](https://www.google.com/search?q=%23templating-with-handlebars) via the `requests` and `responses` dictionaries.
To add an ID, include `id=yourUniqueId` in the `http` block's info string: To add an ID, include `#yourUniqueId` or `id=yourUniqueId` in the `http` block's info string:
::raw-md[./examples/with-multiple-requests.md] ::raw-md[./examples/with-multiple-requests.md]
<details>
<summary>Output</summary>
::raw-md[./examples/with-multiple-requests.md]{render}
</details>
## Templating with Handlebars ## Templating with Handlebars
`http.md` uses [Handlebars](https://handlebarsjs.com/) for templating, allowing you to create dynamic content within your markdown files. You can inject data from request responses, input variables, and other requests into your HTTP blocks or general markdown text. `http.md` uses [Handlebars](https://handlebarsjs.com/) for templating, allowing you to create dynamic content within your markdown files. You can inject data from request responses, input variables, and other requests into your HTTP blocks or general markdown text.
@@ -213,12 +224,12 @@ Assume `_shared_requests.md` contains:
Then, in `main.md`: Then, in `main.md`:
::raw-md[./examples/with-template.md] ::raw-md[./examples/with-shared-requests.md]
<details> <details>
<summary>Output</summary> <summary>Output</summary>
::raw-md[./examples/with-template.md]{render} ::raw-md[./examples/with-shared-requests.md]{render}
</details> </details>

View File

@@ -2,16 +2,16 @@
First, create a resource: First, create a resource:
```http id=createUser ```http #createUser,format=yaml
POST https://httpbin.org/post POST https://httpbin.org/post
Content-Type: application/json Content-Type: application/json
{"username": "alpha"} username: alpha
``` ```
Then, fetch a different resource: Then, fetch a different resource:
```http id=getItem ```http #getItem
GET https://httpbin.org/get?item=123 GET https://httpbin.org/get?item=123
``` ```

View File

@@ -4,7 +4,7 @@ Let's include some shared requests:
::md[./_shared_requests.md] ::md[./_shared_requests.md]
The shared GET request returned: The shared GET request returned: {{response.statusText}}
Now, a request specific to this document: Now, a request specific to this document:
@@ -12,7 +12,7 @@ Now, a request specific to this document:
POST https://httpbin.org/post POST https://httpbin.org/post
Content-Type: application/json Content-Type: application/json
{"dataFromMain": "someValue", "sharedUrl": ""} {"dataFromMain": "someValue", "sharedUrl": "{{requests.sharedGetRequest.url}}"}
``` ```
::response ::response

View File

@@ -17,6 +17,7 @@
"build": "pnpm run build:lib && pnpm run build:readme", "build": "pnpm run build:lib && pnpm run build:readme",
"build:lib": "tsc --build", "build:lib": "tsc --build",
"build:readme": "pnpm run cli build docs/README.md README.md", "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", "dev:readme": "pnpm run cli dev docs/README.md --watch",
"test": "echo \"Error: no test specified\" && exit 1" "test": "echo \"Error: no test specified\" && exit 1"
}, },
@@ -35,6 +36,7 @@
}, },
"dependencies": { "dependencies": {
"blessed": "^0.1.81", "blessed": "^0.1.81",
"chalk": "^5.4.1",
"commander": "^14.0.0", "commander": "^14.0.0",
"dotenv": "^16.5.0", "dotenv": "^16.5.0",
"eventemitter3": "^5.0.1", "eventemitter3": "^5.0.1",
@@ -44,7 +46,9 @@
"marked-terminal": "^7.3.0", "marked-terminal": "^7.3.0",
"mdast-util-to-markdown": "^2.1.2", "mdast-util-to-markdown": "^2.1.2",
"mdast-util-to-string": "^4.0.0", "mdast-util-to-string": "^4.0.0",
"mdast-util-toc": "^7.1.0",
"rehype-stringify": "^10.0.1", "rehype-stringify": "^10.0.1",
"remark-behead": "^3.1.0",
"remark-directive": "^4.0.0", "remark-directive": "^4.0.0",
"remark-gfm": "^4.0.1", "remark-gfm": "^4.0.1",
"remark-parse": "^11.0.0", "remark-parse": "^11.0.0",

128
pnpm-lock.yaml generated
View File

@@ -11,6 +11,9 @@ importers:
blessed: blessed:
specifier: ^0.1.81 specifier: ^0.1.81
version: 0.1.81 version: 0.1.81
chalk:
specifier: ^5.4.1
version: 5.4.1
commander: commander:
specifier: ^14.0.0 specifier: ^14.0.0
version: 14.0.0 version: 14.0.0
@@ -38,9 +41,15 @@ importers:
mdast-util-to-string: mdast-util-to-string:
specifier: ^4.0.0 specifier: ^4.0.0
version: 4.0.0 version: 4.0.0
mdast-util-toc:
specifier: ^7.1.0
version: 7.1.0
rehype-stringify: rehype-stringify:
specifier: ^10.0.1 specifier: ^10.0.1
version: 10.0.1 version: 10.0.1
remark-behead:
specifier: ^3.1.0
version: 3.1.0
remark-directive: remark-directive:
specifier: ^4.0.0 specifier: ^4.0.0
version: 4.0.0 version: 4.0.0
@@ -455,6 +464,9 @@ packages:
'@types/terminal-kit@2.5.7': '@types/terminal-kit@2.5.7':
resolution: {integrity: sha512-IpbCBFSb3OqCEZBZlk368tGftqss88eNQaJdD9msEShRbksEiVahEqroONi60ppUt9/arLM6IDrHMx9jpzzCOw==} resolution: {integrity: sha512-IpbCBFSb3OqCEZBZlk368tGftqss88eNQaJdD9msEShRbksEiVahEqroONi60ppUt9/arLM6IDrHMx9jpzzCOw==}
'@types/ungap__structured-clone@1.2.0':
resolution: {integrity: sha512-ZoaihZNLeZSxESbk9PUAPZOlSpcKx81I1+4emtULDVmBLkYutTcMlCj2K9VNlf9EWODxdO6gkAqEaLorXwZQVA==}
'@types/unist@2.0.11': '@types/unist@2.0.11':
resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==} resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==}
@@ -740,6 +752,9 @@ packages:
get-tsconfig@4.10.0: get-tsconfig@4.10.0:
resolution: {integrity: sha512-kGzZ3LWWQcGIAmg6iWvXn0ei6WDtV26wzHRMwDSzmAbcXrTEXxHy6IehI6/4eT6VRKyMP1eF1VqwrVUmE/LR7A==} resolution: {integrity: sha512-kGzZ3LWWQcGIAmg6iWvXn0ei6WDtV26wzHRMwDSzmAbcXrTEXxHy6IehI6/4eT6VRKyMP1eF1VqwrVUmE/LR7A==}
github-slugger@2.0.0:
resolution: {integrity: sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==}
glob-parent@5.1.2: glob-parent@5.1.2:
resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
engines: {node: '>= 6'} engines: {node: '>= 6'}
@@ -890,6 +905,9 @@ packages:
resolution: {integrity: sha512-gUD/epcRms75Cw8RT1pUdHugZYM5ce64ucs2GEISABwkRsOQr0q2wm/MV2TKThycIe5e0ytRweW2RZxclogCdQ==} resolution: {integrity: sha512-gUD/epcRms75Cw8RT1pUdHugZYM5ce64ucs2GEISABwkRsOQr0q2wm/MV2TKThycIe5e0ytRweW2RZxclogCdQ==}
engines: {node: '>=8'} engines: {node: '>=8'}
lodash.iteratee@4.7.0:
resolution: {integrity: sha512-yv3cSQZmfpbIKo4Yo45B1taEvxjNvcpF1CEOc0Y6dEyvhPIfEJE3twDwPgWTPQubcSgXyBwBKG6wpQvWMDOf6Q==}
longest-streak@3.1.0: longest-streak@3.1.0:
resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==}
@@ -959,6 +977,9 @@ packages:
mdast-util-to-string@4.0.0: mdast-util-to-string@4.0.0:
resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==} resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==}
mdast-util-toc@7.1.0:
resolution: {integrity: sha512-2TVKotOQzqdY7THOdn2gGzS9d1Sdd66bvxUyw3aNpWfcPXCLYSJCCgfPy30sEtuzkDraJgqF35dzgmz6xlvH/w==}
mem@8.1.1: mem@8.1.1:
resolution: {integrity: sha512-qFCFUDs7U3b8mBDPyz5EToEKoAkgCzqquIgi9nkkR9bixxOVOre+09lbuH7+9Kn2NFpm56M3GUWVbU2hQgdACA==} resolution: {integrity: sha512-qFCFUDs7U3b8mBDPyz5EToEKoAkgCzqquIgi9nkkR9bixxOVOre+09lbuH7+9Kn2NFpm56M3GUWVbU2hQgdACA==}
engines: {node: '>=10'} engines: {node: '>=10'}
@@ -1227,6 +1248,10 @@ packages:
rehype-stringify@10.0.1: rehype-stringify@10.0.1:
resolution: {integrity: sha512-k9ecfXHmIPuFVI61B9DeLPN0qFHfawM6RsuX48hoqlaKSF61RskNjSm1lI8PhBEM0MRdLxVVm4WmTqJQccH9mA==} 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: remark-directive@4.0.0:
resolution: {integrity: sha512-7sxn4RfF1o3izevPV1DheyGDD6X4c9hrGpfdUpm7uC++dqrnJxIZVkk7CoKqcLm0VUMAuOol7Mno3m6g8cfMuA==} resolution: {integrity: sha512-7sxn4RfF1o3izevPV1DheyGDD6X4c9hrGpfdUpm7uC++dqrnJxIZVkk7CoKqcLm0VUMAuOol7Mno3m6g8cfMuA==}
@@ -1431,6 +1456,25 @@ packages:
resolution: {integrity: sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==} resolution: {integrity: sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==}
engines: {node: '>=8'} 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: unist-util-is@6.0.0:
resolution: {integrity: sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==} resolution: {integrity: sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==}
@@ -1440,9 +1484,21 @@ packages:
unist-util-stringify-position@4.0.0: unist-util-stringify-position@4.0.0:
resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==} 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: unist-util-visit-parents@6.0.1:
resolution: {integrity: sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==} 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: unist-util-visit@5.0.0:
resolution: {integrity: sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==} resolution: {integrity: sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==}
@@ -1895,6 +1951,8 @@ snapshots:
dependencies: dependencies:
'@types/nextgen-events': 1.1.4 '@types/nextgen-events': 1.1.4
'@types/ungap__structured-clone@1.2.0': {}
'@types/unist@2.0.11': {} '@types/unist@2.0.11': {}
'@types/unist@3.0.3': {} '@types/unist@3.0.3': {}
@@ -2182,6 +2240,8 @@ snapshots:
dependencies: dependencies:
resolve-pkg-maps: 1.0.0 resolve-pkg-maps: 1.0.0
github-slugger@2.0.0: {}
glob-parent@5.1.2: glob-parent@5.1.2:
dependencies: dependencies:
is-glob: 4.0.3 is-glob: 4.0.3
@@ -2313,6 +2373,8 @@ snapshots:
strip-bom: 4.0.0 strip-bom: 4.0.0
type-fest: 0.6.0 type-fest: 0.6.0
lodash.iteratee@4.7.0: {}
longest-streak@3.1.0: {} longest-streak@3.1.0: {}
map-age-cleaner@0.1.3: map-age-cleaner@0.1.3:
@@ -2466,6 +2528,16 @@ snapshots:
dependencies: dependencies:
'@types/mdast': 4.0.4 '@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: mem@8.1.1:
dependencies: dependencies:
map-age-cleaner: 0.1.3 map-age-cleaner: 0.1.3
@@ -2833,6 +2905,14 @@ snapshots:
hast-util-to-html: 9.0.5 hast-util-to-html: 9.0.5
unified: 11.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: remark-directive@4.0.0:
dependencies: dependencies:
'@types/mdast': 4.0.4 '@types/mdast': 4.0.4
@@ -3051,6 +3131,32 @@ snapshots:
dependencies: dependencies:
crypto-random-string: 2.0.0 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: unist-util-is@6.0.0:
dependencies: dependencies:
'@types/unist': 3.0.3 '@types/unist': 3.0.3
@@ -3063,11 +3169,33 @@ snapshots:
dependencies: dependencies:
'@types/unist': 3.0.3 '@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: unist-util-visit-parents@6.0.1:
dependencies: dependencies:
'@types/unist': 3.0.3 '@types/unist': 3.0.3
unist-util-is: 6.0.0 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: unist-util-visit@5.0.0:
dependencies: dependencies:
'@types/unist': 3.0.3 '@types/unist': 3.0.3

View File

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

@@ -10,6 +10,7 @@ type Response = {
statusText: string; statusText: string;
headers: Record<string, string>; headers: Record<string, string>;
body?: string; body?: string;
rawBody?: string;
}; };
type AddRequestOptios = { type AddRequestOptios = {

View File

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

View File

@@ -59,7 +59,8 @@ const httpHandler: ExecutionHandler = ({
body body
}); });
let responseText = await response.text(); const rawBody = await response.text();
let responseText = rawBody;
if (options.json) { if (options.json) {
try { try {
responseText = JSON.parse(responseText); responseText = JSON.parse(responseText);
@@ -68,7 +69,7 @@ const httpHandler: ExecutionHandler = ({
} }
} }
node.value = content; node.value = [head, parsedBody].filter(Boolean).join('\n\n');
node.meta = undefined; node.meta = undefined;
context.addRequest({ context.addRequest({
@@ -84,6 +85,7 @@ const httpHandler: ExecutionHandler = ({
statusText: response.statusText, statusText: response.statusText,
headers: Object.fromEntries(response.headers.entries()), headers: Object.fromEntries(response.headers.entries()),
body: responseText, body: responseText,
rawBody: rawBody,
}, },
}); });
}, },

View File

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

View File

@@ -53,6 +53,7 @@ const responseHandler: ExecutionHandler = ({
const codeNode = { const codeNode = {
type: 'code', type: 'code',
lang: 'http',
value: responseContent, value: responseContent,
}; };
if (!parent || !('children' in parent) || index === undefined) { 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 { responseHandler } from "./handlers.response.js";
import { textHandler } from "./handlers.text.js"; import { textHandler } from "./handlers.text.js";
import { codeHandler } from "./handlers.code.js"; import { codeHandler } from "./handlers.code.js";
import { tocHandler } from "./handlers.toc.js";
const handlers = [ const handlers = [
fileHandler, fileHandler,
@@ -17,4 +18,8 @@ const handlers = [
codeHandler, codeHandler,
] satisfies ExecutionHandler[]; ] 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 };