mirror of
https://github.com/morten-olsen/http.md.git
synced 2026-02-08 00:46:28 +01:00
Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e0707e74fb | ||
|
|
6b74a28989 | ||
|
|
ad342e5f10 | ||
|
|
5d485acc97 | ||
|
|
68f5025527 | ||
|
|
a9a7bae28f | ||
|
|
b800290d72 |
7
.github/workflows/main.yaml
vendored
7
.github/workflows/main.yaml
vendored
@@ -92,7 +92,7 @@ jobs:
|
|||||||
|
|
||||||
release:
|
release:
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: write
|
||||||
packages: write
|
packages: write
|
||||||
attestations: write
|
attestations: write
|
||||||
id-token: write
|
id-token: write
|
||||||
@@ -135,3 +135,8 @@ jobs:
|
|||||||
pnpm publish --no-git-checks --access public
|
pnpm publish --no-git-checks --access public
|
||||||
env:
|
env:
|
||||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||||
|
|
||||||
|
- uses: stefanzweifel/git-auto-commit-action@v5
|
||||||
|
with:
|
||||||
|
commit_message: "docs: generated README"
|
||||||
|
file_pattern: "*.md"
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,2 +1,3 @@
|
|||||||
/node_modules/
|
/node_modules/
|
||||||
/dist/
|
/dist/
|
||||||
|
/*.html
|
||||||
|
|||||||
242
README.md
242
README.md
@@ -2,24 +2,24 @@
|
|||||||
|
|
||||||
**`http.md` is a powerful tool that transforms your markdown files into living, executable API documentation and testing suites. Write your HTTP requests directly within markdown, see their responses, and use templating to build dynamic examples and test flows.**
|
**`http.md` is a powerful tool that transforms your markdown files into living, executable API documentation and testing suites. Write your HTTP requests directly within markdown, see their responses, and use templating to build dynamic examples and test flows.**
|
||||||
|
|
||||||
It allows developers to create API documentation that is always accurate and up-to-date because the documentation itself _is_ the set of executable requests. This ensures that your examples work and your tests run directly from the documents you share.
|
It allows developers to create API documentation that is always accurate and up-to-date because the documentation itself *is* the set of executable requests. This ensures that your examples work and your tests run directly from the documents you share.
|
||||||
|
|
||||||
## Key Features
|
## Key Features
|
||||||
|
|
||||||
- **Markdown-Native:** Define HTTP requests using familiar markdown code blocks.
|
* **Markdown-Native:** Define HTTP requests using familiar markdown code blocks.
|
||||||
- **Live Requests:** Execute requests and embed their responses directly into your documentation.
|
* **Live Requests:** Execute requests and embed their responses directly into your documentation.
|
||||||
- **Templating:** Use Handlebars syntax to chain requests, extract data from responses, and use external inputs.
|
* **Templating:** Use Handlebars syntax to chain requests, extract data from responses, and use external inputs.
|
||||||
- **File Embedding:** Include and reuse requests from other markdown files.
|
* **File Embedding:** Include and reuse requests from other markdown files.
|
||||||
- **Terminal & File Output:** View live previews in your terminal or build static markdown files for sharing or static site generation.
|
* **Terminal & File Output:** View live previews in your terminal or build static markdown files for sharing or static site generation.
|
||||||
- **Watch Mode:** Automatically re-render documents on file changes for a fast development loop.
|
* **Watch Mode:** Automatically re-render documents on file changes for a fast development loop.
|
||||||
- **Flexible Configuration:** Control request execution, output formatting, and visibility.
|
* **Flexible Configuration:** Control request execution, output formatting, and visibility.
|
||||||
|
|
||||||
## Use Cases
|
## Use Cases
|
||||||
|
|
||||||
- **API Documentation:** Create clear, executable examples that users can trust.
|
* **API Documentation:** Create clear, executable examples that users can trust.
|
||||||
- **Integration Testing:** Write simple integration test suites that verify API behavior.
|
* **Integration Testing:** Write simple integration test suites that verify API behavior.
|
||||||
- **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.
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
@@ -52,6 +52,7 @@ Content-Type: application/json
|
|||||||
And here is the response:
|
And here is the response:
|
||||||
|
|
||||||
::response
|
::response
|
||||||
|
|
||||||
````
|
````
|
||||||
|
|
||||||
### Rendering Documents
|
### Rendering Documents
|
||||||
@@ -109,37 +110,39 @@ HTTP/200 OK
|
|||||||
access-control-allow-credentials: true
|
access-control-allow-credentials: true
|
||||||
access-control-allow-origin: *
|
access-control-allow-origin: *
|
||||||
connection: keep-alive
|
connection: keep-alive
|
||||||
content-length: 559
|
content-length: 555
|
||||||
content-type: application/json
|
content-type: application/json
|
||||||
date: Sun, 18 May 2025 18:31:46 GMT
|
date: Sun, 18 May 2025 19:12:17 GMT
|
||||||
server: gunicorn/19.9.0
|
server: gunicorn/19.9.0
|
||||||
|
|
||||||
{
|
{
|
||||||
"args": {},
|
"args": {},
|
||||||
"data": "{\"greeting\": \"Hello, http.md!\"}",
|
"data": "{\"greeting\": \"Hello, http.md!\"}",
|
||||||
"files": {},
|
"files": {},
|
||||||
"form": {},
|
"form": {},
|
||||||
"headers": {
|
"headers": {
|
||||||
"Accept": "*/*",
|
"Accept": "*/*",
|
||||||
"Accept-Encoding": "br, gzip, deflate",
|
"Accept-Encoding": "br, gzip, deflate",
|
||||||
"Accept-Language": "*",
|
"Accept-Language": "*",
|
||||||
"Content-Length": "31",
|
"Content-Length": "31",
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
"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-682a2792-7df702ce77a3b3696937eaeb"
|
"X-Amzn-Trace-Id": "Root=1-682a3111-131bcbff690b03fd64aa4617"
|
||||||
},
|
},
|
||||||
"json": {
|
"json": {
|
||||||
"greeting": "Hello, http.md!"
|
"greeting": "Hello, http.md!"
|
||||||
},
|
},
|
||||||
|
"origin": "23.96.180.7",
|
||||||
"url": "https://httpbin.org/post"
|
"url": "https://httpbin.org/post"
|
||||||
}
|
}
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
````
|
````
|
||||||
|
|
||||||
_(Note: Actual headers and some response fields might vary.)_
|
*(Note: Actual headers and some response fields might vary.)*
|
||||||
|
|
||||||
## Core Concepts
|
## Core Concepts
|
||||||
|
|
||||||
@@ -180,8 +183,8 @@ All requests in a document are executed sequentially from top to bottom by defau
|
|||||||
|
|
||||||
The `::response` directive is used to render the full HTTP response (status line, headers, and body) of an HTTP request.
|
The `::response` directive is used to render the full HTTP response (status line, headers, and body) of an HTTP request.
|
||||||
|
|
||||||
- **Implicit Last:** If used without any arguments (i.e., `::response`), it renders the response of the most recently defined `http` block above it.
|
* **Implicit Last:** If used without any arguments (i.e., `::response`), it renders the response of the most recently defined `http` block above it.
|
||||||
- **Explicit by ID:** You can render the response of a specific request by referencing its ID (see [Request IDs](https://www.google.com/search?q=%23request-ids)).
|
* **Explicit by ID:** You can render the response of a specific request by referencing its ID (see [Request IDs](https://www.google.com/search?q=%23request-ids)).
|
||||||
|
|
||||||
### Request IDs
|
### Request IDs
|
||||||
|
|
||||||
@@ -215,6 +218,7 @@ Response from creating the user:
|
|||||||
|
|
||||||
Response from getting the item:
|
Response from getting the item:
|
||||||
::response{#getItem}
|
::response{#getItem}
|
||||||
|
|
||||||
````
|
````
|
||||||
|
|
||||||
## Templating with Handlebars
|
## Templating with Handlebars
|
||||||
@@ -227,47 +231,47 @@ Templating syntax uses double curly braces: `{{expression}}`.
|
|||||||
|
|
||||||
Within your markdown document, the following variables are available in the Handlebars context:
|
Within your markdown document, the following variables are available in the Handlebars context:
|
||||||
|
|
||||||
- **`request`** (Object): Details of the most recently processed HTTP request _before_ it's sent.
|
* **`request`** (Object): Details of the most recently processed HTTP request *before* it's sent.
|
||||||
|
|
||||||
- `request.method` (String): The HTTP method (e.g., "GET", "POST").
|
* `request.method` (String): The HTTP method (e.g., "GET", "POST").
|
||||||
- `request.url` (String): The request URL.
|
* `request.url` (String): The request URL.
|
||||||
- `request.headers` (Object): An object containing request headers.
|
* `request.headers` (Object): An object containing request headers.
|
||||||
- `request.body` (String): The raw request body.
|
* `request.body` (String): The raw request body.
|
||||||
|
|
||||||
- **`response`** (Object): Details of the most recently received HTTP response.
|
* **`response`** (Object): Details of the most recently received HTTP response.
|
||||||
|
|
||||||
- `response.status` (Number): The HTTP status code (e.g., 200, 404).
|
* `response.status` (Number): The HTTP status code (e.g., 200, 404).
|
||||||
- `response.statusText` (String): The HTTP status message (e.g., "OK", "Not Found").
|
* `response.statusText` (String): The HTTP status message (e.g., "OK", "Not Found").
|
||||||
- `response.headers` (Object): An object containing response headers.
|
* `response.headers` (Object): An object containing response headers.
|
||||||
- `response.body` (String/Object): The response body. If the `http` block had the `json` option and the response was valid JSON, this will be a parsed JSON object. Otherwise, it's a raw string.
|
* `response.body` (String/Object): The response body. If the `http` block had the `json` option and the response was valid JSON, this will be a parsed JSON object. Otherwise, it's a raw string.
|
||||||
- `response.rawBody` (String): The raw response body as a string, regardless of parsing.
|
* `response.rawBody` (String): The raw response body as a string, regardless of parsing.
|
||||||
- _(In case of network errors or non-HTTP errors, `status` and `body` might reflect error information.)_
|
* *(In case of network errors or non-HTTP errors, `status` and `body` might reflect error information.)*
|
||||||
|
|
||||||
- **`requests`** (Object): A dictionary mapping request IDs to their respective `request` objects (as defined above).
|
* **`requests`** (Object): A dictionary mapping request IDs to their respective `request` objects (as defined above).
|
||||||
|
|
||||||
- Example: `{{requests.createUser.url}}`
|
* Example: `{{requests.createUser.url}}`
|
||||||
|
|
||||||
- **`responses`** (Object): A dictionary mapping request IDs to their respective `response` objects (as defined above).
|
* **`responses`** (Object): A dictionary mapping request IDs to their respective `response` objects (as defined above).
|
||||||
|
|
||||||
- Example: `{{responses.createUser.status}}`, `{{responses.createUser.body.id}}` (if `body` is a parsed JSON object).
|
* Example: `{{responses.createUser.status}}`, `{{responses.createUser.body.id}}` (if `body` is a parsed JSON object).
|
||||||
|
|
||||||
- **`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 `httpmd 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
|
||||||
|
|
||||||
**1. Using a value from a previous response in a new request:**
|
**1. Using a value from a previous response in a new request:**
|
||||||
|
|
||||||
````markdown
|
````markdown
|
||||||
```http id=createItem json
|
```http #createItem,json
|
||||||
POST https://httpbin.org/post
|
POST https://httpbin.org/post
|
||||||
Content-Type: application/json
|
Content-Type: application/json
|
||||||
|
|
||||||
{"name": "My New Item"}
|
{"name": "My New Item"}
|
||||||
```
|
```
|
||||||
|
|
||||||
The new item ID is: {{responses.createItem.body.json.name}}
|
The new item ID is: {{response.body.json.name}}
|
||||||
|
|
||||||
Now, let's fetch the item using a (mocked) ID from the response:
|
Now, let's fetch the item using a (mocked) ID from the response:
|
||||||
|
|
||||||
@@ -276,9 +280,65 @@ GET https://httpbin.org/anything/{{responses.createItem.body.json.name}}
|
|||||||
```
|
```
|
||||||
|
|
||||||
::response{#fetchItem}
|
::response{#fetchItem}
|
||||||
|
|
||||||
````
|
````
|
||||||
|
|
||||||
_(Note: `httpbin.org/post` wraps the JSON sent in a "json" field in its response. If your API returns the ID directly at the root of the JSON body, you'd use `{{responses.createItem.body.id}}` assuming the `createItem` request had the `json` option.)_
|
<details>
|
||||||
|
<summary>Output</summary>
|
||||||
|
|
||||||
|
````markdown
|
||||||
|
```http
|
||||||
|
POST https://httpbin.org/post
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{"name": "My New Item"}
|
||||||
|
```
|
||||||
|
|
||||||
|
The new item ID is: My New Item
|
||||||
|
|
||||||
|
Now, let's fetch the item using a (mocked) ID from the response:
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET https://httpbin.org/anything/My New Item
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
HTTP/200 OK
|
||||||
|
access-control-allow-credentials: true
|
||||||
|
access-control-allow-origin: *
|
||||||
|
connection: keep-alive
|
||||||
|
content-length: 451
|
||||||
|
content-type: application/json
|
||||||
|
date: Sun, 18 May 2025 19:12:18 GMT
|
||||||
|
server: gunicorn/19.9.0
|
||||||
|
|
||||||
|
{
|
||||||
|
"args": {},
|
||||||
|
"data": "",
|
||||||
|
"files": {},
|
||||||
|
"form": {},
|
||||||
|
"headers": {
|
||||||
|
"Accept": "*/*",
|
||||||
|
"Accept-Encoding": "br, gzip, deflate",
|
||||||
|
"Accept-Language": "*",
|
||||||
|
"Host": "httpbin.org",
|
||||||
|
"Sec-Fetch-Mode": "cors",
|
||||||
|
"User-Agent": "node",
|
||||||
|
"X-Amzn-Trace-Id": "Root=1-682a3112-4bbb29111129c1556c487ca1"
|
||||||
|
},
|
||||||
|
"json": null,
|
||||||
|
"method": "GET",
|
||||||
|
"origin": "23.96.180.7",
|
||||||
|
"url": "https://httpbin.org/anything/My New Item"
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
````
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
*(Note: `httpbin.org/post` wraps the JSON sent in a "json" field in its response. If your API returns the ID directly at the root of the JSON body, you'd use `{{responses.createItem.body.id}}` assuming the `createItem` request had the `json` option.)*
|
||||||
|
|
||||||
**2. Displaying a status code in markdown text:**
|
**2. Displaying a status code in markdown text:**
|
||||||
|
|
||||||
@@ -287,7 +347,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: \*\*\*\*.
|
The request to `/status/201` completed with status code: ****.
|
||||||
````
|
````
|
||||||
|
|
||||||
## Managing Documents
|
## Managing Documents
|
||||||
@@ -319,7 +379,7 @@ Let's include some shared requests:
|
|||||||
|
|
||||||
::md[./_shared_requests.md]
|
::md[./_shared_requests.md]
|
||||||
|
|
||||||
The shared GET request returned:
|
The shared GET request returned:
|
||||||
|
|
||||||
Now, a request specific to this document:
|
Now, a request specific to this document:
|
||||||
|
|
||||||
@@ -352,7 +412,7 @@ httpmd build mydoc.md output.md -i baseUrl=https://api.production.example.com -i
|
|||||||
````markdown
|
````markdown
|
||||||
```http
|
```http
|
||||||
GET /users/1
|
GET /users/1
|
||||||
Authorization: Bearer
|
Authorization: Bearer
|
||||||
```
|
```
|
||||||
|
|
||||||
::response
|
::response
|
||||||
@@ -364,17 +424,17 @@ Authorization: Bearer
|
|||||||
|
|
||||||
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.
|
||||||
|
|
||||||
- `id={your-id}`: Assigns a unique ID to the request. This ID can be used to reference the request's response in the `::response` directive and in templating variables (`requests.your-id`, `responses.your-id`).
|
* `id={your-id}`: Assigns a unique ID to the request. This ID can be used to reference the request's response in the `::response` directive and in templating variables (`requests.your-id`, `responses.your-id`).
|
||||||
|
|
||||||
- Example: ` ```http id=getUser,json `
|
* Example: ` ```http id=getUser,json `
|
||||||
|
|
||||||
- `json`: If present, `http.md` will attempt to parse the **response body** as JSON. If successful, `response.body` (and `responses.id.body`) will be the parsed JavaScript object/array, making it easier to access its properties in templates (e.g., `{{response.body.fieldName}}`).
|
* `json`: If present, `http.md` will attempt to parse the **response body** as JSON. If successful, `response.body` (and `responses.id.body`) will be the parsed JavaScript object/array, making it easier to access its properties in templates (e.g., `{{response.body.fieldName}}`).
|
||||||
|
|
||||||
- Example: ` ```http json `
|
* Example: ` ```http json `
|
||||||
|
|
||||||
- `yaml`: If present, the **request body** written in YAML format within the code block will be automatically converted to JSON before the request is sent. This allows for writing complex request bodies in a more human-readable YAML syntax. You should still set the `Content-Type` header appropriately (e.g., to `application/json`) if the server expects JSON.
|
* `yaml`: If present, the **request body** written in YAML format within the code block will be automatically converted to JSON before the request is sent. This allows for writing complex request bodies in a more human-readable YAML syntax. You should still set the `Content-Type` header appropriately (e.g., to `application/json`) if the server expects JSON.
|
||||||
|
|
||||||
- Example:
|
* Example:
|
||||||
|
|
||||||
````markdown
|
````markdown
|
||||||
```http yaml
|
```http yaml
|
||||||
@@ -389,20 +449,20 @@ You can configure the behavior of each `http` code block by adding options to it
|
|||||||
```
|
```
|
||||||
````
|
````
|
||||||
|
|
||||||
- `disable`: If present, the HTTP request will **not** be executed. No actual network call will be made. The corresponding `response` variable will be undefined or empty, and `::response` will typically render a "Request disabled" message or similar.
|
* `disable`: If present, the HTTP request will **not** be executed. No actual network call will be made. The corresponding `response` variable will be undefined or empty, and `::response` will typically render a "Request disabled" message or similar.
|
||||||
|
|
||||||
- Example: ` ```http disable `
|
* Example: ` ```http disable `
|
||||||
|
|
||||||
- `hidden`: If present, the `http` code block itself will **not be included** in the rendered output document. However, the request _is still made_ (unless `disable` is also specified), and its response data can be used in templates or displayed with an explicit `::response{#id}` directive. This is useful for prerequisite requests (like authentication) whose details you don't want to clutter the main documentation.
|
* `hidden`: If present, the `http` code block itself will **not be included** in the rendered output document. However, the request *is still made* (unless `disable` is also specified), and its response data can be used in templates or displayed with an explicit `::response{#id}` directive. This is useful for prerequisite requests (like authentication) whose details you don't want to clutter the main documentation.
|
||||||
|
|
||||||
- Example: ` ```http id=authRequest,hidden `
|
* Example: ` ```http id=authRequest,hidden `
|
||||||
|
|
||||||
**Combined Example:**
|
**Combined Example:**
|
||||||
|
|
||||||
````markdown
|
````markdown
|
||||||
```http id=complexRequest,json,yaml,hidden
|
```http id=complexRequest,json,yaml,hidden
|
||||||
POST /data
|
POST /data
|
||||||
X-API-Key:
|
X-API-Key:
|
||||||
Content-Type: application/json
|
Content-Type: application/json
|
||||||
|
|
||||||
# Request body written in YAML, will be converted to JSON
|
# Request body written in YAML, will be converted to JSON
|
||||||
@@ -421,12 +481,12 @@ Directives can also have options, specified similarly.
|
|||||||
|
|
||||||
#### `::response` Directive Options
|
#### `::response` Directive Options
|
||||||
|
|
||||||
- `id={id}` (or `#{id}` as a shorthand): Renders the output of a specific request identified by `{id}`.
|
* `id={id}` (or `#{id}` as a shorthand): Renders the output of a specific request identified by `{id}`.
|
||||||
- Example: `::response{#getUser}` or `::response{id=getUser}`
|
* Example: `::response{#getUser}` or `::response{id=getUser}`
|
||||||
- `yaml`: Renders the (typically JSON) response body formatted as YAML. This is for display purposes.
|
* `yaml`: Renders the (typically JSON) response body formatted as YAML. This is for display purposes.
|
||||||
- Example: `::response{yaml}`
|
* Example: `::response{yaml}`
|
||||||
- `truncate={chars}`: Truncates the displayed **response body** to the specified number of characters. Headers and status line are not affected.
|
* `truncate={chars}`: Truncates the displayed **response body** to the specified number of characters. Headers and status line are not affected.
|
||||||
- Example: `::response{truncate=100}`
|
* Example: `::response{truncate=100}`
|
||||||
|
|
||||||
**Combined Example for `::response`:**
|
**Combined Example for `::response`:**
|
||||||
`::response{#getUser,yaml,truncate=500}` - Displays the response for request `getUser`, formats its body as YAML, and truncates the body display to 500 characters.
|
`::response{#getUser,yaml,truncate=500}` - Displays the response for request `getUser`, formats its body as YAML, and truncates the body display to 500 characters.
|
||||||
@@ -435,21 +495,21 @@ Directives can also have options, specified similarly.
|
|||||||
|
|
||||||
The `::md` directive embeds another markdown document.
|
The `::md` directive embeds another markdown document.
|
||||||
|
|
||||||
- **File Path:** The first argument (required) is the path to the markdown file to embed.
|
* **File Path:** The first argument (required) is the path to the markdown file to embed.
|
||||||
- Example: `::md[./includes/authentication.md]`
|
* Example: `::md[./includes/authentication.md]`
|
||||||
- `hidden`: If present, the actual content (markdown) of the embedded document will not be rendered in the output. However, any `http` requests within the embedded document _are still processed_, and their `request` and `response` data become available in the parent document's templating context (via `requests.id` and `responses.id`). This is useful if you only want to execute the requests from an included file (e.g., a common setup sequence) and use their results, without displaying the embedded file's content.
|
* `hidden`: If present, the actual content (markdown) of the embedded document will not be rendered in the output. However, any `http` requests within the embedded document *are still processed*, and their `request` and `response` data become available in the parent document's templating context (via `requests.id` and `responses.id`). This is useful if you only want to execute the requests from an included file (e.g., a common setup sequence) and use their results, without displaying the embedded file's content.
|
||||||
- Example: `::md[./setup_requests.md]{hidden}`
|
* Example: `::md[./setup_requests.md]{hidden}`
|
||||||
|
|
||||||
#### `::input[{name}]` Directive Options
|
#### `::input[{name}]` Directive Options
|
||||||
|
|
||||||
The `::input` directive is used to declare expected input variables
|
The `::input` directive is used to declare expected input variables
|
||||||
|
|
||||||
- **Variable Name:** The first argument (required) is the name of the variable
|
* **Variable Name:** The first argument (required) is the name of the variable
|
||||||
- Example: `::input[myVariable]` will define `input.myVariable`
|
* Example: `::input[myVariable]` will define `input.myVariable`
|
||||||
- `required`: If present it will require that the variable is provided
|
* `required`: If present it will require that the variable is provided
|
||||||
- `default={value}`: Defines the default value if no value has been provided
|
* `default={value}`: Defines the default value if no value has been provided
|
||||||
- `format=string|number|bool|json|date`: If provided the value will be parsed using the specified format
|
* `format=string|number|bool|json|date`: If provided the value will be parsed using the specified format
|
||||||
- \`\`
|
* \`\`
|
||||||
|
|
||||||
## Command-Line Interface (CLI)
|
## Command-Line Interface (CLI)
|
||||||
|
|
||||||
@@ -459,10 +519,10 @@ The `httpmd` tool provides the following commands:
|
|||||||
|
|
||||||
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)**.
|
||||||
|
|
||||||
- **Purpose:** Useful for live development and quick previews.
|
* **Purpose:** Useful for live development and quick previews.
|
||||||
- **Options:**
|
* **Options:**
|
||||||
- `--watch`: Monitors the `<source_file.md>` (and any embedded files) for changes. On detection of a change, it automatically re-processes and re-renders the output to the terminal.
|
* `--watch`: Monitors the `<source_file.md>` (and any embedded files) for changes. On detection of a change, it automatically re-processes and re-renders the output to the terminal.
|
||||||
- `-i <key=value>`, `--input <key=value>`: Defines an input variable for templating (see [Using Input Variables](https://www.google.com/search?q=%23using-input-variables)). Can be specified multiple times for multiple variables.
|
* `-i <key=value>`, `--input <key=value>`: Defines an input variable for templating (see [Using Input Variables](https://www.google.com/search?q=%23using-input-variables)). Can be specified multiple times for multiple variables.
|
||||||
|
|
||||||
**Example:**
|
**Example:**
|
||||||
|
|
||||||
@@ -474,10 +534,10 @@ httpmd dev api_tests.md --watch -i host=localhost:3000
|
|||||||
|
|
||||||
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>`.
|
||||||
|
|
||||||
- **Purpose:** Generates a static, shareable markdown file with all dynamic content resolved. Ideal for version control, static site generation, or distributing documentation.
|
* **Purpose:** Generates a static, shareable markdown file with all dynamic content resolved. Ideal for version control, static site generation, or distributing documentation.
|
||||||
- **Options:**
|
* **Options:**
|
||||||
- `--watch`: Monitors the `<source_file.md>` (and any embedded files) for changes. On detection of a change, it automatically re-processes and re-builds the `<output_file.md>`.
|
* `--watch`: Monitors the `<source_file.md>` (and any embedded files) for changes. On detection of a change, it automatically re-processes and re-builds the `<output_file.md>`.
|
||||||
- `-i <key=value>`, `--input <key=value>`: Defines an input variable for templating.
|
* `-i <key=value>`, `--input <key=value>`: Defines an input variable for templating.
|
||||||
|
|
||||||
**Example:**
|
**Example:**
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,10 @@ It allows developers to create API documentation that is always accurate and up-
|
|||||||
- **Tutorials & Guides:** Build step-by-step guides where each HTTP interaction is shown with its real output.
|
- **Tutorials & Guides:** Build step-by-step guides where each HTTP interaction is shown with its real output.
|
||||||
- **Rapid Prototyping:** Quickly experiment with APIs and document your findings.
|
- **Rapid Prototyping:** Quickly experiment with APIs and document your findings.
|
||||||
|
|
||||||
|
## Content
|
||||||
|
|
||||||
|
::toc
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
Install `http.md` globally using npm:
|
Install `http.md` globally using npm:
|
||||||
@@ -85,7 +89,7 @@ _(Note: Actual headers and some response fields might vary.)_
|
|||||||
|
|
||||||
HTTP requests are defined in fenced code blocks annotated with `http`. The syntax is similar to the raw HTTP format:
|
HTTP requests are defined in fenced code blocks annotated with `http`. The syntax is similar to the raw HTTP format:
|
||||||
|
|
||||||
```
|
```http disable
|
||||||
<METHOD> <URL>
|
<METHOD> <URL>
|
||||||
<Header-Name>: <Header-Value>
|
<Header-Name>: <Header-Value>
|
||||||
...
|
...
|
||||||
@@ -176,6 +180,13 @@ Within your markdown document, the following variables are available in the Hand
|
|||||||
|
|
||||||
::raw-md[./examples/with-template.md]
|
::raw-md[./examples/with-template.md]
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Output</summary>
|
||||||
|
|
||||||
|
::raw-md[./examples/with-template.md]{render}
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
_(Note: `httpbin.org/post` wraps the JSON sent in a "json" field in its response. If your API returns the ID directly at the root of the JSON body, you'd use `{{responses.createItem.body.id}}` assuming the `createItem` request had the `json` option.)_
|
_(Note: `httpbin.org/post` wraps the JSON sent in a "json" field in its response. If your API returns the ID directly at the root of the JSON body, you'd use `{{responses.createItem.body.id}}` assuming the `createItem` request had the `json` option.)_
|
||||||
|
|
||||||
**2. Displaying a status code in markdown text:**
|
**2. Displaying a status code in markdown text:**
|
||||||
@@ -202,34 +213,18 @@ The requests from the embedded document are processed, and their `request` and `
|
|||||||
|
|
||||||
Assume `_shared_requests.md` contains:
|
Assume `_shared_requests.md` contains:
|
||||||
|
|
||||||
````markdown
|
::raw-md[./examples/_shared_requests.md]
|
||||||
```http id=sharedGetRequest
|
|
||||||
GET https://httpbin.org/get
|
|
||||||
```
|
|
||||||
````
|
|
||||||
|
|
||||||
Then, in `main.md`:
|
Then, in `main.md`:
|
||||||
|
|
||||||
````markdown
|
::raw-md[./examples/with-shared-requests.md]
|
||||||
# Main Document
|
|
||||||
|
|
||||||
Let's include some shared requests:
|
<details>
|
||||||
|
<summary>Output</summary>
|
||||||
|
|
||||||
::md[./_shared_requests.md]
|
::raw-md[./examples/with-shared-requests.md]{render}
|
||||||
|
|
||||||
The shared GET request returned: {{responses.sharedGetRequest.status}}
|
</details>
|
||||||
|
|
||||||
Now, a request specific to this document:
|
|
||||||
|
|
||||||
```http
|
|
||||||
POST https://httpbin.org/post
|
|
||||||
Content-Type: application/json
|
|
||||||
|
|
||||||
{"dataFromMain": "someValue", "sharedUrl": "{{requests.sharedGetRequest.url}}"}
|
|
||||||
```
|
|
||||||
|
|
||||||
::response
|
|
||||||
````
|
|
||||||
|
|
||||||
When `main.md` is processed, `_shared_requests.md` will be embedded, its `sharedGetRequest` will be executed, and its data will be available for templating.
|
When `main.md` is processed, `_shared_requests.md` will be embedded, its `sharedGetRequest` will be executed, and its data will be available for templating.
|
||||||
|
|
||||||
|
|||||||
3
docs/examples/_shared_requests.md
Normal file
3
docs/examples/_shared_requests.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
```http #sharedGetRequest
|
||||||
|
GET https://httpbin.org/get
|
||||||
|
```
|
||||||
18
docs/examples/with-shared-requests.md
Normal file
18
docs/examples/with-shared-requests.md
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# Main Document
|
||||||
|
|
||||||
|
Let's include some shared requests:
|
||||||
|
|
||||||
|
::md[./_shared_requests.md]
|
||||||
|
|
||||||
|
The shared GET request returned: {{response.statusText}}
|
||||||
|
|
||||||
|
Now, a request specific to this document:
|
||||||
|
|
||||||
|
```http
|
||||||
|
POST https://httpbin.org/post
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{"dataFromMain": "someValue", "sharedUrl": "{{requests.sharedGetRequest.url}}"}
|
||||||
|
```
|
||||||
|
|
||||||
|
::response
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
```http id=createItem json
|
```http #createItem,json
|
||||||
POST https://httpbin.org/post
|
POST https://httpbin.org/post
|
||||||
Content-Type: application/json
|
Content-Type: application/json
|
||||||
|
|
||||||
{"name": "My New Item"}
|
{"name": "My New Item"}
|
||||||
```
|
```
|
||||||
|
|
||||||
The new item ID is: {{responses.createItem.body.json.name}}
|
The new item ID is: {{response.body.json.name}}
|
||||||
|
|
||||||
Now, let's fetch the item using a (mocked) ID from the response:
|
Now, let's fetch the item using a (mocked) ID from the response:
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
"build": "pnpm run build:lib && pnpm run build:readme",
|
"build": "pnpm run build:lib && pnpm run build:readme",
|
||||||
"build:lib": "tsc --build",
|
"build:lib": "tsc --build",
|
||||||
"build:readme": "pnpm run cli build docs/README.md README.md",
|
"build:readme": "pnpm run cli build docs/README.md README.md",
|
||||||
|
"build:readme-html": "pnpm run cli build docs/README.md README.html -f html",
|
||||||
"dev:readme": "pnpm run cli dev docs/README.md --watch",
|
"dev:readme": "pnpm run cli dev docs/README.md --watch",
|
||||||
"test": "echo \"Error: no test specified\" && exit 1"
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
},
|
},
|
||||||
@@ -35,6 +36,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"blessed": "^0.1.81",
|
"blessed": "^0.1.81",
|
||||||
|
"chalk": "^5.4.1",
|
||||||
"commander": "^14.0.0",
|
"commander": "^14.0.0",
|
||||||
"dotenv": "^16.5.0",
|
"dotenv": "^16.5.0",
|
||||||
"eventemitter3": "^5.0.1",
|
"eventemitter3": "^5.0.1",
|
||||||
@@ -44,7 +46,9 @@
|
|||||||
"marked-terminal": "^7.3.0",
|
"marked-terminal": "^7.3.0",
|
||||||
"mdast-util-to-markdown": "^2.1.2",
|
"mdast-util-to-markdown": "^2.1.2",
|
||||||
"mdast-util-to-string": "^4.0.0",
|
"mdast-util-to-string": "^4.0.0",
|
||||||
|
"mdast-util-toc": "^7.1.0",
|
||||||
"rehype-stringify": "^10.0.1",
|
"rehype-stringify": "^10.0.1",
|
||||||
|
"remark-behead": "^3.1.0",
|
||||||
"remark-directive": "^4.0.0",
|
"remark-directive": "^4.0.0",
|
||||||
"remark-gfm": "^4.0.1",
|
"remark-gfm": "^4.0.1",
|
||||||
"remark-parse": "^11.0.0",
|
"remark-parse": "^11.0.0",
|
||||||
|
|||||||
128
pnpm-lock.yaml
generated
128
pnpm-lock.yaml
generated
@@ -11,6 +11,9 @@ importers:
|
|||||||
blessed:
|
blessed:
|
||||||
specifier: ^0.1.81
|
specifier: ^0.1.81
|
||||||
version: 0.1.81
|
version: 0.1.81
|
||||||
|
chalk:
|
||||||
|
specifier: ^5.4.1
|
||||||
|
version: 5.4.1
|
||||||
commander:
|
commander:
|
||||||
specifier: ^14.0.0
|
specifier: ^14.0.0
|
||||||
version: 14.0.0
|
version: 14.0.0
|
||||||
@@ -38,9 +41,15 @@ importers:
|
|||||||
mdast-util-to-string:
|
mdast-util-to-string:
|
||||||
specifier: ^4.0.0
|
specifier: ^4.0.0
|
||||||
version: 4.0.0
|
version: 4.0.0
|
||||||
|
mdast-util-toc:
|
||||||
|
specifier: ^7.1.0
|
||||||
|
version: 7.1.0
|
||||||
rehype-stringify:
|
rehype-stringify:
|
||||||
specifier: ^10.0.1
|
specifier: ^10.0.1
|
||||||
version: 10.0.1
|
version: 10.0.1
|
||||||
|
remark-behead:
|
||||||
|
specifier: ^3.1.0
|
||||||
|
version: 3.1.0
|
||||||
remark-directive:
|
remark-directive:
|
||||||
specifier: ^4.0.0
|
specifier: ^4.0.0
|
||||||
version: 4.0.0
|
version: 4.0.0
|
||||||
@@ -455,6 +464,9 @@ packages:
|
|||||||
'@types/terminal-kit@2.5.7':
|
'@types/terminal-kit@2.5.7':
|
||||||
resolution: {integrity: sha512-IpbCBFSb3OqCEZBZlk368tGftqss88eNQaJdD9msEShRbksEiVahEqroONi60ppUt9/arLM6IDrHMx9jpzzCOw==}
|
resolution: {integrity: sha512-IpbCBFSb3OqCEZBZlk368tGftqss88eNQaJdD9msEShRbksEiVahEqroONi60ppUt9/arLM6IDrHMx9jpzzCOw==}
|
||||||
|
|
||||||
|
'@types/ungap__structured-clone@1.2.0':
|
||||||
|
resolution: {integrity: sha512-ZoaihZNLeZSxESbk9PUAPZOlSpcKx81I1+4emtULDVmBLkYutTcMlCj2K9VNlf9EWODxdO6gkAqEaLorXwZQVA==}
|
||||||
|
|
||||||
'@types/unist@2.0.11':
|
'@types/unist@2.0.11':
|
||||||
resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==}
|
resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==}
|
||||||
|
|
||||||
@@ -740,6 +752,9 @@ packages:
|
|||||||
get-tsconfig@4.10.0:
|
get-tsconfig@4.10.0:
|
||||||
resolution: {integrity: sha512-kGzZ3LWWQcGIAmg6iWvXn0ei6WDtV26wzHRMwDSzmAbcXrTEXxHy6IehI6/4eT6VRKyMP1eF1VqwrVUmE/LR7A==}
|
resolution: {integrity: sha512-kGzZ3LWWQcGIAmg6iWvXn0ei6WDtV26wzHRMwDSzmAbcXrTEXxHy6IehI6/4eT6VRKyMP1eF1VqwrVUmE/LR7A==}
|
||||||
|
|
||||||
|
github-slugger@2.0.0:
|
||||||
|
resolution: {integrity: sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==}
|
||||||
|
|
||||||
glob-parent@5.1.2:
|
glob-parent@5.1.2:
|
||||||
resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
|
resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
|
||||||
engines: {node: '>= 6'}
|
engines: {node: '>= 6'}
|
||||||
@@ -890,6 +905,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-gUD/epcRms75Cw8RT1pUdHugZYM5ce64ucs2GEISABwkRsOQr0q2wm/MV2TKThycIe5e0ytRweW2RZxclogCdQ==}
|
resolution: {integrity: sha512-gUD/epcRms75Cw8RT1pUdHugZYM5ce64ucs2GEISABwkRsOQr0q2wm/MV2TKThycIe5e0ytRweW2RZxclogCdQ==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
lodash.iteratee@4.7.0:
|
||||||
|
resolution: {integrity: sha512-yv3cSQZmfpbIKo4Yo45B1taEvxjNvcpF1CEOc0Y6dEyvhPIfEJE3twDwPgWTPQubcSgXyBwBKG6wpQvWMDOf6Q==}
|
||||||
|
|
||||||
longest-streak@3.1.0:
|
longest-streak@3.1.0:
|
||||||
resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==}
|
resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==}
|
||||||
|
|
||||||
@@ -959,6 +977,9 @@ packages:
|
|||||||
mdast-util-to-string@4.0.0:
|
mdast-util-to-string@4.0.0:
|
||||||
resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==}
|
resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==}
|
||||||
|
|
||||||
|
mdast-util-toc@7.1.0:
|
||||||
|
resolution: {integrity: sha512-2TVKotOQzqdY7THOdn2gGzS9d1Sdd66bvxUyw3aNpWfcPXCLYSJCCgfPy30sEtuzkDraJgqF35dzgmz6xlvH/w==}
|
||||||
|
|
||||||
mem@8.1.1:
|
mem@8.1.1:
|
||||||
resolution: {integrity: sha512-qFCFUDs7U3b8mBDPyz5EToEKoAkgCzqquIgi9nkkR9bixxOVOre+09lbuH7+9Kn2NFpm56M3GUWVbU2hQgdACA==}
|
resolution: {integrity: sha512-qFCFUDs7U3b8mBDPyz5EToEKoAkgCzqquIgi9nkkR9bixxOVOre+09lbuH7+9Kn2NFpm56M3GUWVbU2hQgdACA==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
@@ -1227,6 +1248,10 @@ packages:
|
|||||||
rehype-stringify@10.0.1:
|
rehype-stringify@10.0.1:
|
||||||
resolution: {integrity: sha512-k9ecfXHmIPuFVI61B9DeLPN0qFHfawM6RsuX48hoqlaKSF61RskNjSm1lI8PhBEM0MRdLxVVm4WmTqJQccH9mA==}
|
resolution: {integrity: sha512-k9ecfXHmIPuFVI61B9DeLPN0qFHfawM6RsuX48hoqlaKSF61RskNjSm1lI8PhBEM0MRdLxVVm4WmTqJQccH9mA==}
|
||||||
|
|
||||||
|
remark-behead@3.1.0:
|
||||||
|
resolution: {integrity: sha512-rKns7st91lgppaD5YaH58O4ECFVXTVnkyYQBuCw4ISRE2TFK/iVySMaKbvV2pVbUVIjAaDciugrTI/tyuPOlWQ==}
|
||||||
|
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
||||||
|
|
||||||
remark-directive@4.0.0:
|
remark-directive@4.0.0:
|
||||||
resolution: {integrity: sha512-7sxn4RfF1o3izevPV1DheyGDD6X4c9hrGpfdUpm7uC++dqrnJxIZVkk7CoKqcLm0VUMAuOol7Mno3m6g8cfMuA==}
|
resolution: {integrity: sha512-7sxn4RfF1o3izevPV1DheyGDD6X4c9hrGpfdUpm7uC++dqrnJxIZVkk7CoKqcLm0VUMAuOol7Mno3m6g8cfMuA==}
|
||||||
|
|
||||||
@@ -1431,6 +1456,25 @@ packages:
|
|||||||
resolution: {integrity: sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==}
|
resolution: {integrity: sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
unist-util-find-all-after@4.0.1:
|
||||||
|
resolution: {integrity: sha512-AO8++e6HJfwNoTrqkV7xSeW65e6uSsLRQST/9LWi8FmFSz1gS7TBd+DkL/CYiElsSZIQgT4J5U54v5/kJX5Nqg==}
|
||||||
|
|
||||||
|
unist-util-find-all-before@4.0.1:
|
||||||
|
resolution: {integrity: sha512-xg4UHtZ6VbcjQbfDtmLZch6kQYQFF3nfaW05Ie3+t2UectzeqSx/iqLmh/wWogwU+YDWnD40PjZKK7ORmCma+g==}
|
||||||
|
|
||||||
|
unist-util-find-all-between@2.1.0:
|
||||||
|
resolution: {integrity: sha512-OCCUtDD8UHKeODw3TPXyFDxPCbpgBzbGTTaDpR68nvxkwiVcawBqMVrokfBMvUi7ij2F5q7S4s4Jq5dvkcBt+w==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
|
unist-util-find@1.0.4:
|
||||||
|
resolution: {integrity: sha512-T5vI7IkhroDj7KxAIy057VbIeGnCXfso4d4GoUsjbAmDLQUkzAeszlBtzx1+KHgdsYYBygaqUBvrbYCfePedZw==}
|
||||||
|
|
||||||
|
unist-util-is@4.1.0:
|
||||||
|
resolution: {integrity: sha512-ZOQSsnce92GrxSqlnEEseX0gi7GH9zTJZ0p9dtu87WRb/37mMPO2Ilx1s/t9vBHrFhbgweUwb+t7cIn5dxPhZg==}
|
||||||
|
|
||||||
|
unist-util-is@5.2.1:
|
||||||
|
resolution: {integrity: sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw==}
|
||||||
|
|
||||||
unist-util-is@6.0.0:
|
unist-util-is@6.0.0:
|
||||||
resolution: {integrity: sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==}
|
resolution: {integrity: sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==}
|
||||||
|
|
||||||
@@ -1440,9 +1484,21 @@ packages:
|
|||||||
unist-util-stringify-position@4.0.0:
|
unist-util-stringify-position@4.0.0:
|
||||||
resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==}
|
resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==}
|
||||||
|
|
||||||
|
unist-util-visit-parents@3.1.1:
|
||||||
|
resolution: {integrity: sha512-1KROIZWo6bcMrZEwiH2UrXDyalAa0uqzWCxCJj6lPOvTve2WkfgCytoDTPaMnodXh1WrXOq0haVYHj99ynJlsg==}
|
||||||
|
|
||||||
|
unist-util-visit-parents@5.1.3:
|
||||||
|
resolution: {integrity: sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg==}
|
||||||
|
|
||||||
unist-util-visit-parents@6.0.1:
|
unist-util-visit-parents@6.0.1:
|
||||||
resolution: {integrity: sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==}
|
resolution: {integrity: sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==}
|
||||||
|
|
||||||
|
unist-util-visit@2.0.3:
|
||||||
|
resolution: {integrity: sha512-iJ4/RczbJMkD0712mGktuGpm/U4By4FfDonL7N/9tATGIF4imikjOuagyMY53tnZq3NP6BcmlrHhEKAfGWjh7Q==}
|
||||||
|
|
||||||
|
unist-util-visit@4.1.2:
|
||||||
|
resolution: {integrity: sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg==}
|
||||||
|
|
||||||
unist-util-visit@5.0.0:
|
unist-util-visit@5.0.0:
|
||||||
resolution: {integrity: sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==}
|
resolution: {integrity: sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==}
|
||||||
|
|
||||||
@@ -1895,6 +1951,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@types/nextgen-events': 1.1.4
|
'@types/nextgen-events': 1.1.4
|
||||||
|
|
||||||
|
'@types/ungap__structured-clone@1.2.0': {}
|
||||||
|
|
||||||
'@types/unist@2.0.11': {}
|
'@types/unist@2.0.11': {}
|
||||||
|
|
||||||
'@types/unist@3.0.3': {}
|
'@types/unist@3.0.3': {}
|
||||||
@@ -2182,6 +2240,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
resolve-pkg-maps: 1.0.0
|
resolve-pkg-maps: 1.0.0
|
||||||
|
|
||||||
|
github-slugger@2.0.0: {}
|
||||||
|
|
||||||
glob-parent@5.1.2:
|
glob-parent@5.1.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
is-glob: 4.0.3
|
is-glob: 4.0.3
|
||||||
@@ -2313,6 +2373,8 @@ snapshots:
|
|||||||
strip-bom: 4.0.0
|
strip-bom: 4.0.0
|
||||||
type-fest: 0.6.0
|
type-fest: 0.6.0
|
||||||
|
|
||||||
|
lodash.iteratee@4.7.0: {}
|
||||||
|
|
||||||
longest-streak@3.1.0: {}
|
longest-streak@3.1.0: {}
|
||||||
|
|
||||||
map-age-cleaner@0.1.3:
|
map-age-cleaner@0.1.3:
|
||||||
@@ -2466,6 +2528,16 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@types/mdast': 4.0.4
|
'@types/mdast': 4.0.4
|
||||||
|
|
||||||
|
mdast-util-toc@7.1.0:
|
||||||
|
dependencies:
|
||||||
|
'@types/mdast': 4.0.4
|
||||||
|
'@types/ungap__structured-clone': 1.2.0
|
||||||
|
'@ungap/structured-clone': 1.3.0
|
||||||
|
github-slugger: 2.0.0
|
||||||
|
mdast-util-to-string: 4.0.0
|
||||||
|
unist-util-is: 6.0.0
|
||||||
|
unist-util-visit: 5.0.0
|
||||||
|
|
||||||
mem@8.1.1:
|
mem@8.1.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
map-age-cleaner: 0.1.3
|
map-age-cleaner: 0.1.3
|
||||||
@@ -2833,6 +2905,14 @@ snapshots:
|
|||||||
hast-util-to-html: 9.0.5
|
hast-util-to-html: 9.0.5
|
||||||
unified: 11.0.5
|
unified: 11.0.5
|
||||||
|
|
||||||
|
remark-behead@3.1.0:
|
||||||
|
dependencies:
|
||||||
|
unist-util-find: 1.0.4
|
||||||
|
unist-util-find-all-after: 4.0.1
|
||||||
|
unist-util-find-all-before: 4.0.1
|
||||||
|
unist-util-find-all-between: 2.1.0
|
||||||
|
unist-util-visit: 4.1.2
|
||||||
|
|
||||||
remark-directive@4.0.0:
|
remark-directive@4.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/mdast': 4.0.4
|
'@types/mdast': 4.0.4
|
||||||
@@ -3051,6 +3131,32 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
crypto-random-string: 2.0.0
|
crypto-random-string: 2.0.0
|
||||||
|
|
||||||
|
unist-util-find-all-after@4.0.1:
|
||||||
|
dependencies:
|
||||||
|
'@types/unist': 2.0.11
|
||||||
|
unist-util-is: 5.2.1
|
||||||
|
|
||||||
|
unist-util-find-all-before@4.0.1:
|
||||||
|
dependencies:
|
||||||
|
'@types/unist': 2.0.11
|
||||||
|
unist-util-is: 5.2.1
|
||||||
|
|
||||||
|
unist-util-find-all-between@2.1.0:
|
||||||
|
dependencies:
|
||||||
|
unist-util-find: 1.0.4
|
||||||
|
unist-util-is: 4.1.0
|
||||||
|
|
||||||
|
unist-util-find@1.0.4:
|
||||||
|
dependencies:
|
||||||
|
lodash.iteratee: 4.7.0
|
||||||
|
unist-util-visit: 2.0.3
|
||||||
|
|
||||||
|
unist-util-is@4.1.0: {}
|
||||||
|
|
||||||
|
unist-util-is@5.2.1:
|
||||||
|
dependencies:
|
||||||
|
'@types/unist': 2.0.11
|
||||||
|
|
||||||
unist-util-is@6.0.0:
|
unist-util-is@6.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/unist': 3.0.3
|
'@types/unist': 3.0.3
|
||||||
@@ -3063,11 +3169,33 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@types/unist': 3.0.3
|
'@types/unist': 3.0.3
|
||||||
|
|
||||||
|
unist-util-visit-parents@3.1.1:
|
||||||
|
dependencies:
|
||||||
|
'@types/unist': 2.0.11
|
||||||
|
unist-util-is: 4.1.0
|
||||||
|
|
||||||
|
unist-util-visit-parents@5.1.3:
|
||||||
|
dependencies:
|
||||||
|
'@types/unist': 2.0.11
|
||||||
|
unist-util-is: 5.2.1
|
||||||
|
|
||||||
unist-util-visit-parents@6.0.1:
|
unist-util-visit-parents@6.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/unist': 3.0.3
|
'@types/unist': 3.0.3
|
||||||
unist-util-is: 6.0.0
|
unist-util-is: 6.0.0
|
||||||
|
|
||||||
|
unist-util-visit@2.0.3:
|
||||||
|
dependencies:
|
||||||
|
'@types/unist': 2.0.11
|
||||||
|
unist-util-is: 4.1.0
|
||||||
|
unist-util-visit-parents: 3.1.1
|
||||||
|
|
||||||
|
unist-util-visit@4.1.2:
|
||||||
|
dependencies:
|
||||||
|
'@types/unist': 2.0.11
|
||||||
|
unist-util-is: 5.2.1
|
||||||
|
unist-util-visit-parents: 5.1.3
|
||||||
|
|
||||||
unist-util-visit@5.0.0:
|
unist-util-visit@5.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/unist': 3.0.3
|
'@types/unist': 3.0.3
|
||||||
|
|||||||
@@ -1,14 +1,15 @@
|
|||||||
import { program } from 'commander';
|
import { program } from 'commander';
|
||||||
import { resolve } from 'node:path';
|
import { resolve } from 'node:path';
|
||||||
import { marked } from 'marked';
|
import { Marked } from 'marked';
|
||||||
import { markedTerminal } from 'marked-terminal';
|
import { markedTerminal } from 'marked-terminal';
|
||||||
import { execute } from '../execution/execution.js';
|
import { execute } from '../execution/execution.js';
|
||||||
import { Context } from '../context/context.js';
|
import { Context } from '../context/context.js';
|
||||||
import { writeFile } from 'node:fs/promises';
|
import { writeFile } from 'node:fs/promises';
|
||||||
import { Watcher } from '../watcher/watcher.js';
|
import { Watcher } from '../watcher/watcher.js';
|
||||||
|
import { UI } from './ui/ui.js';
|
||||||
|
import { wrapBody } from '../theme/theme.html.js';
|
||||||
|
|
||||||
|
|
||||||
marked.use(markedTerminal() as any);
|
|
||||||
|
|
||||||
program
|
program
|
||||||
.command('dev')
|
.command('dev')
|
||||||
@@ -17,11 +18,15 @@ program
|
|||||||
.option('-w, --watch', 'watch for changes')
|
.option('-w, --watch', 'watch for changes')
|
||||||
.option('-i, --input <input...>', 'input variables (-i foo=bar -i baz=qux)')
|
.option('-i, --input <input...>', 'input variables (-i foo=bar -i baz=qux)')
|
||||||
.action(async (name, options) => {
|
.action(async (name, options) => {
|
||||||
|
const marked = new Marked();
|
||||||
|
marked.use(markedTerminal() as any);
|
||||||
const {
|
const {
|
||||||
watch = false,
|
watch = false,
|
||||||
input: i = [],
|
input: i = [],
|
||||||
} = options;
|
} = options;
|
||||||
|
|
||||||
|
const ui = new UI();
|
||||||
|
|
||||||
const input = Object.fromEntries(
|
const input = Object.fromEntries(
|
||||||
i.map((item: string) => {
|
i.map((item: string) => {
|
||||||
const [key, value] = item.split('=');
|
const [key, value] = item.split('=');
|
||||||
@@ -39,7 +44,7 @@ program
|
|||||||
});
|
});
|
||||||
|
|
||||||
const markdown = await marked.parse(result.markdown);
|
const markdown = await marked.parse(result.markdown);
|
||||||
console.log(markdown);
|
ui.content = markdown;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...result,
|
...result,
|
||||||
@@ -49,6 +54,10 @@ program
|
|||||||
|
|
||||||
const result = await build();
|
const result = await build();
|
||||||
|
|
||||||
|
ui.screen.key(['r'], () => {
|
||||||
|
build();
|
||||||
|
});
|
||||||
|
|
||||||
if (watch) {
|
if (watch) {
|
||||||
const watcher = new Watcher();
|
const watcher = new Watcher();
|
||||||
watcher.watchFiles(Array.from(result.context.files));
|
watcher.watchFiles(Array.from(result.context.files));
|
||||||
@@ -66,12 +75,14 @@ program
|
|||||||
.argument('<name>', 'http.md file name')
|
.argument('<name>', 'http.md file name')
|
||||||
.argument('<output>', 'output file name')
|
.argument('<output>', 'output file name')
|
||||||
.description('Run a http.md document')
|
.description('Run a http.md document')
|
||||||
|
.option('-f, --format <format>', 'output format (html, markdown)')
|
||||||
.option('-w, --watch', 'watch for changes')
|
.option('-w, --watch', 'watch for changes')
|
||||||
.option('-i, --input <input...>', 'input variables (-i foo=bar -i baz=qux)')
|
.option('-i, --input <input...>', 'input variables (-i foo=bar -i baz=qux)')
|
||||||
.action(async (name, output, options) => {
|
.action(async (name, output, options) => {
|
||||||
const {
|
const {
|
||||||
watch = false,
|
watch = false,
|
||||||
input: i = [],
|
input: i = [],
|
||||||
|
format = 'markdown',
|
||||||
} = options;
|
} = options;
|
||||||
|
|
||||||
|
|
||||||
@@ -91,7 +102,15 @@ program
|
|||||||
context,
|
context,
|
||||||
});
|
});
|
||||||
|
|
||||||
await writeFile(output, result.markdown);
|
if (format === 'html') {
|
||||||
|
const marked = new Marked();
|
||||||
|
const html = await marked.parse(result.markdown);
|
||||||
|
await writeFile(output, wrapBody(html));
|
||||||
|
} else if (format === 'markdown') {
|
||||||
|
await writeFile(output, result.markdown);
|
||||||
|
} else {
|
||||||
|
throw new Error('Invalid format');
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
...result,
|
...result,
|
||||||
context,
|
context,
|
||||||
|
|||||||
70
src/cli/ui/ui.ts
Normal file
70
src/cli/ui/ui.ts
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
import blessed from 'blessed';
|
||||||
|
import chalk from 'chalk';
|
||||||
|
|
||||||
|
class UI {
|
||||||
|
#box: blessed.Widgets.BoxElement;
|
||||||
|
#screen: blessed.Widgets.Screen;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
const screen = blessed.screen({
|
||||||
|
smartCSR: true,
|
||||||
|
title: 'Markdown Viewer'
|
||||||
|
});
|
||||||
|
const scrollableBox = blessed.box({ // Or blessed.scrollablebox
|
||||||
|
parent: screen,
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
content: '',
|
||||||
|
scrollable: true,
|
||||||
|
alwaysScroll: true,
|
||||||
|
keys: true,
|
||||||
|
vi: true, // vi-like keybindings
|
||||||
|
mouse: true,
|
||||||
|
scrollbar: {
|
||||||
|
ch: ' ',
|
||||||
|
track: {
|
||||||
|
bg: 'cyan'
|
||||||
|
},
|
||||||
|
style: {
|
||||||
|
inverse: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
style: {
|
||||||
|
fg: 'white',
|
||||||
|
bg: 'black'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.#box = scrollableBox;
|
||||||
|
this.#screen = screen;
|
||||||
|
|
||||||
|
screen.key(['escape', 'q', 'C-c'], () => {
|
||||||
|
return process.exit(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
scrollableBox.focus();
|
||||||
|
screen.render();
|
||||||
|
}
|
||||||
|
|
||||||
|
public get screen() {
|
||||||
|
return this.#screen;
|
||||||
|
}
|
||||||
|
|
||||||
|
public set content(content: string) {
|
||||||
|
const originalLines = content.split('\n');
|
||||||
|
const maxLineNoDigits = String(originalLines.length).length; // For padding
|
||||||
|
|
||||||
|
const linesWithNumbers = originalLines.map((line, index) => {
|
||||||
|
const lineNumber = String(index + 1).padStart(maxLineNoDigits, ' ');
|
||||||
|
const styledLineNumber = chalk.dim.yellow(`${lineNumber} | `);
|
||||||
|
return `${styledLineNumber}${line}`;
|
||||||
|
});
|
||||||
|
|
||||||
|
const contentWithLineNumbers = linesWithNumbers.join('\n');
|
||||||
|
this.#box.setContent(contentWithLineNumbers);
|
||||||
|
this.#screen.render();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { UI };
|
||||||
@@ -10,6 +10,7 @@ type Response = {
|
|||||||
statusText: string;
|
statusText: string;
|
||||||
headers: Record<string, string>;
|
headers: Record<string, string>;
|
||||||
body?: string;
|
body?: string;
|
||||||
|
rawBody?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type AddRequestOptios = {
|
type AddRequestOptios = {
|
||||||
|
|||||||
@@ -5,18 +5,12 @@ import remarkParse from 'remark-parse'
|
|||||||
import remarkRehype from 'remark-rehype'
|
import remarkRehype from 'remark-rehype'
|
||||||
import remarkDirective from 'remark-directive'
|
import remarkDirective from 'remark-directive'
|
||||||
import remarkStringify from 'remark-stringify'
|
import remarkStringify from 'remark-stringify'
|
||||||
|
import behead from 'remark-behead';
|
||||||
import { unified } from 'unified'
|
import { unified } from 'unified'
|
||||||
import { visit } from 'unist-util-visit'
|
import { visit } from 'unist-util-visit'
|
||||||
|
|
||||||
import { Context } from "../context/context.js";
|
import { Context } from "../context/context.js";
|
||||||
import { handlers } from './handlers/handlers.js';
|
import { handlers, postHandlers } from './handlers/handlers.js';
|
||||||
|
|
||||||
const parser = unified()
|
|
||||||
.use(remarkParse)
|
|
||||||
.use(remarkGfm)
|
|
||||||
.use(remarkDirective)
|
|
||||||
.use(remarkStringify)
|
|
||||||
.use(remarkRehype);
|
|
||||||
|
|
||||||
type BaseNode = {
|
type BaseNode = {
|
||||||
type: string;
|
type: string;
|
||||||
@@ -53,6 +47,7 @@ type ExecutionHandler = (options: {
|
|||||||
|
|
||||||
type ExexutionExecuteOptions = {
|
type ExexutionExecuteOptions = {
|
||||||
context: Context;
|
context: Context;
|
||||||
|
behead?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const execute = async (file: string, options: ExexutionExecuteOptions) => {
|
const execute = async (file: string, options: ExexutionExecuteOptions) => {
|
||||||
@@ -61,6 +56,16 @@ const execute = async (file: string, options: ExexutionExecuteOptions) => {
|
|||||||
const content = await readFile(file, 'utf-8');
|
const content = await readFile(file, 'utf-8');
|
||||||
const steps: Set<ExecutionStep> = new Set();
|
const steps: Set<ExecutionStep> = new Set();
|
||||||
|
|
||||||
|
|
||||||
|
const parser = unified()
|
||||||
|
.use(remarkParse)
|
||||||
|
.use(remarkGfm)
|
||||||
|
.use(remarkDirective)
|
||||||
|
.use(remarkStringify)
|
||||||
|
.use(remarkRehype)
|
||||||
|
.use(behead, {
|
||||||
|
depth: options.behead,
|
||||||
|
});
|
||||||
const root = parser.parse(content);
|
const root = parser.parse(content);
|
||||||
|
|
||||||
visit(root, (node, index, parent) => {
|
visit(root, (node, index, parent) => {
|
||||||
@@ -75,6 +80,18 @@ const execute = async (file: string, options: ExexutionExecuteOptions) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
visit(root, (node, index, parent) => {
|
||||||
|
for (const handler of postHandlers) {
|
||||||
|
handler({
|
||||||
|
addStep: (step) => steps.add(step),
|
||||||
|
node: node as BaseNode,
|
||||||
|
root,
|
||||||
|
parent: parent as BaseNode | undefined,
|
||||||
|
index,
|
||||||
|
file,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
for (const step of steps) {
|
for (const step of steps) {
|
||||||
const { node, action } = step;
|
const { node, action } = step;
|
||||||
|
|||||||
@@ -16,6 +16,11 @@ const httpHandler: ExecutionHandler = ({
|
|||||||
return [key.trim(), value?.trim() || true];
|
return [key.trim(), value?.trim() || true];
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
let id = options.id?.toString();
|
||||||
|
const idPart = optionParts.find((option) => option.startsWith('#'));
|
||||||
|
if (idPart) {
|
||||||
|
id = idPart.slice(1);
|
||||||
|
}
|
||||||
|
|
||||||
addStep({
|
addStep({
|
||||||
type: 'http',
|
type: 'http',
|
||||||
@@ -28,7 +33,8 @@ const httpHandler: ExecutionHandler = ({
|
|||||||
const content = template(context);
|
const content = template(context);
|
||||||
const [head, body] = content.split('\n\n');
|
const [head, body] = content.split('\n\n');
|
||||||
const [top, ...headerItems] = head.split('\n');
|
const [top, ...headerItems] = head.split('\n');
|
||||||
const [method, url] = top.split(' ');
|
const [method, ...urlParts] = top.split(' ');
|
||||||
|
const url = urlParts.join(' ').trim();
|
||||||
|
|
||||||
const headers = Object.fromEntries(
|
const headers = Object.fromEntries(
|
||||||
headerItems.map((header) => {
|
headerItems.map((header) => {
|
||||||
@@ -53,7 +59,8 @@ const httpHandler: ExecutionHandler = ({
|
|||||||
body
|
body
|
||||||
});
|
});
|
||||||
|
|
||||||
let responseText = await response.text();
|
const rawBody = await response.text();
|
||||||
|
let responseText = rawBody;
|
||||||
if (options.json) {
|
if (options.json) {
|
||||||
try {
|
try {
|
||||||
responseText = JSON.parse(responseText);
|
responseText = JSON.parse(responseText);
|
||||||
@@ -66,7 +73,7 @@ const httpHandler: ExecutionHandler = ({
|
|||||||
node.meta = undefined;
|
node.meta = undefined;
|
||||||
|
|
||||||
context.addRequest({
|
context.addRequest({
|
||||||
id: options.id?.toString(),
|
id,
|
||||||
request: {
|
request: {
|
||||||
method,
|
method,
|
||||||
url,
|
url,
|
||||||
@@ -78,6 +85,7 @@ const httpHandler: ExecutionHandler = ({
|
|||||||
statusText: response.statusText,
|
statusText: response.statusText,
|
||||||
headers: Object.fromEntries(response.headers.entries()),
|
headers: Object.fromEntries(response.headers.entries()),
|
||||||
body: responseText,
|
body: responseText,
|
||||||
|
rawBody: rawBody,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ const fileHandler: ExecutionHandler = ({
|
|||||||
}
|
}
|
||||||
const { root: newRoot } = await execute(filePath, {
|
const { root: newRoot } = await execute(filePath, {
|
||||||
context,
|
context,
|
||||||
|
behead: node.attributes?.behead ? parseInt(node.attributes.behead) : undefined,
|
||||||
});
|
});
|
||||||
if (!parent) {
|
if (!parent) {
|
||||||
throw new Error('Parent node is required');
|
throw new Error('Parent node is required');
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ const responseHandler: ExecutionHandler = ({
|
|||||||
|
|
||||||
const codeNode = {
|
const codeNode = {
|
||||||
type: 'code',
|
type: 'code',
|
||||||
|
lang: 'http',
|
||||||
value: responseContent,
|
value: responseContent,
|
||||||
};
|
};
|
||||||
if (!parent || !('children' in parent) || index === undefined) {
|
if (!parent || !('children' in parent) || index === undefined) {
|
||||||
|
|||||||
29
src/execution/handlers/handlers.toc.ts
Normal file
29
src/execution/handlers/handlers.toc.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { toc } from 'mdast-util-toc';
|
||||||
|
import { type ExecutionHandler } from '../execution.js';
|
||||||
|
|
||||||
|
const tocHandler: ExecutionHandler = ({
|
||||||
|
addStep,
|
||||||
|
node,
|
||||||
|
root,
|
||||||
|
parent,
|
||||||
|
index,
|
||||||
|
}) => {
|
||||||
|
if (node.type === 'leafDirective' && node.name === 'toc') {
|
||||||
|
addStep({
|
||||||
|
type: 'toc',
|
||||||
|
node,
|
||||||
|
action: async () => {
|
||||||
|
const result = toc(root, {
|
||||||
|
tight: true,
|
||||||
|
minDepth: 2,
|
||||||
|
})
|
||||||
|
if (!parent || !parent.children || index === undefined) {
|
||||||
|
throw new Error('Parent node is not valid');
|
||||||
|
}
|
||||||
|
parent.children.splice(index, 1, result.map as any);
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { tocHandler };
|
||||||
@@ -6,6 +6,7 @@ import { rawMdHandler } from "./handlers.raw-md.js";
|
|||||||
import { responseHandler } from "./handlers.response.js";
|
import { responseHandler } from "./handlers.response.js";
|
||||||
import { textHandler } from "./handlers.text.js";
|
import { textHandler } from "./handlers.text.js";
|
||||||
import { codeHandler } from "./handlers.code.js";
|
import { codeHandler } from "./handlers.code.js";
|
||||||
|
import { tocHandler } from "./handlers.toc.js";
|
||||||
|
|
||||||
const handlers = [
|
const handlers = [
|
||||||
fileHandler,
|
fileHandler,
|
||||||
@@ -17,4 +18,8 @@ const handlers = [
|
|||||||
codeHandler,
|
codeHandler,
|
||||||
] satisfies ExecutionHandler[];
|
] satisfies ExecutionHandler[];
|
||||||
|
|
||||||
export { handlers };
|
const postHandlers = [
|
||||||
|
tocHandler,
|
||||||
|
] satisfies ExecutionHandler[];
|
||||||
|
|
||||||
|
export { handlers, postHandlers };
|
||||||
|
|||||||
29
src/theme/theme.html.ts
Normal file
29
src/theme/theme.html.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
const wrapBody = (body: string) => {
|
||||||
|
return `<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/github-markdown-css/5.8.1/github-markdown.min.css" />
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Document</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.markdown-body {
|
||||||
|
max-width: 800px;
|
||||||
|
padding: 20px;
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<article class="markdown-body">
|
||||||
|
${body}
|
||||||
|
</article>
|
||||||
|
</body>
|
||||||
|
</html>`;
|
||||||
|
};
|
||||||
|
|
||||||
|
export { wrapBody };
|
||||||
Reference in New Issue
Block a user