Installs into .claude/skills of the current project.
Are you the author of Sandbox Markdown?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/nodetool-ai-sandbox-markdown)
---
name: sandbox-markdown
description: Parse markdown into a token stream inside a Code node or CodeAct action, with marked's lexer running in the guest
---
# Markdown in the sandbox
Specifier: `@nodetool-ai/sandbox-markdown`. One module, the `marked` root
export. Import it at the top of the body.
`marked` is pure JavaScript — no Node builtins, no DOM — so it compiles
straight into the QuickJS guest. There is no pre-built "extract headers"
helper; `marked.lexer` gives you the token stream and you read what you need
out of it.
## lexer — markdown text to tokens
```js
import { marked } from "@nodetool-ai/sandbox-markdown";
const tokens = marked.lexer(inputs.markdown);
return { tokenCount: tokens.length };
```
Each token carries a `type` (`heading`, `paragraph`, `list`, `code`, `table`,
`link` tokens nested inside `paragraph`/`text` tokens, etc.) and the fields
that type needs.
## Headers → a document outline
```js
import { marked } from "@nodetool-ai/sandbox-markdown";
const headers = marked.lexer(inputs.markdown)
.filter((t) => t.type === "heading")
.map((t) => ({ level: t.depth, text: t.text }));
return { headers };
```
## Links → a flat list
`marked` nests inline tokens (links, emphasis) inside block tokens. Walk
`tokens.links` for a document's reference-style link definitions, or walk each
block's `.tokens` for inline links:
```js
import { marked } from "@nodetool-ai/sandbox-markdown";
function collectLinks(tokens, out = []) {
for (const t of tokens) {
if (t.type === "link") out.push({ href: t.href, text: t.text });
if (t.tokens) collectLinks(t.tokens, out);
if (t.items) collectLinks(t.items, out);
}
return out;
}
const links = collectLinks(marked.lexer(inputs.markdown));
return { links };
```
## Lists → nested items
```js
import { marked } from "@nodetool-ai/sandbox-markdown";
const lists = marked.lexer(inputs.markdown)
.filter((t) => t.type === "list")
.map((t) => ({
ordered: t.ordered,
items: t.items.map((item) => item.text)
}));
return { lists };
```
## Code blocks → language + source
```js
import { marked } from "@nodetool-ai/sandbox-markdown";
const codeBlocks = marked.lexer(inputs.markdown)
.filter((t) => t.type === "code")
.map((t) => ({ language: t.lang || "text", code: t.text }));
return { codeBlocks };
```
## Tables → rows of records
```js
import { marked } from "@nodetool-ai/sandbox-markdown";
const tables = marked.lexer(inputs.markdown)
.filter((t) => t.type === "table")
.map((t) => {
const headers = t.header.map((cell) => cell.text);
return t.rows.map((row) =>
Object.fromEntries(row.map((cell, i) => [headers[i], cell.text]))
);
});
return { tables };
```
## Markdown → HTML
```js
import { marked } from "@nodetool-ai/sandbox-markdown";
const html = marked.parse(inputs.markdown);
return { html };
```
## Gotchas
- **Everything runs in the guest.** The 64 MB guest heap holds your input text,
the token tree, and the returned value at once.
- **`marked.lexer` is synchronous**, unlike `marked.parse` (which can be async
when you register an async extension — the default configuration is not).
- **Token shapes differ by type.** A `list` token's items are themselves
tokens with their own `.tokens`; a `table` token has `header`/`rows`, not a
flat `cells` array. Log a sample token when a shape is unclear.
- **This pack is the only route to markdown parsing.** There is no
`data.parseMarkdown` global; every library the sandbox offers is an
importable module, and this is the one for markdown.