refact: ink based terminal view (#17)

This commit is contained in:
Morten Olsen
2025-05-19 16:22:59 +02:00
committed by GitHub
parent 4514972880
commit 11f76a7378
17 changed files with 772 additions and 116 deletions

View File

@@ -51,7 +51,7 @@ Create a file named `example.md`:
::raw-md[./examples/getting-started.md]
### Rendering Documents
### Rendering Document
You have two primary ways to render your `http.md` file:

View File

@@ -28,9 +28,11 @@
"@pnpm/find-workspace-packages": "^6.0.9",
"@types/blessed": "^0.1.25",
"@types/chai": "^5.2.2",
"@types/ink": "^2.0.3",
"@types/marked-terminal": "^6.1.1",
"@types/mdast": "^4.0.4",
"@types/node": "^22.15.18",
"@types/react": "^18.3.12",
"@types/terminal-kit": "^2.5.7",
"tsx": "^4.19.4",
"typescript": "^5.8.3"
@@ -44,11 +46,13 @@
"eventemitter3": "^5.0.1",
"handlebars": "^4.7.8",
"hastscript": "^9.0.1",
"ink": "^5.2.1",
"marked": "^15.0.11",
"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",
"react": "^18.3.1",
"rehype-stringify": "^10.0.1",
"remark-behead": "^3.1.0",
"remark-directive": "^4.0.0",
@@ -59,6 +63,7 @@
"terminal-kit": "^3.1.2",
"unified": "^11.0.5",
"unist-util-visit": "^5.0.0",
"wrap-ansi": "^9.0.0",
"yaml": "^2.8.0"
}
}

337
pnpm-lock.yaml generated
View File

@@ -32,6 +32,9 @@ importers:
hastscript:
specifier: ^9.0.1
version: 9.0.1
ink:
specifier: ^5.2.1
version: 5.2.1(@types/react@18.3.21)(react@18.3.1)
marked:
specifier: ^15.0.11
version: 15.0.11
@@ -47,6 +50,9 @@ importers:
mdast-util-toc:
specifier: ^7.1.0
version: 7.1.0
react:
specifier: ^18.3.1
version: 18.3.1
rehype-stringify:
specifier: ^10.0.1
version: 10.0.1
@@ -77,6 +83,9 @@ importers:
unist-util-visit:
specifier: ^5.0.0
version: 5.0.0
wrap-ansi:
specifier: ^9.0.0
version: 9.0.0
yaml:
specifier: ^2.8.0
version: 2.8.0
@@ -90,6 +99,9 @@ importers:
'@types/chai':
specifier: ^5.2.2
version: 5.2.2
'@types/ink':
specifier: ^2.0.3
version: 2.0.3(@types/react@18.3.21)(react@18.3.1)
'@types/marked-terminal':
specifier: ^6.1.1
version: 6.1.1
@@ -99,6 +111,9 @@ importers:
'@types/node':
specifier: ^22.15.18
version: 22.15.18
'@types/react':
specifier: ^18.3.12
version: 18.3.21
'@types/terminal-kit':
specifier: ^2.5.7
version: 2.5.7
@@ -111,6 +126,10 @@ importers:
packages:
'@alcalzone/ansi-tokenize@0.1.3':
resolution: {integrity: sha512-3yWxPTq3UQ/FY9p1ErPxIyfT64elWaMvM9lIHnaqpyft63tkxodF5aUElYHrdisWve5cETkh1+KBw1yJuW0aRw==}
engines: {node: '>=14.13.1'}
'@babel/code-frame@7.27.1':
resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==}
engines: {node: '>=6.9.0'}
@@ -455,6 +474,10 @@ packages:
'@types/hast@3.0.4':
resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==}
'@types/ink@2.0.3':
resolution: {integrity: sha512-DYKIKEJqhsGfQ/jgX0t9BzfHmBJ/9dBBT2MDsHAQRAfOPhEe7LZm5QeNBx1J34/e108StCPuJ3r4bh1y38kCJA==}
deprecated: This is a stub types definition. ink provides its own type definitions, so you do not need this installed.
'@types/marked-terminal@6.1.1':
resolution: {integrity: sha512-DfoUqkmFDCED7eBY9vFUhJ9fW8oZcMAK5EwRDQ9drjTbpQa+DnBTQQCwWhTFVf4WsZ6yYcJTI8D91wxTWXRZZQ==}
@@ -470,6 +493,12 @@ packages:
'@types/node@22.15.18':
resolution: {integrity: sha512-v1DKRfUdyW+jJhZNEI1PYy29S2YRxMV5AOO/x/SjKmW0acCIOqmbj6Haf9eHAhsPmrhlHSxEhv/1WszcLWV4cg==}
'@types/prop-types@15.7.14':
resolution: {integrity: sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==}
'@types/react@18.3.21':
resolution: {integrity: sha512-gXLBtmlcRJeT09/sI4PxVwyrku6SaNUj/6cMubjE6T6XdY1fDmBL7r0nX0jbSZPU/Xr0KuwLLZh6aOYY5d91Xw==}
'@types/ssri@7.1.5':
resolution: {integrity: sha512-odD/56S3B51liILSk5aXJlnYt99S6Rt9EFDDqGtJM26rKHApHcwyU/UoYHrzKkdkHMAIquGWCuHtQTbes+FRQw==}
@@ -522,6 +551,10 @@ packages:
resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
engines: {node: '>=8'}
ansi-styles@6.2.1:
resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==}
engines: {node: '>=12'}
any-promise@1.3.0:
resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==}
@@ -538,6 +571,10 @@ packages:
resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==}
engines: {node: '>=12'}
auto-bind@5.0.1:
resolution: {integrity: sha512-ooviqdwwgfIfNmDwo94wlshcdzfO64XV0Cg6oDsDYBJfITDz1EngD2z7DkbvCWn+XIMsIqW27sEVF6qcpJrRcg==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
bail@2.0.2:
resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==}
@@ -619,10 +656,18 @@ packages:
resolution: {integrity: sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw==}
engines: {node: '>=6'}
cli-boxes@3.0.0:
resolution: {integrity: sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==}
engines: {node: '>=10'}
cli-columns@4.0.0:
resolution: {integrity: sha512-XW2Vg+w+L9on9wtwKpyzluIPCWXjaBahI7mTcYjx+BVIYD9c3yqcv/yKC7CmdCZat4rq2yiE1UMSJC5ivKfMtQ==}
engines: {node: '>= 10'}
cli-cursor@4.0.0:
resolution: {integrity: sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
cli-highlight@2.1.11:
resolution: {integrity: sha512-9KDcoEVwyUXrjcJNvHD0NFc/hiwe/WPVYIleQh2O1N2Zro5gWJZ/K+3DGn8w8P/F6FxOgzyC5bxDyHIgCSPhGg==}
engines: {node: '>=8.0.0', npm: '>=5.0.0'}
@@ -632,6 +677,10 @@ packages:
resolution: {integrity: sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==}
engines: {node: 10.* || >= 12.*}
cli-truncate@4.0.0:
resolution: {integrity: sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==}
engines: {node: '>=18'}
cliui@7.0.4:
resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==}
@@ -639,6 +688,10 @@ packages:
resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==}
engines: {node: '>=0.8'}
code-excerpt@4.0.0:
resolution: {integrity: sha512-xxodCmBen3iy2i0WtAK8FlFNrRzjUqjRsMfho58xT/wvZU1YTM3fCnRjcy1gJPMepaRlgm/0e6w8SpWHpn3/cA==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
color-convert@2.0.1:
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
engines: {node: '>=7.0.0'}
@@ -656,6 +709,10 @@ packages:
config-chain@1.1.13:
resolution: {integrity: sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==}
convert-to-spaces@2.0.1:
resolution: {integrity: sha512-rcQ1bsQO9799wq24uE5AM2tAILy4gXGIK/njFWcVQkGNZ96edlpY+A7bjwvzjYvLDyzmG1MmMLZhpcsb+klNMQ==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
cross-spawn@7.0.6:
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
engines: {node: '>= 8'}
@@ -664,6 +721,9 @@ packages:
resolution: {integrity: sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==}
engines: {node: '>=8'}
csstype@3.1.3:
resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
cwise-compiler@1.1.3:
resolution: {integrity: sha512-WXlK/m+Di8DMMcCjcWr4i+XzcQra9eCdXIJrgh4TUgh0pIS/yJduLxS9JgefsHJ/YVLdgPtXm9r62W92MvanEQ==}
@@ -704,6 +764,9 @@ packages:
resolution: {integrity: sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==}
engines: {node: '>=12'}
emoji-regex@10.4.0:
resolution: {integrity: sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==}
emoji-regex@8.0.0:
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
@@ -717,6 +780,9 @@ packages:
error-ex@1.3.2:
resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==}
es-toolkit@1.38.0:
resolution: {integrity: sha512-OT3AxczYYd3W50bCj4V0hKoOAfqIy9tof0leNQYekEDxVKir3RTVTJOLij7VAe6fsCNsGhC0JqIkURpMXTCSEA==}
esbuild@0.25.4:
resolution: {integrity: sha512-8pgjLUcUjcgDg+2Q4NYXnPbo/vncAY4UmyaCm0jZevERqCHZIaWwdJHkf8XQtu4AxSKCdvrUbT0XUr1IdZzI8Q==}
engines: {node: '>=18'}
@@ -726,6 +792,10 @@ packages:
resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==}
engines: {node: '>=6'}
escape-string-regexp@2.0.0:
resolution: {integrity: sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==}
engines: {node: '>=8'}
escape-string-regexp@4.0.0:
resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==}
engines: {node: '>=10'}
@@ -770,6 +840,10 @@ packages:
resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==}
engines: {node: 6.* || 8.* || >= 10.*}
get-east-asian-width@1.3.0:
resolution: {integrity: sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==}
engines: {node: '>=18'}
get-source@2.0.12:
resolution: {integrity: sha512-X5+4+iD+HoSeEED+uwrQ07BOQr0kEDFMVqqpBuI+RaZBpBpHCuXxo70bjar6f0b0u/DQJsJ7ssurpP0V60Az+w==}
@@ -828,6 +902,10 @@ packages:
resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==}
engines: {node: '>=0.8.19'}
indent-string@5.0.0:
resolution: {integrity: sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==}
engines: {node: '>=12'}
individual@3.0.0:
resolution: {integrity: sha512-rUY5vtT748NMRbEMrTNiFfy29BgGZwGXUi2NFUVMWQrogSLzlJvQV9eeMWi+g1aVaQ53tpyLAQtd5x/JH0Nh1g==}
@@ -841,6 +919,19 @@ packages:
resolution: {integrity: sha512-it4HyVAUTKBc6m8e1iXWvXSTdndF7HbdN713+kvLrymxTaU4AUBWrJ4vEooP+V7fexnVD3LKcBshjGGPefSMUQ==}
engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0}
ink@5.2.1:
resolution: {integrity: sha512-BqcUyWrG9zq5HIwW6JcfFHsIYebJkWWb4fczNah1goUO0vv5vneIlfwuS85twyJ5hYR/y18FlAYUxrO9ChIWVg==}
engines: {node: '>=18'}
peerDependencies:
'@types/react': '>=18.0.0'
react: '>=18.0.0'
react-devtools-core: ^4.19.1
peerDependenciesMeta:
'@types/react':
optional: true
react-devtools-core:
optional: true
iota-array@1.0.0:
resolution: {integrity: sha512-pZ2xT+LOHckCatGQ3DcG/a+QuEqvoxqkiL7tvE8nn3uuu+f6i1TtpB5/FtWFbxUuVr5PZCx8KskuGatbJDXOWA==}
@@ -867,6 +958,14 @@ packages:
resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==}
engines: {node: '>=8'}
is-fullwidth-code-point@4.0.0:
resolution: {integrity: sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==}
engines: {node: '>=12'}
is-fullwidth-code-point@5.0.0:
resolution: {integrity: sha512-OVa3u9kkBbw7b8Xw5F9P+D/T9X+Z4+JruYVNapTjPYZYUznQ5YfWeFkOj606XYYW8yugTfC8Pj0hYqvi4ryAhA==}
engines: {node: '>=18'}
is-glob@4.0.3:
resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
engines: {node: '>=0.10.0'}
@@ -874,6 +973,11 @@ packages:
is-hexadecimal@2.0.1:
resolution: {integrity: sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==}
is-in-ci@1.0.0:
resolution: {integrity: sha512-eUuAjybVTHMYWm/U+vBO1sY/JOCgoPCXRxzdju0K+K0BiGW0SChEL1MLC0PoCIR1OlPo5YAp8HuQoUlsWEICwg==}
engines: {node: '>=18'}
hasBin: true
is-number@7.0.0:
resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
engines: {node: '>=0.12.0'}
@@ -939,6 +1043,10 @@ packages:
longest-streak@3.1.0:
resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==}
loose-envify@1.4.0:
resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==}
hasBin: true
loupe@3.1.3:
resolution: {integrity: sha512-kkIp7XSkP78ZxJEsSxW3712C6teJVoeHHwgo9zJ380de7IYyJ2ISlxojcH2pC5OFLewESmnRi/+XCDIEEVyoug==}
@@ -1210,6 +1318,10 @@ packages:
parse5@6.0.1:
resolution: {integrity: sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==}
patch-console@2.0.0:
resolution: {integrity: sha512-0YNdUceMdaQwoKce1gatDScmMo5pu/tfABfnzEqeG0gtTmd7mh/WcwgUjtAeOU7N8nFFlbQBnFK2gXW5fGvmMA==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
path-absolute@1.0.1:
resolution: {integrity: sha512-gds5iRhSeOcDtj8gfWkRHLtZKTPsFVuh7utbjYtvnclw4XM+ffRzJrwqMhOD1PVqef7nBLmgsu1vIujjvAJrAw==}
engines: {node: '>=4'}
@@ -1264,6 +1376,16 @@ packages:
resolution: {integrity: sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==}
engines: {node: '>=8'}
react-reconciler@0.29.2:
resolution: {integrity: sha512-zZQqIiYgDCTP/f1N/mAR10nJGrPD2ZR+jDSEsKWJHYC7Cm2wodlwbR3upZRdC3cjIjSlTLNVyO7Iu0Yy7t2AYg==}
engines: {node: '>=0.10.0'}
peerDependencies:
react: ^18.3.1
react@18.3.1:
resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==}
engines: {node: '>=0.10.0'}
read-ini-file@4.0.0:
resolution: {integrity: sha512-zz4qv/sKETv7nAkATqSJ9YMbKD8NXRPuA8d17VdYCuNYrVstB1S6UAMU6aytf5vRa9MESbZN7jLZdcmrOxz4gg==}
engines: {node: '>=14.6'}
@@ -1309,6 +1431,10 @@ packages:
resolve-pkg-maps@1.0.0:
resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==}
restore-cursor@4.0.0:
resolution: {integrity: sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
reusify@1.1.0:
resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==}
engines: {iojs: '>=1.0.0', node: '>=0.10.0'}
@@ -1331,6 +1457,9 @@ packages:
resolution: {integrity: sha512-vdTshSQ2JsRCgT8eKZWNJIL26C6bVqy1SOmuCMlKHegVeo8KYRobRrefOdUq9OozSPUUiSxrylteeRmLOMFfWg==}
engines: {node: '>=12'}
scheduler@0.23.2:
resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==}
semver@7.7.2:
resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==}
engines: {node: '>=10'}
@@ -1362,6 +1491,14 @@ packages:
resolution: {integrity: sha512-kUMbT1oBJCpgrnKoSr0o6wPtvRWT9W9UKvGLwfJYO2WuahZRHOpEyL1ckyMGgMWh0UdpmaoFqKKD29WTomNEGA==}
engines: {node: '>=8'}
slice-ansi@5.0.0:
resolution: {integrity: sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==}
engines: {node: '>=12'}
slice-ansi@7.1.0:
resolution: {integrity: sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg==}
engines: {node: '>=18'}
sort-keys@4.2.0:
resolution: {integrity: sha512-aUYIEU/UviqPgc8mHR6IW1EGxkAXpeRETYcrzg8cLAvUPZcpAlleSXHV2mY7G12GphSH6Gzv+4MMVSSkbdteHg==}
engines: {node: '>=8'}
@@ -1376,6 +1513,10 @@ packages:
split2@3.2.2:
resolution: {integrity: sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg==}
stack-utils@2.0.6:
resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==}
engines: {node: '>=10'}
stacktracey@2.1.8:
resolution: {integrity: sha512-Kpij9riA+UNg7TnphqjH7/CzctQ/owJGNbFkfEeve4Z4uxT5+JapVLFXcsurIfN34gnTWZNJ/f7NMG0E8JDzTw==}
@@ -1391,6 +1532,10 @@ packages:
resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
engines: {node: '>=8'}
string-width@7.2.0:
resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==}
engines: {node: '>=18'}
string_decoder@1.3.0:
resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==}
@@ -1401,6 +1546,10 @@ packages:
resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
engines: {node: '>=8'}
strip-ansi@7.1.0:
resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==}
engines: {node: '>=12'}
strip-bom@4.0.0:
resolution: {integrity: sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==}
engines: {node: '>=8'}
@@ -1464,6 +1613,10 @@ packages:
resolution: {integrity: sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==}
engines: {node: '>=8'}
type-fest@4.41.0:
resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==}
engines: {node: '>=16'}
typescript@5.8.3:
resolution: {integrity: sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==}
engines: {node: '>=14.17'}
@@ -1563,6 +1716,10 @@ packages:
resolution: {integrity: sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg==}
engines: {node: '>=8'}
widest-line@5.0.0:
resolution: {integrity: sha512-c9bZp7b5YtRj2wOe6dlj32MK+Bx/M/d+9VB2SHM1OtsUHR0aV0tdP6DWh/iMt0kWi1t5g1Iudu6hQRNd1A4PVA==}
engines: {node: '>=18'}
wordwrap@1.0.0:
resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==}
@@ -1570,6 +1727,10 @@ packages:
resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==}
engines: {node: '>=10'}
wrap-ansi@9.0.0:
resolution: {integrity: sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==}
engines: {node: '>=18'}
write-file-atomic@5.0.1:
resolution: {integrity: sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==}
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
@@ -1578,6 +1739,18 @@ packages:
resolution: {integrity: sha512-FdNA4RyH1L43TlvGG8qOMIfcEczwA5ij+zLXUy3Z83CjxhLvcV7/Q/8pk22wnCgYw7PJhtK+7lhO+qqyT4NdvQ==}
engines: {node: '>=16.14'}
ws@8.18.2:
resolution: {integrity: sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==}
engines: {node: '>=10.0.0'}
peerDependencies:
bufferutil: ^4.0.1
utf-8-validate: '>=5.0.2'
peerDependenciesMeta:
bufferutil:
optional: true
utf-8-validate:
optional: true
y18n@5.0.8:
resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==}
engines: {node: '>=10'}
@@ -1595,11 +1768,19 @@ packages:
resolution: {integrity: sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==}
engines: {node: '>=10'}
yoga-layout@3.2.1:
resolution: {integrity: sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ==}
zwitch@2.0.4:
resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==}
snapshots:
'@alcalzone/ansi-tokenize@0.1.3':
dependencies:
ansi-styles: 6.2.1
is-fullwidth-code-point: 4.0.0
'@babel/code-frame@7.27.1':
dependencies:
'@babel/helper-validator-identifier': 7.27.1
@@ -1965,6 +2146,16 @@ snapshots:
dependencies:
'@types/unist': 3.0.3
'@types/ink@2.0.3(@types/react@18.3.21)(react@18.3.1)':
dependencies:
ink: 5.2.1(@types/react@18.3.21)(react@18.3.1)
transitivePeerDependencies:
- '@types/react'
- bufferutil
- react
- react-devtools-core
- utf-8-validate
'@types/marked-terminal@6.1.1':
dependencies:
'@types/cardinal': 2.1.1
@@ -1984,6 +2175,13 @@ snapshots:
dependencies:
undici-types: 6.21.0
'@types/prop-types@15.7.14': {}
'@types/react@18.3.21':
dependencies:
'@types/prop-types': 15.7.14
csstype: 3.1.3
'@types/ssri@7.1.5':
dependencies:
'@types/node': 22.15.18
@@ -2031,6 +2229,8 @@ snapshots:
dependencies:
color-convert: 2.0.1
ansi-styles@6.2.1: {}
any-promise@1.3.0: {}
archy@1.0.0: {}
@@ -2043,6 +2243,8 @@ snapshots:
assertion-error@2.0.1: {}
auto-bind@5.0.1: {}
bail@2.0.2: {}
better-path-resolve@1.0.0:
@@ -2118,11 +2320,17 @@ snapshots:
cli-boxes@2.2.1: {}
cli-boxes@3.0.0: {}
cli-columns@4.0.0:
dependencies:
string-width: 4.2.3
strip-ansi: 6.0.1
cli-cursor@4.0.0:
dependencies:
restore-cursor: 4.0.0
cli-highlight@2.1.11:
dependencies:
chalk: 4.1.2
@@ -2138,6 +2346,11 @@ snapshots:
optionalDependencies:
'@colors/colors': 1.5.0
cli-truncate@4.0.0:
dependencies:
slice-ansi: 5.0.0
string-width: 7.2.0
cliui@7.0.4:
dependencies:
string-width: 4.2.3
@@ -2146,6 +2359,10 @@ snapshots:
clone@1.0.4: {}
code-excerpt@4.0.0:
dependencies:
convert-to-spaces: 2.0.1
color-convert@2.0.1:
dependencies:
color-name: 1.1.4
@@ -2161,6 +2378,8 @@ snapshots:
ini: 1.3.8
proto-list: 1.2.4
convert-to-spaces@2.0.1: {}
cross-spawn@7.0.6:
dependencies:
path-key: 3.1.1
@@ -2169,6 +2388,8 @@ snapshots:
crypto-random-string@2.0.0: {}
csstype@3.1.3: {}
cwise-compiler@1.1.3:
dependencies:
uniq: 1.0.1
@@ -2199,6 +2420,8 @@ snapshots:
dotenv@16.5.0: {}
emoji-regex@10.4.0: {}
emoji-regex@8.0.0: {}
emojilib@2.4.0: {}
@@ -2209,6 +2432,8 @@ snapshots:
dependencies:
is-arrayish: 0.2.1
es-toolkit@1.38.0: {}
esbuild@0.25.4:
optionalDependencies:
'@esbuild/aix-ppc64': 0.25.4
@@ -2239,6 +2464,8 @@ snapshots:
escalade@3.2.0: {}
escape-string-regexp@2.0.0: {}
escape-string-regexp@4.0.0: {}
escape-string-regexp@5.0.0: {}
@@ -2284,6 +2511,8 @@ snapshots:
get-caller-file@2.0.5: {}
get-east-asian-width@1.3.0: {}
get-source@2.0.12:
dependencies:
data-uri-to-buffer: 2.0.2
@@ -2354,6 +2583,8 @@ snapshots:
imurmurhash@0.1.4: {}
indent-string@5.0.0: {}
individual@3.0.0: {}
inherits@2.0.4: {}
@@ -2362,6 +2593,39 @@ snapshots:
ini@3.0.1: {}
ink@5.2.1(@types/react@18.3.21)(react@18.3.1):
dependencies:
'@alcalzone/ansi-tokenize': 0.1.3
ansi-escapes: 7.0.0
ansi-styles: 6.2.1
auto-bind: 5.0.1
chalk: 5.4.1
cli-boxes: 3.0.0
cli-cursor: 4.0.0
cli-truncate: 4.0.0
code-excerpt: 4.0.0
es-toolkit: 1.38.0
indent-string: 5.0.0
is-in-ci: 1.0.0
patch-console: 2.0.0
react: 18.3.1
react-reconciler: 0.29.2(react@18.3.1)
scheduler: 0.23.2
signal-exit: 3.0.7
slice-ansi: 7.1.0
stack-utils: 2.0.6
string-width: 7.2.0
type-fest: 4.41.0
widest-line: 5.0.0
wrap-ansi: 9.0.0
ws: 8.18.2
yoga-layout: 3.2.1
optionalDependencies:
'@types/react': 18.3.21
transitivePeerDependencies:
- bufferutil
- utf-8-validate
iota-array@1.0.0: {}
is-alphabetical@2.0.1: {}
@@ -2381,12 +2645,20 @@ snapshots:
is-fullwidth-code-point@3.0.0: {}
is-fullwidth-code-point@4.0.0: {}
is-fullwidth-code-point@5.0.0:
dependencies:
get-east-asian-width: 1.3.0
is-glob@4.0.3:
dependencies:
is-extglob: 2.1.1
is-hexadecimal@2.0.1: {}
is-in-ci@1.0.0: {}
is-number@7.0.0: {}
is-plain-obj@2.1.0: {}
@@ -2432,6 +2704,10 @@ snapshots:
longest-streak@3.1.0: {}
loose-envify@1.4.0:
dependencies:
js-tokens: 4.0.0
loupe@3.1.3: {}
map-age-cleaner@0.1.3:
@@ -2906,6 +3182,8 @@ snapshots:
parse5@6.0.1: {}
patch-console@2.0.0: {}
path-absolute@1.0.1: {}
path-key@3.1.1: {}
@@ -2940,6 +3218,16 @@ snapshots:
quick-lru@4.0.1: {}
react-reconciler@0.29.2(react@18.3.1):
dependencies:
loose-envify: 1.4.0
react: 18.3.1
scheduler: 0.23.2
react@18.3.1:
dependencies:
loose-envify: 1.4.0
read-ini-file@4.0.0:
dependencies:
ini: 3.0.1
@@ -3019,6 +3307,11 @@ snapshots:
resolve-pkg-maps@1.0.0: {}
restore-cursor@4.0.0:
dependencies:
onetime: 5.1.2
signal-exit: 3.0.7
reusify@1.1.0: {}
right-pad@1.1.1: {}
@@ -3039,6 +3332,10 @@ snapshots:
execa: 5.1.1
path-name: 1.0.0
scheduler@0.23.2:
dependencies:
loose-envify: 1.4.0
semver@7.7.2: {}
setimmediate@1.0.5: {}
@@ -3061,6 +3358,16 @@ snapshots:
dependencies:
unicode-emoji-modifier-base: 1.0.0
slice-ansi@5.0.0:
dependencies:
ansi-styles: 6.2.1
is-fullwidth-code-point: 4.0.0
slice-ansi@7.1.0:
dependencies:
ansi-styles: 6.2.1
is-fullwidth-code-point: 5.0.0
sort-keys@4.2.0:
dependencies:
is-plain-obj: 2.1.0
@@ -3073,6 +3380,10 @@ snapshots:
dependencies:
readable-stream: 3.6.2
stack-utils@2.0.6:
dependencies:
escape-string-regexp: 2.0.0
stacktracey@2.1.8:
dependencies:
as-table: 1.0.55
@@ -3091,6 +3402,12 @@ snapshots:
is-fullwidth-code-point: 3.0.0
strip-ansi: 6.0.1
string-width@7.2.0:
dependencies:
emoji-regex: 10.4.0
get-east-asian-width: 1.3.0
strip-ansi: 7.1.0
string_decoder@1.3.0:
dependencies:
safe-buffer: 5.2.1
@@ -3104,6 +3421,10 @@ snapshots:
dependencies:
ansi-regex: 5.0.1
strip-ansi@7.1.0:
dependencies:
ansi-regex: 6.1.0
strip-bom@4.0.0: {}
strip-comments-strings@1.2.0: {}
@@ -3165,6 +3486,8 @@ snapshots:
type-fest@0.6.0: {}
type-fest@4.41.0: {}
typescript@5.8.3: {}
uglify-js@3.19.3:
@@ -3289,6 +3612,10 @@ snapshots:
dependencies:
string-width: 4.2.3
widest-line@5.0.0:
dependencies:
string-width: 7.2.0
wordwrap@1.0.0: {}
wrap-ansi@7.0.0:
@@ -3297,6 +3624,12 @@ snapshots:
string-width: 4.2.3
strip-ansi: 6.0.1
wrap-ansi@9.0.0:
dependencies:
ansi-styles: 6.2.1
string-width: 7.2.0
strip-ansi: 7.1.0
write-file-atomic@5.0.1:
dependencies:
imurmurhash: 0.1.4
@@ -3307,6 +3640,8 @@ snapshots:
js-yaml: 4.1.0
write-file-atomic: 5.0.1
ws@8.18.2: {}
y18n@5.0.8: {}
yaml@2.8.0: {}
@@ -3323,4 +3658,6 @@ snapshots:
y18n: 5.0.8
yargs-parser: 20.2.9
yoga-layout@3.2.1: {}
zwitch@2.0.4: {}

View File

@@ -1,13 +1,14 @@
import { program } from 'commander';
import { resolve } from 'node:path';
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';
import { loadInputFiles } from '../utils/input.js';
import { InvalidFormatError } from '../utils/errors.js';
import { renderUI, State } from './ui/ui.js';
import { Marked } from 'marked';
@@ -16,23 +17,27 @@ program
.argument('<name>', 'http.md file name')
.description('Run a http.md document')
.option('-w, --watch', 'watch for changes')
.option('-f, --file <file...>', 'input files (-f foo.js -f bar.json)')
.option('-i, --input <input...>', 'input variables (-i foo=bar -i baz=qux)')
.action(async (name, options) => {
const marked = new Marked();
marked.use(markedTerminal() as any);
const {
file: f = [],
watch = false,
input: i = [],
} = options;
const ui = new UI();
const input = Object.fromEntries(
const input = {
...Object.fromEntries(
i.map((item: string) => {
const [key, value] = item.split('=');
return [key, value];
})
);
),
...loadInputFiles(f),
};
const state = new State<any>({
markdown: 'Loading',
});
const filePath = resolve(process.cwd(), name);
const build = async () => {
@@ -43,8 +48,10 @@ program
context,
});
const markdown = await marked.parse(result.markdown);
ui.content = markdown;
state.setState({
error: result.error ? result.error instanceof Error ? result.error.message : result.error : undefined,
markdown: result.markdown,
});
return {
...result,
@@ -53,10 +60,7 @@ program
}
const result = await build();
ui.screen.key(['r'], () => {
build();
});
renderUI(state);
if (watch) {
const watcher = new Watcher();
@@ -75,23 +79,28 @@ 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('-f, --file <file...>', 'input files (-f foo.js -f bar.json)')
.option('--format <format>', 'output format (html, markdown)')
.option('-w, --watch', 'watch for changes')
.option('-i, --input <input...>', 'input variables (-i foo=bar -i baz=qux)')
.action(async (name, output, options) => {
const {
watch = false,
file: f = [],
input: i = [],
format = 'markdown',
} = options;
const input = Object.fromEntries(
const input = {
...Object.fromEntries(
i.map((item: string) => {
const [key, value] = item.split('=');
return [key, value];
})
);
),
...loadInputFiles(f),
}
const filePath = resolve(process.cwd(), name);
const build = async () => {
@@ -102,6 +111,10 @@ program
context,
});
if (result.error) {
console.error(result.error);
}
if (format === 'html') {
const marked = new Marked();
const html = await marked.parse(result.markdown);
@@ -109,7 +122,7 @@ program
} else if (format === 'markdown') {
await writeFile(output, result.markdown);
} else {
throw new Error('Invalid format');
throw new InvalidFormatError('Invalid format');
}
return {
...result,
@@ -119,6 +132,10 @@ program
const result = await build();
if (result.error && !watch) {
process.exit(1);
}
if (watch) {
const watcher = new Watcher();
watcher.watchFiles(Array.from(result.context.files));

View File

@@ -0,0 +1,55 @@
import React, { useMemo } from 'react';
import { Box, Text, useInput } from 'ink';
import { marked } from 'marked';
import TerminalRenderer from 'marked-terminal';
import wrapAnsi from 'wrap-ansi';
import os from 'os';
import { useTerminalHeight, useTerminalWidth } from '../hooks/terminal.js';
type ScrollableMarkdownProps = {
markdownContent: string;
};
const renderer = new TerminalRenderer({});
marked.setOptions({ renderer: renderer as any });
const ScrollableMarkdown = ({ markdownContent }: ScrollableMarkdownProps) => {
const [scrollPosition, setScrollPosition] = React.useState(0);
const terminalHeight = useTerminalHeight()
const terminalWidth = useTerminalWidth();
const rendered = useMemo(() => {
return marked(markdownContent) as string;
}, [markdownContent]);
const wrapped = useMemo(() => {
return wrapAnsi(rendered, terminalWidth, {
hard: true,
trim: false,
wordWrap: true,
}).split(os.EOL);
}, [rendered, terminalWidth]);
const buffer = useMemo(() => {
return wrapped.slice(scrollPosition, scrollPosition + terminalHeight).join(os.EOL);
}, [wrapped, scrollPosition, terminalHeight]);
useInput((input, key) => {
if (key.downArrow) {
setScrollPosition(prev => prev + 1);
}
if (key.upArrow) {
setScrollPosition(prev => Math.max(0, prev - 1));
}
if (key.escape) {
process.exit(0);
}
console.error('a', input, key);
});
return (
<Box flexDirection="column" flexShrink={1} flexGrow={1} overflow="hidden">
<Text>{buffer}</Text>
</Box>
);
};
export { ScrollableMarkdown };

View File

@@ -0,0 +1,39 @@
import { useEffect, useState } from "react"
const useTerminalWidth = () => {
const [width, setWidth] = useState(process.stdout.columns || 80);
useEffect(() => {
const handleResize = () => {
setWidth(process.stdout.columns);
};
process.stdout.on("resize", handleResize);
return () => {
process.stdout.off("resize", handleResize);
};
}, []);
return width;
}
const useTerminalHeight = () => {
const [height, setHeight] = useState(process.stdout.rows || 24);
useEffect(() => {
const handleResize = () => {
setHeight(process.stdout.rows);
};
process.stdout.on("resize", handleResize);
return () => {
process.stdout.off("resize", handleResize);
};
}, []);
return height;
}
export { useTerminalWidth, useTerminalHeight }

View File

@@ -0,0 +1,80 @@
import { EventEmitter } from 'eventemitter3';
import React, { createContext, useRef, useState, useSyncExternalStore } from "react";
type StateEvents = {
update: () => void;
}
class State<T = any> extends EventEmitter<StateEvents> {
state: T;
constructor(initialState: T) {
super();
this.state = initialState;
}
public get value() {
return this.state;
}
public setState = (state: T) => {
this.state = state;
this.emit('update');
}
public subscribe = (callback: () => void) => {
this.on('update', callback);
return () => {
this.off('update', callback);
};
}
}
type StateContextValue<T> = {
state: State<T>;
}
const StateContext = createContext<StateContextValue<any> | null>(null);
type StateProviderProps<T> = {
state: State<T>;
children: React.ReactNode;
}
const StateProvider = <T,>({ state, children }: StateProviderProps<T>) => {
return (
<StateContext.Provider value={{ state }}>
{children}
</StateContext.Provider>
);
};
const useStateContext = <T = any>() => {
const context = React.useContext(StateContext);
if (!context) {
throw new Error("useStateContext must be used within a StateProvider");
}
return context as StateContextValue<T>;
}
const useStateValue = <T = any>(
selector: (state: T) => any = (state) => state,
) => {
const context = useStateContext<T>();
const value = useRef<T>(selector(context.state.value));
useSyncExternalStore(
context.state.subscribe,
() => {
const next = selector(context.state.value);
if (next !== value.current) {
value.current = next;
}
return value.current;
},
() => value.current,
);
return value.current;
}
export { State, StateProvider, useStateContext, useStateValue };

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 };

69
src/cli/ui/ui.tsx Normal file
View File

@@ -0,0 +1,69 @@
import { Box, render, Text, useApp, useInput } from "ink"
import { ScrollableMarkdown } from './components/scrollable-markdown.js';
import { useStateValue, State, StateProvider } from './state/state.js';
import { useEffect, useState } from "react";
import { useTerminalHeight } from "./hooks/terminal.js";
const MarkdownView = () => {
const markdown = useStateValue((state) => state.markdown);
const error = useStateValue((state) => state.error);
return (
<Box flexGrow={1} flexDirection="column">
<Box flexDirection="column" flexGrow={1} overflow="hidden">
<ScrollableMarkdown markdownContent={markdown} />
</Box>
{error && (
<Box flexDirection="column" padding={1}>
<Text color="red">{error}</Text>
</Box>
)}
</Box>
);
}
const App = () => {
const { exit } = useApp();
const height = useTerminalHeight();
useInput((_input, key) => {
if (key.escape) {
process.exit(0);
}
});
useEffect(
() => {
const enterAltScreenCommand = "\x1b[?1049h";
const leaveAltScreenCommand = "\x1b[?1049l";
process.stdout.write(enterAltScreenCommand);
const onExit = () => {
exit();
process.stdout.write(leaveAltScreenCommand);
}
process.on("exit", onExit);
return () => {
process.stdout.write(leaveAltScreenCommand);
process.off("exit", onExit);
}
}, []
);
return (
<Box height={height} flexDirection="column">
<MarkdownView />
<Box>
<Text>Press esc to exit</Text>
</Box>
</Box>
)
}
const renderUI = (state: State) => {
render(
<StateProvider state={state}>
<App />
</StateProvider>
);
}
export { renderUI, State }

View File

@@ -51,6 +51,7 @@ type ExexutionExecuteOptions = {
}
const execute = async (file: string, options: ExexutionExecuteOptions) => {
let error: unknown | undefined;
const { context } = options;
context.files.add(file);
const content = await readFile(file, 'utf-8');
@@ -94,6 +95,7 @@ const execute = async (file: string, options: ExexutionExecuteOptions) => {
});
for (const step of steps) {
try {
const { node, action } = step;
const options: ExecutionStepOptions = {
file,
@@ -103,11 +105,16 @@ const execute = async (file: string, options: ExexutionExecuteOptions) => {
root,
};
await action(options);
} catch (e) {
error = e;
break;
}
}
const markdown = parser.stringify(root);
return {
error,
root,
markdown,
};

View File

@@ -1,5 +1,6 @@
import { toString } from 'mdast-util-to-string';
import { type ExecutionHandler } from '../execution.js';
import { ParsingError, RequiredError } from '../../utils/errors.js';
const inputHandler: ExecutionHandler = ({
addStep,
@@ -15,7 +16,7 @@ const inputHandler: ExecutionHandler = ({
const name = toString(node);
if (node.attributes?.required === '' && context.input[name] === undefined) {
throw new Error(`Input "${name}" is required`);
throw new RequiredError(name);
}
if (node.attributes?.default !== undefined && context.input[name] === undefined) {
@@ -27,7 +28,7 @@ const inputHandler: ExecutionHandler = ({
if (format === 'number') {
context.input[name] = Number(context.input[name]);
if (context.input[name] !== undefined && isNaN(Number(context.input[name]))) {
throw new Error(`Input "${name}" must be a number, but got "${context.input[name]}"`);
throw new ParsingError(`Input "${name}" must be a number, but got "${context.input[name]}"`);
}
}
if (format === 'boolean') {
@@ -40,14 +41,14 @@ const inputHandler: ExecutionHandler = ({
try {
context.input[name] = JSON.parse(String(context.input[name]));
} catch (error) {
throw new Error(`Input "${name}" must be a valid JSON, but got "${context.input[name]}"`);
throw new ParsingError(`Input "${name}" must be a valid JSON, but got "${context.input[name]}"`);
}
}
if (format === 'date') {
const date = new Date(context.input[name] as string);
if (isNaN(date.getTime())) {
throw new Error(`Input "${name}" must be a valid date, but got "${context.input[name]}"`);
throw new ParsingError(`Input "${name}" must be a valid date, but got "${context.input[name]}"`);
}
context.input[name] = date;
}

View File

@@ -2,6 +2,7 @@ import Handlebars from "handlebars";
import YAML from "yaml";
import { should, expect, assert } from 'chai';
import { ExecutionHandler } from "../execution.js";
import { ScriptError } from "../../utils/errors.js";
const javascriptHandler: ExecutionHandler = ({
node,
@@ -60,7 +61,7 @@ const javascriptHandler: ExecutionHandler = ({
meta: undefined,
});
}
throw error;
throw new ScriptError(error instanceof Error ? error.message : String(error))
}
}
if (options.hidden === true && parent && index !== undefined) {

View File

@@ -1,6 +1,8 @@
import { dirname, resolve } from 'path';
import { toString } from 'mdast-util-to-string'
import { execute, type ExecutionHandler } from '../execution.js';
import { FileNotFoundError } from '../../utils/errors.js';
import { existsSync } from 'fs';
const fileHandler: ExecutionHandler = ({
addStep,
@@ -18,8 +20,8 @@ const fileHandler: ExecutionHandler = ({
dirname(file),
toString(node)
);
if (!filePath) {
throw new Error('File path is required');
if (!existsSync(filePath)) {
throw new FileNotFoundError(filePath);
}
const { root: newRoot } = await execute(filePath, {
context,

View File

@@ -3,6 +3,8 @@ import { Context } from '../../context/context.js';
import { execute, type ExecutionHandler } from '../execution.js';
import { dirname, resolve } from 'path';
import { toString } from 'mdast-util-to-string';
import { FileNotFoundError } from '../../utils/errors.js';
import { existsSync } from 'fs';
const rawMdHandler: ExecutionHandler = ({
addStep,
@@ -20,6 +22,9 @@ const rawMdHandler: ExecutionHandler = ({
dirname(file),
toString(node),
);
if (!existsSync(name)) {
throw new FileNotFoundError(name);
}
const context = new Context({
input: {},
});

44
src/utils/errors.ts Normal file
View File

@@ -0,0 +1,44 @@
class BaseError extends Error {
constructor(message: string) {
super(message);
this.name = this.constructor.name;
}
}
class FileNotFoundError extends BaseError {
constructor(filePath: string) {
super(`File not found: ${filePath}`);
}
}
class InvalidFileError extends BaseError {
constructor(filePath: string) {
super(`Invalid file: ${filePath}`);
}
}
class InvalidFormatError extends BaseError {
constructor(format: string) {
super(`Invalid format: ${format}`);
}
}
class ScriptError extends BaseError {
constructor(message: string) {
super(`Script error: ${message}`);
}
}
class ParsingError extends BaseError {
constructor(message: string) {
super(`Parsing error: ${message}`);
}
}
class RequiredError extends BaseError {
constructor(name: string) {
super(`Required input "${name}" is missing`);
}
}
export { BaseError, FileNotFoundError, InvalidFileError, InvalidFormatError, ScriptError, ParsingError, RequiredError };

60
src/utils/input.ts Normal file
View File

@@ -0,0 +1,60 @@
import { extname } from "path";
import { FileNotFoundError, InvalidFileError } from "./errors.js";
import { existsSync } from "fs";
import YAML from "yaml";
import { readFile } from "fs/promises";
const loadJsonFile = async (filePath: string) => {
const content = await readFile(filePath, "utf-8");
return JSON.parse(content);
}
const loadYamlFile = async (filePath: string) => {
const content = await readFile(filePath, "utf-8");
return YAML.parse(content);
}
const loadJsFile = async (filePath: string) => {
const { default: content } = await import(filePath);
return content;
}
const loadInputFiles = async (filePaths: string[]) => {
let inputs: Record<string, unknown> = {};
for (const filePath of filePaths) {
const type = extname(filePath);
if (!existsSync(filePath)) {
throw new FileNotFoundError(filePath);
}
try {
switch (type) {
case ".json":
inputs = {
...inputs,
...(await loadJsonFile(filePath)),
};
break;
case ".yaml":
case ".yml":
inputs = {
...inputs,
...(await loadYamlFile(filePath)),
};
break;
case ".js":
inputs = {
...inputs,
...(await loadJsFile(filePath)),
};
break;
default:
throw new InvalidFileError(filePath);
}
} catch (error) {
throw new InvalidFileError(filePath);
}
}
}
export { loadInputFiles };

View File

@@ -10,7 +10,11 @@
"resolveJsonModule": true,
"allowSyntheticDefaultImports": true,
"skipLibCheck": true,
"outDir": "dist"
"outDir": "dist",
"jsx": "react-jsx"
},
"include": ["src/**/*.ts"]
"include": [
"src/**/*.ts",
"src/cli/ui/ui.tsx"
]
}