sandbox-markdown
GitHub在沙箱环境中将 Markdown 解析为令牌流或 HTML,支持提取标题、链接、列表、代码块和表格等结构信息。
Trigger Scenarios
Install
npx skills add nodetool-ai/nodetool --skill sandbox-markdown -g -y
SKILL.md
Frontmatter
{
"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
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
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:
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
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
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
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
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.lexeris synchronous, unlikemarked.parse(which can be async when you register an async extension — the default configuration is not).- Token shapes differ by type. A
listtoken's items are themselves tokens with their own.tokens; atabletoken hasheader/rows, not a flatcellsarray. Log a sample token when a shape is unclear. - This pack is the only route to markdown parsing. There is no
data.parseMarkdownglobal; every library the sandbox offers is an importable module, and this is the one for markdown.
Version History
- a6a7e57 Current 2026-08-20 09:52


