marked v17 で Markdown を HTML に変換する
marked v17 系で Markdown → HTML を最小コードでパースする方法、GFM 拡張、サニタイズの注意点を触れる demo で確認する実装メモ。
検証日: 2026-05-09
使用バージョン:
marked@17.0.3対象: ブラウザ / Node で MD → HTML 変換が必要な場面、軽量 Markdown プレビューを実装したい人
marked v17 で Markdown → HTML 変換 を最小コードで動かすパターン。GFM 拡張、サニタイゼーション(DOMPurify 併用)、カスタムレンダラ、同期 vs 非同期、を動く demo で確認します。
触って試す
最小サンプル(marked v17、3 行で動かす)
marked.parse(src) で Markdown 文字列を HTML 文字列に変換するだけ。ブラウザでも Node でも同じ API:
import { marked } from "marked";
const html = marked.parse("# Hello\n\n**bold** _italic_");
// "<h1>Hello</h1>\n<p><strong>bold</strong> <em>italic</em></p>"
ブラウザでも Node でも同じ API。
marked 内部のパイプライン
marked.parse() の中身は 3 段のパイプ。各段で hook できるので、カスタマイズしたい時にどこに割り込むかが見えやすい。
実用例:
- link を全部
target="_blank" rel="noopener"にしたい → renderer 上書き(後述) :::noteのようなカスタム記法を追加 → lexer 拡張で新トークンを定義- AST だけ欲しい(HTML 不要) →
marked.lexer(src)で tokens を直接取得
GFM 拡張(GitHub Flavored Markdown)
オプションで GFM テーブル / ストライクスルー / タスクリスト / autolink を有効化:
const html = marked.parse(src, { gfm: true, breaks: true });
| オプション | 効果 |
|---|---|
gfm: true | Table、-[x] チェックリスト、autolink を有効化 |
breaks: true | 単純改行を <br> に変換(GitHub の挙動) |
pedantic: false | true にすると Markdown.pl 厳密、false の方が一般的 |
サニタイゼーション(必須)
marked.parse() は HTML をそのまま透過する(<script> も含む)。ユーザー入力をパースする時は サニタイズ必須。
import { marked } from "marked";
import DOMPurify from "dompurify";
const html = DOMPurify.sanitize(marked.parse(userInput) as string);
container.innerHTML = html;
カスタムレンダラ
marked.Renderer を継承して特定要素の出力 HTML を上書き。link に rel="noopener" を強制する例:
const renderer = new marked.Renderer();
renderer.link = ({ href, text }) => `<a href="${href}" rel="noopener noreferrer" target="_blank">${text} ↗</a>`;
marked.use({ renderer });
特定の要素だけ書き換えたい時に使う。リンクに rel="noopener" を強制する、画像に loading="lazy" を付けるなど。
同期 vs 非同期
デフォルトは同期で文字列を返す。async プラグイン(remote fetch する transformer 等)を使う場合だけ async: true で Promise になる:
// 同期(デフォルト、文字列を返す)
const html = marked.parse(src) as string;
// 非同期(Promise を返す、async プラグインを使う時)
const html = await marked.parse(src, { async: true });
カスタム async プラグインを使わない通常用途は同期で OK。
つまずいたポイント
- HTML エスケープしない仕様 — XSS 対策は呼び出し側の責任
async: trueの戻り値はPromise<string>— 同期と混ぜると型エラー- GFM table の
|区切りはセル間にスペース必須(|a|b|ではなく| a | b |) - 改行モード:
breaks: trueにすると一般的な MD と挙動が変わる(段落内改行が<br>になる) - コードブロックハイライトは別途:Shiki / Prism / highlight.js を
marked.use({ extensions })で繋げる
評価
| 観点 | 評価 | コメント |
|---|---|---|
| 学習コスト | ◎ | API は parse(text, options) 中心 |
| 速度 | ◎ | ベンチで markdown-it と僅差、十分速い |
| サニタイズ | △ | 自前で DOMPurify 等を噛ませる |
| 拡張 | ○ | renderer / extensions / hooks で柔軟 |
| バンドルサイズ | ○ | 30KB 前後(min+gzip) |
向く / 向かないケース
- 向く: ブラウザ内 MD プレビュー、Node での HTML 生成、軽量 docs viewer
- 向かない: HTML / MDX の expression が必要(MDX 自体を使う)、AST 操作が中心(remark / unified を直接使う)
- 向かない: 高度なサニタイズ要件 → markdown-it + DOMPurify か rehype-sanitize 系
関連 Topic / 関連書籍
この記事と関係する tech-book.net の Topic と、それぞれの Topic に紐づく書籍:
JavaScriptによるはじめてのアルゴリズム入門
Python と JavaScriptではじめるデータビジュアライゼーション
React Native+Expoではじめるスマホアプリ開発 : JavaScriptによるアプリ構築の実際
「React Native」は、Facebookが開発しているスマートフォンアプリ向けの開発環境です。ほとんどのコードをJav…