mirror of
https://github.com/morten-olsen/http.md.git
synced 2026-02-08 00:46:28 +01:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e0707e74fb | ||
|
|
6b74a28989 | ||
|
|
ad342e5f10 | ||
|
|
5d485acc97 | ||
|
|
68f5025527 | ||
|
|
a9a7bae28f | ||
|
|
b800290d72 | ||
|
|
d7b6a3880e | ||
|
|
0eff8cf603 |
7
.github/workflows/main.yaml
vendored
7
.github/workflows/main.yaml
vendored
@@ -92,7 +92,7 @@ jobs:
|
||||
|
||||
release:
|
||||
permissions:
|
||||
contents: read
|
||||
contents: write
|
||||
packages: write
|
||||
attestations: write
|
||||
id-token: write
|
||||
@@ -135,3 +135,8 @@ jobs:
|
||||
pnpm publish --no-git-checks --access public
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
- uses: stefanzweifel/git-auto-commit-action@v5
|
||||
with:
|
||||
commit_message: "docs: generated README"
|
||||
file_pattern: "*.md"
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,2 +1,3 @@
|
||||
/node_modules/
|
||||
/dist/
|
||||
/*.html
|
||||
|
||||
67
README.md
67
README.md
@@ -110,9 +110,9 @@ HTTP/200 OK
|
||||
access-control-allow-credentials: true
|
||||
access-control-allow-origin: *
|
||||
connection: keep-alive
|
||||
content-length: 559
|
||||
content-length: 555
|
||||
content-type: application/json
|
||||
date: Sun, 18 May 2025 18:31:46 GMT
|
||||
date: Sun, 18 May 2025 19:12:17 GMT
|
||||
server: gunicorn/19.9.0
|
||||
|
||||
{
|
||||
@@ -129,12 +129,12 @@ server: gunicorn/19.9.0
|
||||
"Host": "httpbin.org",
|
||||
"Sec-Fetch-Mode": "cors",
|
||||
"User-Agent": "node",
|
||||
"X-Amzn-Trace-Id": "Root=1-682a2792-7df702ce77a3b3696937eaeb"
|
||||
"X-Amzn-Trace-Id": "Root=1-682a3111-131bcbff690b03fd64aa4617"
|
||||
},
|
||||
"json": {
|
||||
"greeting": "Hello, http.md!"
|
||||
},
|
||||
"origin": "185.181.220.204",
|
||||
"origin": "23.96.180.7",
|
||||
"url": "https://httpbin.org/post"
|
||||
}
|
||||
|
||||
@@ -264,14 +264,14 @@ Within your markdown document, the following variables are available in the Hand
|
||||
**1. Using a value from a previous response in a new request:**
|
||||
|
||||
````markdown
|
||||
```http id=createItem json
|
||||
```http #createItem,json
|
||||
POST https://httpbin.org/post
|
||||
Content-Type: application/json
|
||||
|
||||
{"name": "My New Item"}
|
||||
```
|
||||
|
||||
The new item ID is: {{responses.createItem.body.json.name}}
|
||||
The new item ID is: {{response.body.json.name}}
|
||||
|
||||
Now, let's fetch the item using a (mocked) ID from the response:
|
||||
|
||||
@@ -283,6 +283,61 @@ GET https://httpbin.org/anything/{{responses.createItem.body.json.name}}
|
||||
|
||||
````
|
||||
|
||||
<details>
|
||||
<summary>Output</summary>
|
||||
|
||||
````markdown
|
||||
```http
|
||||
POST https://httpbin.org/post
|
||||
Content-Type: application/json
|
||||
|
||||
{"name": "My New Item"}
|
||||
```
|
||||
|
||||
The new item ID is: My New Item
|
||||
|
||||
Now, let's fetch the item using a (mocked) ID from the response:
|
||||
|
||||
```http
|
||||
GET https://httpbin.org/anything/My New Item
|
||||
```
|
||||
|
||||
```
|
||||
HTTP/200 OK
|
||||
access-control-allow-credentials: true
|
||||
access-control-allow-origin: *
|
||||
connection: keep-alive
|
||||
content-length: 451
|
||||
content-type: application/json
|
||||
date: Sun, 18 May 2025 19:12:18 GMT
|
||||
server: gunicorn/19.9.0
|
||||
|
||||
{
|
||||
"args": {},
|
||||
"data": "",
|
||||
"files": {},
|
||||
"form": {},
|
||||
"headers": {
|
||||
"Accept": "*/*",
|
||||
"Accept-Encoding": "br, gzip, deflate",
|
||||
"Accept-Language": "*",
|
||||
"Host": "httpbin.org",
|
||||
"Sec-Fetch-Mode": "cors",
|
||||
"User-Agent": "node",
|
||||
"X-Amzn-Trace-Id": "Root=1-682a3112-4bbb29111129c1556c487ca1"
|
||||
},
|
||||
"json": null,
|
||||
"method": "GET",
|
||||
"origin": "23.96.180.7",
|
||||
"url": "https://httpbin.org/anything/My New Item"
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
````
|
||||
|
||||
</details>
|
||||
|
||||
*(Note: `httpbin.org/post` wraps the JSON sent in a "json" field in its response. If your API returns the ID directly at the root of the JSON body, you'd use `{{responses.createItem.body.id}}` assuming the `createItem` request had the `json` option.)*
|
||||
|
||||
**2. Displaying a status code in markdown text:**
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import 'dotenv/config.js';
|
||||
import '../dist/cli/cli.js';
|
||||
|
||||
@@ -21,6 +21,10 @@ It allows developers to create API documentation that is always accurate and up-
|
||||
- **Tutorials & Guides:** Build step-by-step guides where each HTTP interaction is shown with its real output.
|
||||
- **Rapid Prototyping:** Quickly experiment with APIs and document your findings.
|
||||
|
||||
## Content
|
||||
|
||||
::toc
|
||||
|
||||
## Installation
|
||||
|
||||
Install `http.md` globally using npm:
|
||||
@@ -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 disable
|
||||
<METHOD> <URL>
|
||||
<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]
|
||||
|
||||
<details>
|
||||
<summary>Output</summary>
|
||||
|
||||
::raw-md[./examples/with-template.md]{render}
|
||||
|
||||
</details>
|
||||
|
||||
_(Note: `httpbin.org/post` wraps the JSON sent in a "json" field in its response. If your API returns the ID directly at the root of the JSON body, you'd use `{{responses.createItem.body.id}}` assuming the `createItem` request had the `json` option.)_
|
||||
|
||||
**2. Displaying a status code in markdown text:**
|
||||
@@ -202,34 +213,18 @@ The requests from the embedded document are processed, and their `request` and `
|
||||
|
||||
Assume `_shared_requests.md` contains:
|
||||
|
||||
````markdown
|
||||
```http id=sharedGetRequest
|
||||
GET https://httpbin.org/get
|
||||
```
|
||||
````
|
||||
::raw-md[./examples/_shared_requests.md]
|
||||
|
||||
Then, in `main.md`:
|
||||
|
||||
````markdown
|
||||
# Main Document
|
||||
::raw-md[./examples/with-shared-requests.md]
|
||||
|
||||
Let's include some shared requests:
|
||||
<details>
|
||||
<summary>Output</summary>
|
||||
|
||||
::md[./_shared_requests.md]
|
||||
::raw-md[./examples/with-shared-requests.md]{render}
|
||||
|
||||
The shared GET request returned: {{responses.sharedGetRequest.status}}
|
||||
|
||||
Now, a request specific to this document:
|
||||
|
||||
```http
|
||||
POST https://httpbin.org/post
|
||||
Content-Type: application/json
|
||||
|
||||
{"dataFromMain": "someValue", "sharedUrl": "{{requests.sharedGetRequest.url}}"}
|
||||
```
|
||||
|
||||
::response
|
||||
````
|
||||
</details>
|
||||
|
||||
When `main.md` is processed, `_shared_requests.md` will be embedded, its `sharedGetRequest` will be executed, and its data will be available for templating.
|
||||
|
||||
|
||||
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
|
||||
Content-Type: application/json
|
||||
|
||||
{"name": "My New Item"}
|
||||
```
|
||||
|
||||
The new item ID is: {{responses.createItem.body.json.name}}
|
||||
The new item ID is: {{response.body.json.name}}
|
||||
|
||||
Now, let's fetch the item using a (mocked) ID from the response:
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
"build": "pnpm run build:lib && pnpm run build:readme",
|
||||
"build:lib": "tsc --build",
|
||||
"build:readme": "pnpm run cli build docs/README.md README.md",
|
||||
"build:readme-html": "pnpm run cli build docs/README.md README.html -f html",
|
||||
"dev:readme": "pnpm run cli dev docs/README.md --watch",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
@@ -35,7 +36,9 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"blessed": "^0.1.81",
|
||||
"chalk": "^5.4.1",
|
||||
"commander": "^14.0.0",
|
||||
"dotenv": "^16.5.0",
|
||||
"eventemitter3": "^5.0.1",
|
||||
"handlebars": "^4.7.8",
|
||||
"hastscript": "^9.0.1",
|
||||
@@ -43,7 +46,9 @@
|
||||
"marked-terminal": "^7.3.0",
|
||||
"mdast-util-to-markdown": "^2.1.2",
|
||||
"mdast-util-to-string": "^4.0.0",
|
||||
"mdast-util-toc": "^7.1.0",
|
||||
"rehype-stringify": "^10.0.1",
|
||||
"remark-behead": "^3.1.0",
|
||||
"remark-directive": "^4.0.0",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"remark-parse": "^11.0.0",
|
||||
|
||||
137
pnpm-lock.yaml
generated
137
pnpm-lock.yaml
generated
@@ -11,9 +11,15 @@ importers:
|
||||
blessed:
|
||||
specifier: ^0.1.81
|
||||
version: 0.1.81
|
||||
chalk:
|
||||
specifier: ^5.4.1
|
||||
version: 5.4.1
|
||||
commander:
|
||||
specifier: ^14.0.0
|
||||
version: 14.0.0
|
||||
dotenv:
|
||||
specifier: ^16.5.0
|
||||
version: 16.5.0
|
||||
eventemitter3:
|
||||
specifier: ^5.0.1
|
||||
version: 5.0.1
|
||||
@@ -35,9 +41,15 @@ importers:
|
||||
mdast-util-to-string:
|
||||
specifier: ^4.0.0
|
||||
version: 4.0.0
|
||||
mdast-util-toc:
|
||||
specifier: ^7.1.0
|
||||
version: 7.1.0
|
||||
rehype-stringify:
|
||||
specifier: ^10.0.1
|
||||
version: 10.0.1
|
||||
remark-behead:
|
||||
specifier: ^3.1.0
|
||||
version: 3.1.0
|
||||
remark-directive:
|
||||
specifier: ^4.0.0
|
||||
version: 4.0.0
|
||||
@@ -452,6 +464,9 @@ packages:
|
||||
'@types/terminal-kit@2.5.7':
|
||||
resolution: {integrity: sha512-IpbCBFSb3OqCEZBZlk368tGftqss88eNQaJdD9msEShRbksEiVahEqroONi60ppUt9/arLM6IDrHMx9jpzzCOw==}
|
||||
|
||||
'@types/ungap__structured-clone@1.2.0':
|
||||
resolution: {integrity: sha512-ZoaihZNLeZSxESbk9PUAPZOlSpcKx81I1+4emtULDVmBLkYutTcMlCj2K9VNlf9EWODxdO6gkAqEaLorXwZQVA==}
|
||||
|
||||
'@types/unist@2.0.11':
|
||||
resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==}
|
||||
|
||||
@@ -657,6 +672,10 @@ packages:
|
||||
devlop@1.1.0:
|
||||
resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==}
|
||||
|
||||
dotenv@16.5.0:
|
||||
resolution: {integrity: sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
emoji-regex@8.0.0:
|
||||
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
|
||||
|
||||
@@ -733,6 +752,9 @@ packages:
|
||||
get-tsconfig@4.10.0:
|
||||
resolution: {integrity: sha512-kGzZ3LWWQcGIAmg6iWvXn0ei6WDtV26wzHRMwDSzmAbcXrTEXxHy6IehI6/4eT6VRKyMP1eF1VqwrVUmE/LR7A==}
|
||||
|
||||
github-slugger@2.0.0:
|
||||
resolution: {integrity: sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==}
|
||||
|
||||
glob-parent@5.1.2:
|
||||
resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
|
||||
engines: {node: '>= 6'}
|
||||
@@ -883,6 +905,9 @@ packages:
|
||||
resolution: {integrity: sha512-gUD/epcRms75Cw8RT1pUdHugZYM5ce64ucs2GEISABwkRsOQr0q2wm/MV2TKThycIe5e0ytRweW2RZxclogCdQ==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
lodash.iteratee@4.7.0:
|
||||
resolution: {integrity: sha512-yv3cSQZmfpbIKo4Yo45B1taEvxjNvcpF1CEOc0Y6dEyvhPIfEJE3twDwPgWTPQubcSgXyBwBKG6wpQvWMDOf6Q==}
|
||||
|
||||
longest-streak@3.1.0:
|
||||
resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==}
|
||||
|
||||
@@ -952,6 +977,9 @@ packages:
|
||||
mdast-util-to-string@4.0.0:
|
||||
resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==}
|
||||
|
||||
mdast-util-toc@7.1.0:
|
||||
resolution: {integrity: sha512-2TVKotOQzqdY7THOdn2gGzS9d1Sdd66bvxUyw3aNpWfcPXCLYSJCCgfPy30sEtuzkDraJgqF35dzgmz6xlvH/w==}
|
||||
|
||||
mem@8.1.1:
|
||||
resolution: {integrity: sha512-qFCFUDs7U3b8mBDPyz5EToEKoAkgCzqquIgi9nkkR9bixxOVOre+09lbuH7+9Kn2NFpm56M3GUWVbU2hQgdACA==}
|
||||
engines: {node: '>=10'}
|
||||
@@ -1220,6 +1248,10 @@ packages:
|
||||
rehype-stringify@10.0.1:
|
||||
resolution: {integrity: sha512-k9ecfXHmIPuFVI61B9DeLPN0qFHfawM6RsuX48hoqlaKSF61RskNjSm1lI8PhBEM0MRdLxVVm4WmTqJQccH9mA==}
|
||||
|
||||
remark-behead@3.1.0:
|
||||
resolution: {integrity: sha512-rKns7st91lgppaD5YaH58O4ECFVXTVnkyYQBuCw4ISRE2TFK/iVySMaKbvV2pVbUVIjAaDciugrTI/tyuPOlWQ==}
|
||||
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
||||
|
||||
remark-directive@4.0.0:
|
||||
resolution: {integrity: sha512-7sxn4RfF1o3izevPV1DheyGDD6X4c9hrGpfdUpm7uC++dqrnJxIZVkk7CoKqcLm0VUMAuOol7Mno3m6g8cfMuA==}
|
||||
|
||||
@@ -1424,6 +1456,25 @@ packages:
|
||||
resolution: {integrity: sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
unist-util-find-all-after@4.0.1:
|
||||
resolution: {integrity: sha512-AO8++e6HJfwNoTrqkV7xSeW65e6uSsLRQST/9LWi8FmFSz1gS7TBd+DkL/CYiElsSZIQgT4J5U54v5/kJX5Nqg==}
|
||||
|
||||
unist-util-find-all-before@4.0.1:
|
||||
resolution: {integrity: sha512-xg4UHtZ6VbcjQbfDtmLZch6kQYQFF3nfaW05Ie3+t2UectzeqSx/iqLmh/wWogwU+YDWnD40PjZKK7ORmCma+g==}
|
||||
|
||||
unist-util-find-all-between@2.1.0:
|
||||
resolution: {integrity: sha512-OCCUtDD8UHKeODw3TPXyFDxPCbpgBzbGTTaDpR68nvxkwiVcawBqMVrokfBMvUi7ij2F5q7S4s4Jq5dvkcBt+w==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
unist-util-find@1.0.4:
|
||||
resolution: {integrity: sha512-T5vI7IkhroDj7KxAIy057VbIeGnCXfso4d4GoUsjbAmDLQUkzAeszlBtzx1+KHgdsYYBygaqUBvrbYCfePedZw==}
|
||||
|
||||
unist-util-is@4.1.0:
|
||||
resolution: {integrity: sha512-ZOQSsnce92GrxSqlnEEseX0gi7GH9zTJZ0p9dtu87WRb/37mMPO2Ilx1s/t9vBHrFhbgweUwb+t7cIn5dxPhZg==}
|
||||
|
||||
unist-util-is@5.2.1:
|
||||
resolution: {integrity: sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw==}
|
||||
|
||||
unist-util-is@6.0.0:
|
||||
resolution: {integrity: sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==}
|
||||
|
||||
@@ -1433,9 +1484,21 @@ packages:
|
||||
unist-util-stringify-position@4.0.0:
|
||||
resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==}
|
||||
|
||||
unist-util-visit-parents@3.1.1:
|
||||
resolution: {integrity: sha512-1KROIZWo6bcMrZEwiH2UrXDyalAa0uqzWCxCJj6lPOvTve2WkfgCytoDTPaMnodXh1WrXOq0haVYHj99ynJlsg==}
|
||||
|
||||
unist-util-visit-parents@5.1.3:
|
||||
resolution: {integrity: sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg==}
|
||||
|
||||
unist-util-visit-parents@6.0.1:
|
||||
resolution: {integrity: sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==}
|
||||
|
||||
unist-util-visit@2.0.3:
|
||||
resolution: {integrity: sha512-iJ4/RczbJMkD0712mGktuGpm/U4By4FfDonL7N/9tATGIF4imikjOuagyMY53tnZq3NP6BcmlrHhEKAfGWjh7Q==}
|
||||
|
||||
unist-util-visit@4.1.2:
|
||||
resolution: {integrity: sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg==}
|
||||
|
||||
unist-util-visit@5.0.0:
|
||||
resolution: {integrity: sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==}
|
||||
|
||||
@@ -1888,6 +1951,8 @@ snapshots:
|
||||
dependencies:
|
||||
'@types/nextgen-events': 1.1.4
|
||||
|
||||
'@types/ungap__structured-clone@1.2.0': {}
|
||||
|
||||
'@types/unist@2.0.11': {}
|
||||
|
||||
'@types/unist@3.0.3': {}
|
||||
@@ -2077,6 +2142,8 @@ snapshots:
|
||||
dependencies:
|
||||
dequal: 2.0.3
|
||||
|
||||
dotenv@16.5.0: {}
|
||||
|
||||
emoji-regex@8.0.0: {}
|
||||
|
||||
emojilib@2.4.0: {}
|
||||
@@ -2173,6 +2240,8 @@ snapshots:
|
||||
dependencies:
|
||||
resolve-pkg-maps: 1.0.0
|
||||
|
||||
github-slugger@2.0.0: {}
|
||||
|
||||
glob-parent@5.1.2:
|
||||
dependencies:
|
||||
is-glob: 4.0.3
|
||||
@@ -2304,6 +2373,8 @@ snapshots:
|
||||
strip-bom: 4.0.0
|
||||
type-fest: 0.6.0
|
||||
|
||||
lodash.iteratee@4.7.0: {}
|
||||
|
||||
longest-streak@3.1.0: {}
|
||||
|
||||
map-age-cleaner@0.1.3:
|
||||
@@ -2457,6 +2528,16 @@ snapshots:
|
||||
dependencies:
|
||||
'@types/mdast': 4.0.4
|
||||
|
||||
mdast-util-toc@7.1.0:
|
||||
dependencies:
|
||||
'@types/mdast': 4.0.4
|
||||
'@types/ungap__structured-clone': 1.2.0
|
||||
'@ungap/structured-clone': 1.3.0
|
||||
github-slugger: 2.0.0
|
||||
mdast-util-to-string: 4.0.0
|
||||
unist-util-is: 6.0.0
|
||||
unist-util-visit: 5.0.0
|
||||
|
||||
mem@8.1.1:
|
||||
dependencies:
|
||||
map-age-cleaner: 0.1.3
|
||||
@@ -2824,6 +2905,14 @@ snapshots:
|
||||
hast-util-to-html: 9.0.5
|
||||
unified: 11.0.5
|
||||
|
||||
remark-behead@3.1.0:
|
||||
dependencies:
|
||||
unist-util-find: 1.0.4
|
||||
unist-util-find-all-after: 4.0.1
|
||||
unist-util-find-all-before: 4.0.1
|
||||
unist-util-find-all-between: 2.1.0
|
||||
unist-util-visit: 4.1.2
|
||||
|
||||
remark-directive@4.0.0:
|
||||
dependencies:
|
||||
'@types/mdast': 4.0.4
|
||||
@@ -3042,6 +3131,32 @@ snapshots:
|
||||
dependencies:
|
||||
crypto-random-string: 2.0.0
|
||||
|
||||
unist-util-find-all-after@4.0.1:
|
||||
dependencies:
|
||||
'@types/unist': 2.0.11
|
||||
unist-util-is: 5.2.1
|
||||
|
||||
unist-util-find-all-before@4.0.1:
|
||||
dependencies:
|
||||
'@types/unist': 2.0.11
|
||||
unist-util-is: 5.2.1
|
||||
|
||||
unist-util-find-all-between@2.1.0:
|
||||
dependencies:
|
||||
unist-util-find: 1.0.4
|
||||
unist-util-is: 4.1.0
|
||||
|
||||
unist-util-find@1.0.4:
|
||||
dependencies:
|
||||
lodash.iteratee: 4.7.0
|
||||
unist-util-visit: 2.0.3
|
||||
|
||||
unist-util-is@4.1.0: {}
|
||||
|
||||
unist-util-is@5.2.1:
|
||||
dependencies:
|
||||
'@types/unist': 2.0.11
|
||||
|
||||
unist-util-is@6.0.0:
|
||||
dependencies:
|
||||
'@types/unist': 3.0.3
|
||||
@@ -3054,11 +3169,33 @@ snapshots:
|
||||
dependencies:
|
||||
'@types/unist': 3.0.3
|
||||
|
||||
unist-util-visit-parents@3.1.1:
|
||||
dependencies:
|
||||
'@types/unist': 2.0.11
|
||||
unist-util-is: 4.1.0
|
||||
|
||||
unist-util-visit-parents@5.1.3:
|
||||
dependencies:
|
||||
'@types/unist': 2.0.11
|
||||
unist-util-is: 5.2.1
|
||||
|
||||
unist-util-visit-parents@6.0.1:
|
||||
dependencies:
|
||||
'@types/unist': 3.0.3
|
||||
unist-util-is: 6.0.0
|
||||
|
||||
unist-util-visit@2.0.3:
|
||||
dependencies:
|
||||
'@types/unist': 2.0.11
|
||||
unist-util-is: 4.1.0
|
||||
unist-util-visit-parents: 3.1.1
|
||||
|
||||
unist-util-visit@4.1.2:
|
||||
dependencies:
|
||||
'@types/unist': 2.0.11
|
||||
unist-util-is: 5.2.1
|
||||
unist-util-visit-parents: 5.1.3
|
||||
|
||||
unist-util-visit@5.0.0:
|
||||
dependencies:
|
||||
'@types/unist': 3.0.3
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import { program } from 'commander';
|
||||
import { resolve } from 'node:path';
|
||||
import { marked } from 'marked';
|
||||
import { Marked } from 'marked';
|
||||
import { markedTerminal } from 'marked-terminal';
|
||||
import { execute } from '../execution/execution.js';
|
||||
import { Context } from '../context/context.js';
|
||||
import { writeFile } from 'node:fs/promises';
|
||||
import { Watcher } from '../watcher/watcher.js';
|
||||
import { UI } from './ui/ui.js';
|
||||
import { wrapBody } from '../theme/theme.html.js';
|
||||
|
||||
|
||||
marked.use(markedTerminal() as any);
|
||||
|
||||
program
|
||||
.command('dev')
|
||||
@@ -17,11 +18,15 @@ program
|
||||
.option('-w, --watch', 'watch for changes')
|
||||
.option('-i, --input <input...>', 'input variables (-i foo=bar -i baz=qux)')
|
||||
.action(async (name, options) => {
|
||||
const marked = new Marked();
|
||||
marked.use(markedTerminal() as any);
|
||||
const {
|
||||
watch = false,
|
||||
input: i = [],
|
||||
} = options;
|
||||
|
||||
const ui = new UI();
|
||||
|
||||
const input = Object.fromEntries(
|
||||
i.map((item: string) => {
|
||||
const [key, value] = item.split('=');
|
||||
@@ -39,7 +44,7 @@ program
|
||||
});
|
||||
|
||||
const markdown = await marked.parse(result.markdown);
|
||||
console.log(markdown);
|
||||
ui.content = markdown;
|
||||
|
||||
return {
|
||||
...result,
|
||||
@@ -49,6 +54,10 @@ program
|
||||
|
||||
const result = await build();
|
||||
|
||||
ui.screen.key(['r'], () => {
|
||||
build();
|
||||
});
|
||||
|
||||
if (watch) {
|
||||
const watcher = new Watcher();
|
||||
watcher.watchFiles(Array.from(result.context.files));
|
||||
@@ -66,12 +75,14 @@ program
|
||||
.argument('<name>', 'http.md file name')
|
||||
.argument('<output>', 'output file name')
|
||||
.description('Run a http.md document')
|
||||
.option('-f, --format <format>', 'output format (html, markdown)')
|
||||
.option('-w, --watch', 'watch for changes')
|
||||
.option('-i, --input <input...>', 'input variables (-i foo=bar -i baz=qux)')
|
||||
.action(async (name, output, options) => {
|
||||
const {
|
||||
watch = false,
|
||||
input: i = [],
|
||||
format = 'markdown',
|
||||
} = options;
|
||||
|
||||
|
||||
@@ -91,7 +102,15 @@ program
|
||||
context,
|
||||
});
|
||||
|
||||
await writeFile(output, result.markdown);
|
||||
if (format === 'html') {
|
||||
const marked = new Marked();
|
||||
const html = await marked.parse(result.markdown);
|
||||
await writeFile(output, wrapBody(html));
|
||||
} else if (format === 'markdown') {
|
||||
await writeFile(output, result.markdown);
|
||||
} else {
|
||||
throw new Error('Invalid format');
|
||||
}
|
||||
return {
|
||||
...result,
|
||||
context,
|
||||
|
||||
70
src/cli/ui/ui.ts
Normal file
70
src/cli/ui/ui.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import blessed from 'blessed';
|
||||
import chalk from 'chalk';
|
||||
|
||||
class UI {
|
||||
#box: blessed.Widgets.BoxElement;
|
||||
#screen: blessed.Widgets.Screen;
|
||||
|
||||
constructor() {
|
||||
const screen = blessed.screen({
|
||||
smartCSR: true,
|
||||
title: 'Markdown Viewer'
|
||||
});
|
||||
const scrollableBox = blessed.box({ // Or blessed.scrollablebox
|
||||
parent: screen,
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
content: '',
|
||||
scrollable: true,
|
||||
alwaysScroll: true,
|
||||
keys: true,
|
||||
vi: true, // vi-like keybindings
|
||||
mouse: true,
|
||||
scrollbar: {
|
||||
ch: ' ',
|
||||
track: {
|
||||
bg: 'cyan'
|
||||
},
|
||||
style: {
|
||||
inverse: true
|
||||
}
|
||||
},
|
||||
style: {
|
||||
fg: 'white',
|
||||
bg: 'black'
|
||||
}
|
||||
});
|
||||
this.#box = scrollableBox;
|
||||
this.#screen = screen;
|
||||
|
||||
screen.key(['escape', 'q', 'C-c'], () => {
|
||||
return process.exit(0);
|
||||
});
|
||||
|
||||
scrollableBox.focus();
|
||||
screen.render();
|
||||
}
|
||||
|
||||
public get screen() {
|
||||
return this.#screen;
|
||||
}
|
||||
|
||||
public set content(content: string) {
|
||||
const originalLines = content.split('\n');
|
||||
const maxLineNoDigits = String(originalLines.length).length; // For padding
|
||||
|
||||
const linesWithNumbers = originalLines.map((line, index) => {
|
||||
const lineNumber = String(index + 1).padStart(maxLineNoDigits, ' ');
|
||||
const styledLineNumber = chalk.dim.yellow(`${lineNumber} | `);
|
||||
return `${styledLineNumber}${line}`;
|
||||
});
|
||||
|
||||
const contentWithLineNumbers = linesWithNumbers.join('\n');
|
||||
this.#box.setContent(contentWithLineNumbers);
|
||||
this.#screen.render();
|
||||
}
|
||||
}
|
||||
|
||||
export { UI };
|
||||
@@ -10,6 +10,7 @@ type Response = {
|
||||
statusText: string;
|
||||
headers: Record<string, string>;
|
||||
body?: string;
|
||||
rawBody?: string;
|
||||
};
|
||||
|
||||
type AddRequestOptios = {
|
||||
@@ -36,7 +37,7 @@ class Context {
|
||||
|
||||
constructor(options: ContextOptions = {}) {
|
||||
this.input = options.input || {};
|
||||
this.env = options.env || {};
|
||||
this.env = options.env || process.env;
|
||||
this.requests = options.requests || {};
|
||||
this.responses = options.responses || {};
|
||||
}
|
||||
|
||||
@@ -5,18 +5,12 @@ import remarkParse from 'remark-parse'
|
||||
import remarkRehype from 'remark-rehype'
|
||||
import remarkDirective from 'remark-directive'
|
||||
import remarkStringify from 'remark-stringify'
|
||||
import behead from 'remark-behead';
|
||||
import { unified } from 'unified'
|
||||
import { visit } from 'unist-util-visit'
|
||||
|
||||
import { Context } from "../context/context.js";
|
||||
import { handlers } from './handlers/handlers.js';
|
||||
|
||||
const parser = unified()
|
||||
.use(remarkParse)
|
||||
.use(remarkGfm)
|
||||
.use(remarkDirective)
|
||||
.use(remarkStringify)
|
||||
.use(remarkRehype);
|
||||
import { handlers, postHandlers } from './handlers/handlers.js';
|
||||
|
||||
type BaseNode = {
|
||||
type: string;
|
||||
@@ -53,6 +47,7 @@ type ExecutionHandler = (options: {
|
||||
|
||||
type ExexutionExecuteOptions = {
|
||||
context: Context;
|
||||
behead?: number;
|
||||
}
|
||||
|
||||
const execute = async (file: string, options: ExexutionExecuteOptions) => {
|
||||
@@ -61,6 +56,16 @@ const execute = async (file: string, options: ExexutionExecuteOptions) => {
|
||||
const content = await readFile(file, 'utf-8');
|
||||
const steps: Set<ExecutionStep> = new Set();
|
||||
|
||||
|
||||
const parser = unified()
|
||||
.use(remarkParse)
|
||||
.use(remarkGfm)
|
||||
.use(remarkDirective)
|
||||
.use(remarkStringify)
|
||||
.use(remarkRehype)
|
||||
.use(behead, {
|
||||
depth: options.behead,
|
||||
});
|
||||
const root = parser.parse(content);
|
||||
|
||||
visit(root, (node, index, parent) => {
|
||||
@@ -75,6 +80,18 @@ const execute = async (file: string, options: ExexutionExecuteOptions) => {
|
||||
});
|
||||
}
|
||||
});
|
||||
visit(root, (node, index, parent) => {
|
||||
for (const handler of postHandlers) {
|
||||
handler({
|
||||
addStep: (step) => steps.add(step),
|
||||
node: node as BaseNode,
|
||||
root,
|
||||
parent: parent as BaseNode | undefined,
|
||||
index,
|
||||
file,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
for (const step of steps) {
|
||||
const { node, action } = step;
|
||||
|
||||
@@ -16,6 +16,11 @@ const httpHandler: ExecutionHandler = ({
|
||||
return [key.trim(), value?.trim() || true];
|
||||
})
|
||||
);
|
||||
let id = options.id?.toString();
|
||||
const idPart = optionParts.find((option) => option.startsWith('#'));
|
||||
if (idPart) {
|
||||
id = idPart.slice(1);
|
||||
}
|
||||
|
||||
addStep({
|
||||
type: 'http',
|
||||
@@ -28,7 +33,8 @@ const httpHandler: ExecutionHandler = ({
|
||||
const content = template(context);
|
||||
const [head, body] = content.split('\n\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(
|
||||
headerItems.map((header) => {
|
||||
@@ -53,7 +59,8 @@ const httpHandler: ExecutionHandler = ({
|
||||
body
|
||||
});
|
||||
|
||||
let responseText = await response.text();
|
||||
const rawBody = await response.text();
|
||||
let responseText = rawBody;
|
||||
if (options.json) {
|
||||
try {
|
||||
responseText = JSON.parse(responseText);
|
||||
@@ -66,7 +73,7 @@ const httpHandler: ExecutionHandler = ({
|
||||
node.meta = undefined;
|
||||
|
||||
context.addRequest({
|
||||
id: options.id?.toString(),
|
||||
id,
|
||||
request: {
|
||||
method,
|
||||
url,
|
||||
@@ -78,6 +85,7 @@ const httpHandler: ExecutionHandler = ({
|
||||
statusText: response.statusText,
|
||||
headers: Object.fromEntries(response.headers.entries()),
|
||||
body: responseText,
|
||||
rawBody: rawBody,
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
@@ -23,6 +23,7 @@ const fileHandler: ExecutionHandler = ({
|
||||
}
|
||||
const { root: newRoot } = await execute(filePath, {
|
||||
context,
|
||||
behead: node.attributes?.behead ? parseInt(node.attributes.behead) : undefined,
|
||||
});
|
||||
if (!parent) {
|
||||
throw new Error('Parent node is required');
|
||||
|
||||
@@ -53,6 +53,7 @@ const responseHandler: ExecutionHandler = ({
|
||||
|
||||
const codeNode = {
|
||||
type: 'code',
|
||||
lang: 'http',
|
||||
value: responseContent,
|
||||
};
|
||||
if (!parent || !('children' in parent) || index === undefined) {
|
||||
|
||||
29
src/execution/handlers/handlers.toc.ts
Normal file
29
src/execution/handlers/handlers.toc.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { toc } from 'mdast-util-toc';
|
||||
import { type ExecutionHandler } from '../execution.js';
|
||||
|
||||
const tocHandler: ExecutionHandler = ({
|
||||
addStep,
|
||||
node,
|
||||
root,
|
||||
parent,
|
||||
index,
|
||||
}) => {
|
||||
if (node.type === 'leafDirective' && node.name === 'toc') {
|
||||
addStep({
|
||||
type: 'toc',
|
||||
node,
|
||||
action: async () => {
|
||||
const result = toc(root, {
|
||||
tight: true,
|
||||
minDepth: 2,
|
||||
})
|
||||
if (!parent || !parent.children || index === undefined) {
|
||||
throw new Error('Parent node is not valid');
|
||||
}
|
||||
parent.children.splice(index, 1, result.map as any);
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export { tocHandler };
|
||||
@@ -6,6 +6,7 @@ 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,
|
||||
@@ -17,4 +18,8 @@ const handlers = [
|
||||
codeHandler,
|
||||
] satisfies ExecutionHandler[];
|
||||
|
||||
export { handlers };
|
||||
const postHandlers = [
|
||||
tocHandler,
|
||||
] satisfies ExecutionHandler[];
|
||||
|
||||
export { handlers, postHandlers };
|
||||
|
||||
29
src/theme/theme.html.ts
Normal file
29
src/theme/theme.html.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
const wrapBody = (body: string) => {
|
||||
return `<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/github-markdown-css/5.8.1/github-markdown.min.css" />
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Document</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
margin: 0;
|
||||
}
|
||||
.markdown-body {
|
||||
max-width: 800px;
|
||||
padding: 20px;
|
||||
margin: auto;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<article class="markdown-body">
|
||||
${body}
|
||||
</article>
|
||||
</body>
|
||||
</html>`;
|
||||
};
|
||||
|
||||
export { wrapBody };
|
||||
Reference in New Issue
Block a user