marked.jsを利用してMarkdownをHTMLに変換

Markdownで書かれた内容をHTMLに変換してみます。ライブラリとして「marked.js」を利用します。シンタックスハイライト、独自クラスの追加、目次の生成などについても取り上げます。

MarkdownをHTMLに変換

markedをインストール

npm install marked

ソース

const marked = require('marked')

const markdown = '# heading\n' +
  '## list\n' +
  '- list1\n' +
  '- list2\n\n' +
  '## code\n' +
  '```js\n' +
  'let x = 1;\n' +
  '```\n' +
  '## table\n' +
  '| col1 | col2 |\n' +
  '---|---\n' +
  'abc|123\n' +
  'xyz|456';

console.log(marked(markdown))

実行結果

以下のように、HTMLに変換されました。

$ node app.js 
<h1 id="heading">heading</h1>
<h2 id="list">list</h2>
<ul>
<li>list1</li>
<li>list2</li>
</ul>
<h2 id="code">code</h2>
<pre><code class="language-js">let x = 1;</code></pre>
<h2 id="table">table</h2>
<table>
<thead>
<tr>
<th>col1</th>
<th>col2</th>
</tr>
</thead>
<tbody><tr>
<td>abc</td>
<td>123</td>
</tr>
<tr>
<td>xyz</td>
<td>456</td>
</tr>
</tbody></table>

オプション|setOptions

オプションで marked.js の動作を変更できます。利用可能なオプションは以下ページで確認できます。
https://marked.js.org/#/USING_ADVANCED.md#options

シンタックスハイライト

highlight.jsをインストール

npm install highlight.js

ソース

const marked = require('marked')
const highlight = require('highlight.js')

marked.setOptions({
  highlight: code => {
    return highlight.highlightAuto(code).value
  }
})

const markdown = '## code\n' +
  '```js\n' +
  'let x = 1;\n' +
  '```'

console.log(marked(markdown))

実行結果

highlight.js によって hljs-attribute のクラスが追加されました。

$ node app.js 
<h2 id="code">code</h2>
<pre><code class="language-js"><span class="hljs-attribute">let x</span> = 1;</code></pre>

対応するスタイルは、node_modules/highlight.js/stylesディレクトリ 配下に存在します。

拡張|独自クラスの追加

テーブルタグに独自のClassを追加するよう拡張してみます。

オリジナルの処理を探す

https://github.com/markedjs/marked/blob/master/lib/marked.js にて Renderer.prototype.table で検索すると以下処理が見つかりました。

Renderer.prototype.table = function(header, body) {
  if (body) body = '<tbody>' + body + '</tbody>';

  return '<table>\n'
    + '<thead>\n'
    + header
    + '</thead>\n'
    + body
    + '</table>\n';
};

ソース

先ほど見つけたtableタグの処理を拡張してみます。

const marked = require('marked')

const renderer = new marked.Renderer()
renderer.table = (header, body) => {
  if (body) body = '<tbody>' + body + '</tbody>'

  return '<table class="wakuwaku">\n'
    + '<thead>\n'
    + header
    + '</thead>\n'
    + body
    + '</table>\n'
}
marked.setOptions({ renderer: renderer })

const markdown = '## table\n' +
  '| col1 | col2 |\n' +
  '---|---\n' +
  'abc|123\n' +
  'xyz|456'

console.log(marked(markdown))

実行結果

tableタグclass="wakuwaku" が追加されました。

$ node app.js 
<h2 id="table">table</h2>
<table class="wakuwaku">
<thead>
<tr>
<th>col1</th>
<th>col2</th>
</tr>
</thead>
<tbody><tr>
<td>abc</td>
<td>123</td>
</tr>
<tr>
<td>xyz</td>
<td>456</td>
</tr>
</tbody></table>

拡張|目次(toc)の生成

目次(table of contents)用のリストを生成してみます。

ソース

toc変数 に目次データを格納します。

const marked = require('marked')
const toc = []

const renderer = new marked.Renderer()
renderer.heading = (text, level) => {
  const slug = encodeURI(text.toLowerCase())
  toc.push({
    level: level,
    slug: slug,
    title: text
  })
  return '<h'
    + level
    + ' id="'
    + slug
    + '">'
    + text
    + '</h'
    + level
    + '>\n'
}
marked.setOptions({ renderer: renderer })

const markdown =
  '# 見出し1\n' +
  '## 見出し1-1\n' +
  '### 見出し1-1-1\n' +
  '### 見出し1-1-2\n' +
  '## 見出し1-2\n' +
  '## 見出し1-3\n' +
  '# 見出し2\n' +
  '## 見出し2-1\n' +
  '## 見出し2-2\n' +
  '# 見出し3'

console.log(marked(markdown))
console.log(toc)

実行結果

$ node app.js
<h1 id="%E8%A6%8B%E5%87%BA%E3%81%971">見出し1</h1>
<h2 id="%E8%A6%8B%E5%87%BA%E3%81%971-1">見出し1-1</h2>
<h3 id="%E8%A6%8B%E5%87%BA%E3%81%971-1-1">見出し1-1-1</h3>
<h3 id="%E8%A6%8B%E5%87%BA%E3%81%971-1-2">見出し1-1-2</h3>
<h2 id="%E8%A6%8B%E5%87%BA%E3%81%971-2">見出し1-2</h2>
<h2 id="%E8%A6%8B%E5%87%BA%E3%81%971-3">見出し1-3</h2>
<h1 id="%E8%A6%8B%E5%87%BA%E3%81%972">見出し2</h1>
<h2 id="%E8%A6%8B%E5%87%BA%E3%81%972-1">見出し2-1</h2>
<h2 id="%E8%A6%8B%E5%87%BA%E3%81%972-2">見出し2-2</h2>
<h1 id="%E8%A6%8B%E5%87%BA%E3%81%973">見出し3</h1>

[ { level: 1, slug: '%E8%A6%8B%E5%87%BA%E3%81%971', title: '見出し1' },
  { level: 2, slug: '%E8%A6%8B%E5%87%BA%E3%81%971-1', title: '見出し1-1' },
  { level: 3, slug: '%E8%A6%8B%E5%87%BA%E3%81%971-1-1', title: '見出し1-1-1' },
  { level: 3, slug: '%E8%A6%8B%E5%87%BA%E3%81%971-1-2', title: '見出し1-1-2' },
  { level: 2, slug: '%E8%A6%8B%E5%87%BA%E3%81%971-2', title: '見出し1-2' },
  { level: 2, slug: '%E8%A6%8B%E5%87%BA%E3%81%971-3', title: '見出し1-3' },
  { level: 1, slug: '%E8%A6%8B%E5%87%BA%E3%81%972', title: '見出し2' },
  { level: 2, slug: '%E8%A6%8B%E5%87%BA%E3%81%972-1', title: '見出し2-1' },
  { level: 2, slug: '%E8%A6%8B%E5%87%BA%E3%81%972-2', title: '見出し2-2' },
  { level: 1, slug: '%E8%A6%8B%E5%87%BA%E3%81%973', title: '見出し3' } ]

参考・関連