HTML↔Markdown conversion, GFM table normalization, and TOC generation.
Install
# from GitHub (first run asks for allowBuilds approval — follow the hint, retry)
dsh plugin --profile web add github:omdsh-dev/dsh-tool-markdown
Any plugin you install runs third-party code with your own permissions — it can read your files, use your credentials, and reach the network, and tool approvals don’t sandbox it. GitHub-sourced plugins also run build scripts at install time — pnpm blocks those until you allow them, so an install can stop with ERR_PNPM_GIT_DEP_PREPARE_NOT_ALLOWED or ERR_PNPM_IGNORED_BUILDS; dsh prints the exact key to add under allowBuilds in your profile’s pnpm-workspace.yaml, and the install works on the next run. Allowing a build is a trust decision: only install sources you trust, and pin a commit (github:owner/repo#sha).
README
DSH Markdown tool plugin — HTML↔Markdown conversion, GFM table normalization, table-of-contents generation. Zero dependencies, pure functions, hand-written lightweight parser.
Motivation
The most common document scenarios for models: the user pastes a block of HTML (web page source, email, exported file), pastes a table asking for cleanup, or asks to "convert to markdown". Without a tool, the model can only hard-code the handling — HTML entities go wrong, tables get mangled, and web noise (nav/scripts/ads) leaks in. This plugin provides deterministic conversion and complements dsh-tool-csv: csv handles structured tables, markdown handles documents/web pages.
Security model
- Zero dependencies: hand-written recursive-descent HTML parser (no cheerio/jsdom — they add tens of MB and are an attack surface)
- Zero execution surface: no eval, no new Function, no remote resource loading, no CSS layout parsing
- Content stripping: script/style/iframe/object/noscript contents are stripped entirely (security + noise)
- md2html whitelist: only outputs
p h1-h6 ul ol li blockquote pre code a img strong em br hr table thead tbody tr th td; all text is HTML-escaped — a<script>embedded in markdown is only displayed as text - Link scheme whitelist:
http/https/mailto;javascript:/data:links degrade to plain text - Resource limits: nesting deeper than 64 levels errors (prevents stack overflow); input
maxBytesdefaults to 256 KB with a hard cap of 1 MB (errors on exceeding, never truncates) - Tool arguments are recorded in session logs; do not pass in HTML containing secrets/session data
Tool declaration
Registers the markdown tool (@deepseek-ai/dsh-tool-markdown, row id tool-markdown) with unified text output.
| parameter | type | required | description |
|---|---|---|---|
action |
string | ✅ | html2md / md2html / table / toc |
html |
string | html2md input (fragment or full document) | |
markdown |
string | md2html/toc input | |
text |
string | table input (HTML <table> or pipe-delimited text) |
|
baseUrl |
string | Base for resolving relative href/src (naive join) | |
maxBytes |
integer | Input cap, default 256000, hard max 1000000 |
Actions
| action | function |
|---|---|
html2md |
HTML → GFM Markdown: full block/inline mapping, tables to GFM (colspan placeholder padding), entity decoding, script/style stripping, link scheme filtering (no readability article extraction — nav/header/footer pass through by default) |
md2html |
Markdown → whitelist-safe HTML: all text escaped, no tag can escape |
table |
HTML <table> or pipe-delimited text containing unescaped | → GFM table (column padding, | escaping, separator row detection; errors on input without pipes) |
toc |
Markdown headings → nested table-of-contents list (simplified GitHub-style anchors: duplicate headings get automatic -1/-2 suffixes, fake headings inside code fences are skipped) |
Examples
markdown { action: "html2md", html: "<h1>标题</h1><p>你好 <b>世界</b></p>" }
→ # 标题\n\n你好 **世界**
markdown { action: "md2html", markdown: "[x](javascript:alert(1))" }
→ <p>x</p> ← javascript: 链接降级为纯文本
markdown { action: "table", text: "a|b\n1|2" }
→ | a | b |\n| --- | --- |\n| 1 | 2 |
markdown { action: "toc", markdown: "# 标题一\n## 小节" }
→ - [标题一](#标题一)\n - [小节](#小节)
Edge-case behavior
| case | handling |
|---|---|
| Unclosed tags | Auto-closed at EOF (consistent with browsers); auto-closed on block-level elements inside <p> |
| Case | Tag/attribute names normalized to lowercase |
| Comments/DOCTYPE/CDATA | Stripped |
| Entities | Named + numeric entities decoded; unknown entities kept literally (&lt; → <, no double decoding) |
| Nested lists | li is only closed by a new li — nested ul/ol is legal (2 spaces/level indentation) |
| Whitespace | Inline collapsed to single spaces; pre/code preserved as-is |
| Tables | First row (thead th or first-row td) is the header; colspan=N pads N-1 placeholder cells |
| Depth > 64 levels | markdown: HTML nesting exceeds 64 levels |
| Input over limit | markdown: <label> exceeds N bytes (no truncation) |
| Embedded HTML in md2html | Escaped as text as-is; whitelist-external tags are never output |
DSH 0.1.5-rc.1 compatibility (verified)
This plugin has been migrated to the DSH 0.1.5-rc.1 harness and fully verified in an isolated consumer of local harness 0.1.5-rc.1:
- Types/runtime:
@deepseek-ai/cordis@^4.0.1+@deepseek-ai/dsh-tools@>=0.0.1-rc.1 <0.2.0+@deepseek-ai/dsh-invariants@>=0.0.1-rc.1 <0.2.0(peer); no longer depends on unscopedcordis - Standalone build:
npm install(devDependencies are self-contained: typescript/vitest/@types/node) →npm run typecheck→npm test→npm run build→npm pack - Consumption verification: tarball installed into the 0.1.5-rc.1 consumer →
dsh --profile compat --dump-configshows this plugin's row → the tool actually registers and executes - Startup:
npx -p @deepseek-ai/dsh@next dsh web(lib production mode; do notinstall -gglobally)
Installation
Plugin source repository: https://github.com/omdsh-dev/dsh-tool-markdown (public).
Profile Bundle (recommended)
Install this plugin as a standalone bundle into a profile (DSH 0.1.5-rc.1, npm):
# 交互式(web)profile
dsh plugin --profile web add github:omdsh-dev/dsh-tool-markdown
# 一次性任务(headless)profile —— dsh run 默认使用 headless
dsh plugin --profile headless add github:omdsh-dev/dsh-tool-markdown
The dsh.bundle.patch inside the package automatically adds the plugin to the profile's layer stack after installation (row id: tool-markdown).
⚠️ web and headless are different profiles: installing into web does not automatically cover headless;
dsh runuses the headless profile by default.
Install via npm pack tarball
npm pack # generates dsh-tool-markdown-*.tgz
dsh plugin --profile web add ./dsh-tool-markdown-*.tgz
dsh plugin --profile headless add ./dsh-tool-markdown-*.tgz
Verify installation
dsh --profile web --dump-config | grep tool-markdown
Run verification
dsh run "使用 markdown 工具把 <h1>标题</h1> 转成 Markdown"
Manual installation and legacy compatibility
Only for legacy snapshots that do not support Profile Bundle, or plugin development/debugging environments (local junction/symlink, manually editing profile layers).
Testing
node <monorepo>/node_modules/vitest/vitest.mjs run tests
html.spec.ts: parser boundaries (nesting/unclosed/case/self-closing/comments/script stripping/entities/malformed attributes/depth guard)html2md.spec.ts: full block/inline mapping table, tables (colspan/header/escaping), scheme filtering, baseUrlmd2html.spec.ts: whitelist structure + security (embedded script escaping, javascript: degradation, quote escaping)table.spec.ts: HTML/pipe inputs, column padding,\|escaping;toc.specTOC and slugifyregister.spec.ts: registration contract (AUDIT-CROSS-02 style)
License
MIT
Links
More in this category
tt-a1i/archify#integrations/deepseek-harness★ 92724
Generate validated, self-contained interactive architecture, workflow, sequence, data-flow, and lifecycle diagrams from repositories or system descriptions.
dream-num/dsh-univer-office★ 657
Give DeepSeek Harness a real office environment. Univer Office Plugin brings spreadsheets, docs, slides, canvases, relational tables, and more into one runtime — with connected data, validation, versioned changes, and isolated worktrees for multi-agent collaboration.
ECNU/EduWork#dsh-knowledge-studio★ 275
A workspace-based Studio for source-grounded reports, slides, tables, mind maps, quizzes, flashcards, audio and video, using DSH models and shared artifact services. Office generation requires Python 3.12+ with its dependencies and DSH_OFFICE_PYTHON configured; media rendering requires Chromium, and local ASR additionally requires whisper.cpp and a model.
PerryLink/dsh-industry-research★ 215
Deterministic industry research reports for DeepSeek Harness — company and industry research flows produce structured, verifiable reports from staged evidence.
HuanLinOTO/dsh-plugin-mineru★ 51
Expose MineRU document parsing tools to the model.
PolinniZhong/dsh-knit★ 48
Puts the Markdown documents, source files, images and video that already exist anywhere in the session workspace into the DSH sidebar, ranked by relevance to the current conversation. The default lexical mode matches your recent messages against title, summary and body locally with IDF weighting (BM25) — no model calls, no network, nothing leaves the machine. The list comes from scanning the workspace rather than from a recent-files list, so restarting DSH or opening a new session does not empty it. Results are organised into a three-tier reading order (primary / supporting / related) with a verifiable reason on every row — which field matched, which document cites this one — and one click opens an in-place preview: Markdown as rendered text, relative-path images actually drawn, source code with the host's own syntax highlighting, video streamed over HTTP Range. The same Context Pack is exposed to the agent as a knit_docs tool. Later versions added read evidence and lifecycle tracking (attribution is frozen at the moment of reading), pin / exclude (exclusion applies before retrieval, and is undoable), relation evidence (references / imports / tests, with line numbers) and an incremental index reused across restarts. Since v0.23 there is also an optional hybrid mode: it is off by default and only runs once you configure an Ollama or OpenAI-compatible embedding provider yourself. When it runs, what leaves the machine is your query text plus each candidate file's title and summary — a Markdown summary is an excerpt of the body's first paragraph (at most 60 characters), a code file's summary is its workspace-relative directory, and an image or video contributes its file name; full bodies and absolute paths are not sent, and a loopback provider keeps everything on this machine. Since v0.24 it also associates uncommitted changes: a read-only git status (fixed argv, no shell, whitelisted child-process environment) combined with the same reference / import / test / mention evidence shows which files a change is likely to drag along, and it is off by default. The benefit of semantic retrieval still awaits a real-provider run, so no recall improvement is claimed.
Community comments
Comments are public GitHub Discussions. Loading them connects to GitHub and Giscus; a GitHub account is required to post.