1 Commits

Author SHA1 Message Date
Morten Olsen
9e79f9e958 commit on publish 2025-05-18 20:52:03 +02:00
20 changed files with 79 additions and 480 deletions

View File

@@ -20,7 +20,7 @@ env:
IMAGE_NAME: ${{ github.repository }}
permissions:
contents: read
contents: write
packages: read
jobs:
@@ -92,7 +92,7 @@ jobs:
release:
permissions:
contents: write
contents: read
packages: write
attestations: write
id-token: write
@@ -132,11 +132,6 @@ jobs:
git config user.name "Github Actions Bot"
git config user.email "<>"
node ./scripts/set-version.mjs $(git describe --tag --abbrev=0)
pnpm publish --no-git-checks --access public
pnpm publish --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
- uses: stefanzweifel/git-auto-commit-action@v5
with:
commit_message: "docs: generated README"
file_pattern: "*.md"

1
.gitignore vendored
View File

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

View File

@@ -26,11 +26,9 @@ It allows developers to create API documentation that is always accurate and up-
Install `http.md` globally using npm:
```shell
npm i -g http.md
npm i -g @morten-olsen/httpmd
```
Or use `npx http.md` instead of `http.md`
## Getting Started
### Your First Request
@@ -64,13 +62,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
http.md dev example.md
httpmd dev example.md
```
With watch mode:
```shell
http.md dev --watch example.md
httpmd dev --watch example.md
```
This command will process `example.md`, execute the HTTP requests, and print the resulting markdown (with responses filled in) to the terminal. With `--watch`, any changes to `example.md` will trigger a re-run.
@@ -79,13 +77,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
http.md build example.md output.md
httpmd build example.md output.md
```
With watch mode:
```shell
http.md build --watch example.md output.md
httpmd build --watch example.md output.md
```
This creates `output.md`, which is a static snapshot of `example.md` after all requests have been executed and templating applied. This file is suitable for version control, sharing, or integration with static site generators.
@@ -111,9 +109,9 @@ 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: Sun, 18 May 2025 18:31:46 GMT
server: gunicorn/19.9.0
{
@@ -130,12 +128,11 @@ 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-682a2792-7df702ce77a3b3696937eaeb"
},
"json": {
"greeting": "Hello, http.md!"
},
"origin": "23.96.180.7",
"url": "https://httpbin.org/post"
}
@@ -256,21 +253,21 @@ 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 `http.md dev -i userId=123 -i apiKey=secret myfile.md`, you can use `{{input.userId}}` and `{{input.apiKey}}`.
- Example: If you run `httpmd dev -i userId=123 -i apiKey=secret myfile.md`, you can use `{{input.userId}}` and `{{input.apiKey}}`.
### Templating Examples
**1. Using a value from a previous response in a new request:**
````markdown
```http #createItem,json
```http id=createItem json
POST https://httpbin.org/post
Content-Type: application/json
{"name": "My New Item"}
```
The new item ID is: {{response.body.json.name}}
The new item ID is: {{responses.createItem.body.json.name}}
Now, let's fetch the item using a (mocked) ID from the response:
@@ -281,60 +278,6 @@ GET https://httpbin.org/anything/{{responses.createItem.body.json.name}}
::response{#fetchItem}
````
<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:**
@@ -401,7 +344,7 @@ You can pass external data into your `http.md` documents using the `-i` (or `--i
**CLI Command:**
```shell
http.md build mydoc.md output.md -i baseUrl=https://api.production.example.com -i apiKey=YOUR_SECRET_KEY
httpmd build mydoc.md output.md -i baseUrl=https://api.production.example.com -i apiKey=YOUR_SECRET_KEY
```
**Markdown Usage (`mydoc.md`):**
@@ -510,9 +453,9 @@ The `::input` directive is used to declare expected input variables
## Command-Line Interface (CLI)
The `http.md` tool provides the following commands:
The `httpmd` tool provides the following commands:
### `http.md dev <source_file.md>`
### `httpmd dev <source_file.md>`
Processes the `<source_file.md>`, executes all HTTP requests, resolves templates, and prints the resulting markdown to the **terminal (stdout)**.
@@ -524,10 +467,10 @@ Processes the `<source_file.md>`, executes all HTTP requests, resolves templates
**Example:**
```shell
http.md dev api_tests.md --watch -i host=localhost:3000
httpmd dev api_tests.md --watch -i host=localhost:3000
```
### `http.md build <source_file.md> <output_file.md>`
### `httpmd build <source_file.md> <output_file.md>`
Processes the `<source_file.md>`, executes all HTTP requests, resolves templates, and saves the resulting markdown to `<output_file.md>`.
@@ -539,5 +482,5 @@ Processes the `<source_file.md>`, executes all HTTP requests, resolves templates
**Example:**
```shell
http.md build official_api_docs.md public/api_docs_v1.md -i version=v1.0
httpmd build official_api_docs.md public/api_docs_v1.md -i version=v1.0
```

View File

@@ -21,24 +21,12 @@ It allows developers to create API documentation that is always accurate and up-
- **Tutorials & Guides:** Build step-by-step guides where each HTTP interaction is shown with its real output.
- **Rapid Prototyping:** Quickly experiment with APIs and document your findings.
## Roadmap
- **Programmatic API** Use `http.md` inside existing scripts and pipelines
- **Environment Varaiables** Support using the runners environment variables in templates
- **JavaScript script support** Add JavaScript code blocks with execution, which will allow more advanced use-cases
- **Asserts** Add the ability to make HTTP assertions to use the document for testing
- **Templates** Write re-usable templates which can be used in documents
## Content
::toc
## Installation
Install `http.md` globally using npm:
```shell
npm i -g @morten-olsen/http.md
npm i -g @morten-olsen/httpmd
```
## Getting Started
@@ -59,13 +47,13 @@ You have two primary ways to render your `http.md` file:
For a development server that outputs to your terminal and watches for changes:
```shell
http.md dev example.md
httpmd dev example.md
```
With watch mode:
```shell
http.md dev --watch example.md
httpmd dev --watch example.md
```
This command will process `example.md`, execute the HTTP requests, and print the resulting markdown (with responses filled in) to the terminal. With `--watch`, any changes to `example.md` will trigger a re-run.
@@ -74,13 +62,13 @@ You have two primary ways to render your `http.md` file:
To generate a new markdown file with the responses and templated values rendered:
```shell
http.md build example.md output.md
httpmd build example.md output.md
```
With watch mode:
```shell
http.md build --watch example.md output.md
httpmd build --watch example.md output.md
```
This creates `output.md`, which is a static snapshot of `example.md` after all requests have been executed and templating applied. This file is suitable for version control, sharing, or integration with static site generators.
@@ -140,17 +128,10 @@ You can assign a unique ID to an `http` request block. This allows you to:
1. Reference its specific response in a `::response` directive.
2. Access its request and response data in [Templating](https://www.google.com/search?q=%23templating-with-handlebars) via the `requests` and `responses` dictionaries.
To add an ID, include `#yourUniqueId` or `id=yourUniqueId` in the `http` block's info string:
To add an ID, include `id=yourUniqueId` in the `http` block's info string:
::raw-md[./examples/with-multiple-requests.md]
<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.
@@ -187,7 +168,7 @@ Within your markdown document, the following variables are available in the Hand
- **`input`** (Object): A dictionary of variables passed to `http.md` via the command line using the `-i` or `--input` flag.
- Example: If you run `http.md dev -i userId=123 -i apiKey=secret myfile.md`, you can use `{{input.userId}}` and `{{input.apiKey}}`.
- Example: If you run `httpmd dev -i userId=123 -i apiKey=secret myfile.md`, you can use `{{input.userId}}` and `{{input.apiKey}}`.
### Templating Examples
@@ -195,13 +176,6 @@ Within your markdown document, the following variables are available in the Hand
::raw-md[./examples/with-template.md]
<details>
<summary>Output</summary>
::raw-md[./examples/with-template.md]{render}
</details>
_(Note: `httpbin.org/post` wraps the JSON sent in a "json" field in its response. If your API returns the ID directly at the root of the JSON body, you'd use `{{responses.createItem.body.id}}` assuming the `createItem` request had the `json` option.)_
**2. Displaying a status code in markdown text:**
@@ -228,18 +202,34 @@ The requests from the embedded document are processed, and their `request` and `
Assume `_shared_requests.md` contains:
::raw-md[./examples/_shared_requests.md]
````markdown
```http id=sharedGetRequest
GET https://httpbin.org/get
```
````
Then, in `main.md`:
::raw-md[./examples/with-shared-requests.md]
````markdown
# Main Document
<details>
<summary>Output</summary>
Let's include some shared requests:
::raw-md[./examples/with-shared-requests.md]{render}
::md[./_shared_requests.md]
</details>
The shared GET request returned: {{responses.sharedGetRequest.status}}
Now, a request specific to this document:
```http
POST https://httpbin.org/post
Content-Type: application/json
{"dataFromMain": "someValue", "sharedUrl": "{{requests.sharedGetRequest.url}}"}
```
::response
````
When `main.md` is processed, `_shared_requests.md` will be embedded, its `sharedGetRequest` will be executed, and its data will be available for templating.
@@ -252,7 +242,7 @@ You can pass external data into your `http.md` documents using the `-i` (or `--i
**CLI Command:**
```shell
http.md build mydoc.md output.md -i baseUrl=https://api.production.example.com -i apiKey=YOUR_SECRET_KEY
httpmd build mydoc.md output.md -i baseUrl=https://api.production.example.com -i apiKey=YOUR_SECRET_KEY
```
**Markdown Usage (`mydoc.md`):**
@@ -361,9 +351,9 @@ The `::input` directive is used to declare expected input variables
## Command-Line Interface (CLI)
The `http.md` tool provides the following commands:
The `httpmd` tool provides the following commands:
### `http.md dev <source_file.md>`
### `httpmd dev <source_file.md>`
Processes the `<source_file.md>`, executes all HTTP requests, resolves templates, and prints the resulting markdown to the **terminal (stdout)**.
@@ -375,10 +365,10 @@ Processes the `<source_file.md>`, executes all HTTP requests, resolves templates
**Example:**
```shell
http.md dev api_tests.md --watch -i host=localhost:3000
httpmd dev api_tests.md --watch -i host=localhost:3000
```
### `http.md build <source_file.md> <output_file.md>`
### `httpmd build <source_file.md> <output_file.md>`
Processes the `<source_file.md>`, executes all HTTP requests, resolves templates, and saves the resulting markdown to `<output_file.md>`.
@@ -390,5 +380,5 @@ Processes the `<source_file.md>`, executes all HTTP requests, resolves templates
**Example:**
```shell
http.md build official_api_docs.md public/api_docs_v1.md -i version=v1.0
httpmd build official_api_docs.md public/api_docs_v1.md -i version=v1.0
```

View File

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

View File

@@ -2,16 +2,16 @@
First, create a resource:
```http #createUser,yaml,json
```http id=createUser
POST https://httpbin.org/post
Content-Type: application/json
username: alpha
{"username": "alpha"}
```
Then, fetch a different resource:
```http #getItem
```http id=getItem
GET https://httpbin.org/get?item=123
```

View File

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

View File

@@ -1,11 +1,11 @@
```http #createItem,json
```http id=createItem json
POST https://httpbin.org/post
Content-Type: application/json
{"name": "My New Item"}
```
The new item ID is: {{response.body.json.name}}
The new item ID is: {{responses.createItem.body.json.name}}
Now, let's fetch the item using a (mocked) ID from the response:

View File

@@ -1,11 +1,11 @@
{
"name": "@morten-olsen/http.md",
"name": "@morten-olsen/httpmd",
"version": "1.0.0",
"description": "",
"main": "dist/exports.js",
"type": "module",
"bin": {
"http.md": "./bin/cli.mjs"
"httpmd": "./bin/cli.mjs"
},
"files": [
"dist",
@@ -17,7 +17,6 @@
"build": "pnpm run build:lib && pnpm run build:readme",
"build:lib": "tsc --build",
"build:readme": "pnpm run cli build docs/README.md README.md",
"build:readme-html": "pnpm run cli build docs/README.md README.html -f html",
"dev:readme": "pnpm run cli dev docs/README.md --watch",
"test": "echo \"Error: no test specified\" && exit 1"
},
@@ -36,7 +35,6 @@
},
"dependencies": {
"blessed": "^0.1.81",
"chalk": "^5.4.1",
"commander": "^14.0.0",
"dotenv": "^16.5.0",
"eventemitter3": "^5.0.1",
@@ -46,9 +44,7 @@
"marked-terminal": "^7.3.0",
"mdast-util-to-markdown": "^2.1.2",
"mdast-util-to-string": "^4.0.0",
"mdast-util-toc": "^7.1.0",
"rehype-stringify": "^10.0.1",
"remark-behead": "^3.1.0",
"remark-directive": "^4.0.0",
"remark-gfm": "^4.0.1",
"remark-parse": "^11.0.0",

128
pnpm-lock.yaml generated
View File

@@ -11,9 +11,6 @@ importers:
blessed:
specifier: ^0.1.81
version: 0.1.81
chalk:
specifier: ^5.4.1
version: 5.4.1
commander:
specifier: ^14.0.0
version: 14.0.0
@@ -41,15 +38,9 @@ importers:
mdast-util-to-string:
specifier: ^4.0.0
version: 4.0.0
mdast-util-toc:
specifier: ^7.1.0
version: 7.1.0
rehype-stringify:
specifier: ^10.0.1
version: 10.0.1
remark-behead:
specifier: ^3.1.0
version: 3.1.0
remark-directive:
specifier: ^4.0.0
version: 4.0.0
@@ -464,9 +455,6 @@ packages:
'@types/terminal-kit@2.5.7':
resolution: {integrity: sha512-IpbCBFSb3OqCEZBZlk368tGftqss88eNQaJdD9msEShRbksEiVahEqroONi60ppUt9/arLM6IDrHMx9jpzzCOw==}
'@types/ungap__structured-clone@1.2.0':
resolution: {integrity: sha512-ZoaihZNLeZSxESbk9PUAPZOlSpcKx81I1+4emtULDVmBLkYutTcMlCj2K9VNlf9EWODxdO6gkAqEaLorXwZQVA==}
'@types/unist@2.0.11':
resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==}
@@ -752,9 +740,6 @@ packages:
get-tsconfig@4.10.0:
resolution: {integrity: sha512-kGzZ3LWWQcGIAmg6iWvXn0ei6WDtV26wzHRMwDSzmAbcXrTEXxHy6IehI6/4eT6VRKyMP1eF1VqwrVUmE/LR7A==}
github-slugger@2.0.0:
resolution: {integrity: sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==}
glob-parent@5.1.2:
resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
engines: {node: '>= 6'}
@@ -905,9 +890,6 @@ packages:
resolution: {integrity: sha512-gUD/epcRms75Cw8RT1pUdHugZYM5ce64ucs2GEISABwkRsOQr0q2wm/MV2TKThycIe5e0ytRweW2RZxclogCdQ==}
engines: {node: '>=8'}
lodash.iteratee@4.7.0:
resolution: {integrity: sha512-yv3cSQZmfpbIKo4Yo45B1taEvxjNvcpF1CEOc0Y6dEyvhPIfEJE3twDwPgWTPQubcSgXyBwBKG6wpQvWMDOf6Q==}
longest-streak@3.1.0:
resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==}
@@ -977,9 +959,6 @@ packages:
mdast-util-to-string@4.0.0:
resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==}
mdast-util-toc@7.1.0:
resolution: {integrity: sha512-2TVKotOQzqdY7THOdn2gGzS9d1Sdd66bvxUyw3aNpWfcPXCLYSJCCgfPy30sEtuzkDraJgqF35dzgmz6xlvH/w==}
mem@8.1.1:
resolution: {integrity: sha512-qFCFUDs7U3b8mBDPyz5EToEKoAkgCzqquIgi9nkkR9bixxOVOre+09lbuH7+9Kn2NFpm56M3GUWVbU2hQgdACA==}
engines: {node: '>=10'}
@@ -1248,10 +1227,6 @@ packages:
rehype-stringify@10.0.1:
resolution: {integrity: sha512-k9ecfXHmIPuFVI61B9DeLPN0qFHfawM6RsuX48hoqlaKSF61RskNjSm1lI8PhBEM0MRdLxVVm4WmTqJQccH9mA==}
remark-behead@3.1.0:
resolution: {integrity: sha512-rKns7st91lgppaD5YaH58O4ECFVXTVnkyYQBuCw4ISRE2TFK/iVySMaKbvV2pVbUVIjAaDciugrTI/tyuPOlWQ==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
remark-directive@4.0.0:
resolution: {integrity: sha512-7sxn4RfF1o3izevPV1DheyGDD6X4c9hrGpfdUpm7uC++dqrnJxIZVkk7CoKqcLm0VUMAuOol7Mno3m6g8cfMuA==}
@@ -1456,25 +1431,6 @@ packages:
resolution: {integrity: sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==}
engines: {node: '>=8'}
unist-util-find-all-after@4.0.1:
resolution: {integrity: sha512-AO8++e6HJfwNoTrqkV7xSeW65e6uSsLRQST/9LWi8FmFSz1gS7TBd+DkL/CYiElsSZIQgT4J5U54v5/kJX5Nqg==}
unist-util-find-all-before@4.0.1:
resolution: {integrity: sha512-xg4UHtZ6VbcjQbfDtmLZch6kQYQFF3nfaW05Ie3+t2UectzeqSx/iqLmh/wWogwU+YDWnD40PjZKK7ORmCma+g==}
unist-util-find-all-between@2.1.0:
resolution: {integrity: sha512-OCCUtDD8UHKeODw3TPXyFDxPCbpgBzbGTTaDpR68nvxkwiVcawBqMVrokfBMvUi7ij2F5q7S4s4Jq5dvkcBt+w==}
engines: {node: '>=10'}
unist-util-find@1.0.4:
resolution: {integrity: sha512-T5vI7IkhroDj7KxAIy057VbIeGnCXfso4d4GoUsjbAmDLQUkzAeszlBtzx1+KHgdsYYBygaqUBvrbYCfePedZw==}
unist-util-is@4.1.0:
resolution: {integrity: sha512-ZOQSsnce92GrxSqlnEEseX0gi7GH9zTJZ0p9dtu87WRb/37mMPO2Ilx1s/t9vBHrFhbgweUwb+t7cIn5dxPhZg==}
unist-util-is@5.2.1:
resolution: {integrity: sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw==}
unist-util-is@6.0.0:
resolution: {integrity: sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==}
@@ -1484,21 +1440,9 @@ packages:
unist-util-stringify-position@4.0.0:
resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==}
unist-util-visit-parents@3.1.1:
resolution: {integrity: sha512-1KROIZWo6bcMrZEwiH2UrXDyalAa0uqzWCxCJj6lPOvTve2WkfgCytoDTPaMnodXh1WrXOq0haVYHj99ynJlsg==}
unist-util-visit-parents@5.1.3:
resolution: {integrity: sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg==}
unist-util-visit-parents@6.0.1:
resolution: {integrity: sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==}
unist-util-visit@2.0.3:
resolution: {integrity: sha512-iJ4/RczbJMkD0712mGktuGpm/U4By4FfDonL7N/9tATGIF4imikjOuagyMY53tnZq3NP6BcmlrHhEKAfGWjh7Q==}
unist-util-visit@4.1.2:
resolution: {integrity: sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg==}
unist-util-visit@5.0.0:
resolution: {integrity: sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==}
@@ -1951,8 +1895,6 @@ snapshots:
dependencies:
'@types/nextgen-events': 1.1.4
'@types/ungap__structured-clone@1.2.0': {}
'@types/unist@2.0.11': {}
'@types/unist@3.0.3': {}
@@ -2240,8 +2182,6 @@ snapshots:
dependencies:
resolve-pkg-maps: 1.0.0
github-slugger@2.0.0: {}
glob-parent@5.1.2:
dependencies:
is-glob: 4.0.3
@@ -2373,8 +2313,6 @@ snapshots:
strip-bom: 4.0.0
type-fest: 0.6.0
lodash.iteratee@4.7.0: {}
longest-streak@3.1.0: {}
map-age-cleaner@0.1.3:
@@ -2528,16 +2466,6 @@ snapshots:
dependencies:
'@types/mdast': 4.0.4
mdast-util-toc@7.1.0:
dependencies:
'@types/mdast': 4.0.4
'@types/ungap__structured-clone': 1.2.0
'@ungap/structured-clone': 1.3.0
github-slugger: 2.0.0
mdast-util-to-string: 4.0.0
unist-util-is: 6.0.0
unist-util-visit: 5.0.0
mem@8.1.1:
dependencies:
map-age-cleaner: 0.1.3
@@ -2905,14 +2833,6 @@ snapshots:
hast-util-to-html: 9.0.5
unified: 11.0.5
remark-behead@3.1.0:
dependencies:
unist-util-find: 1.0.4
unist-util-find-all-after: 4.0.1
unist-util-find-all-before: 4.0.1
unist-util-find-all-between: 2.1.0
unist-util-visit: 4.1.2
remark-directive@4.0.0:
dependencies:
'@types/mdast': 4.0.4
@@ -3131,32 +3051,6 @@ snapshots:
dependencies:
crypto-random-string: 2.0.0
unist-util-find-all-after@4.0.1:
dependencies:
'@types/unist': 2.0.11
unist-util-is: 5.2.1
unist-util-find-all-before@4.0.1:
dependencies:
'@types/unist': 2.0.11
unist-util-is: 5.2.1
unist-util-find-all-between@2.1.0:
dependencies:
unist-util-find: 1.0.4
unist-util-is: 4.1.0
unist-util-find@1.0.4:
dependencies:
lodash.iteratee: 4.7.0
unist-util-visit: 2.0.3
unist-util-is@4.1.0: {}
unist-util-is@5.2.1:
dependencies:
'@types/unist': 2.0.11
unist-util-is@6.0.0:
dependencies:
'@types/unist': 3.0.3
@@ -3169,33 +3063,11 @@ snapshots:
dependencies:
'@types/unist': 3.0.3
unist-util-visit-parents@3.1.1:
dependencies:
'@types/unist': 2.0.11
unist-util-is: 4.1.0
unist-util-visit-parents@5.1.3:
dependencies:
'@types/unist': 2.0.11
unist-util-is: 5.2.1
unist-util-visit-parents@6.0.1:
dependencies:
'@types/unist': 3.0.3
unist-util-is: 6.0.0
unist-util-visit@2.0.3:
dependencies:
'@types/unist': 2.0.11
unist-util-is: 4.1.0
unist-util-visit-parents: 3.1.1
unist-util-visit@4.1.2:
dependencies:
'@types/unist': 2.0.11
unist-util-is: 5.2.1
unist-util-visit-parents: 5.1.3
unist-util-visit@5.0.0:
dependencies:
'@types/unist': 3.0.3

View File

@@ -1,15 +1,14 @@
import { program } from 'commander';
import { resolve } from 'node:path';
import { Marked } from 'marked';
import { marked } from 'marked';
import { markedTerminal } from 'marked-terminal';
import { execute } from '../execution/execution.js';
import { Context } from '../context/context.js';
import { writeFile } from 'node:fs/promises';
import { Watcher } from '../watcher/watcher.js';
import { UI } from './ui/ui.js';
import { wrapBody } from '../theme/theme.html.js';
marked.use(markedTerminal() as any);
program
.command('dev')
@@ -18,15 +17,11 @@ program
.option('-w, --watch', 'watch for changes')
.option('-i, --input <input...>', 'input variables (-i foo=bar -i baz=qux)')
.action(async (name, options) => {
const marked = new Marked();
marked.use(markedTerminal() as any);
const {
watch = false,
input: i = [],
} = options;
const ui = new UI();
const input = Object.fromEntries(
i.map((item: string) => {
const [key, value] = item.split('=');
@@ -44,7 +39,7 @@ program
});
const markdown = await marked.parse(result.markdown);
ui.content = markdown;
console.log(markdown);
return {
...result,
@@ -54,10 +49,6 @@ program
const result = await build();
ui.screen.key(['r'], () => {
build();
});
if (watch) {
const watcher = new Watcher();
watcher.watchFiles(Array.from(result.context.files));
@@ -75,14 +66,12 @@ program
.argument('<name>', 'http.md file name')
.argument('<output>', 'output file name')
.description('Run a http.md document')
.option('-f, --format <format>', 'output format (html, markdown)')
.option('-w, --watch', 'watch for changes')
.option('-i, --input <input...>', 'input variables (-i foo=bar -i baz=qux)')
.action(async (name, output, options) => {
const {
watch = false,
input: i = [],
format = 'markdown',
} = options;
@@ -102,15 +91,7 @@ program
context,
});
if (format === 'html') {
const marked = new Marked();
const html = await marked.parse(result.markdown);
await writeFile(output, wrapBody(html));
} else if (format === 'markdown') {
await writeFile(output, result.markdown);
} else {
throw new Error('Invalid format');
}
return {
...result,
context,

View File

@@ -1,70 +0,0 @@
import blessed from 'blessed';
import chalk from 'chalk';
class UI {
#box: blessed.Widgets.BoxElement;
#screen: blessed.Widgets.Screen;
constructor() {
const screen = blessed.screen({
smartCSR: true,
title: 'Markdown Viewer'
});
const scrollableBox = blessed.box({ // Or blessed.scrollablebox
parent: screen,
top: 0,
left: 0,
width: '100%',
height: '100%',
content: '',
scrollable: true,
alwaysScroll: true,
keys: true,
vi: true, // vi-like keybindings
mouse: true,
scrollbar: {
ch: ' ',
track: {
bg: 'cyan'
},
style: {
inverse: true
}
},
style: {
fg: 'white',
bg: 'black'
}
});
this.#box = scrollableBox;
this.#screen = screen;
screen.key(['escape', 'q', 'C-c'], () => {
return process.exit(0);
});
scrollableBox.focus();
screen.render();
}
public get screen() {
return this.#screen;
}
public set content(content: string) {
const originalLines = content.split('\n');
const maxLineNoDigits = String(originalLines.length).length; // For padding
const linesWithNumbers = originalLines.map((line, index) => {
const lineNumber = String(index + 1).padStart(maxLineNoDigits, ' ');
const styledLineNumber = chalk.dim.yellow(`${lineNumber} | `);
return `${styledLineNumber}${line}`;
});
const contentWithLineNumbers = linesWithNumbers.join('\n');
this.#box.setContent(contentWithLineNumbers);
this.#screen.render();
}
}
export { UI };

View File

@@ -10,7 +10,6 @@ type Response = {
statusText: string;
headers: Record<string, string>;
body?: string;
rawBody?: string;
};
type AddRequestOptios = {

View File

@@ -5,12 +5,18 @@ import remarkParse from 'remark-parse'
import remarkRehype from 'remark-rehype'
import remarkDirective from 'remark-directive'
import remarkStringify from 'remark-stringify'
import behead from 'remark-behead';
import { unified } from 'unified'
import { visit } from 'unist-util-visit'
import { Context } from "../context/context.js";
import { handlers, postHandlers } from './handlers/handlers.js';
import { handlers } from './handlers/handlers.js';
const parser = unified()
.use(remarkParse)
.use(remarkGfm)
.use(remarkDirective)
.use(remarkStringify)
.use(remarkRehype);
type BaseNode = {
type: string;
@@ -47,7 +53,6 @@ type ExecutionHandler = (options: {
type ExexutionExecuteOptions = {
context: Context;
behead?: number;
}
const execute = async (file: string, options: ExexutionExecuteOptions) => {
@@ -56,16 +61,6 @@ const execute = async (file: string, options: ExexutionExecuteOptions) => {
const content = await readFile(file, 'utf-8');
const steps: Set<ExecutionStep> = new Set();
const parser = unified()
.use(remarkParse)
.use(remarkGfm)
.use(remarkDirective)
.use(remarkStringify)
.use(remarkRehype)
.use(behead, {
depth: options.behead,
});
const root = parser.parse(content);
visit(root, (node, index, parent) => {
@@ -80,18 +75,6 @@ const execute = async (file: string, options: ExexutionExecuteOptions) => {
});
}
});
visit(root, (node, index, parent) => {
for (const handler of postHandlers) {
handler({
addStep: (step) => steps.add(step),
node: node as BaseNode,
root,
parent: parent as BaseNode | undefined,
index,
file,
});
}
});
for (const step of steps) {
const { node, action } = step;

View File

@@ -33,8 +33,7 @@ const httpHandler: ExecutionHandler = ({
const content = template(context);
const [head, body] = content.split('\n\n');
const [top, ...headerItems] = head.split('\n');
const [method, ...urlParts] = top.split(' ');
const url = urlParts.join(' ').trim();
const [method, url] = top.split(' ');
const headers = Object.fromEntries(
headerItems.map((header) => {
@@ -44,7 +43,7 @@ const httpHandler: ExecutionHandler = ({
);
let parsedBody = body;
if (options.yaml) {
if (options.format === 'yaml') {
try {
const parsed = YAML.parse(body);
parsedBody = JSON.stringify(parsed);
@@ -59,8 +58,7 @@ const httpHandler: ExecutionHandler = ({
body
});
const rawBody = await response.text();
let responseText = rawBody;
let responseText = await response.text();
if (options.json) {
try {
responseText = JSON.parse(responseText);
@@ -69,7 +67,7 @@ const httpHandler: ExecutionHandler = ({
}
}
node.value = [head, parsedBody].filter(Boolean).join('\n\n');
node.value = content;
node.meta = undefined;
context.addRequest({
@@ -85,7 +83,6 @@ const httpHandler: ExecutionHandler = ({
statusText: response.statusText,
headers: Object.fromEntries(response.headers.entries()),
body: responseText,
rawBody: rawBody,
},
});
},

View File

@@ -23,7 +23,6 @@ const fileHandler: ExecutionHandler = ({
}
const { root: newRoot } = await execute(filePath, {
context,
behead: node.attributes?.behead ? parseInt(node.attributes.behead) : undefined,
});
if (!parent) {
throw new Error('Parent node is required');

View File

@@ -53,7 +53,6 @@ const responseHandler: ExecutionHandler = ({
const codeNode = {
type: 'code',
lang: 'http',
value: responseContent,
};
if (!parent || !('children' in parent) || index === undefined) {

View File

@@ -1,29 +0,0 @@
import { toc } from 'mdast-util-toc';
import { type ExecutionHandler } from '../execution.js';
const tocHandler: ExecutionHandler = ({
addStep,
node,
root,
parent,
index,
}) => {
if (node.type === 'leafDirective' && node.name === 'toc') {
addStep({
type: 'toc',
node,
action: async () => {
const result = toc(root, {
tight: true,
minDepth: 2,
})
if (!parent || !parent.children || index === undefined) {
throw new Error('Parent node is not valid');
}
parent.children.splice(index, 1, result.map as any);
},
})
}
}
export { tocHandler };

View File

@@ -6,7 +6,6 @@ import { rawMdHandler } from "./handlers.raw-md.js";
import { responseHandler } from "./handlers.response.js";
import { textHandler } from "./handlers.text.js";
import { codeHandler } from "./handlers.code.js";
import { tocHandler } from "./handlers.toc.js";
const handlers = [
fileHandler,
@@ -18,8 +17,4 @@ const handlers = [
codeHandler,
] satisfies ExecutionHandler[];
const postHandlers = [
tocHandler,
] satisfies ExecutionHandler[];
export { handlers, postHandlers };
export { handlers };

View File

@@ -1,29 +0,0 @@
const wrapBody = (body: string) => {
return `<!DOCTYPE html>
<html lang="en">
<head>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/github-markdown-css/5.8.1/github-markdown.min.css" />
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 0;
}
.markdown-body {
max-width: 800px;
padding: 20px;
margin: auto;
}
</style>
</head>
<body>
<article class="markdown-body">
${body}
</article>
</body>
</html>`;
};
export { wrapBody };