mirror of
https://github.com/morten-olsen/http.md.git
synced 2026-02-08 00:46:28 +01:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c7b9abf868 | ||
|
|
bf14ef97b8 | ||
|
|
ab2bb38f39 | ||
|
|
1d055e49f1 | ||
|
|
b308c7f9fe | ||
|
|
ba56b66222 |
251
README.md
251
README.md
@@ -21,12 +21,50 @@ 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.
|
||||
|
||||
## 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)
|
||||
* [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
|
||||
|
||||
Install `http.md` globally using npm:
|
||||
|
||||
```shell
|
||||
npm i -g @morten-olsen/httpmd
|
||||
npm i -g @morten-olsen/http.md
|
||||
```
|
||||
|
||||
## Getting Started
|
||||
@@ -63,13 +101,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:
|
||||
|
||||
```shell
|
||||
httpmd dev example.md
|
||||
http.md dev example.md
|
||||
```
|
||||
|
||||
With watch mode:
|
||||
|
||||
```shell
|
||||
httpmd dev --watch example.md
|
||||
http.md 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.
|
||||
@@ -78,13 +116,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:
|
||||
|
||||
```shell
|
||||
httpmd build example.md output.md
|
||||
http.md build example.md output.md
|
||||
```
|
||||
|
||||
With watch mode:
|
||||
|
||||
```shell
|
||||
httpmd build --watch example.md output.md
|
||||
http.md 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.
|
||||
@@ -105,14 +143,14 @@ Content-Type: application/json
|
||||
|
||||
And here is the response:
|
||||
|
||||
```
|
||||
```http
|
||||
HTTP/200 OK
|
||||
access-control-allow-credentials: true
|
||||
access-control-allow-origin: *
|
||||
connection: keep-alive
|
||||
content-length: 555
|
||||
content-length: 559
|
||||
content-type: application/json
|
||||
date: Sun, 18 May 2025 19:12:17 GMT
|
||||
date: Mon, 19 May 2025 07:15:17 GMT
|
||||
server: gunicorn/19.9.0
|
||||
|
||||
{
|
||||
@@ -129,12 +167,12 @@ server: gunicorn/19.9.0
|
||||
"Host": "httpbin.org",
|
||||
"Sec-Fetch-Mode": "cors",
|
||||
"User-Agent": "node",
|
||||
"X-Amzn-Trace-Id": "Root=1-682a3111-131bcbff690b03fd64aa4617"
|
||||
"X-Amzn-Trace-Id": "Root=1-682ada85-516dfea550431bd2238aa456"
|
||||
},
|
||||
"json": {
|
||||
"greeting": "Hello, http.md!"
|
||||
},
|
||||
"origin": "23.96.180.7",
|
||||
"origin": "172.214.199.239",
|
||||
"url": "https://httpbin.org/post"
|
||||
}
|
||||
|
||||
@@ -193,23 +231,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.
|
||||
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:
|
||||
|
||||
````markdown
|
||||
# Document with Multiple Requests
|
||||
|
||||
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
|
||||
```
|
||||
|
||||
@@ -221,6 +259,96 @@ 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: 504
|
||||
content-type: application/json
|
||||
date: Mon, 19 May 2025 07:15:18 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-682ada85-5841d69c253c03e450c0cfc8"
|
||||
},
|
||||
"json": null,
|
||||
"origin": "172.214.199.239",
|
||||
"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: 385
|
||||
content-type: application/json
|
||||
date: Mon, 19 May 2025 07:15:18 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-682ada86-50ccd79351313f742de31921"
|
||||
},
|
||||
"origin": "172.214.199.239",
|
||||
"url": "https://httpbin.org/get?item=123"
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
````
|
||||
|
||||
</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.
|
||||
@@ -257,7 +385,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.
|
||||
|
||||
* Example: If you run `httpmd dev -i userId=123 -i apiKey=secret myfile.md`, you can use `{{input.userId}}` and `{{input.apiKey}}`.
|
||||
* Example: If you run `http.md dev -i userId=123 -i apiKey=secret myfile.md`, you can use `{{input.userId}}` and `{{input.apiKey}}`.
|
||||
|
||||
### Templating Examples
|
||||
|
||||
@@ -302,14 +430,14 @@ Now, let's fetch the item using a (mocked) ID from the response:
|
||||
GET https://httpbin.org/anything/My New Item
|
||||
```
|
||||
|
||||
```
|
||||
```http
|
||||
HTTP/200 OK
|
||||
access-control-allow-credentials: true
|
||||
access-control-allow-origin: *
|
||||
connection: keep-alive
|
||||
content-length: 451
|
||||
content-length: 455
|
||||
content-type: application/json
|
||||
date: Sun, 18 May 2025 19:12:18 GMT
|
||||
date: Mon, 19 May 2025 07:15:18 GMT
|
||||
server: gunicorn/19.9.0
|
||||
|
||||
{
|
||||
@@ -324,11 +452,11 @@ server: gunicorn/19.9.0
|
||||
"Host": "httpbin.org",
|
||||
"Sec-Fetch-Mode": "cors",
|
||||
"User-Agent": "node",
|
||||
"X-Amzn-Trace-Id": "Root=1-682a3112-4bbb29111129c1556c487ca1"
|
||||
"X-Amzn-Trace-Id": "Root=1-682ada86-5c212bfd7fd8d81a7749fe52"
|
||||
},
|
||||
"json": null,
|
||||
"method": "GET",
|
||||
"origin": "23.96.180.7",
|
||||
"origin": "172.214.199.239",
|
||||
"url": "https://httpbin.org/anything/My New Item"
|
||||
}
|
||||
|
||||
@@ -365,9 +493,10 @@ The requests from the embedded document are processed, and their `request` and `
|
||||
Assume `_shared_requests.md` contains:
|
||||
|
||||
````markdown
|
||||
```http id=sharedGetRequest
|
||||
```http #sharedGetRequest
|
||||
GET https://httpbin.org/get
|
||||
```
|
||||
|
||||
````
|
||||
|
||||
Then, in `main.md`:
|
||||
@@ -379,7 +508,7 @@ Let's include some shared requests:
|
||||
|
||||
::md[./_shared_requests.md]
|
||||
|
||||
The shared GET request returned:
|
||||
The shared GET request returned: {{response.statusText}}
|
||||
|
||||
Now, a request specific to this document:
|
||||
|
||||
@@ -387,12 +516,76 @@ Now, a request specific to this document:
|
||||
POST https://httpbin.org/post
|
||||
Content-Type: application/json
|
||||
|
||||
{"dataFromMain": "someValue", "sharedUrl": ""}
|
||||
{"dataFromMain": "someValue", "sharedUrl": "{{requests.sharedGetRequest.url}}"}
|
||||
```
|
||||
|
||||
::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: 644
|
||||
content-type: application/json
|
||||
date: Mon, 19 May 2025 07:15:18 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-682ada86-4c99fed83b21605713289d8a"
|
||||
},
|
||||
"json": {
|
||||
"dataFromMain": "someValue",
|
||||
"sharedUrl": "https://httpbin.org/get"
|
||||
},
|
||||
"origin": "172.214.199.239",
|
||||
"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.
|
||||
|
||||
## Advanced Usage
|
||||
@@ -404,7 +597,7 @@ You can pass external data into your `http.md` documents using the `-i` (or `--i
|
||||
**CLI Command:**
|
||||
|
||||
```shell
|
||||
httpmd build mydoc.md output.md -i baseUrl=https://api.production.example.com -i apiKey=YOUR_SECRET_KEY
|
||||
http.md build mydoc.md output.md -i baseUrl=https://api.production.example.com -i apiKey=YOUR_SECRET_KEY
|
||||
```
|
||||
|
||||
**Markdown Usage (`mydoc.md`):**
|
||||
@@ -513,9 +706,9 @@ The `::input` directive is used to declare expected input variables
|
||||
|
||||
## Command-Line Interface (CLI)
|
||||
|
||||
The `httpmd` tool provides the following commands:
|
||||
The `http.md` tool provides the following commands:
|
||||
|
||||
### `httpmd dev <source_file.md>`
|
||||
### `http.md dev <source_file.md>`
|
||||
|
||||
Processes the `<source_file.md>`, executes all HTTP requests, resolves templates, and prints the resulting markdown to the **terminal (stdout)**.
|
||||
|
||||
@@ -527,10 +720,10 @@ Processes the `<source_file.md>`, executes all HTTP requests, resolves templates
|
||||
**Example:**
|
||||
|
||||
```shell
|
||||
httpmd dev api_tests.md --watch -i host=localhost:3000
|
||||
http.md dev api_tests.md --watch -i host=localhost:3000
|
||||
```
|
||||
|
||||
### `httpmd build <source_file.md> <output_file.md>`
|
||||
### `http.md 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>`.
|
||||
|
||||
@@ -542,5 +735,5 @@ Processes the `<source_file.md>`, executes all HTTP requests, resolves templates
|
||||
**Example:**
|
||||
|
||||
```shell
|
||||
httpmd build official_api_docs.md public/api_docs_v1.md -i version=v1.0
|
||||
http.md build official_api_docs.md public/api_docs_v1.md -i version=v1.0
|
||||
```
|
||||
|
||||
@@ -21,6 +21,14 @@ 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.
|
||||
|
||||
## 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
|
||||
@@ -30,7 +38,7 @@ It allows developers to create API documentation that is always accurate and up-
|
||||
Install `http.md` globally using npm:
|
||||
|
||||
```shell
|
||||
npm i -g @morten-olsen/httpmd
|
||||
npm i -g @morten-olsen/http.md
|
||||
```
|
||||
|
||||
## Getting Started
|
||||
@@ -51,13 +59,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:
|
||||
|
||||
```shell
|
||||
httpmd dev example.md
|
||||
http.md dev example.md
|
||||
```
|
||||
|
||||
With watch mode:
|
||||
|
||||
```shell
|
||||
httpmd dev --watch example.md
|
||||
http.md 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.
|
||||
@@ -66,13 +74,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:
|
||||
|
||||
```shell
|
||||
httpmd build example.md output.md
|
||||
http.md build example.md output.md
|
||||
```
|
||||
|
||||
With watch mode:
|
||||
|
||||
```shell
|
||||
httpmd build --watch example.md output.md
|
||||
http.md 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.
|
||||
@@ -89,7 +97,7 @@ _(Note: Actual headers and some response fields might vary.)_
|
||||
|
||||
HTTP requests are defined in fenced code blocks annotated with `http`. The syntax is similar to the raw HTTP format:
|
||||
|
||||
```http disable
|
||||
```
|
||||
<METHOD> <URL>
|
||||
<Header-Name>: <Header-Value>
|
||||
...
|
||||
@@ -132,10 +140,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.
|
||||
@@ -172,7 +187,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.
|
||||
|
||||
- Example: If you run `httpmd dev -i userId=123 -i apiKey=secret myfile.md`, you can use `{{input.userId}}` and `{{input.apiKey}}`.
|
||||
- Example: If you run `http.md dev -i userId=123 -i apiKey=secret myfile.md`, you can use `{{input.userId}}` and `{{input.apiKey}}`.
|
||||
|
||||
### Templating Examples
|
||||
|
||||
@@ -196,7 +211,7 @@ _(Note: `httpbin.org/post` wraps the JSON sent in a "json" field in its response
|
||||
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
|
||||
@@ -237,15 +252,15 @@ You can pass external data into your `http.md` documents using the `-i` (or `--i
|
||||
**CLI Command:**
|
||||
|
||||
```shell
|
||||
httpmd build mydoc.md output.md -i baseUrl=https://api.production.example.com -i apiKey=YOUR_SECRET_KEY
|
||||
http.md build mydoc.md output.md -i baseUrl=https://api.production.example.com -i apiKey=YOUR_SECRET_KEY
|
||||
```
|
||||
|
||||
**Markdown Usage (`mydoc.md`):**
|
||||
|
||||
````markdown
|
||||
```http
|
||||
GET {{input.baseUrl}}/users/1
|
||||
Authorization: Bearer {{input.apiKey}}
|
||||
GET {{{input.baseUrl}}}/users/1
|
||||
Authorization: Bearer {{{input.apiKey}}}
|
||||
```
|
||||
|
||||
::response
|
||||
@@ -253,6 +268,33 @@ 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.
|
||||
|
||||
### 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
|
||||
|
||||
You can configure the behavior of each `http` code block by adding options to its info string, separated by commas.
|
||||
@@ -294,8 +336,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 {{{input.apiEndpoint}}}/data
|
||||
X-API-Key: {{{input.apiKey}}}
|
||||
Content-Type: application/json
|
||||
|
||||
# Request body written in YAML, will be converted to JSON
|
||||
@@ -346,9 +388,9 @@ The `::input` directive is used to declare expected input variables
|
||||
|
||||
## Command-Line Interface (CLI)
|
||||
|
||||
The `httpmd` tool provides the following commands:
|
||||
The `http.md` tool provides the following commands:
|
||||
|
||||
### `httpmd dev <source_file.md>`
|
||||
### `http.md dev <source_file.md>`
|
||||
|
||||
Processes the `<source_file.md>`, executes all HTTP requests, resolves templates, and prints the resulting markdown to the **terminal (stdout)**.
|
||||
|
||||
@@ -360,10 +402,10 @@ Processes the `<source_file.md>`, executes all HTTP requests, resolves templates
|
||||
**Example:**
|
||||
|
||||
```shell
|
||||
httpmd dev api_tests.md --watch -i host=localhost:3000
|
||||
http.md dev api_tests.md --watch -i host=localhost:3000
|
||||
```
|
||||
|
||||
### `httpmd build <source_file.md> <output_file.md>`
|
||||
### `http.md 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>`.
|
||||
|
||||
@@ -375,5 +417,5 @@ Processes the `<source_file.md>`, executes all HTTP requests, resolves templates
|
||||
**Example:**
|
||||
|
||||
```shell
|
||||
httpmd build official_api_docs.md public/api_docs_v1.md -i version=v1.0
|
||||
http.md build official_api_docs.md public/api_docs_v1.md -i version=v1.0
|
||||
```
|
||||
|
||||
17
docs/examples/with-javascript.md
Normal file
17
docs/examples/with-javascript.md
Normal file
@@ -0,0 +1,17 @@
|
||||
```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:
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"name": "@morten-olsen/httpmd",
|
||||
"name": "@morten-olsen/http.md",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "dist/exports.js",
|
||||
"type": "module",
|
||||
"bin": {
|
||||
"httpmd": "./bin/cli.mjs"
|
||||
"http.md": "./bin/cli.mjs"
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
@@ -27,6 +27,7 @@
|
||||
"devDependencies": {
|
||||
"@pnpm/find-workspace-packages": "^6.0.9",
|
||||
"@types/blessed": "^0.1.25",
|
||||
"@types/chai": "^5.2.2",
|
||||
"@types/marked-terminal": "^6.1.1",
|
||||
"@types/mdast": "^4.0.4",
|
||||
"@types/node": "^22.15.18",
|
||||
@@ -36,6 +37,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"blessed": "^0.1.81",
|
||||
"chai": "^5.2.0",
|
||||
"chalk": "^5.4.1",
|
||||
"commander": "^14.0.0",
|
||||
"dotenv": "^16.5.0",
|
||||
|
||||
59
pnpm-lock.yaml
generated
59
pnpm-lock.yaml
generated
@@ -11,6 +11,9 @@ importers:
|
||||
blessed:
|
||||
specifier: ^0.1.81
|
||||
version: 0.1.81
|
||||
chai:
|
||||
specifier: ^5.2.0
|
||||
version: 5.2.0
|
||||
chalk:
|
||||
specifier: ^5.4.1
|
||||
version: 5.4.1
|
||||
@@ -84,6 +87,9 @@ importers:
|
||||
'@types/blessed':
|
||||
specifier: ^0.1.25
|
||||
version: 0.1.25
|
||||
'@types/chai':
|
||||
specifier: ^5.2.2
|
||||
version: 5.2.2
|
||||
'@types/marked-terminal':
|
||||
specifier: ^6.1.1
|
||||
version: 6.1.1
|
||||
@@ -437,9 +443,15 @@ packages:
|
||||
'@types/cardinal@2.1.1':
|
||||
resolution: {integrity: sha512-/xCVwg8lWvahHsV2wXZt4i64H1sdL+sN1Uoq7fAc8/FA6uYHjuIveDwPwvGUYp4VZiv85dVl6J/Bum3NDAOm8g==}
|
||||
|
||||
'@types/chai@5.2.2':
|
||||
resolution: {integrity: sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg==}
|
||||
|
||||
'@types/debug@4.1.12':
|
||||
resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==}
|
||||
|
||||
'@types/deep-eql@4.0.2':
|
||||
resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==}
|
||||
|
||||
'@types/hast@3.0.4':
|
||||
resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==}
|
||||
|
||||
@@ -522,6 +534,10 @@ packages:
|
||||
as-table@1.0.55:
|
||||
resolution: {integrity: sha512-xvsWESUJn0JN421Xb9MQw6AsMHRCUknCe0Wjlxvjud80mU4E6hQf1A6NzQKcYNmYw62MfzEtXc+badstZP3JpQ==}
|
||||
|
||||
assertion-error@2.0.1:
|
||||
resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
bail@2.0.2:
|
||||
resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==}
|
||||
|
||||
@@ -564,6 +580,10 @@ packages:
|
||||
ccount@2.0.1:
|
||||
resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==}
|
||||
|
||||
chai@5.2.0:
|
||||
resolution: {integrity: sha512-mCuXncKXk5iCLhfhwTc0izo0gtEmpz5CtG2y8GiOINBlMVS6v8TMRc5TaLWKS6692m9+dVVfzgeVxR5UxWHTYw==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
chalk@4.1.2:
|
||||
resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
|
||||
engines: {node: '>=10'}
|
||||
@@ -588,6 +608,10 @@ packages:
|
||||
character-reference-invalid@2.0.1:
|
||||
resolution: {integrity: sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==}
|
||||
|
||||
check-error@2.1.1:
|
||||
resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==}
|
||||
engines: {node: '>= 16'}
|
||||
|
||||
chroma-js@2.6.0:
|
||||
resolution: {integrity: sha512-BLHvCB9s8Z1EV4ethr6xnkl/P2YRFOGqfgvuMG/MyCbZPrTA+NeiByY6XvgF0zP4/2deU2CXnWyMa3zu1LqQ3A==}
|
||||
|
||||
@@ -658,6 +682,10 @@ packages:
|
||||
decode-named-character-reference@1.1.0:
|
||||
resolution: {integrity: sha512-Wy+JTSbFThEOXQIR2L6mxJvEs+veIzpmqD7ynWxMXGpnk3smkHQOp6forLdHsKpAMW9iJpaBBIxz285t1n1C3w==}
|
||||
|
||||
deep-eql@5.0.2:
|
||||
resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
defaults@1.0.4:
|
||||
resolution: {integrity: sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==}
|
||||
|
||||
@@ -911,6 +939,9 @@ packages:
|
||||
longest-streak@3.1.0:
|
||||
resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==}
|
||||
|
||||
loupe@3.1.3:
|
||||
resolution: {integrity: sha512-kkIp7XSkP78ZxJEsSxW3712C6teJVoeHHwgo9zJ380de7IYyJ2ISlxojcH2pC5OFLewESmnRi/+XCDIEEVyoug==}
|
||||
|
||||
map-age-cleaner@0.1.3:
|
||||
resolution: {integrity: sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==}
|
||||
engines: {node: '>=6'}
|
||||
@@ -1194,6 +1225,10 @@ packages:
|
||||
resolution: {integrity: sha512-cMMJTAZlion/RWRRC48UbrDymEIt+/YSD/l8NqjneyDw2rDOBQcP5yRkMB4CYGn47KMhZvbblBP7Z79OsMw72w==}
|
||||
engines: {node: '>=8.15'}
|
||||
|
||||
pathval@2.0.0:
|
||||
resolution: {integrity: sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==}
|
||||
engines: {node: '>= 14.16'}
|
||||
|
||||
picocolors@1.1.1:
|
||||
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
|
||||
|
||||
@@ -1916,10 +1951,16 @@ snapshots:
|
||||
|
||||
'@types/cardinal@2.1.1': {}
|
||||
|
||||
'@types/chai@5.2.2':
|
||||
dependencies:
|
||||
'@types/deep-eql': 4.0.2
|
||||
|
||||
'@types/debug@4.1.12':
|
||||
dependencies:
|
||||
'@types/ms': 2.1.0
|
||||
|
||||
'@types/deep-eql@4.0.2': {}
|
||||
|
||||
'@types/hast@3.0.4':
|
||||
dependencies:
|
||||
'@types/unist': 3.0.3
|
||||
@@ -2000,6 +2041,8 @@ snapshots:
|
||||
dependencies:
|
||||
printable-characters: 1.0.42
|
||||
|
||||
assertion-error@2.0.1: {}
|
||||
|
||||
bail@2.0.2: {}
|
||||
|
||||
better-path-resolve@1.0.0:
|
||||
@@ -2044,6 +2087,14 @@ snapshots:
|
||||
|
||||
ccount@2.0.1: {}
|
||||
|
||||
chai@5.2.0:
|
||||
dependencies:
|
||||
assertion-error: 2.0.1
|
||||
check-error: 2.1.1
|
||||
deep-eql: 5.0.2
|
||||
loupe: 3.1.3
|
||||
pathval: 2.0.0
|
||||
|
||||
chalk@4.1.2:
|
||||
dependencies:
|
||||
ansi-styles: 4.3.0
|
||||
@@ -2061,6 +2112,8 @@ snapshots:
|
||||
|
||||
character-reference-invalid@2.0.1: {}
|
||||
|
||||
check-error@2.1.1: {}
|
||||
|
||||
chroma-js@2.6.0: {}
|
||||
|
||||
cli-boxes@2.2.1: {}
|
||||
@@ -2130,6 +2183,8 @@ snapshots:
|
||||
dependencies:
|
||||
character-entities: 2.0.2
|
||||
|
||||
deep-eql@5.0.2: {}
|
||||
|
||||
defaults@1.0.4:
|
||||
dependencies:
|
||||
clone: 1.0.4
|
||||
@@ -2377,6 +2432,8 @@ snapshots:
|
||||
|
||||
longest-streak@3.1.0: {}
|
||||
|
||||
loupe@3.1.3: {}
|
||||
|
||||
map-age-cleaner@0.1.3:
|
||||
dependencies:
|
||||
p-defer: 1.0.0
|
||||
@@ -2859,6 +2916,8 @@ snapshots:
|
||||
dependencies:
|
||||
unique-string: 2.0.0
|
||||
|
||||
pathval@2.0.0: {}
|
||||
|
||||
picocolors@1.1.1: {}
|
||||
|
||||
picomatch@2.3.1: {}
|
||||
|
||||
@@ -5,7 +5,7 @@ const codeHandler: ExecutionHandler = ({
|
||||
node,
|
||||
addStep,
|
||||
}) => {
|
||||
if (node.type !== 'code' || node.lang === 'http') {
|
||||
if (node.type !== 'code' || node.lang === 'http' || node.lang === 'javascript') {
|
||||
return;
|
||||
}
|
||||
const optionParts = node.meta?.split(',') || [];
|
||||
|
||||
@@ -44,7 +44,7 @@ const httpHandler: ExecutionHandler = ({
|
||||
);
|
||||
|
||||
let parsedBody = body;
|
||||
if (options.format === 'yaml') {
|
||||
if (options.yaml) {
|
||||
try {
|
||||
const parsed = YAML.parse(body);
|
||||
parsedBody = JSON.stringify(parsed);
|
||||
@@ -69,7 +69,7 @@ const httpHandler: ExecutionHandler = ({
|
||||
}
|
||||
}
|
||||
|
||||
node.value = content;
|
||||
node.value = [head, parsedBody].filter(Boolean).join('\n\n');
|
||||
node.meta = undefined;
|
||||
|
||||
context.addRequest({
|
||||
|
||||
73
src/execution/handlers/handlers.javascript.ts
Normal file
73
src/execution/handlers/handlers.javascript.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import Handlebars from "handlebars";
|
||||
import YAML from "yaml";
|
||||
import { should, expect, assert } from 'chai';
|
||||
import { ExecutionHandler } from "../execution.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 error;
|
||||
}
|
||||
}
|
||||
if (options.hidden === true && parent && index !== undefined) {
|
||||
parent.children?.splice(index, 1);
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export { javascriptHandler };
|
||||
@@ -7,6 +7,7 @@ import { responseHandler } from "./handlers.response.js";
|
||||
import { textHandler } from "./handlers.text.js";
|
||||
import { codeHandler } from "./handlers.code.js";
|
||||
import { tocHandler } from "./handlers.toc.js";
|
||||
import { javascriptHandler } from "./handlers.javascript.js";
|
||||
|
||||
const handlers = [
|
||||
fileHandler,
|
||||
@@ -16,6 +17,7 @@ const handlers = [
|
||||
inputHandler,
|
||||
rawMdHandler,
|
||||
codeHandler,
|
||||
javascriptHandler,
|
||||
] satisfies ExecutionHandler[];
|
||||
|
||||
const postHandlers = [
|
||||
|
||||
Reference in New Issue
Block a user