mirror of
https://github.com/morten-olsen/http.md.git
synced 2026-02-08 00:46:28 +01:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6ce6eeeedb |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,3 +1,2 @@
|
|||||||
/node_modules/
|
/node_modules/
|
||||||
/dist/
|
/dist/
|
||||||
/*.html
|
|
||||||
|
|||||||
314
README.md
314
README.md
@@ -21,51 +21,12 @@ 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.
|
||||||
|
|
||||||
## Roadmap
|
|
||||||
|
|
||||||
* **Programmatic API** Use `http.md` inside existing scripts and pipelines
|
|
||||||
* **Environment Varaiables** Support using the runners environment variables in templates
|
|
||||||
* **JavaScript script support** Add JavaScript code blocks with execution, which will allow more advanced use-cases
|
|
||||||
* **Asserts** Add the ability to make HTTP assertions to use the document for testing
|
|
||||||
* **Templates** Write re-usable templates which can be used in documents
|
|
||||||
|
|
||||||
## Content
|
|
||||||
|
|
||||||
* [Key Features](#key-features)
|
|
||||||
* [Use Cases](#use-cases)
|
|
||||||
* [Roadmap](#roadmap)
|
|
||||||
* [Content](#content)
|
|
||||||
* [Installation](#installation)
|
|
||||||
* [Getting Started](#getting-started)
|
|
||||||
* [Your First Request](#your-first-request)
|
|
||||||
* [Rendering Documents](#rendering-documents)
|
|
||||||
* [Core Concepts](#core-concepts)
|
|
||||||
* [HTTP Request Blocks](#http-request-blocks)
|
|
||||||
* [The `::response` Directive](#the-response-directive)
|
|
||||||
* [Request IDs](#request-ids)
|
|
||||||
* [Templating with Handlebars](#templating-with-handlebars)
|
|
||||||
* [Available Variables for Templating](#available-variables-for-templating)
|
|
||||||
* [Templating Examples](#templating-examples)
|
|
||||||
* [Managing Documents](#managing-documents)
|
|
||||||
* [Embedding Other Documents (`::md`)](#embedding-other-documents-md)
|
|
||||||
* [Advanced Usage](#advanced-usage)
|
|
||||||
* [Using Input Variables](#using-input-variables)
|
|
||||||
* [JavaScript Execution](#javascript-execution)
|
|
||||||
* [HTTP Block Configuration Options](#http-block-configuration-options)
|
|
||||||
* [Directive Options](#directive-options)
|
|
||||||
* [`::response` Directive Options](#response-directive-options)
|
|
||||||
* [`::md[{file}]` Directive Options](#mdfile-directive-options)
|
|
||||||
* [`::input[{name}]` Directive Options](#inputname-directive-options)
|
|
||||||
* [Command-Line Interface (CLI)](#command-line-interface-cli)
|
|
||||||
* [`http.md dev <source_file.md>`](#httpmd-dev-source_filemd)
|
|
||||||
* [`http.md build <source_file.md> <output_file.md>`](#httpmd-build-source_filemd-output_filemd)
|
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
Install `http.md` globally using npm:
|
Install `http.md` globally using npm:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
npm i -g @morten-olsen/http.md
|
npm i -g @morten-olsen/httpmd
|
||||||
```
|
```
|
||||||
|
|
||||||
## Getting Started
|
## Getting Started
|
||||||
@@ -102,13 +63,13 @@ You have two primary ways to render your `http.md` file:
|
|||||||
For a development server that outputs to your terminal and watches for changes:
|
For a development server that outputs to your terminal and watches for changes:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
http.md dev example.md
|
httpmd dev example.md
|
||||||
```
|
```
|
||||||
|
|
||||||
With watch mode:
|
With watch mode:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
http.md dev --watch example.md
|
httpmd dev --watch example.md
|
||||||
```
|
```
|
||||||
|
|
||||||
This command will process `example.md`, execute the HTTP requests, and print the resulting markdown (with responses filled in) to the terminal. With `--watch`, any changes to `example.md` will trigger a re-run.
|
This command will process `example.md`, execute the HTTP requests, and print the resulting markdown (with responses filled in) to the terminal. With `--watch`, any changes to `example.md` will trigger a re-run.
|
||||||
@@ -117,13 +78,13 @@ You have two primary ways to render your `http.md` file:
|
|||||||
To generate a new markdown file with the responses and templated values rendered:
|
To generate a new markdown file with the responses and templated values rendered:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
http.md build example.md output.md
|
httpmd build example.md output.md
|
||||||
```
|
```
|
||||||
|
|
||||||
With watch mode:
|
With watch mode:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
http.md build --watch example.md output.md
|
httpmd build --watch example.md output.md
|
||||||
```
|
```
|
||||||
|
|
||||||
This creates `output.md`, which is a static snapshot of `example.md` after all requests have been executed and templating applied. This file is suitable for version control, sharing, or integration with static site generators.
|
This creates `output.md`, which is a static snapshot of `example.md` after all requests have been executed and templating applied. This file is suitable for version control, sharing, or integration with static site generators.
|
||||||
@@ -144,14 +105,14 @@ Content-Type: application/json
|
|||||||
|
|
||||||
And here is the response:
|
And here is the response:
|
||||||
|
|
||||||
```http
|
```
|
||||||
HTTP/200 OK
|
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: 557
|
content-length: 555
|
||||||
content-type: application/json
|
content-type: application/json
|
||||||
date: Mon, 19 May 2025 08:31:44 GMT
|
date: Sun, 18 May 2025 19:12:17 GMT
|
||||||
server: gunicorn/19.9.0
|
server: gunicorn/19.9.0
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -168,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-682aec70-4f9d0a877f1453210a7009b6"
|
"X-Amzn-Trace-Id": "Root=1-682a3111-131bcbff690b03fd64aa4617"
|
||||||
},
|
},
|
||||||
"json": {
|
"json": {
|
||||||
"greeting": "Hello, http.md!"
|
"greeting": "Hello, http.md!"
|
||||||
},
|
},
|
||||||
"origin": "40.71.224.172",
|
"origin": "23.96.180.7",
|
||||||
"url": "https://httpbin.org/post"
|
"url": "https://httpbin.org/post"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -232,23 +193,23 @@ 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 `#yourUniqueId` or `id=yourUniqueId` in the `http` block's info string:
|
To add an ID, include `id=yourUniqueId` in the `http` block's info string:
|
||||||
|
|
||||||
````markdown
|
````markdown
|
||||||
# Document with Multiple Requests
|
# Document with Multiple Requests
|
||||||
|
|
||||||
First, create a resource:
|
First, create a resource:
|
||||||
|
|
||||||
```http #createUser,yaml,json
|
```http id=createUser
|
||||||
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 #getItem
|
```http id=getItem
|
||||||
GET https://httpbin.org/get?item=123
|
GET https://httpbin.org/get?item=123
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -260,96 +221,6 @@ Response from getting the item:
|
|||||||
|
|
||||||
````
|
````
|
||||||
|
|
||||||
<details>
|
|
||||||
<summary>Output</summary>
|
|
||||||
|
|
||||||
````markdown
|
|
||||||
# Document with Multiple Requests
|
|
||||||
|
|
||||||
First, create a resource:
|
|
||||||
|
|
||||||
```http
|
|
||||||
POST https://httpbin.org/post
|
|
||||||
Content-Type: application/json
|
|
||||||
|
|
||||||
{"username":"alpha"}
|
|
||||||
```
|
|
||||||
|
|
||||||
Then, fetch a different resource:
|
|
||||||
|
|
||||||
```http
|
|
||||||
GET https://httpbin.org/get?item=123
|
|
||||||
```
|
|
||||||
|
|
||||||
Response from creating the user:
|
|
||||||
|
|
||||||
```http
|
|
||||||
HTTP/200 OK
|
|
||||||
access-control-allow-credentials: true
|
|
||||||
access-control-allow-origin: *
|
|
||||||
connection: keep-alive
|
|
||||||
content-length: 502
|
|
||||||
content-type: application/json
|
|
||||||
date: Mon, 19 May 2025 08:31:44 GMT
|
|
||||||
server: gunicorn/19.9.0
|
|
||||||
|
|
||||||
{
|
|
||||||
"args": {},
|
|
||||||
"data": "username: alpha",
|
|
||||||
"files": {},
|
|
||||||
"form": {},
|
|
||||||
"headers": {
|
|
||||||
"Accept": "*/*",
|
|
||||||
"Accept-Encoding": "br, gzip, deflate",
|
|
||||||
"Accept-Language": "*",
|
|
||||||
"Content-Length": "15",
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
"Host": "httpbin.org",
|
|
||||||
"Sec-Fetch-Mode": "cors",
|
|
||||||
"User-Agent": "node",
|
|
||||||
"X-Amzn-Trace-Id": "Root=1-682aec70-52423dd76328a4e37066ba0e"
|
|
||||||
},
|
|
||||||
"json": null,
|
|
||||||
"origin": "40.71.224.172",
|
|
||||||
"url": "https://httpbin.org/post"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Response from getting the item:
|
|
||||||
|
|
||||||
```http
|
|
||||||
HTTP/200 OK
|
|
||||||
access-control-allow-credentials: true
|
|
||||||
access-control-allow-origin: *
|
|
||||||
connection: keep-alive
|
|
||||||
content-length: 383
|
|
||||||
content-type: application/json
|
|
||||||
date: Mon, 19 May 2025 08:31:44 GMT
|
|
||||||
server: gunicorn/19.9.0
|
|
||||||
|
|
||||||
{
|
|
||||||
"args": {
|
|
||||||
"item": "123"
|
|
||||||
},
|
|
||||||
"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-682aec70-1509df0125913c7c042c1116"
|
|
||||||
},
|
|
||||||
"origin": "40.71.224.172",
|
|
||||||
"url": "https://httpbin.org/get?item=123"
|
|
||||||
}
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
````
|
|
||||||
|
|
||||||
</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.
|
||||||
@@ -386,7 +257,7 @@ Within your markdown document, the following variables are available in the Hand
|
|||||||
|
|
||||||
* **`input`** (Object): A dictionary of variables passed to `http.md` via the command line using the `-i` or `--input` flag.
|
* **`input`** (Object): A dictionary of variables passed to `http.md` via the command line using the `-i` or `--input` flag.
|
||||||
|
|
||||||
* Example: If you run `http.md dev -i userId=123 -i apiKey=secret myfile.md`, you can use `{{input.userId}}` and `{{input.apiKey}}`.
|
* Example: If you run `httpmd dev -i userId=123 -i apiKey=secret myfile.md`, you can use `{{input.userId}}` and `{{input.apiKey}}`.
|
||||||
|
|
||||||
### Templating Examples
|
### Templating Examples
|
||||||
|
|
||||||
@@ -431,14 +302,14 @@ Now, let's fetch the item using a (mocked) ID from the response:
|
|||||||
GET https://httpbin.org/anything/My New Item
|
GET https://httpbin.org/anything/My New Item
|
||||||
```
|
```
|
||||||
|
|
||||||
```http
|
```
|
||||||
HTTP/200 OK
|
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: 453
|
content-length: 451
|
||||||
content-type: application/json
|
content-type: application/json
|
||||||
date: Mon, 19 May 2025 08:31:44 GMT
|
date: Sun, 18 May 2025 19:12:18 GMT
|
||||||
server: gunicorn/19.9.0
|
server: gunicorn/19.9.0
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -453,11 +324,11 @@ 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-682aec70-0086cbec627144c72e8dd560"
|
"X-Amzn-Trace-Id": "Root=1-682a3112-4bbb29111129c1556c487ca1"
|
||||||
},
|
},
|
||||||
"json": null,
|
"json": null,
|
||||||
"method": "GET",
|
"method": "GET",
|
||||||
"origin": "40.71.224.172",
|
"origin": "23.96.180.7",
|
||||||
"url": "https://httpbin.org/anything/My New Item"
|
"url": "https://httpbin.org/anything/My New Item"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -494,10 +365,9 @@ The requests from the embedded document are processed, and their `request` and `
|
|||||||
Assume `_shared_requests.md` contains:
|
Assume `_shared_requests.md` contains:
|
||||||
|
|
||||||
````markdown
|
````markdown
|
||||||
```http #sharedGetRequest
|
```http id=sharedGetRequest
|
||||||
GET https://httpbin.org/get
|
GET https://httpbin.org/get
|
||||||
```
|
```
|
||||||
|
|
||||||
````
|
````
|
||||||
|
|
||||||
Then, in `main.md`:
|
Then, in `main.md`:
|
||||||
@@ -509,7 +379,7 @@ Let's include some shared requests:
|
|||||||
|
|
||||||
::md[./_shared_requests.md]
|
::md[./_shared_requests.md]
|
||||||
|
|
||||||
The shared GET request returned: {{response.statusText}}
|
The shared GET request returned:
|
||||||
|
|
||||||
Now, a request specific to this document:
|
Now, a request specific to this document:
|
||||||
|
|
||||||
@@ -517,76 +387,12 @@ 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
|
||||||
|
|
||||||
````
|
````
|
||||||
|
|
||||||
<details>
|
|
||||||
<summary>Output</summary>
|
|
||||||
|
|
||||||
````markdown
|
|
||||||
# Main Document
|
|
||||||
|
|
||||||
Let's include some shared requests:
|
|
||||||
|
|
||||||
```http
|
|
||||||
GET https://httpbin.org/get
|
|
||||||
```
|
|
||||||
|
|
||||||
The shared GET request returned: OK
|
|
||||||
|
|
||||||
Now, a request specific to this document:
|
|
||||||
|
|
||||||
```http
|
|
||||||
POST https://httpbin.org/post
|
|
||||||
Content-Type: application/json
|
|
||||||
|
|
||||||
{"dataFromMain": "someValue", "sharedUrl": "https://httpbin.org/get"}
|
|
||||||
```
|
|
||||||
|
|
||||||
```http
|
|
||||||
HTTP/200 OK
|
|
||||||
access-control-allow-credentials: true
|
|
||||||
access-control-allow-origin: *
|
|
||||||
connection: keep-alive
|
|
||||||
content-length: 642
|
|
||||||
content-type: application/json
|
|
||||||
date: Mon, 19 May 2025 08:31:44 GMT
|
|
||||||
server: gunicorn/19.9.0
|
|
||||||
|
|
||||||
{
|
|
||||||
"args": {},
|
|
||||||
"data": "{\"dataFromMain\": \"someValue\", \"sharedUrl\": \"https://httpbin.org/get\"}",
|
|
||||||
"files": {},
|
|
||||||
"form": {},
|
|
||||||
"headers": {
|
|
||||||
"Accept": "*/*",
|
|
||||||
"Accept-Encoding": "br, gzip, deflate",
|
|
||||||
"Accept-Language": "*",
|
|
||||||
"Content-Length": "69",
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
"Host": "httpbin.org",
|
|
||||||
"Sec-Fetch-Mode": "cors",
|
|
||||||
"User-Agent": "node",
|
|
||||||
"X-Amzn-Trace-Id": "Root=1-682aec70-2a4b678f42e2be503f693fd5"
|
|
||||||
},
|
|
||||||
"json": {
|
|
||||||
"dataFromMain": "someValue",
|
|
||||||
"sharedUrl": "https://httpbin.org/get"
|
|
||||||
},
|
|
||||||
"origin": "40.71.224.172",
|
|
||||||
"url": "https://httpbin.org/post"
|
|
||||||
}
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
````
|
|
||||||
|
|
||||||
</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.
|
When `main.md` is processed, `_shared_requests.md` will be embedded, its `sharedGetRequest` will be executed, and its data will be available for templating.
|
||||||
|
|
||||||
## Advanced Usage
|
## Advanced Usage
|
||||||
@@ -598,7 +404,7 @@ You can pass external data into your `http.md` documents using the `-i` (or `--i
|
|||||||
**CLI Command:**
|
**CLI Command:**
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
http.md build mydoc.md output.md -i baseUrl=https://api.production.example.com -i apiKey=YOUR_SECRET_KEY
|
httpmd build mydoc.md output.md -i baseUrl=https://api.production.example.com -i apiKey=YOUR_SECRET_KEY
|
||||||
```
|
```
|
||||||
|
|
||||||
**Markdown Usage (`mydoc.md`):**
|
**Markdown Usage (`mydoc.md`):**
|
||||||
@@ -614,68 +420,6 @@ Authorization: Bearer
|
|||||||
|
|
||||||
**Security Note:** For sensitive data like API keys, using input variables is highly recommended over hardcoding them in your markdown files. Avoid committing files with plaintext secrets; instead, provide them at runtime via the CLI.
|
**Security Note:** For sensitive data like API keys, using input variables is highly recommended over hardcoding them in your markdown files. Avoid committing files with plaintext secrets; instead, provide them at runtime via the CLI.
|
||||||
|
|
||||||
### JavaScript Execution
|
|
||||||
|
|
||||||
You can execute `javascript` blocks by adding a `run` option which allows programmatically changing the context, making request assertions and solve other more advanced use cases
|
|
||||||
|
|
||||||
**Example:**
|
|
||||||
|
|
||||||
````markdown
|
|
||||||
```javascript run
|
|
||||||
input.test = "Hello World";
|
|
||||||
```
|
|
||||||
|
|
||||||
::input[test]
|
|
||||||
|
|
||||||
```http json
|
|
||||||
POST https://httpbin.org/post
|
|
||||||
|
|
||||||
{"input": "{{input.test}}"}
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
```javascript run,hidden
|
|
||||||
// Use chai's `expect`, `assert` or `should` to make assumptions
|
|
||||||
expect(response.body.json.input).to.equal("Hello World");
|
|
||||||
```
|
|
||||||
|
|
||||||
````
|
|
||||||
|
|
||||||
<details>
|
|
||||||
<summary>Output</summary>
|
|
||||||
|
|
||||||
````markdown
|
|
||||||
```javascript
|
|
||||||
input.test = "Hello World";
|
|
||||||
```
|
|
||||||
|
|
||||||
```
|
|
||||||
test=Hello World
|
|
||||||
```
|
|
||||||
|
|
||||||
```http
|
|
||||||
POST https://httpbin.org/post
|
|
||||||
|
|
||||||
{"input": "Hello World"}
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
````
|
|
||||||
|
|
||||||
</details>
|
|
||||||
|
|
||||||
**Options:**
|
|
||||||
|
|
||||||
* `run`: If present the code block will be executed
|
|
||||||
|
|
||||||
* Example: ` ```javascript run `
|
|
||||||
|
|
||||||
* `hidden`: If present the code block will not be included in the resulting output
|
|
||||||
|
|
||||||
* Example: ` ```javascript hidden `
|
|
||||||
|
|
||||||
* `output`: If present the code blocks return value will be rendered as a `yaml` code block
|
|
||||||
|
|
||||||
### HTTP Block Configuration Options
|
### HTTP Block Configuration Options
|
||||||
|
|
||||||
You can configure the behavior of each `http` code block by adding options to its info string, separated by commas.
|
You can configure the behavior of each `http` code block by adding options to its info string, separated by commas.
|
||||||
@@ -769,9 +513,9 @@ The `::input` directive is used to declare expected input variables
|
|||||||
|
|
||||||
## Command-Line Interface (CLI)
|
## Command-Line Interface (CLI)
|
||||||
|
|
||||||
The `http.md` tool provides the following commands:
|
The `httpmd` tool provides the following commands:
|
||||||
|
|
||||||
### `http.md dev <source_file.md>`
|
### `httpmd dev <source_file.md>`
|
||||||
|
|
||||||
Processes the `<source_file.md>`, executes all HTTP requests, resolves templates, and prints the resulting markdown to the **terminal (stdout)**.
|
Processes the `<source_file.md>`, executes all HTTP requests, resolves templates, and prints the resulting markdown to the **terminal (stdout)**.
|
||||||
|
|
||||||
@@ -783,10 +527,10 @@ Processes the `<source_file.md>`, executes all HTTP requests, resolves templates
|
|||||||
**Example:**
|
**Example:**
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
http.md dev api_tests.md --watch -i host=localhost:3000
|
httpmd dev api_tests.md --watch -i host=localhost:3000
|
||||||
```
|
```
|
||||||
|
|
||||||
### `http.md build <source_file.md> <output_file.md>`
|
### `httpmd build <source_file.md> <output_file.md>`
|
||||||
|
|
||||||
Processes the `<source_file.md>`, executes all HTTP requests, resolves templates, and saves the resulting markdown to `<output_file.md>`.
|
Processes the `<source_file.md>`, executes all HTTP requests, resolves templates, and saves the resulting markdown to `<output_file.md>`.
|
||||||
|
|
||||||
@@ -798,5 +542,5 @@ Processes the `<source_file.md>`, executes all HTTP requests, resolves templates
|
|||||||
**Example:**
|
**Example:**
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
http.md build official_api_docs.md public/api_docs_v1.md -i version=v1.0
|
httpmd build official_api_docs.md public/api_docs_v1.md -i version=v1.0
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -21,24 +21,12 @@ 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.
|
||||||
|
|
||||||
## Roadmap
|
|
||||||
|
|
||||||
- **Programmatic API** Use `http.md` inside existing scripts and pipelines
|
|
||||||
- **Environment Varaiables** Support using the runners environment variables in templates
|
|
||||||
- **JavaScript script support** Add JavaScript code blocks with execution, which will allow more advanced use-cases
|
|
||||||
- **Asserts** Add the ability to make HTTP assertions to use the document for testing
|
|
||||||
- **Templates** Write re-usable templates which can be used in documents
|
|
||||||
|
|
||||||
## Content
|
|
||||||
|
|
||||||
::toc
|
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
Install `http.md` globally using npm:
|
Install `http.md` globally using npm:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
npm i -g @morten-olsen/http.md
|
npm i -g @morten-olsen/httpmd
|
||||||
```
|
```
|
||||||
|
|
||||||
## Getting Started
|
## Getting Started
|
||||||
@@ -51,7 +39,7 @@ Create a file named `example.md`:
|
|||||||
|
|
||||||
::raw-md[./examples/getting-started.md]
|
::raw-md[./examples/getting-started.md]
|
||||||
|
|
||||||
### Rendering Document
|
### Rendering Documents
|
||||||
|
|
||||||
You have two primary ways to render your `http.md` file:
|
You have two primary ways to render your `http.md` file:
|
||||||
|
|
||||||
@@ -59,13 +47,13 @@ You have two primary ways to render your `http.md` file:
|
|||||||
For a development server that outputs to your terminal and watches for changes:
|
For a development server that outputs to your terminal and watches for changes:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
http.md dev example.md
|
httpmd dev example.md
|
||||||
```
|
```
|
||||||
|
|
||||||
With watch mode:
|
With watch mode:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
http.md dev --watch example.md
|
httpmd dev --watch example.md
|
||||||
```
|
```
|
||||||
|
|
||||||
This command will process `example.md`, execute the HTTP requests, and print the resulting markdown (with responses filled in) to the terminal. With `--watch`, any changes to `example.md` will trigger a re-run.
|
This command will process `example.md`, execute the HTTP requests, and print the resulting markdown (with responses filled in) to the terminal. With `--watch`, any changes to `example.md` will trigger a re-run.
|
||||||
@@ -74,13 +62,13 @@ You have two primary ways to render your `http.md` file:
|
|||||||
To generate a new markdown file with the responses and templated values rendered:
|
To generate a new markdown file with the responses and templated values rendered:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
http.md build example.md output.md
|
httpmd build example.md output.md
|
||||||
```
|
```
|
||||||
|
|
||||||
With watch mode:
|
With watch mode:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
http.md build --watch example.md output.md
|
httpmd build --watch example.md output.md
|
||||||
```
|
```
|
||||||
|
|
||||||
This creates `output.md`, which is a static snapshot of `example.md` after all requests have been executed and templating applied. This file is suitable for version control, sharing, or integration with static site generators.
|
This creates `output.md`, which is a static snapshot of `example.md` after all requests have been executed and templating applied. This file is suitable for version control, sharing, or integration with static site generators.
|
||||||
@@ -140,17 +128,10 @@ 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 `#yourUniqueId` or `id=yourUniqueId` in the `http` block's info string:
|
To add an ID, include `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.
|
||||||
@@ -187,7 +168,7 @@ Within your markdown document, the following variables are available in the Hand
|
|||||||
|
|
||||||
- **`input`** (Object): A dictionary of variables passed to `http.md` via the command line using the `-i` or `--input` flag.
|
- **`input`** (Object): A dictionary of variables passed to `http.md` via the command line using the `-i` or `--input` flag.
|
||||||
|
|
||||||
- Example: If you run `http.md dev -i userId=123 -i apiKey=secret myfile.md`, you can use `{{input.userId}}` and `{{input.apiKey}}`.
|
- Example: If you run `httpmd dev -i userId=123 -i apiKey=secret myfile.md`, you can use `{{input.userId}}` and `{{input.apiKey}}`.
|
||||||
|
|
||||||
### Templating Examples
|
### Templating Examples
|
||||||
|
|
||||||
@@ -211,7 +192,7 @@ _(Note: `httpbin.org/post` wraps the JSON sent in a "json" field in its response
|
|||||||
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: **{{response.status}}**.
|
||||||
````
|
````
|
||||||
|
|
||||||
## Managing Documents
|
## Managing Documents
|
||||||
@@ -232,12 +213,12 @@ Assume `_shared_requests.md` contains:
|
|||||||
|
|
||||||
Then, in `main.md`:
|
Then, in `main.md`:
|
||||||
|
|
||||||
::raw-md[./examples/with-shared-requests.md]
|
::raw-md[./examples/with-template.md]
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary>Output</summary>
|
<summary>Output</summary>
|
||||||
|
|
||||||
::raw-md[./examples/with-shared-requests.md]{render}
|
::raw-md[./examples/with-template.md]{render}
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
@@ -252,15 +233,15 @@ You can pass external data into your `http.md` documents using the `-i` (or `--i
|
|||||||
**CLI Command:**
|
**CLI Command:**
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
http.md build mydoc.md output.md -i baseUrl=https://api.production.example.com -i apiKey=YOUR_SECRET_KEY
|
httpmd build mydoc.md output.md -i baseUrl=https://api.production.example.com -i apiKey=YOUR_SECRET_KEY
|
||||||
```
|
```
|
||||||
|
|
||||||
**Markdown Usage (`mydoc.md`):**
|
**Markdown Usage (`mydoc.md`):**
|
||||||
|
|
||||||
````markdown
|
````markdown
|
||||||
```http
|
```http
|
||||||
GET {{{input.baseUrl}}}/users/1
|
GET {{input.baseUrl}}/users/1
|
||||||
Authorization: Bearer {{{input.apiKey}}}
|
Authorization: Bearer {{input.apiKey}}
|
||||||
```
|
```
|
||||||
|
|
||||||
::response
|
::response
|
||||||
@@ -268,33 +249,6 @@ Authorization: Bearer {{{input.apiKey}}}
|
|||||||
|
|
||||||
**Security Note:** For sensitive data like API keys, using input variables is highly recommended over hardcoding them in your markdown files. Avoid committing files with plaintext secrets; instead, provide them at runtime via the CLI.
|
**Security Note:** For sensitive data like API keys, using input variables is highly recommended over hardcoding them in your markdown files. Avoid committing files with plaintext secrets; instead, provide them at runtime via the CLI.
|
||||||
|
|
||||||
### JavaScript Execution
|
|
||||||
|
|
||||||
You can execute `javascript` blocks by adding a `run` option which allows programmatically changing the context, making request assertions and solve other more advanced use cases
|
|
||||||
|
|
||||||
**Example:**
|
|
||||||
|
|
||||||
::raw-md[./examples/with-javascript.md]
|
|
||||||
|
|
||||||
<details>
|
|
||||||
<summary>Output</summary>
|
|
||||||
|
|
||||||
::raw-md[./examples/with-javascript.md]{render}
|
|
||||||
|
|
||||||
</details>
|
|
||||||
|
|
||||||
**Options:**
|
|
||||||
|
|
||||||
- `run`: If present the code block will be executed
|
|
||||||
|
|
||||||
- Example: ` ```javascript run `
|
|
||||||
|
|
||||||
- `hidden`: If present the code block will not be included in the resulting output
|
|
||||||
|
|
||||||
- Example: ` ```javascript hidden `
|
|
||||||
|
|
||||||
- `output`: If present the code blocks return value will be rendered as a `yaml` code block
|
|
||||||
|
|
||||||
### HTTP Block Configuration Options
|
### HTTP Block Configuration Options
|
||||||
|
|
||||||
You can configure the behavior of each `http` code block by adding options to its info string, separated by commas.
|
You can configure the behavior of each `http` code block by adding options to its info string, separated by commas.
|
||||||
@@ -336,8 +290,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 {{input.apiEndpoint}}/data
|
||||||
X-API-Key: {{{input.apiKey}}}
|
X-API-Key: {{input.apiKey}}
|
||||||
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
|
||||||
@@ -388,9 +342,9 @@ The `::input` directive is used to declare expected input variables
|
|||||||
|
|
||||||
## Command-Line Interface (CLI)
|
## Command-Line Interface (CLI)
|
||||||
|
|
||||||
The `http.md` tool provides the following commands:
|
The `httpmd` tool provides the following commands:
|
||||||
|
|
||||||
### `http.md dev <source_file.md>`
|
### `httpmd dev <source_file.md>`
|
||||||
|
|
||||||
Processes the `<source_file.md>`, executes all HTTP requests, resolves templates, and prints the resulting markdown to the **terminal (stdout)**.
|
Processes the `<source_file.md>`, executes all HTTP requests, resolves templates, and prints the resulting markdown to the **terminal (stdout)**.
|
||||||
|
|
||||||
@@ -402,10 +356,10 @@ Processes the `<source_file.md>`, executes all HTTP requests, resolves templates
|
|||||||
**Example:**
|
**Example:**
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
http.md dev api_tests.md --watch -i host=localhost:3000
|
httpmd dev api_tests.md --watch -i host=localhost:3000
|
||||||
```
|
```
|
||||||
|
|
||||||
### `http.md build <source_file.md> <output_file.md>`
|
### `httpmd build <source_file.md> <output_file.md>`
|
||||||
|
|
||||||
Processes the `<source_file.md>`, executes all HTTP requests, resolves templates, and saves the resulting markdown to `<output_file.md>`.
|
Processes the `<source_file.md>`, executes all HTTP requests, resolves templates, and saves the resulting markdown to `<output_file.md>`.
|
||||||
|
|
||||||
@@ -417,5 +371,5 @@ Processes the `<source_file.md>`, executes all HTTP requests, resolves templates
|
|||||||
**Example:**
|
**Example:**
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
http.md build official_api_docs.md public/api_docs_v1.md -i version=v1.0
|
httpmd build official_api_docs.md public/api_docs_v1.md -i version=v1.0
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -1,17 +0,0 @@
|
|||||||
```javascript run
|
|
||||||
input.test = "Hello World";
|
|
||||||
```
|
|
||||||
|
|
||||||
::input[test]
|
|
||||||
|
|
||||||
```http json
|
|
||||||
POST https://httpbin.org/post
|
|
||||||
|
|
||||||
{"input": "{{input.test}}"}
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
```javascript run,hidden
|
|
||||||
// Use chai's `expect`, `assert` or `should` to make assumptions
|
|
||||||
expect(response.body.json.input).to.equal("Hello World");
|
|
||||||
```
|
|
||||||
@@ -2,16 +2,16 @@
|
|||||||
|
|
||||||
First, create a resource:
|
First, create a resource:
|
||||||
|
|
||||||
```http #createUser,yaml,json
|
```http id=createUser
|
||||||
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 #getItem
|
```http id=getItem
|
||||||
GET https://httpbin.org/get?item=123
|
GET https://httpbin.org/get?item=123
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ Let's include some shared requests:
|
|||||||
|
|
||||||
::md[./_shared_requests.md]
|
::md[./_shared_requests.md]
|
||||||
|
|
||||||
The shared GET request returned: {{response.statusText}}
|
The shared GET request returned:
|
||||||
|
|
||||||
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": "{{requests.sharedGetRequest.url}}"}
|
{"dataFromMain": "someValue", "sharedUrl": ""}
|
||||||
```
|
```
|
||||||
|
|
||||||
::response
|
::response
|
||||||
15
package.json
15
package.json
@@ -1,11 +1,11 @@
|
|||||||
{
|
{
|
||||||
"name": "@morten-olsen/http.md",
|
"name": "@morten-olsen/httpmd",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"description": "",
|
"description": "",
|
||||||
"main": "dist/exports.js",
|
"main": "dist/exports.js",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"bin": {
|
"bin": {
|
||||||
"http.md": "./bin/cli.mjs"
|
"httpmd": "./bin/cli.mjs"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"dist",
|
"dist",
|
||||||
@@ -17,7 +17,6 @@
|
|||||||
"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"
|
||||||
},
|
},
|
||||||
@@ -27,34 +26,25 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@pnpm/find-workspace-packages": "^6.0.9",
|
"@pnpm/find-workspace-packages": "^6.0.9",
|
||||||
"@types/blessed": "^0.1.25",
|
"@types/blessed": "^0.1.25",
|
||||||
"@types/chai": "^5.2.2",
|
|
||||||
"@types/ink": "^2.0.3",
|
|
||||||
"@types/marked-terminal": "^6.1.1",
|
"@types/marked-terminal": "^6.1.1",
|
||||||
"@types/mdast": "^4.0.4",
|
"@types/mdast": "^4.0.4",
|
||||||
"@types/node": "^22.15.18",
|
"@types/node": "^22.15.18",
|
||||||
"@types/react": "^18.3.12",
|
|
||||||
"@types/terminal-kit": "^2.5.7",
|
"@types/terminal-kit": "^2.5.7",
|
||||||
"tsx": "^4.19.4",
|
"tsx": "^4.19.4",
|
||||||
"typescript": "^5.8.3"
|
"typescript": "^5.8.3"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"blessed": "^0.1.81",
|
"blessed": "^0.1.81",
|
||||||
"chai": "^5.2.0",
|
|
||||||
"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",
|
||||||
"handlebars": "^4.7.8",
|
"handlebars": "^4.7.8",
|
||||||
"hastscript": "^9.0.1",
|
"hastscript": "^9.0.1",
|
||||||
"ink": "^5.2.1",
|
|
||||||
"marked": "^15.0.11",
|
"marked": "^15.0.11",
|
||||||
"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",
|
|
||||||
"react": "^18.3.1",
|
|
||||||
"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",
|
||||||
@@ -63,7 +53,6 @@
|
|||||||
"terminal-kit": "^3.1.2",
|
"terminal-kit": "^3.1.2",
|
||||||
"unified": "^11.0.5",
|
"unified": "^11.0.5",
|
||||||
"unist-util-visit": "^5.0.0",
|
"unist-util-visit": "^5.0.0",
|
||||||
"wrap-ansi": "^9.0.0",
|
|
||||||
"yaml": "^2.8.0"
|
"yaml": "^2.8.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
524
pnpm-lock.yaml
generated
524
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1,43 +1,33 @@
|
|||||||
import { program } from 'commander';
|
import { program } from 'commander';
|
||||||
import { resolve } from 'node:path';
|
import { resolve } from 'node:path';
|
||||||
|
import { marked } from 'marked';
|
||||||
|
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 { wrapBody } from '../theme/theme.html.js';
|
|
||||||
import { loadInputFiles } from '../utils/input.js';
|
|
||||||
import { InvalidFormatError } from '../utils/errors.js';
|
|
||||||
import { renderUI, State } from './ui/ui.js';
|
|
||||||
import { Marked } from 'marked';
|
|
||||||
|
|
||||||
|
|
||||||
|
marked.use(markedTerminal() as any);
|
||||||
|
|
||||||
program
|
program
|
||||||
.command('dev')
|
.command('dev')
|
||||||
.argument('<name>', 'http.md file name')
|
.argument('<name>', 'http.md file name')
|
||||||
.description('Run a http.md document')
|
.description('Run a http.md document')
|
||||||
.option('-w, --watch', 'watch for changes')
|
.option('-w, --watch', 'watch for changes')
|
||||||
.option('-f, --file <file...>', 'input files (-f foo.js -f bar.json)')
|
|
||||||
.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 {
|
const {
|
||||||
file: f = [],
|
|
||||||
watch = false,
|
watch = false,
|
||||||
input: i = [],
|
input: i = [],
|
||||||
} = options;
|
} = options;
|
||||||
|
|
||||||
const input = {
|
const input = Object.fromEntries(
|
||||||
...Object.fromEntries(
|
|
||||||
i.map((item: string) => {
|
i.map((item: string) => {
|
||||||
const [key, value] = item.split('=');
|
const [key, value] = item.split('=');
|
||||||
return [key, value];
|
return [key, value];
|
||||||
})
|
})
|
||||||
),
|
);
|
||||||
...loadInputFiles(f),
|
|
||||||
};
|
|
||||||
const state = new State<any>({
|
|
||||||
markdown: 'Loading',
|
|
||||||
});
|
|
||||||
const filePath = resolve(process.cwd(), name);
|
const filePath = resolve(process.cwd(), name);
|
||||||
|
|
||||||
const build = async () => {
|
const build = async () => {
|
||||||
@@ -48,10 +38,8 @@ program
|
|||||||
context,
|
context,
|
||||||
});
|
});
|
||||||
|
|
||||||
state.setState({
|
const markdown = await marked.parse(result.markdown);
|
||||||
error: result.error ? result.error instanceof Error ? result.error.message : result.error : undefined,
|
console.log(markdown);
|
||||||
markdown: result.markdown,
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...result,
|
...result,
|
||||||
@@ -60,7 +48,6 @@ program
|
|||||||
}
|
}
|
||||||
|
|
||||||
const result = await build();
|
const result = await build();
|
||||||
renderUI(state);
|
|
||||||
|
|
||||||
if (watch) {
|
if (watch) {
|
||||||
const watcher = new Watcher();
|
const watcher = new Watcher();
|
||||||
@@ -79,28 +66,21 @@ 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, --file <file...>', 'input files (-f foo.js -f bar.json)')
|
|
||||||
.option('--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,
|
||||||
file: f = [],
|
|
||||||
input: i = [],
|
input: i = [],
|
||||||
format = 'markdown',
|
|
||||||
} = options;
|
} = options;
|
||||||
|
|
||||||
|
|
||||||
const input = {
|
const input = Object.fromEntries(
|
||||||
...Object.fromEntries(
|
|
||||||
i.map((item: string) => {
|
i.map((item: string) => {
|
||||||
const [key, value] = item.split('=');
|
const [key, value] = item.split('=');
|
||||||
return [key, value];
|
return [key, value];
|
||||||
})
|
})
|
||||||
),
|
);
|
||||||
...loadInputFiles(f),
|
|
||||||
}
|
|
||||||
const filePath = resolve(process.cwd(), name);
|
const filePath = resolve(process.cwd(), name);
|
||||||
|
|
||||||
const build = async () => {
|
const build = async () => {
|
||||||
@@ -111,19 +91,7 @@ program
|
|||||||
context,
|
context,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (result.error) {
|
|
||||||
console.error(result.error);
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
await writeFile(output, result.markdown);
|
||||||
} else {
|
|
||||||
throw new InvalidFormatError('Invalid format');
|
|
||||||
}
|
|
||||||
return {
|
return {
|
||||||
...result,
|
...result,
|
||||||
context,
|
context,
|
||||||
@@ -132,10 +100,6 @@ program
|
|||||||
|
|
||||||
const result = await build();
|
const result = await build();
|
||||||
|
|
||||||
if (result.error && !watch) {
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
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));
|
||||||
|
|||||||
@@ -1,55 +0,0 @@
|
|||||||
import React, { useMemo } from 'react';
|
|
||||||
import { Box, Text, useInput } from 'ink';
|
|
||||||
import { marked } from 'marked';
|
|
||||||
import TerminalRenderer from 'marked-terminal';
|
|
||||||
import wrapAnsi from 'wrap-ansi';
|
|
||||||
import os from 'os';
|
|
||||||
import { useTerminalHeight, useTerminalWidth } from '../hooks/terminal.js';
|
|
||||||
|
|
||||||
type ScrollableMarkdownProps = {
|
|
||||||
markdownContent: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderer = new TerminalRenderer({});
|
|
||||||
marked.setOptions({ renderer: renderer as any });
|
|
||||||
|
|
||||||
const ScrollableMarkdown = ({ markdownContent }: ScrollableMarkdownProps) => {
|
|
||||||
const [scrollPosition, setScrollPosition] = React.useState(0);
|
|
||||||
const terminalHeight = useTerminalHeight()
|
|
||||||
const terminalWidth = useTerminalWidth();
|
|
||||||
const rendered = useMemo(() => {
|
|
||||||
return marked(markdownContent) as string;
|
|
||||||
}, [markdownContent]);
|
|
||||||
const wrapped = useMemo(() => {
|
|
||||||
return wrapAnsi(rendered, terminalWidth, {
|
|
||||||
hard: true,
|
|
||||||
trim: false,
|
|
||||||
wordWrap: true,
|
|
||||||
}).split(os.EOL);
|
|
||||||
}, [rendered, terminalWidth]);
|
|
||||||
|
|
||||||
const buffer = useMemo(() => {
|
|
||||||
return wrapped.slice(scrollPosition, scrollPosition + terminalHeight).join(os.EOL);
|
|
||||||
}, [wrapped, scrollPosition, terminalHeight]);
|
|
||||||
|
|
||||||
useInput((input, key) => {
|
|
||||||
if (key.downArrow) {
|
|
||||||
setScrollPosition(prev => prev + 1);
|
|
||||||
}
|
|
||||||
if (key.upArrow) {
|
|
||||||
setScrollPosition(prev => Math.max(0, prev - 1));
|
|
||||||
}
|
|
||||||
if (key.escape) {
|
|
||||||
process.exit(0);
|
|
||||||
}
|
|
||||||
console.error('a', input, key);
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box flexDirection="column" flexShrink={1} flexGrow={1} overflow="hidden">
|
|
||||||
<Text>{buffer}</Text>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export { ScrollableMarkdown };
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
import { useEffect, useState } from "react"
|
|
||||||
|
|
||||||
const useTerminalWidth = () => {
|
|
||||||
const [width, setWidth] = useState(process.stdout.columns || 80);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const handleResize = () => {
|
|
||||||
setWidth(process.stdout.columns);
|
|
||||||
};
|
|
||||||
|
|
||||||
process.stdout.on("resize", handleResize);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
process.stdout.off("resize", handleResize);
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return width;
|
|
||||||
}
|
|
||||||
|
|
||||||
const useTerminalHeight = () => {
|
|
||||||
const [height, setHeight] = useState(process.stdout.rows || 24);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const handleResize = () => {
|
|
||||||
setHeight(process.stdout.rows);
|
|
||||||
};
|
|
||||||
|
|
||||||
process.stdout.on("resize", handleResize);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
process.stdout.off("resize", handleResize);
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return height;
|
|
||||||
}
|
|
||||||
|
|
||||||
export { useTerminalWidth, useTerminalHeight }
|
|
||||||
@@ -1,80 +0,0 @@
|
|||||||
import { EventEmitter } from 'eventemitter3';
|
|
||||||
import React, { createContext, useRef, useState, useSyncExternalStore } from "react";
|
|
||||||
|
|
||||||
type StateEvents = {
|
|
||||||
update: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
class State<T = any> extends EventEmitter<StateEvents> {
|
|
||||||
state: T;
|
|
||||||
|
|
||||||
constructor(initialState: T) {
|
|
||||||
super();
|
|
||||||
this.state = initialState;
|
|
||||||
}
|
|
||||||
|
|
||||||
public get value() {
|
|
||||||
return this.state;
|
|
||||||
}
|
|
||||||
|
|
||||||
public setState = (state: T) => {
|
|
||||||
this.state = state;
|
|
||||||
this.emit('update');
|
|
||||||
}
|
|
||||||
|
|
||||||
public subscribe = (callback: () => void) => {
|
|
||||||
this.on('update', callback);
|
|
||||||
return () => {
|
|
||||||
this.off('update', callback);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type StateContextValue<T> = {
|
|
||||||
state: State<T>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const StateContext = createContext<StateContextValue<any> | null>(null);
|
|
||||||
|
|
||||||
type StateProviderProps<T> = {
|
|
||||||
state: State<T>;
|
|
||||||
children: React.ReactNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
const StateProvider = <T,>({ state, children }: StateProviderProps<T>) => {
|
|
||||||
return (
|
|
||||||
<StateContext.Provider value={{ state }}>
|
|
||||||
{children}
|
|
||||||
</StateContext.Provider>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const useStateContext = <T = any>() => {
|
|
||||||
const context = React.useContext(StateContext);
|
|
||||||
if (!context) {
|
|
||||||
throw new Error("useStateContext must be used within a StateProvider");
|
|
||||||
}
|
|
||||||
return context as StateContextValue<T>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const useStateValue = <T = any>(
|
|
||||||
selector: (state: T) => any = (state) => state,
|
|
||||||
) => {
|
|
||||||
const context = useStateContext<T>();
|
|
||||||
const value = useRef<T>(selector(context.state.value));
|
|
||||||
useSyncExternalStore(
|
|
||||||
context.state.subscribe,
|
|
||||||
() => {
|
|
||||||
const next = selector(context.state.value);
|
|
||||||
if (next !== value.current) {
|
|
||||||
value.current = next;
|
|
||||||
}
|
|
||||||
return value.current;
|
|
||||||
},
|
|
||||||
() => value.current,
|
|
||||||
);
|
|
||||||
|
|
||||||
return value.current;
|
|
||||||
}
|
|
||||||
|
|
||||||
export { State, StateProvider, useStateContext, useStateValue };
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
import { Box, render, Text, useApp, useInput } from "ink"
|
|
||||||
import { ScrollableMarkdown } from './components/scrollable-markdown.js';
|
|
||||||
import { useStateValue, State, StateProvider } from './state/state.js';
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import { useTerminalHeight } from "./hooks/terminal.js";
|
|
||||||
|
|
||||||
const MarkdownView = () => {
|
|
||||||
const markdown = useStateValue((state) => state.markdown);
|
|
||||||
const error = useStateValue((state) => state.error);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box flexGrow={1} flexDirection="column">
|
|
||||||
<Box flexDirection="column" flexGrow={1} overflow="hidden">
|
|
||||||
<ScrollableMarkdown markdownContent={markdown} />
|
|
||||||
</Box>
|
|
||||||
{error && (
|
|
||||||
<Box flexDirection="column" padding={1}>
|
|
||||||
<Text color="red">{error}</Text>
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const App = () => {
|
|
||||||
const { exit } = useApp();
|
|
||||||
const height = useTerminalHeight();
|
|
||||||
useInput((_input, key) => {
|
|
||||||
if (key.escape) {
|
|
||||||
process.exit(0);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
useEffect(
|
|
||||||
() => {
|
|
||||||
const enterAltScreenCommand = "\x1b[?1049h";
|
|
||||||
const leaveAltScreenCommand = "\x1b[?1049l";
|
|
||||||
process.stdout.write(enterAltScreenCommand);
|
|
||||||
const onExit = () => {
|
|
||||||
exit();
|
|
||||||
process.stdout.write(leaveAltScreenCommand);
|
|
||||||
}
|
|
||||||
process.on("exit", onExit);
|
|
||||||
return () => {
|
|
||||||
process.stdout.write(leaveAltScreenCommand);
|
|
||||||
process.off("exit", onExit);
|
|
||||||
}
|
|
||||||
}, []
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box height={height} flexDirection="column">
|
|
||||||
<MarkdownView />
|
|
||||||
<Box>
|
|
||||||
<Text>Press esc to exit</Text>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const renderUI = (state: State) => {
|
|
||||||
|
|
||||||
render(
|
|
||||||
<StateProvider state={state}>
|
|
||||||
<App />
|
|
||||||
</StateProvider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export { renderUI, State }
|
|
||||||
@@ -10,7 +10,6 @@ type Response = {
|
|||||||
statusText: string;
|
statusText: string;
|
||||||
headers: Record<string, string>;
|
headers: Record<string, string>;
|
||||||
body?: string;
|
body?: string;
|
||||||
rawBody?: string;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
type AddRequestOptios = {
|
type AddRequestOptios = {
|
||||||
|
|||||||
@@ -5,12 +5,18 @@ 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, postHandlers } from './handlers/handlers.js';
|
import { handlers } 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;
|
||||||
@@ -47,26 +53,14 @@ 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) => {
|
||||||
let error: unknown | undefined;
|
|
||||||
const { context } = options;
|
const { context } = options;
|
||||||
context.files.add(file);
|
context.files.add(file);
|
||||||
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) => {
|
||||||
@@ -81,21 +75,8 @@ 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) {
|
||||||
try {
|
|
||||||
const { node, action } = step;
|
const { node, action } = step;
|
||||||
const options: ExecutionStepOptions = {
|
const options: ExecutionStepOptions = {
|
||||||
file,
|
file,
|
||||||
@@ -105,16 +86,11 @@ const execute = async (file: string, options: ExexutionExecuteOptions) => {
|
|||||||
root,
|
root,
|
||||||
};
|
};
|
||||||
await action(options);
|
await action(options);
|
||||||
} catch (e) {
|
|
||||||
error = e;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const markdown = parser.stringify(root);
|
const markdown = parser.stringify(root);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
error,
|
|
||||||
root,
|
root,
|
||||||
markdown,
|
markdown,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ const codeHandler: ExecutionHandler = ({
|
|||||||
node,
|
node,
|
||||||
addStep,
|
addStep,
|
||||||
}) => {
|
}) => {
|
||||||
if (node.type !== 'code' || node.lang === 'http' || node.lang === 'javascript') {
|
if (node.type !== 'code' || node.lang === 'http') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const optionParts = node.meta?.split(',') || [];
|
const optionParts = node.meta?.split(',') || [];
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ const httpHandler: ExecutionHandler = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
let parsedBody = body;
|
let parsedBody = body;
|
||||||
if (options.yaml) {
|
if (options.format === 'yaml') {
|
||||||
try {
|
try {
|
||||||
const parsed = YAML.parse(body);
|
const parsed = YAML.parse(body);
|
||||||
parsedBody = JSON.stringify(parsed);
|
parsedBody = JSON.stringify(parsed);
|
||||||
@@ -59,8 +59,7 @@ const httpHandler: ExecutionHandler = ({
|
|||||||
body
|
body
|
||||||
});
|
});
|
||||||
|
|
||||||
const rawBody = await response.text();
|
let responseText = await response.text();
|
||||||
let responseText = rawBody;
|
|
||||||
if (options.json) {
|
if (options.json) {
|
||||||
try {
|
try {
|
||||||
responseText = JSON.parse(responseText);
|
responseText = JSON.parse(responseText);
|
||||||
@@ -69,7 +68,7 @@ const httpHandler: ExecutionHandler = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
node.value = [head, parsedBody].filter(Boolean).join('\n\n');
|
node.value = content;
|
||||||
node.meta = undefined;
|
node.meta = undefined;
|
||||||
|
|
||||||
context.addRequest({
|
context.addRequest({
|
||||||
@@ -85,7 +84,6 @@ 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,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { toString } from 'mdast-util-to-string';
|
import { toString } from 'mdast-util-to-string';
|
||||||
import { type ExecutionHandler } from '../execution.js';
|
import { type ExecutionHandler } from '../execution.js';
|
||||||
import { ParsingError, RequiredError } from '../../utils/errors.js';
|
|
||||||
|
|
||||||
const inputHandler: ExecutionHandler = ({
|
const inputHandler: ExecutionHandler = ({
|
||||||
addStep,
|
addStep,
|
||||||
@@ -16,7 +15,7 @@ const inputHandler: ExecutionHandler = ({
|
|||||||
const name = toString(node);
|
const name = toString(node);
|
||||||
|
|
||||||
if (node.attributes?.required === '' && context.input[name] === undefined) {
|
if (node.attributes?.required === '' && context.input[name] === undefined) {
|
||||||
throw new RequiredError(name);
|
throw new Error(`Input "${name}" is required`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (node.attributes?.default !== undefined && context.input[name] === undefined) {
|
if (node.attributes?.default !== undefined && context.input[name] === undefined) {
|
||||||
@@ -28,7 +27,7 @@ const inputHandler: ExecutionHandler = ({
|
|||||||
if (format === 'number') {
|
if (format === 'number') {
|
||||||
context.input[name] = Number(context.input[name]);
|
context.input[name] = Number(context.input[name]);
|
||||||
if (context.input[name] !== undefined && isNaN(Number(context.input[name]))) {
|
if (context.input[name] !== undefined && isNaN(Number(context.input[name]))) {
|
||||||
throw new ParsingError(`Input "${name}" must be a number, but got "${context.input[name]}"`);
|
throw new Error(`Input "${name}" must be a number, but got "${context.input[name]}"`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (format === 'boolean') {
|
if (format === 'boolean') {
|
||||||
@@ -41,14 +40,14 @@ const inputHandler: ExecutionHandler = ({
|
|||||||
try {
|
try {
|
||||||
context.input[name] = JSON.parse(String(context.input[name]));
|
context.input[name] = JSON.parse(String(context.input[name]));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new ParsingError(`Input "${name}" must be a valid JSON, but got "${context.input[name]}"`);
|
throw new Error(`Input "${name}" must be a valid JSON, but got "${context.input[name]}"`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (format === 'date') {
|
if (format === 'date') {
|
||||||
const date = new Date(context.input[name] as string);
|
const date = new Date(context.input[name] as string);
|
||||||
if (isNaN(date.getTime())) {
|
if (isNaN(date.getTime())) {
|
||||||
throw new ParsingError(`Input "${name}" must be a valid date, but got "${context.input[name]}"`);
|
throw new Error(`Input "${name}" must be a valid date, but got "${context.input[name]}"`);
|
||||||
}
|
}
|
||||||
context.input[name] = date;
|
context.input[name] = date;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,74 +0,0 @@
|
|||||||
import Handlebars from "handlebars";
|
|
||||||
import YAML from "yaml";
|
|
||||||
import { should, expect, assert } from 'chai';
|
|
||||||
import { ExecutionHandler } from "../execution.js";
|
|
||||||
import { ScriptError } from "../../utils/errors.js";
|
|
||||||
|
|
||||||
const javascriptHandler: ExecutionHandler = ({
|
|
||||||
node,
|
|
||||||
parent,
|
|
||||||
index,
|
|
||||||
addStep,
|
|
||||||
}) => {
|
|
||||||
if (node.type !== 'code' || node.lang !== 'javascript') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const optionParts = node.meta?.split(',') || [];
|
|
||||||
node.meta = undefined;
|
|
||||||
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 }) => {
|
|
||||||
const template = Handlebars.compile(node.value);
|
|
||||||
const content = template(context);
|
|
||||||
node.value = content;
|
|
||||||
if (options['run'] === true) {
|
|
||||||
const api = {
|
|
||||||
assert,
|
|
||||||
should,
|
|
||||||
expect,
|
|
||||||
...context,
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
// eslint-disable-next-line no-new-func
|
|
||||||
const asyncFunc = new Function(
|
|
||||||
...Object.keys(api),
|
|
||||||
`return (async () => { ${content} })()`
|
|
||||||
);
|
|
||||||
const result = await asyncFunc(...Object.values(api));
|
|
||||||
if (options.output === true && index !== undefined) {
|
|
||||||
if (result !== undefined) {
|
|
||||||
parent?.children?.splice(index + 1, 0, {
|
|
||||||
type: 'code',
|
|
||||||
lang: 'yaml',
|
|
||||||
value: YAML.stringify(result, null, 2),
|
|
||||||
meta: undefined,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
if (index !== undefined) {
|
|
||||||
parent?.children?.splice(index + 1, 0, {
|
|
||||||
type: 'code',
|
|
||||||
value: `Error: ${error instanceof Error ? error.message : String(error)}`,
|
|
||||||
meta: undefined,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
throw new ScriptError(error instanceof Error ? error.message : String(error))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (options.hidden === true && parent && index !== undefined) {
|
|
||||||
parent.children?.splice(index, 1);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export { javascriptHandler };
|
|
||||||
@@ -1,8 +1,6 @@
|
|||||||
import { dirname, resolve } from 'path';
|
import { dirname, resolve } from 'path';
|
||||||
import { toString } from 'mdast-util-to-string'
|
import { toString } from 'mdast-util-to-string'
|
||||||
import { execute, type ExecutionHandler } from '../execution.js';
|
import { execute, type ExecutionHandler } from '../execution.js';
|
||||||
import { FileNotFoundError } from '../../utils/errors.js';
|
|
||||||
import { existsSync } from 'fs';
|
|
||||||
|
|
||||||
const fileHandler: ExecutionHandler = ({
|
const fileHandler: ExecutionHandler = ({
|
||||||
addStep,
|
addStep,
|
||||||
@@ -20,12 +18,11 @@ const fileHandler: ExecutionHandler = ({
|
|||||||
dirname(file),
|
dirname(file),
|
||||||
toString(node)
|
toString(node)
|
||||||
);
|
);
|
||||||
if (!existsSync(filePath)) {
|
if (!filePath) {
|
||||||
throw new FileNotFoundError(filePath);
|
throw new Error('File path is required');
|
||||||
}
|
}
|
||||||
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');
|
||||||
|
|||||||
@@ -3,8 +3,6 @@ import { Context } from '../../context/context.js';
|
|||||||
import { execute, type ExecutionHandler } from '../execution.js';
|
import { execute, type ExecutionHandler } from '../execution.js';
|
||||||
import { dirname, resolve } from 'path';
|
import { dirname, resolve } from 'path';
|
||||||
import { toString } from 'mdast-util-to-string';
|
import { toString } from 'mdast-util-to-string';
|
||||||
import { FileNotFoundError } from '../../utils/errors.js';
|
|
||||||
import { existsSync } from 'fs';
|
|
||||||
|
|
||||||
const rawMdHandler: ExecutionHandler = ({
|
const rawMdHandler: ExecutionHandler = ({
|
||||||
addStep,
|
addStep,
|
||||||
@@ -22,9 +20,6 @@ const rawMdHandler: ExecutionHandler = ({
|
|||||||
dirname(file),
|
dirname(file),
|
||||||
toString(node),
|
toString(node),
|
||||||
);
|
);
|
||||||
if (!existsSync(name)) {
|
|
||||||
throw new FileNotFoundError(name);
|
|
||||||
}
|
|
||||||
const context = new Context({
|
const context = new Context({
|
||||||
input: {},
|
input: {},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -53,7 +53,6 @@ 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) {
|
||||||
|
|||||||
@@ -1,29 +0,0 @@
|
|||||||
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 };
|
|
||||||
@@ -6,8 +6,6 @@ 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";
|
|
||||||
import { javascriptHandler } from "./handlers.javascript.js";
|
|
||||||
|
|
||||||
const handlers = [
|
const handlers = [
|
||||||
fileHandler,
|
fileHandler,
|
||||||
@@ -17,11 +15,6 @@ const handlers = [
|
|||||||
inputHandler,
|
inputHandler,
|
||||||
rawMdHandler,
|
rawMdHandler,
|
||||||
codeHandler,
|
codeHandler,
|
||||||
javascriptHandler,
|
|
||||||
] satisfies ExecutionHandler[];
|
] satisfies ExecutionHandler[];
|
||||||
|
|
||||||
const postHandlers = [
|
export { handlers };
|
||||||
tocHandler,
|
|
||||||
] satisfies ExecutionHandler[];
|
|
||||||
|
|
||||||
export { handlers, postHandlers };
|
|
||||||
|
|||||||
@@ -1,29 +0,0 @@
|
|||||||
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 };
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
class BaseError extends Error {
|
|
||||||
constructor(message: string) {
|
|
||||||
super(message);
|
|
||||||
this.name = this.constructor.name;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class FileNotFoundError extends BaseError {
|
|
||||||
constructor(filePath: string) {
|
|
||||||
super(`File not found: ${filePath}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class InvalidFileError extends BaseError {
|
|
||||||
constructor(filePath: string) {
|
|
||||||
super(`Invalid file: ${filePath}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class InvalidFormatError extends BaseError {
|
|
||||||
constructor(format: string) {
|
|
||||||
super(`Invalid format: ${format}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class ScriptError extends BaseError {
|
|
||||||
constructor(message: string) {
|
|
||||||
super(`Script error: ${message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class ParsingError extends BaseError {
|
|
||||||
constructor(message: string) {
|
|
||||||
super(`Parsing error: ${message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class RequiredError extends BaseError {
|
|
||||||
constructor(name: string) {
|
|
||||||
super(`Required input "${name}" is missing`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export { BaseError, FileNotFoundError, InvalidFileError, InvalidFormatError, ScriptError, ParsingError, RequiredError };
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
import { extname } from "path";
|
|
||||||
import { FileNotFoundError, InvalidFileError } from "./errors.js";
|
|
||||||
import { existsSync } from "fs";
|
|
||||||
import YAML from "yaml";
|
|
||||||
import { readFile } from "fs/promises";
|
|
||||||
|
|
||||||
const loadJsonFile = async (filePath: string) => {
|
|
||||||
const content = await readFile(filePath, "utf-8");
|
|
||||||
return JSON.parse(content);
|
|
||||||
}
|
|
||||||
|
|
||||||
const loadYamlFile = async (filePath: string) => {
|
|
||||||
const content = await readFile(filePath, "utf-8");
|
|
||||||
return YAML.parse(content);
|
|
||||||
}
|
|
||||||
|
|
||||||
const loadJsFile = async (filePath: string) => {
|
|
||||||
const { default: content } = await import(filePath);
|
|
||||||
return content;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
const loadInputFiles = async (filePaths: string[]) => {
|
|
||||||
let inputs: Record<string, unknown> = {};
|
|
||||||
for (const filePath of filePaths) {
|
|
||||||
const type = extname(filePath);
|
|
||||||
if (!existsSync(filePath)) {
|
|
||||||
throw new FileNotFoundError(filePath);
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
switch (type) {
|
|
||||||
case ".json":
|
|
||||||
inputs = {
|
|
||||||
...inputs,
|
|
||||||
...(await loadJsonFile(filePath)),
|
|
||||||
};
|
|
||||||
break;
|
|
||||||
case ".yaml":
|
|
||||||
case ".yml":
|
|
||||||
inputs = {
|
|
||||||
...inputs,
|
|
||||||
...(await loadYamlFile(filePath)),
|
|
||||||
};
|
|
||||||
break;
|
|
||||||
case ".js":
|
|
||||||
inputs = {
|
|
||||||
...inputs,
|
|
||||||
...(await loadJsFile(filePath)),
|
|
||||||
};
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
throw new InvalidFileError(filePath);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
throw new InvalidFileError(filePath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export { loadInputFiles };
|
|
||||||
@@ -10,11 +10,7 @@
|
|||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"allowSyntheticDefaultImports": true,
|
"allowSyntheticDefaultImports": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"outDir": "dist",
|
"outDir": "dist"
|
||||||
"jsx": "react-jsx"
|
|
||||||
},
|
},
|
||||||
"include": [
|
"include": ["src/**/*.ts"]
|
||||||
"src/**/*.ts",
|
|
||||||
"src/cli/ui/ui.tsx"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user