@jondotsoy/tmd
v0.0.9
Published
`tmd` scans fenced JS/TS code blocks in a Markdown file for annotation comments and rewrites them in place with the evaluated runtime value and/or the inferred TypeScript type — so the results in your documentation are always in sync with the code that pr
Downloads
2,732
Readme
trusted-markdown
tmd scans fenced JS/TS code blocks in a Markdown file for annotation
comments and rewrites them in place with the evaluated runtime value
and/or the inferred TypeScript type — so the results in your
documentation are always in sync with the code that produces them.
Table of contents
Install
bunx @jondotsoy/tmd <path>Or add it to a project:
bun add -d @jondotsoy/tmdUsage
tmd <path> [<path> ...] [--cwd <dir>] [--setup-block-keyword <name> ...] [--render-block-keyword <name> ...] [--help]Runs against one or more Markdown files, overwriting each in place with every annotation resolved:
tmd docs/one.md docs/two.md--cwd <dir>— directory relative imports (e.g. from asetup-blockpreamble) resolve from. Defaults to each file's own directory, so this is only needed to point at a different one — when given, it applies to every file passed.--setup-block-keyword <name>— recognizes an additional keyword as a hidden-preamble comment, alongside the built-inbefore-blockandsetup-block. Repeatable to register more than one. Handy whenbefore-block/setup-blockcollide with another tool's comment convention in your docs.--render-block-keyword <name>— recognizes an additional keyword as an eval-directive comment, alongside the built-inbefore-block-evalandrender-block. Repeatable.--help— prints usage and exits. Works anywhere in the arguments (tmd --help,tmd doc.md --help), even alongside other invalid flags — it always wins.
Annotation syntax
Inside a fenced ```js / ```ts block, annotate an
expression with a // ? const <expr> comment on its own line right
after the code it refers to — that reads better than trailing it on
the same line, especially once the resolved value gets long:
| Form | Resolves to |
| --- | --- |
| // ? const <expr> = | the evaluated runtime value |
| // ? const <expr>: | the inferred TypeScript type |
| // ? const <expr> : = | both, as <expr>: <type> = <value> |
Value
```ts
const a = 1 + 2
// ? const a =
```becomes
```ts
const a = 1 + 2
// ? const a = 3
```Type
```ts
const a = 1 + 2
// ? const a:
```becomes
```ts
const a = 1 + 2
// ? const a: number
```Both at once
```ts
const a = 1 + 2
// ? const a : =
```becomes
```ts
const a = 1 + 2
// ? const a: number = 3
```Whitespace
Whitespace around : and = is always optional — a=, a =, a:,
a :, a:=, a : =, a:= , etc. all work.
Trailing on the same line
The annotation doesn't have to be on its own line — it also works right after the code as a trailing comment, if that reads better for a short result:
```ts
const a = 1 + 2 // ? const a =
```becomes
```ts
const a = 1 + 2 // ? const a = 3
```Inside a nested scope
An annotation can sit inside a function body (or any other nested scope); it resolves against whatever invokes that code further down in the same block:
```ts
function sum(x: number, y: number) {
const total = x + y
// ? const total =
return total;
}
sum(1, 2);
```becomes
```ts
function sum(x: number, y: number) {
const total = x + y
// ? const total = 3
return total;
}
sum(1, 2);
```If the annotated line runs more than once (e.g. the function is
called twice), tmd can't tell which result to pick, so it leaves
that annotation unresolved instead of silently guessing — rewrite the
block so the line only runs once. This doesn't stop the rest of the
document from being processed: see Error handling
below.
Annotating a function itself
The annotated expression doesn't have to be a value produced inside a function — it can be the function itself, inspected after its declaration:
```ts
function foo() {
return 1 + 2;
}
// ? const foo =
```becomes
```ts
function foo() {
return 1 + 2;
}
// ? const foo = [Function: foo]
```Asking for the type instead (// ? const foo:) resolves to its
signature, () => number.
Multi-line values
When the evaluated value doesn't fit on one line, the first line stays
on the annotation and the rest are rendered as trailing // comment
lines so the block stays valid code:
```ts
const obj = { alpha: 1, beta: 2 }
// ? const obj =
```becomes
```ts
const obj = { alpha: 1, beta: 2 }
// ? const obj = {
// alpha: 1,
// beta: 2,
// }
```Hiding setup code
A <!-- setup-block ... --> comment (single or multi-line) directly
above a block makes its code available to that block's annotations
without showing it in the rendered block itself — handy for imports:
<!-- setup-block
import path from "node:path";
-->
```ts
const sep = path.sep
// ? const sep =
```becomes
<!-- setup-block
import path from "node:path";
-->
```ts
const sep = path.sep
// ? const sep = "/"
```before-block is an older, equivalent alias for setup-block — both
spellings behave identically and are supported side by side, so
existing docs keep working unchanged.
A plain <!-- ... --> comment without the setup-block/before-block
prefix is left untouched and has no effect on the block below it.
Need a different word entirely (e.g. init to match another tool's
convention)? Register it with --setup-block-keyword, which adds to
the built-in keywords rather than replacing them:
tmd docs/doc.md --setup-block-keyword init<!-- init
import path from "node:path";
-->Customizing how a value is rendered
By default a resolved value is rendered with Bun.inspect. Pass a
custom-inspect="<fn name>" attribute right after setup-block, on the
same line, to render it with a function of that name instead (defined
in the preamble itself, or otherwise in scope) — handy for a value that
needs a different presentation than Bun.inspect's default:
<!-- setup-block custom-inspect="foo"
const foo = (value: unknown) => [String(value)];
-->
```ts
const biz = 3
// ? const biz =
```becomes
<!-- setup-block custom-inspect="foo"
const foo = (value: unknown) => [String(value)];
-->
```ts
const biz = 3
// ? const biz = [ "3" ]
```Importing from another file
A setup-block preamble can also import from a file next to the
Markdown document, not just built-in/installed modules. Relative
imports resolve against the directory the Markdown file lives in,
regardless of where tmd is run from.
script.ts:
export function foo() {
return 1 + 2;
}
export class Foo {
foo = 1;
}doc.md:
<!-- setup-block
import { foo, Foo } from "./script.ts";
-->
```ts
const result = [foo, new Foo()]
// ? const result =
```becomes
<!-- setup-block
import { foo, Foo } from "./script.ts";
-->
```ts
const result = [foo, new Foo()]
// ? const result = [
// [Function: foo], Foo {
// foo: 1,
// }
// ]
```If the doc and its imports don't live next to each other, point
--cwd at the directory the imports should resolve from instead:
tmd docs/doc.md --cwd scripts/Filling a block from computed code
A <!-- render-block ... --> comment directly above a
```json or ```js block runs its code (which must
assign a result value) and replaces the whole block's content with
that result — handy for keeping a sample payload or config in sync
with the code that builds it, without hand-writing any annotations.
A json block is filled with JSON.stringify(result, null, 2):
<!-- render-block
const result = { a: 1, b: "two", c: [1, 2, 3] };
-->
```json
```becomes
<!-- render-block
const result = { a: 1, b: "two", c: [1, 2, 3] };
-->
```json
{
"a": 1,
"b": "two",
"c": [
1,
2,
3
]
}
```A yaml/yml block is filled with the yaml
package's stringify(result), straightening (properly indenting)
nested objects and arrays of any depth into a block-style document:
<!-- render-block
const result = {
a: 1,
b: "two",
nested: { c: [1, 2, 3], deeper: { d: 4 } },
};
-->
```yaml
```becomes
<!-- render-block
const result = {
a: 1,
b: "two",
nested: { c: [1, 2, 3], deeper: { d: 4 } },
};
-->
```yaml
a: 1
b: two
nested:
c:
- 1
- 2
- 3
deeper:
d: 4
```A js/javascript block is filled with
util.inspect(result, { depth: null }) instead, so it can hold values
JSON.stringify can't represent (functions, undefined, etc.). The
unlimited depth keeps a multi-level nested result fully expanded
rather than collapsing anything past util.inspect's default depth of
2 into [Object]:
<!-- render-block
const result = { a: 1, b: "two", c: [1, 2, 3] };
-->
```js
```becomes
<!-- render-block
const result = { a: 1, b: "two", c: [1, 2, 3] };
-->
```js
{
a: 1,
b: 'two',
c: [
1,
2,
3
]
}
```Declare a JS_INSPECT_DEEP variable in the directive's code to
override that default depth, e.g. to intentionally re-introduce
truncation on a very large structure:
<!-- render-block
const result = { a: { b: { c: { d: 1 } } } };
const JS_INSPECT_DEEP = 1;
-->
```js
```becomes
<!-- render-block
const result = { a: { b: { c: { d: 1 } } } };
const JS_INSPECT_DEEP = 1;
-->
```js
{
a: {
b: [Object]
}
}
```An inspect-depth="<n>" attribute right after render-block, on the
same line, sets the same depth directly instead — no code change
needed, and it takes precedence over JS_INSPECT_DEEP when both are
present:
<!-- render-block inspect-depth="1"
const result = { a: { b: { c: { d: 1 } } } };
-->
```js
```becomes
<!-- render-block inspect-depth="1"
const result = { a: { b: { c: { d: 1 } } } };
-->
```js
{
a: {
b: [Object]
}
}
```The same code-variable/attribute pair is available for three other
util.inspect options, each named after the option itself
(JS_INSPECT_MAX_ARRAY_LENGTH/inspect-max-array-length,
JS_INSPECT_MAX_STRING_LENGTH/inspect-max-string-length,
JS_INSPECT_COMPACT/inspect-compact). maxArrayLength and
maxStringLength default to null (unlimited) for the same reason as
depth; compact defaults to false (one entry per line) rather
than util.inspect's own default (3, which groups short entries
onto a single line) — easier to read and diff once nothing is being
silently truncated:
<!-- render-block inspect-max-array-length="2"
const result = [0, 1, 2, 3, 4];
-->
```js
```becomes
<!-- render-block inspect-max-array-length="2"
const result = [0, 1, 2, 3, 4];
-->
```js
[
0,
1,
... 3 more items
]
```compact defaulting to false means a small object like
{ a: 1, b: 2 } is filled one entry per line rather than grouped onto
a single line. inspect-compact accepts the same values
util.inspect's compact option does — true, false, or an
integer — to opt back into grouping:
<!-- render-block inspect-compact="true"
const result = { a: 1, b: 2 };
-->
```js
```becomes
<!-- render-block inspect-compact="true"
const result = { a: 1, b: 2 };
-->
```js
{ a: 1, b: 2 }
```Any other fence language (md, txt, sh, and so on) is filled with
the plain String(result):
<!-- render-block
const result = "hello";
-->
```txt
```becomes
<!-- render-block
const result = "hello";
-->
```txt
hello
```before-block-eval is an older, equivalent alias for render-block —
both spellings behave identically and are supported side by side, so
existing docs keep working unchanged.
Like setup-block, an extra keyword can be registered with
--render-block-keyword on top of the built-in ones:
tmd docs/doc.md --render-block-keyword computeRe-running tmd overwrites whatever was previously rendered in the
block with the current result, the same way annotations are
re-resolved on every run. Relative imports in the directive's code
resolve the same way as in a setup-block preamble (against the
Markdown file's directory, or --cwd).
Naming the result variable
By default render-block reads its result from a variable named
result. Pass a result-variable="<name>" attribute right after
render-block, on the same line, to use a different variable name
instead:
<!-- render-block result-variable="foo"
const foo = 3;
-->
```json
```becomes
<!-- render-block result-variable="foo"
const foo = 3;
-->
```json
3
```Customizing how the result is rendered
Like setup-block, a custom-inspect="<fn name>" attribute right
after render-block names a function (defined in the directive's code,
or otherwise in scope) used to render the result instead of the
default JSON.stringify/Bun.inspect — this overrides the block's
language-based default (json vs. js/javascript) entirely:
<!-- render-block custom-inspect="foo"
const result = { a: 1 };
const foo = (value) => [String(value)];
-->
```json
```becomes
<!-- render-block custom-inspect="foo"
const result = { a: 1 };
const foo = (value) => [String(value)];
-->
```json
[ "[object Object]" ]
```Supported blocks
render-block fills three fence languages with a dedicated renderer
(see Filling a block from computed code
for the full details); any other language falls back to String(result).
| Fence | Renderer | Notes |
| --- | --- | --- |
| ```json | JSON.stringify(result, null, 2) | |
| ```js / ```javascript | util.inspect(result, { depth: null }) | Can render values JSON.stringify can't (functions, undefined, etc.); depth and other util.inspect options are configurable. |
| ```yaml / ```yml | yaml's stringify(result) | Straightens nested objects/arrays into a block-style document. |
Error handling
A single block failing to resolve — an annotated expression that
throws (e.g. references an undefined variable), or a
render-block directive that does — never aborts the whole run.
tmd keeps processing every other block, leaves the failing block's
annotation or content exactly as it was in the input, and prints a
summary of every block it attempted once it's done:
docs/doc.md: 3 block(s) processed, 1 failed
ok lines 2-4
FAIL lines 7-9: ReferenceError: b is not defined
ok lines 12-14The process still exits with code 0 and the file is written with
every block that did resolve — re-run tmd after fixing the failing
block to pick it up.
Development
bun install
bun test