mirror of
https://github.com/morten-olsen/http.md.git
synced 2026-02-08 00:46:28 +01:00
Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b308c7f9fe | ||
|
|
ba56b66222 | ||
|
|
e0707e74fb | ||
|
|
6b74a28989 | ||
|
|
ad342e5f10 | ||
|
|
5d485acc97 | ||
|
|
68f5025527 | ||
|
|
a9a7bae28f | ||
|
|
b800290d72 | ||
|
|
d7b6a3880e | ||
|
|
0eff8cf603 | ||
|
|
1b2d345420 | ||
|
|
1cb885bb32 |
7
.github/workflows/main.yaml
vendored
7
.github/workflows/main.yaml
vendored
@@ -92,7 +92,7 @@ jobs:
|
||||
|
||||
release:
|
||||
permissions:
|
||||
contents: read
|
||||
contents: write
|
||||
packages: write
|
||||
attestations: write
|
||||
id-token: write
|
||||
@@ -135,3 +135,8 @@ jobs:
|
||||
pnpm publish --no-git-checks --access public
|
||||
env:
|
||||
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
1
.gitignore
vendored
@@ -1,2 +1,3 @@
|
||||
/node_modules/
|
||||
/dist/
|
||||
/*.html
|
||||
|
||||
92
README.md
92
README.md
@@ -110,9 +110,9 @@ HTTP/200 OK
|
||||
access-control-allow-credentials: true
|
||||
access-control-allow-origin: *
|
||||
connection: keep-alive
|
||||
content-length: 559
|
||||
content-length: 555
|
||||
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
|
||||
|
||||
{
|
||||
@@ -129,12 +129,12 @@ server: gunicorn/19.9.0
|
||||
"Host": "httpbin.org",
|
||||
"Sec-Fetch-Mode": "cors",
|
||||
"User-Agent": "node",
|
||||
"X-Amzn-Trace-Id": "Root=1-682a161b-6f8d778138665a8f22ffbe94"
|
||||
"X-Amzn-Trace-Id": "Root=1-682a3111-131bcbff690b03fd64aa4617"
|
||||
},
|
||||
"json": {
|
||||
"greeting": "Hello, http.md!"
|
||||
},
|
||||
"origin": "185.181.220.204",
|
||||
"origin": "23.96.180.7",
|
||||
"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:**
|
||||
|
||||
````markdown
|
||||
```http id=createItem json
|
||||
```http #createItem,json
|
||||
POST https://httpbin.org/post
|
||||
Content-Type: application/json
|
||||
|
||||
{"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:
|
||||
|
||||
@@ -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.)*
|
||||
|
||||
**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
|
||||
```
|
||||
|
||||
The request to `/status/201` completed with status code: **{{response.status}}**.
|
||||
The request to `/status/201` completed with status code: ****.
|
||||
````
|
||||
|
||||
## Managing Documents
|
||||
@@ -324,7 +379,7 @@ Let's include some shared requests:
|
||||
|
||||
::md[./_shared_requests.md]
|
||||
|
||||
The shared GET request returned: {{responses.sharedGetRequest.status}}
|
||||
The shared GET request returned:
|
||||
|
||||
Now, a request specific to this document:
|
||||
|
||||
@@ -332,7 +387,7 @@ Now, a request specific to this document:
|
||||
POST https://httpbin.org/post
|
||||
Content-Type: application/json
|
||||
|
||||
{"dataFromMain": "someValue", "sharedUrl": "{{requests.sharedGetRequest.url}}"}
|
||||
{"dataFromMain": "someValue", "sharedUrl": ""}
|
||||
```
|
||||
|
||||
::response
|
||||
@@ -356,8 +411,8 @@ httpmd build mydoc.md output.md -i baseUrl=https://api.production.example.com -i
|
||||
|
||||
````markdown
|
||||
```http
|
||||
GET {{input.baseUrl}}/users/1
|
||||
Authorization: Bearer {{input.apiKey}}
|
||||
GET /users/1
|
||||
Authorization: Bearer
|
||||
```
|
||||
|
||||
::response
|
||||
@@ -406,8 +461,8 @@ You can configure the behavior of each `http` code block by adding options to it
|
||||
|
||||
````markdown
|
||||
```http id=complexRequest,json,yaml,hidden
|
||||
POST {{input.apiEndpoint}}/data
|
||||
X-API-Key: {{input.apiKey}}
|
||||
POST /data
|
||||
X-API-Key:
|
||||
Content-Type: application/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.
|
||||
* 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)
|
||||
|
||||
The `httpmd` tool provides the following commands:
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import 'dotenv/config.js';
|
||||
import '../dist/cli/cli.js';
|
||||
|
||||
@@ -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:
|
||||
@@ -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.
|
||||
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]
|
||||
|
||||
<details>
|
||||
<summary>Output</summary>
|
||||
|
||||
::raw-md[./examples/with-multiple-requests.md]{render}
|
||||
|
||||
</details>
|
||||
|
||||
## 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.
|
||||
@@ -176,6 +187,13 @@ Within your markdown document, the following variables are available in the Hand
|
||||
|
||||
::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.)_
|
||||
|
||||
**2. Displaying a status code in markdown text:**
|
||||
@@ -202,34 +220,18 @@ The requests from the embedded document are processed, and their `request` and `
|
||||
|
||||
Assume `_shared_requests.md` contains:
|
||||
|
||||
````markdown
|
||||
```http id=sharedGetRequest
|
||||
GET https://httpbin.org/get
|
||||
```
|
||||
````
|
||||
::raw-md[./examples/_shared_requests.md]
|
||||
|
||||
Then, in `main.md`:
|
||||
|
||||
````markdown
|
||||
# Main Document
|
||||
::raw-md[./examples/with-shared-requests.md]
|
||||
|
||||
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}}
|
||||
|
||||
Now, a request specific to this document:
|
||||
|
||||
```http
|
||||
POST https://httpbin.org/post
|
||||
Content-Type: application/json
|
||||
|
||||
{"dataFromMain": "someValue", "sharedUrl": "{{requests.sharedGetRequest.url}}"}
|
||||
```
|
||||
|
||||
::response
|
||||
````
|
||||
</details>
|
||||
|
||||
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 +340,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.
|
||||
- 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)
|
||||
|
||||
The `httpmd` tool provides the following commands:
|
||||
|
||||
3
docs/examples/_shared_requests.md
Normal file
3
docs/examples/_shared_requests.md
Normal file
@@ -0,0 +1,3 @@
|
||||
```http #sharedGetRequest
|
||||
GET https://httpbin.org/get
|
||||
```
|
||||
@@ -2,16 +2,16 @@
|
||||
|
||||
First, create a resource:
|
||||
|
||||
```http id=createUser
|
||||
```http #createUser,yaml,json
|
||||
POST https://httpbin.org/post
|
||||
Content-Type: application/json
|
||||
|
||||
{"username": "alpha"}
|
||||
username: alpha
|
||||
```
|
||||
|
||||
Then, fetch a different resource:
|
||||
|
||||
```http id=getItem
|
||||
```http #getItem
|
||||
GET https://httpbin.org/get?item=123
|
||||
```
|
||||
|
||||
|
||||
18
docs/examples/with-shared-requests.md
Normal file
18
docs/examples/with-shared-requests.md
Normal 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
|
||||
@@ -1,11 +1,11 @@
|
||||
```http id=createItem json
|
||||
```http #createItem,json
|
||||
POST https://httpbin.org/post
|
||||
Content-Type: application/json
|
||||
|
||||
{"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:
|
||||
|
||||
|
||||
@@ -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,7 +36,9 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"blessed": "^0.1.81",
|
||||
"chalk": "^5.4.1",
|
||||
"commander": "^14.0.0",
|
||||
"dotenv": "^16.5.0",
|
||||
"eventemitter3": "^5.0.1",
|
||||
"handlebars": "^4.7.8",
|
||||
"hastscript": "^9.0.1",
|
||||
@@ -43,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",
|
||||
|
||||
137
pnpm-lock.yaml
generated
137
pnpm-lock.yaml
generated
@@ -11,9 +11,15 @@ 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
|
||||
dotenv:
|
||||
specifier: ^16.5.0
|
||||
version: 16.5.0
|
||||
eventemitter3:
|
||||
specifier: ^5.0.1
|
||||
version: 5.0.1
|
||||
@@ -35,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
|
||||
@@ -452,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==}
|
||||
|
||||
@@ -657,6 +672,10 @@ packages:
|
||||
devlop@1.1.0:
|
||||
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:
|
||||
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
|
||||
|
||||
@@ -733,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'}
|
||||
@@ -883,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==}
|
||||
|
||||
@@ -952,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'}
|
||||
@@ -1220,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==}
|
||||
|
||||
@@ -1424,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==}
|
||||
|
||||
@@ -1433,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==}
|
||||
|
||||
@@ -1888,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': {}
|
||||
@@ -2077,6 +2142,8 @@ snapshots:
|
||||
dependencies:
|
||||
dequal: 2.0.3
|
||||
|
||||
dotenv@16.5.0: {}
|
||||
|
||||
emoji-regex@8.0.0: {}
|
||||
|
||||
emojilib@2.4.0: {}
|
||||
@@ -2173,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
|
||||
@@ -2304,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:
|
||||
@@ -2457,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
|
||||
@@ -2824,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
|
||||
@@ -3042,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
|
||||
@@ -3054,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
|
||||
|
||||
@@ -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
70
src/cli/ui/ui.ts
Normal 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 };
|
||||
@@ -10,6 +10,7 @@ type Response = {
|
||||
statusText: string;
|
||||
headers: Record<string, string>;
|
||||
body?: string;
|
||||
rawBody?: string;
|
||||
};
|
||||
|
||||
type AddRequestOptios = {
|
||||
@@ -36,7 +37,7 @@ class Context {
|
||||
|
||||
constructor(options: ContextOptions = {}) {
|
||||
this.input = options.input || {};
|
||||
this.env = options.env || {};
|
||||
this.env = options.env || process.env;
|
||||
this.requests = options.requests || {};
|
||||
this.responses = options.responses || {};
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
34
src/execution/handlers/handlers.code.ts
Normal file
34
src/execution/handlers/handlers.code.ts
Normal 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 };
|
||||
@@ -6,85 +6,90 @@ const httpHandler: ExecutionHandler = ({
|
||||
node,
|
||||
addStep,
|
||||
}) => {
|
||||
if (node.type === 'code') {
|
||||
if (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: '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,
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
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];
|
||||
})
|
||||
);
|
||||
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.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 = [head, parsedBody].filter(Boolean).join('\n\n');
|
||||
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 };
|
||||
|
||||
@@ -22,10 +22,34 @@ const inputHandler: ExecutionHandler = ({
|
||||
context.input[name] = node.attributes.default;
|
||||
}
|
||||
|
||||
if (node.attributes?.format === 'number' && context.input[name] !== undefined) {
|
||||
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 (node.attributes?.format && context.input[name] !== undefined) {
|
||||
const format = node.attributes.format;
|
||||
if (format === 'number') {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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');
|
||||
@@ -53,6 +53,7 @@ const responseHandler: ExecutionHandler = ({
|
||||
|
||||
const codeNode = {
|
||||
type: 'code',
|
||||
lang: 'http',
|
||||
value: responseContent,
|
||||
};
|
||||
if (!parent || !('children' in parent) || index === undefined) {
|
||||
|
||||
29
src/execution/handlers/handlers.toc.ts
Normal file
29
src/execution/handlers/handlers.toc.ts
Normal 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 };
|
||||
@@ -1,10 +1,12 @@
|
||||
import { ExecutionHandler } from "../execution.js";
|
||||
import { fileHandler } from "./handlers.file.js";
|
||||
import { fileHandler } from "./handlers.md.js";
|
||||
import { httpHandler } from "./handlers.http.js";
|
||||
import { inputHandler } from "./handlers.input.js";
|
||||
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,
|
||||
@@ -13,6 +15,11 @@ const handlers = [
|
||||
textHandler,
|
||||
inputHandler,
|
||||
rawMdHandler,
|
||||
codeHandler,
|
||||
] satisfies ExecutionHandler[];
|
||||
|
||||
export { handlers };
|
||||
const postHandlers = [
|
||||
tocHandler,
|
||||
] satisfies ExecutionHandler[];
|
||||
|
||||
export { handlers, postHandlers };
|
||||
|
||||
29
src/theme/theme.html.ts
Normal file
29
src/theme/theme.html.ts
Normal 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 };
|
||||
Reference in New Issue
Block a user