11 Commits

Author SHA1 Message Date
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
morten-olsen
ad342e5f10 docs: generated README 2025-05-18 19:12:53 +00:00
Morten Olsen
5d485acc97 fix: fix parsing issue 2025-05-18 21:11:51 +02:00
morten-olsen
68f5025527 docs: generated README 2025-05-18 18:56:34 +00:00
Morten Olsen
a9a7bae28f commit on publish 2025-05-18 20:55:28 +02:00
Morten Olsen
b800290d72 feat: support hash ids in http blocks 2025-05-18 20:45:08 +02:00
Morten Olsen
d7b6a3880e feat: load env vars 2025-05-18 20:36:12 +02:00
Morten Olsen
0eff8cf603 feat: auto set env to host envs 2025-05-18 20:34:11 +02:00
Morten Olsen
1b2d345420 feat: support template in all code blocks 2025-05-18 20:31:54 +02:00
Morten Olsen
1cb885bb32 feat: improved input format 2025-05-18 20:26:15 +02:00
22 changed files with 615 additions and 136 deletions

View File

@@ -92,7 +92,7 @@ jobs:
release: release:
permissions: permissions:
contents: read contents: write
packages: write packages: write
attestations: write attestations: write
id-token: write id-token: write
@@ -135,3 +135,8 @@ jobs:
pnpm publish --no-git-checks --access public pnpm publish --no-git-checks --access public
env: env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
- uses: stefanzweifel/git-auto-commit-action@v5
with:
commit_message: "docs: generated README"
file_pattern: "*.md"

1
.gitignore vendored
View File

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

View File

@@ -110,9 +110,9 @@ HTTP/200 OK
access-control-allow-credentials: true access-control-allow-credentials: true
access-control-allow-origin: * access-control-allow-origin: *
connection: keep-alive connection: keep-alive
content-length: 559 content-length: 555
content-type: application/json content-type: application/json
date: Sun, 18 May 2025 17:17:15 GMT date: Sun, 18 May 2025 19:12:17 GMT
server: gunicorn/19.9.0 server: gunicorn/19.9.0
{ {
@@ -129,12 +129,12 @@ server: gunicorn/19.9.0
"Host": "httpbin.org", "Host": "httpbin.org",
"Sec-Fetch-Mode": "cors", "Sec-Fetch-Mode": "cors",
"User-Agent": "node", "User-Agent": "node",
"X-Amzn-Trace-Id": "Root=1-682a161b-6f8d778138665a8f22ffbe94" "X-Amzn-Trace-Id": "Root=1-682a3111-131bcbff690b03fd64aa4617"
}, },
"json": { "json": {
"greeting": "Hello, http.md!" "greeting": "Hello, http.md!"
}, },
"origin": "185.181.220.204", "origin": "23.96.180.7",
"url": "https://httpbin.org/post" "url": "https://httpbin.org/post"
} }
@@ -264,14 +264,14 @@ Within your markdown document, the following variables are available in the Hand
**1. Using a value from a previous response in a new request:** **1. Using a value from a previous response in a new request:**
````markdown ````markdown
```http id=createItem json ```http #createItem,json
POST https://httpbin.org/post POST https://httpbin.org/post
Content-Type: application/json Content-Type: application/json
{"name": "My New Item"} {"name": "My New Item"}
``` ```
The new item ID is: {{responses.createItem.body.json.name}} The new item ID is: {{response.body.json.name}}
Now, let's fetch the item using a (mocked) ID from the response: Now, let's fetch the item using a (mocked) ID from the response:
@@ -283,6 +283,61 @@ GET https://httpbin.org/anything/{{responses.createItem.body.json.name}}
```` ````
<details>
<summary>Output</summary>
````markdown
```http
POST https://httpbin.org/post
Content-Type: application/json
{"name": "My New Item"}
```
The new item ID is: My New Item
Now, let's fetch the item using a (mocked) ID from the response:
```http
GET https://httpbin.org/anything/My New Item
```
```
HTTP/200 OK
access-control-allow-credentials: true
access-control-allow-origin: *
connection: keep-alive
content-length: 451
content-type: application/json
date: Sun, 18 May 2025 19:12:18 GMT
server: gunicorn/19.9.0
{
"args": {},
"data": "",
"files": {},
"form": {},
"headers": {
"Accept": "*/*",
"Accept-Encoding": "br, gzip, deflate",
"Accept-Language": "*",
"Host": "httpbin.org",
"Sec-Fetch-Mode": "cors",
"User-Agent": "node",
"X-Amzn-Trace-Id": "Root=1-682a3112-4bbb29111129c1556c487ca1"
},
"json": null,
"method": "GET",
"origin": "23.96.180.7",
"url": "https://httpbin.org/anything/My New Item"
}
```
````
</details>
*(Note: `httpbin.org/post` wraps the JSON sent in a "json" field in its response. If your API returns the ID directly at the root of the JSON body, you'd use `{{responses.createItem.body.id}}` assuming the `createItem` request had the `json` option.)* *(Note: `httpbin.org/post` wraps the JSON sent in a "json" field in its response. If your API returns the ID directly at the root of the JSON body, you'd use `{{responses.createItem.body.id}}` assuming the `createItem` request had the `json` option.)*
**2. Displaying a status code in markdown text:** **2. Displaying a status code in markdown text:**
@@ -292,7 +347,7 @@ GET https://httpbin.org/anything/{{responses.createItem.body.json.name}}
GET https://httpbin.org/status/201 GET https://httpbin.org/status/201
``` ```
The request to `/status/201` completed with status code: **{{response.status}}**. The request to `/status/201` completed with status code: ****.
```` ````
## Managing Documents ## Managing Documents
@@ -324,7 +379,7 @@ Let's include some shared requests:
::md[./_shared_requests.md] ::md[./_shared_requests.md]
The shared GET request returned: {{responses.sharedGetRequest.status}} The shared GET request returned:
Now, a request specific to this document: Now, a request specific to this document:
@@ -332,7 +387,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": "{{requests.sharedGetRequest.url}}"} {"dataFromMain": "someValue", "sharedUrl": ""}
``` ```
::response ::response
@@ -356,8 +411,8 @@ httpmd build mydoc.md output.md -i baseUrl=https://api.production.example.com -i
````markdown ````markdown
```http ```http
GET {{input.baseUrl}}/users/1 GET /users/1
Authorization: Bearer {{input.apiKey}} Authorization: Bearer
``` ```
::response ::response
@@ -406,8 +461,8 @@ You can configure the behavior of each `http` code block by adding options to it
````markdown ````markdown
```http id=complexRequest,json,yaml,hidden ```http id=complexRequest,json,yaml,hidden
POST {{input.apiEndpoint}}/data POST /data
X-API-Key: {{input.apiKey}} X-API-Key:
Content-Type: application/json Content-Type: application/json
# Request body written in YAML, will be converted to JSON # Request body written in YAML, will be converted to JSON
@@ -445,6 +500,17 @@ The `::md` directive embeds another markdown document.
* `hidden`: If present, the actual content (markdown) of the embedded document will not be rendered in the output. However, any `http` requests within the embedded document *are still processed*, and their `request` and `response` data become available in the parent document's templating context (via `requests.id` and `responses.id`). This is useful if you only want to execute the requests from an included file (e.g., a common setup sequence) and use their results, without displaying the embedded file's content. * `hidden`: If present, the actual content (markdown) of the embedded document will not be rendered in the output. However, any `http` requests within the embedded document *are still processed*, and their `request` and `response` data become available in the parent document's templating context (via `requests.id` and `responses.id`). This is useful if you only want to execute the requests from an included file (e.g., a common setup sequence) and use their results, without displaying the embedded file's content.
* Example: `::md[./setup_requests.md]{hidden}` * Example: `::md[./setup_requests.md]{hidden}`
#### `::input[{name}]` Directive Options
The `::input` directive is used to declare expected input variables
* **Variable Name:** The first argument (required) is the name of the variable
* Example: `::input[myVariable]` will define `input.myVariable`
* `required`: If present it will require that the variable is provided
* `default={value}`: Defines the default value if no value has been provided
* `format=string|number|bool|json|date`: If provided the value will be parsed using the specified format
* \`\`
## Command-Line Interface (CLI) ## Command-Line Interface (CLI)
The `httpmd` tool provides the following commands: The `httpmd` tool provides the following commands:

View File

@@ -1,3 +1,4 @@
#!/usr/bin/env node #!/usr/bin/env node
import 'dotenv/config.js';
import '../dist/cli/cli.js'; import '../dist/cli/cli.js';

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:
@@ -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 requests are defined in fenced code blocks annotated with `http`. The syntax is similar to the raw HTTP format:
``` ```http disable
<METHOD> <URL> <METHOD> <URL>
<Header-Name>: <Header-Value> <Header-Name>: <Header-Value>
... ...
@@ -176,6 +180,13 @@ Within your markdown document, the following variables are available in the Hand
::raw-md[./examples/with-template.md] ::raw-md[./examples/with-template.md]
<details>
<summary>Output</summary>
::raw-md[./examples/with-template.md]{render}
</details>
_(Note: `httpbin.org/post` wraps the JSON sent in a "json" field in its response. If your API returns the ID directly at the root of the JSON body, you'd use `{{responses.createItem.body.id}}` assuming the `createItem` request had the `json` option.)_ _(Note: `httpbin.org/post` wraps the JSON sent in a "json" field in its response. If your API returns the ID directly at the root of the JSON body, you'd use `{{responses.createItem.body.id}}` assuming the `createItem` request had the `json` option.)_
**2. Displaying a status code in markdown text:** **2. Displaying a status code in markdown text:**
@@ -202,34 +213,18 @@ The requests from the embedded document are processed, and their `request` and `
Assume `_shared_requests.md` contains: Assume `_shared_requests.md` contains:
````markdown ::raw-md[./examples/_shared_requests.md]
```http id=sharedGetRequest
GET https://httpbin.org/get
```
````
Then, in `main.md`: Then, in `main.md`:
````markdown ::raw-md[./examples/with-shared-requests.md]
# Main Document
Let's include some shared requests: <details>
<summary>Output</summary>
::md[./_shared_requests.md] ::raw-md[./examples/with-shared-requests.md]{render}
The shared GET request returned: {{responses.sharedGetRequest.status}} </details>
Now, a request specific to this document:
```http
POST https://httpbin.org/post
Content-Type: application/json
{"dataFromMain": "someValue", "sharedUrl": "{{requests.sharedGetRequest.url}}"}
```
::response
````
When `main.md` is processed, `_shared_requests.md` will be embedded, its `sharedGetRequest` will be executed, and its data will be available for templating. When `main.md` is processed, `_shared_requests.md` will be embedded, its `sharedGetRequest` will be executed, and its data will be available for templating.
@@ -338,6 +333,17 @@ The `::md` directive embeds another markdown document.
- `hidden`: If present, the actual content (markdown) of the embedded document will not be rendered in the output. However, any `http` requests within the embedded document _are still processed_, and their `request` and `response` data become available in the parent document's templating context (via `requests.id` and `responses.id`). This is useful if you only want to execute the requests from an included file (e.g., a common setup sequence) and use their results, without displaying the embedded file's content. - `hidden`: If present, the actual content (markdown) of the embedded document will not be rendered in the output. However, any `http` requests within the embedded document _are still processed_, and their `request` and `response` data become available in the parent document's templating context (via `requests.id` and `responses.id`). This is useful if you only want to execute the requests from an included file (e.g., a common setup sequence) and use their results, without displaying the embedded file's content.
- Example: `::md[./setup_requests.md]{hidden}` - Example: `::md[./setup_requests.md]{hidden}`
#### `::input[{name}]` Directive Options
The `::input` directive is used to declare expected input variables
- **Variable Name:** The first argument (required) is the name of the variable
- Example: `::input[myVariable]` will define `input.myVariable`
- `required`: If present it will require that the variable is provided
- `default={value}`: Defines the default value if no value has been provided
- `format=string|number|bool|json|date`: If provided the value will be parsed using the specified format
- ``
## Command-Line Interface (CLI) ## Command-Line Interface (CLI)
The `httpmd` tool provides the following commands: The `httpmd` tool provides the following commands:

View File

@@ -0,0 +1,3 @@
```http #sharedGetRequest
GET https://httpbin.org/get
```

View File

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

View File

@@ -1,11 +1,11 @@
```http id=createItem json ```http #createItem,json
POST https://httpbin.org/post POST https://httpbin.org/post
Content-Type: application/json Content-Type: application/json
{"name": "My New Item"} {"name": "My New Item"}
``` ```
The new item ID is: {{responses.createItem.body.json.name}} The new item ID is: {{response.body.json.name}}
Now, let's fetch the item using a (mocked) ID from the response: Now, let's fetch the item using a (mocked) ID from the 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,7 +36,9 @@
}, },
"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",
"eventemitter3": "^5.0.1", "eventemitter3": "^5.0.1",
"handlebars": "^4.7.8", "handlebars": "^4.7.8",
"hastscript": "^9.0.1", "hastscript": "^9.0.1",
@@ -43,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",

137
pnpm-lock.yaml generated
View File

@@ -11,9 +11,15 @@ 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
dotenv:
specifier: ^16.5.0
version: 16.5.0
eventemitter3: eventemitter3:
specifier: ^5.0.1 specifier: ^5.0.1
version: 5.0.1 version: 5.0.1
@@ -35,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
@@ -452,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==}
@@ -657,6 +672,10 @@ packages:
devlop@1.1.0: devlop@1.1.0:
resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==}
dotenv@16.5.0:
resolution: {integrity: sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==}
engines: {node: '>=12'}
emoji-regex@8.0.0: emoji-regex@8.0.0:
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
@@ -733,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'}
@@ -883,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==}
@@ -952,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'}
@@ -1220,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==}
@@ -1424,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==}
@@ -1433,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==}
@@ -1888,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': {}
@@ -2077,6 +2142,8 @@ snapshots:
dependencies: dependencies:
dequal: 2.0.3 dequal: 2.0.3
dotenv@16.5.0: {}
emoji-regex@8.0.0: {} emoji-regex@8.0.0: {}
emojilib@2.4.0: {} emojilib@2.4.0: {}
@@ -2173,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
@@ -2304,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:
@@ -2457,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
@@ -2824,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
@@ -3042,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
@@ -3054,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 = {
@@ -36,7 +37,7 @@ class Context {
constructor(options: ContextOptions = {}) { constructor(options: ContextOptions = {}) {
this.input = options.input || {}; this.input = options.input || {};
this.env = options.env || {}; this.env = options.env || process.env;
this.requests = options.requests || {}; this.requests = options.requests || {};
this.responses = options.responses || {}; this.responses = options.responses || {};
} }

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

@@ -0,0 +1,34 @@
import Handlebars from "handlebars";
import { ExecutionHandler } from "../execution.js";
const codeHandler: ExecutionHandler = ({
node,
addStep,
}) => {
if (node.type !== 'code' || node.lang === 'http') {
return;
}
const optionParts = node.meta?.split(',') || [];
const options = Object.fromEntries(
optionParts.filter(Boolean).map((option) => {
const [key, value] = option.split('=');
return [key.trim(), value?.trim() || true];
})
);
addStep({
type: 'code',
node,
action: async ({ context }) => {
node.meta = undefined;
if (options['no-tmpl'] === true) {
return;
}
const template = Handlebars.compile(node.value);
const content = template(context);
node.value = content;
},
});
};
export { codeHandler };

View File

@@ -6,85 +6,90 @@ const httpHandler: ExecutionHandler = ({
node, node,
addStep, addStep,
}) => { }) => {
if (node.type === 'code') { if (node.type !== 'code' || node.lang !== 'http') {
if (node.lang !== 'http') { return;
return;
}
const optionParts = node.meta?.split(',') || [];
const options = Object.fromEntries(
optionParts.filter(Boolean).map((option) => {
const [key, value] = option.split('=');
return [key.trim(), value?.trim() || true];
})
);
addStep({
type: 'http',
node,
action: async ({ context }) => {
if (options.disable === true) {
return;
}
const template = Handlebars.compile(node.value);
const content = template(context);
const [head, body] = content.split('\n\n');
const [top, ...headerItems] = head.split('\n');
const [method, url] = top.split(' ');
const headers = Object.fromEntries(
headerItems.map((header) => {
const [key, value] = header.split(':');
return [key.trim(), value?.trim() || ''];
})
);
let parsedBody = body;
if (options.format === 'yaml') {
try {
const parsed = YAML.parse(body);
parsedBody = JSON.stringify(parsed);
} catch (error) {
parsedBody = `Error parsing YAML: ${error}`;
}
}
const response = await fetch(url, {
method,
headers,
body
});
let responseText = await response.text();
if (options.json) {
try {
responseText = JSON.parse(responseText);
} catch (e) {
responseText = `Error parsing JSON: ${e}`;
}
}
node.value = content;
node.meta = undefined;
context.addRequest({
id: options.id?.toString(),
request: {
method,
url,
headers,
body,
},
response: {
status: response.status,
statusText: response.statusText,
headers: Object.fromEntries(response.headers.entries()),
body: responseText,
},
});
},
});
} }
const optionParts = node.meta?.split(',') || [];
const options = Object.fromEntries(
optionParts.filter(Boolean).map((option) => {
const [key, value] = option.split('=');
return [key.trim(), value?.trim() || true];
})
);
let id = options.id?.toString();
const idPart = optionParts.find((option) => option.startsWith('#'));
if (idPart) {
id = idPart.slice(1);
}
addStep({
type: 'http',
node,
action: async ({ context }) => {
if (options.disable === true) {
return;
}
const template = Handlebars.compile(node.value);
const content = template(context);
const [head, body] = content.split('\n\n');
const [top, ...headerItems] = head.split('\n');
const [method, ...urlParts] = top.split(' ');
const url = urlParts.join(' ').trim();
const headers = Object.fromEntries(
headerItems.map((header) => {
const [key, value] = header.split(':');
return [key.trim(), value?.trim() || ''];
})
);
let parsedBody = body;
if (options.format === 'yaml') {
try {
const parsed = YAML.parse(body);
parsedBody = JSON.stringify(parsed);
} catch (error) {
parsedBody = `Error parsing YAML: ${error}`;
}
}
const response = await fetch(url, {
method,
headers,
body
});
const rawBody = await response.text();
let responseText = rawBody;
if (options.json) {
try {
responseText = JSON.parse(responseText);
} catch (e) {
responseText = `Error parsing JSON: ${e}`;
}
}
node.value = content;
node.meta = undefined;
context.addRequest({
id,
request: {
method,
url,
headers,
body,
},
response: {
status: response.status,
statusText: response.statusText,
headers: Object.fromEntries(response.headers.entries()),
body: responseText,
rawBody: rawBody,
},
});
},
});
}; };
export { httpHandler }; export { httpHandler };

View File

@@ -22,10 +22,34 @@ const inputHandler: ExecutionHandler = ({
context.input[name] = node.attributes.default; context.input[name] = node.attributes.default;
} }
if (node.attributes?.format === 'number' && context.input[name] !== undefined) { if (node.attributes?.format && context.input[name] !== undefined) {
context.input[name] = Number(context.input[name]); const format = node.attributes.format;
if (context.input[name] !== undefined && isNaN(Number(context.input[name]))) { if (format === 'number') {
throw new Error(`Input "${name}" must be a number, but got "${context.input[name]}"`); context.input[name] = Number(context.input[name]);
if (context.input[name] !== undefined && isNaN(Number(context.input[name]))) {
throw new Error(`Input "${name}" must be a number, but got "${context.input[name]}"`);
}
}
if (format === 'boolean') {
context.input[name] = context.input[name] === 'true';
}
if (format === 'string') {
context.input[name] = String(context.input[name]);
}
if (format === 'json') {
try {
context.input[name] = JSON.parse(String(context.input[name]));
} catch (error) {
throw new Error(`Input "${name}" must be a valid JSON, but got "${context.input[name]}"`);
}
}
if (format === 'date') {
const date = new Date(context.input[name] as string);
if (isNaN(date.getTime())) {
throw new Error(`Input "${name}" must be a valid date, but got "${context.input[name]}"`);
}
context.input[name] = date;
} }
} }

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

@@ -1,10 +1,12 @@
import { ExecutionHandler } from "../execution.js"; import { ExecutionHandler } from "../execution.js";
import { fileHandler } from "./handlers.file.js"; import { fileHandler } from "./handlers.md.js";
import { httpHandler } from "./handlers.http.js"; import { httpHandler } from "./handlers.http.js";
import { inputHandler } from "./handlers.input.js"; import { inputHandler } from "./handlers.input.js";
import { rawMdHandler } from "./handlers.raw-md.js"; 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 { tocHandler } from "./handlers.toc.js";
const handlers = [ const handlers = [
fileHandler, fileHandler,
@@ -13,6 +15,11 @@ const handlers = [
textHandler, textHandler,
inputHandler, inputHandler,
rawMdHandler, rawMdHandler,
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 };