2026-02-16

ブログ記事を Markdown で作成する

今までこのブログは HTML を直接書いていました。しかし、最近は Markdown でテキストを書くことが増え、HTML を直書きすることが億劫に感じます。そこで、Markdown から HTML に変換する仕組みに変えてみました。

あわせて、本ブログの文体も「ですます調」に切り替えてみます。

Pandoc の導入

Markdown から HTML ファイルを生成する仕組みとして Pandoc を使います。

Pandoc は各種文書フォーマットを相互変換するツールです。開発言語は Haskell。2006 年から開発が始まっており、2026-02-16 現在の最新バージョンは 3.9 です。

Pandoc のインストール

Pandoc は brew コマンドでインストールできます。

brew install pandoc
Pandoc の基本的な使い方

Pandoc の基本オプションは --from--to です。--from オプションで入力フォーマット指定します。今回は markdown を選択します。--to オプションは出力フォーマット指定です。出力フォーマットは html5 を選びます。

出力先は --output オプションで指定できます。

利用例は次の通りです:

pandoc --from=markdown --to=html5 --output=entry.html entry.md

上記コマンドは、Markdown ファイル entry.md を HTML に変換し、entry.html ファイルを作成します。

例えば entry.md が次のような中身だった場合

# Pandoc の導入

Markdown から HTML ファイルを生成する仕組みとして [Pandoc](https://pandoc.org/) を使います。

Pandoc は各種文書フォーマットを相互変換するツールです。...

## Pandoc のインストール

Pandoc は `brew` コマンドでインストールできます。

出力の entry.html は下記のようになります。

<h1 id="pandoc-の導入">Pandoc の導入</h1>
<p>Markdown から HTML ファイルを生成する仕組みとして <a
href="https://pandoc.org/">Pandoc</a> を使います。</p>
<p>Pandoc は各種文書フォーマットを相互変換するツールです。…</p>
<h2 id="pandoc-のインストール">Pandoc のインストール</h2>
<p>Pandoc は <code>brew</code> コマンドでインストールできます。</p>
markdown フォーマット

--from オプションに設定する markdown について補足です。 Pandoc には Markdown を表すフォーマットが 8 つあります:

  1. commonmark : CommonMark
  2. commanmark_x : 拡張付き CommonMark
  3. gfm : GitHub-Flavored Markdown
  4. markdown_github : deprecated になった gfm
  5. markdown : Pandoc’s Markdown
  6. markdown_mmd : MultiMarkdown
  7. markdown_phpextra : PHP Markdown Extra
  8. markdown_strict : オリジナルの (拡張なしの) Markdown

これらは Markdown の様々な方言に対応したものです。8 つのフォーマットのうち、どのフォーマットを使うのが適切でしょうか?

最もオリジナルに近い Markdown を使いたい場合は markdown_strict を使います。

GitHub-Flavored Markdown で導入された Fenced Code Block を使いたい場合は gfm です。と言いたいところですが、gfm には罠があります。実は gfm を指定すると、厳密に GitHub-Flavored Markdown な記法に限定されてしまうのです。言い換えると、gfm は Pandoc 用の拡張を受け付けません。

私は markdown フォーマットを選んでいます。markdown フォーマットは Pandoc 用の拡張をサポートした Markdown 用フォーマットです。HTML に変換する時に、例えば classid を付与することが可能になります。先の Fenced Code Block も markdown フォーマットはデフォルトで対応しています。

ブログ出力用のカスタマイズ

自動 ID 付与の抑制

Pandoc はヘッダーに自動で ID を振ってくれます。とても便利な機能ですが、ブログでは少し問題があります。というのは、ブログは複数の記事を 1 つのページに表示することがあるからです。

例えば、ブログの記事毎に「あとがき」なるヘッダーを入れたとします。この時 Pandoc は id="あとがき" という ID を自動生成します。読者がその記事だけを読むのであれば良いでしょう。しかし、「月別アーカイブ」を開くとどうでしょう。「月別アーカイブ」の中にいくつもの id="あとがき" が現れてしまいます。ID が複数あると、<a href="#あとがき">あとがきへのリンク</a> はどこへジャンプすれば良いのか分からなくなってしまいます。

Pandoc の自動 ID 付与機能をオフにするには、--format オプションに追加オプション -auto_identifiers を付けます。

pandoc --from=markdown-auto_identifiers --to=html5 entry.md > entry.html

以下、実行結果です:

<h1>Pandoc の導入</h1>
<p>Markdown から HTML ファイルを生成する仕組みとして <a
href="https://pandoc.org/">Pandoc</a> を使います。</p>
<p>Pandoc は各種文書フォーマットを相互変換するツールです。…</p>
<h2>Pandoc のインストール</h2>
<p>Pandoc は <code>brew</code> コマンドでインストールできます。</p>
ID の指定

ブログ記事を書いていると、ID を付けたいケースもあります。その場合、Pandoc 拡張を利用します。

# Pandoc の導入{#introduce-pandoc}

上記のように、# ヘッダー文字列{#id-text} という形で ID を記入します。すると、次のように ID が振られます。

<h1 id="introduce-pandoc">Pandoc の導入</h1>
ヘディング・レベルの変更

Blogger では HTML のヘディング・レベルは次のようになっています。

  • h1 : ブログのタイトル
  • h2 : 日付ヘッダー
  • h3 : 記事タイトル

従って、ブログ記事内の最初のヘッダーは h4 から始まります。ですので、Markdown でブログ記事を書く時も h4 が始まりとなります。

ですが、いちいち書き手がそんな気を回さなければいけないのは面倒です。幸い Pandoc には Shift Heading Level という機能があり、ヘディング・レベルを変更できます。オプションは --shift-heading-level-by を使います。今回は # (h1 相当) を #### (h4 相当) にシフトさせたいので、ヘディング・レベルを 3 つ上げます。

pandoc --from=markdown-auto_identifiers --to=html5 --shift-heading-level-by=3 entry.md > entry.html

これで次のように #h4 に変換されます。

<h4>Pandoc の導入</h4>
<p>Markdown から HTML ファイルを生成する仕組みとして <a
href="https://pandoc.org/">Pandoc</a> を使います。</p>
<p>Pandoc は各種文書フォーマットを相互変換するツールです。…</p>
<h5>Pandoc のインストール</h5>
<p>Pandoc は <code>brew</code> コマンドでインストールできます。</p>

コードの表示

Pandoc のシンタックス・ハイライト

Pandoc はソースコードのシンタックス・ハイライトをデフォルトでサポートしています。

例えば、次のソースコードを含む Markdown ファイルを HTML に変換してみましょう。

Lua のサンプルコードです。

``` lua
local languages = {
  ['emacs-lisp'] = true,
  elisp = true,
  html = true,
  lua = true,
  markdown = true,
  ruby = true,
  swift = true,
}
```

上記の Markdown ファイルに対して、次のコマンドを実行してみました。

pandoc --from=markdown-auto_identifiers --to=html5 --shift-heading-level-by=3 --output=source1.html source1.md

結果は次のようになります。

<p>Lua のサンプルコードです。</p>
<div class="sourceCode" id="cb2"><pre
class="sourceCode lua"><code class="sourceCode lua"><span id="cb2-1"><a href="#cb2-1" aria-hidden="true" tabindex="-1"></a><span class="kw">local</span> <span class="va">languages</span> <span class="op">=</span> <span class="op">{</span></span>
<span id="cb2-2"><a href="#cb2-2" aria-hidden="true" tabindex="-1"></a>  <span class="op">[</span><span class="st">&#39;emacs-lisp&#39;</span><span class="op">]</span> <span class="op">=</span> <span class="kw">true</span><span class="op">,</span></span>
<span id="cb2-3"><a href="#cb2-3" aria-hidden="true" tabindex="-1"></a>  <span class="va">elisp</span> <span class="op">=</span> <span class="kw">true</span><span class="op">,</span></span>
<span id="cb2-4"><a href="#cb2-4" aria-hidden="true" tabindex="-1"></a>  <span class="va">html</span> <span class="op">=</span> <span class="kw">true</span><span class="op">,</span></span>
<span id="cb2-5"><a href="#cb2-5" aria-hidden="true" tabindex="-1"></a>  <span class="va">lua</span> <span class="op">=</span> <span class="kw">true</span><span class="op">,</span></span>
<span id="cb2-6"><a href="#cb2-6" aria-hidden="true" tabindex="-1"></a>  <span class="va">markdown</span> <span class="op">=</span> <span class="kw">true</span><span class="op">,</span></span>
<span id="cb2-7"><a href="#cb2-7" aria-hidden="true" tabindex="-1"></a>  <span class="va">ruby</span> <span class="op">=</span> <span class="kw">true</span><span class="op">,</span></span>
<span id="cb2-8"><a href="#cb2-8" aria-hidden="true" tabindex="-1"></a>  <span class="va">swift</span> <span class="op">=</span> <span class="kw">true</span><span class="op">,</span></span>
<span id="cb2-9"><a href="#cb2-9" aria-hidden="true" tabindex="-1"></a><span class="op">}</span></span></code></pre></div>

どうでしょう。ソースコードの構文が解析され、変数やオペレーターに span 要素が追加されています。ただ、CSS が当たっていないので色が付いていません。

ここで --standalone オプションを追加すると、HTML ファイルに CSS を追加してくれます。

pandoc --from=markdown-auto_identifiers --to=html5 --shift-heading-level-by=3 --standalone --output=source1.html source1.md
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
  <meta charset="utf-8" />
  <meta name="generator" content="pandoc" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes" />
  <title>source1</title>
  <style>
    /* Default styles provided by pandoc.
    ** See https://pandoc.org/MANUAL.html#variables-for-html for config info.
    */
    html {
      color: #1a1a1a;
      background-color: #fdfdfd;
    }
    body {
      margin: 0 auto;
      max-width: 36em;
      padding-left: 50px;
      padding-right: 50px;
      padding-top: 50px;
      padding-bottom: 50px;
      hyphens: auto;
      overflow-wrap: break-word;
      text-rendering: optimizeLegibility;
      font-kerning: normal;
    }
    @media (max-width: 600px) {
      body {
        font-size: 0.9em;
        padding: 12px;
      }
      h1 {
        font-size: 1.8em;
      }
    }
    @media print {
      html {
        background-color: white;
      }
      body {
        background-color: transparent;
        color: black;
        font-size: 12pt;
      }
      p, h2, h3 {
        orphans: 3;
        widows: 3;
      }
      h2, h3, h4 {
        page-break-after: avoid;
      }
    }
    p {
      margin: 1em 0;
    }
    a {
      color: #1a1a1a;
    }
    a:visited {
      color: #1a1a1a;
    }
    img {
      max-width: 100%;
    }
    svg {
      height: auto;
      max-width: 100%;
    }
    h1, h2, h3, h4, h5, h6 {
      margin-top: 1.4em;
    }
    h5, h6 {
      font-size: 1em;
      font-style: italic;
    }
    h6 {
      font-weight: normal;
    }
    ol, ul {
      padding-left: 1.7em;
      margin-top: 1em;
    }
    li > ol, li > ul {
      margin-top: 0;
    }
    blockquote {
      margin: 1em 0 1em 1.7em;
      padding-left: 1em;
      border-left: 2px solid #e6e6e6;
      color: #606060;
    }
    code {
      font-family: Menlo, Monaco, Consolas, 'Lucida Console', monospace;
      font-size: 85%;
      margin: 0;
      hyphens: manual;
    }
    pre {
      margin: 1em 0;
      overflow: auto;
    }
    pre code {
      padding: 0;
      overflow: visible;
      overflow-wrap: normal;
    }
    .sourceCode {
     background-color: transparent;
     overflow: visible;
    }
    hr {
      border: none;
      border-top: 1px solid #1a1a1a;
      height: 1px;
      margin: 1em 0;
    }
    table {
      margin: 1em 0;
      border-collapse: collapse;
      width: 100%;
      overflow-x: auto;
      display: block;
      font-variant-numeric: lining-nums tabular-nums;
    }
    table caption {
      margin-bottom: 0.75em;
    }
    tbody {
      margin-top: 0.5em;
      border-top: 1px solid #1a1a1a;
      border-bottom: 1px solid #1a1a1a;
    }
    th {
      border-top: 1px solid #1a1a1a;
      padding: 0.25em 0.5em 0.25em 0.5em;
    }
    td {
      padding: 0.125em 0.5em 0.25em 0.5em;
    }
    header {
      margin-bottom: 4em;
      text-align: center;
    }
    #TOC li {
      list-style: none;
    }
    #TOC ul {
      padding-left: 1.3em;
    }
    #TOC > ul {
      padding-left: 0;
    }
    #TOC a:not(:hover) {
      text-decoration: none;
    }
    code{white-space: pre-wrap;}
    span.smallcaps{font-variant: small-caps;}
    div.columns{display: flex; gap: min(4vw, 1.5em);}
    div.column{flex: auto; overflow-x: auto;}
    div.hanging-indent{margin-left: 1.5em; text-indent: -1.5em;}
    /* The extra [class] is a hack that increases specificity enough to
       override a similar rule in reveal.js */
    ul.task-list[class]{list-style: none;}
    ul.task-list li input[type="checkbox"] {
      font-size: inherit;
      width: 0.8em;
      margin: 0 0.8em 0.2em -1.6em;
      vertical-align: middle;
    }
    .display.math{display: block; text-align: center; margin: 0.5rem auto;}
    /* CSS for syntax highlighting */
    html { -webkit-text-size-adjust: 100%; }
    pre > code.sourceCode { white-space: pre; position: relative; }
    pre > code.sourceCode > span { display: inline-block; line-height: 1.25; }
    pre > code.sourceCode > span:empty { height: 1.2em; }
    .sourceCode { overflow: visible; }
    code.sourceCode > span { color: inherit; text-decoration: inherit; }
    div.sourceCode { margin: 1em 0; }
    pre.sourceCode { margin: 0; }
    @media screen {
    div.sourceCode { overflow: auto; }
    }
    @media print {
    pre > code.sourceCode { white-space: pre-wrap; }
    pre > code.sourceCode > span { text-indent: -5em; padding-left: 5em; }
    }
    pre.numberSource code
      { counter-reset: source-line 0; }
    pre.numberSource code > span
      { position: relative; left: -4em; counter-increment: source-line; }
    pre.numberSource code > span > a:first-child::before
      { content: counter(source-line);
        position: relative; left: -1em; text-align: right; vertical-align: baseline;
        border: none; display: inline-block;
        -webkit-touch-callout: none; -webkit-user-select: none;
        -khtml-user-select: none; -moz-user-select: none;
        -ms-user-select: none; user-select: none;
        padding: 0 4px; width: 4em;
        color: #aaaaaa;
      }
    pre.numberSource { margin-left: 3em; border-left: 1px solid #aaaaaa;  padding-left: 4px; }
    div.sourceCode
      {   }
    @media screen {
    pre > code.sourceCode > span > a:first-child::before { text-decoration: underline; }
    }
    code span.al { color: #ff0000; font-weight: bold; } /* Alert */
    code span.an { color: #60a0b0; font-weight: bold; font-style: italic; } /* Annotation */
    code span.at { color: #7d9029; } /* Attribute */
    code span.bn { color: #40a070; } /* BaseN */
    code span.bu { color: #008000; } /* BuiltIn */
    code span.cf { color: #007020; font-weight: bold; } /* ControlFlow */
    code span.ch { color: #4070a0; } /* Char */
    code span.cn { color: #880000; } /* Constant */
    code span.co { color: #60a0b0; font-style: italic; } /* Comment */
    code span.cv { color: #60a0b0; font-weight: bold; font-style: italic; } /* CommentVar */
    code span.do { color: #ba2121; font-style: italic; } /* Documentation */
    code span.dt { color: #902000; } /* DataType */
    code span.dv { color: #40a070; } /* DecVal */
    code span.er { color: #ff0000; font-weight: bold; } /* Error */
    code span.ex { } /* Extension */
    code span.fl { color: #40a070; } /* Float */
    code span.fu { color: #06287e; } /* Function */
    code span.im { color: #008000; font-weight: bold; } /* Import */
    code span.in { color: #60a0b0; font-weight: bold; font-style: italic; } /* Information */
    code span.kw { color: #007020; font-weight: bold; } /* Keyword */
    code span.op { color: #666666; } /* Operator */
    code span.ot { color: #007020; } /* Other */
    code span.pp { color: #bc7a00; } /* Preprocessor */
    code span.sc { color: #4070a0; } /* SpecialChar */
    code span.ss { color: #bb6688; } /* SpecialString */
    code span.st { color: #4070a0; } /* String */
    code span.va { color: #19177c; } /* Variable */
    code span.vs { color: #4070a0; } /* VerbatimString */
    code span.wa { color: #60a0b0; font-weight: bold; font-style: italic; } /* Warning */
  </style>
</head>
<body>
<p>Lua のサンプルコードです。</p>
<div class="sourceCode" id="cb2"><pre
class="sourceCode lua"><code class="sourceCode lua"><span id="cb2-1"><a href="#cb2-1" aria-hidden="true" tabindex="-1"></a><span class="kw">local</span> <span class="va">languages</span> <span class="op">=</span> <span class="op">{</span></span>
<span id="cb2-2"><a href="#cb2-2" aria-hidden="true" tabindex="-1"></a>  <span class="op">[</span><span class="st">&#39;emacs-lisp&#39;</span><span class="op">]</span> <span class="op">=</span> <span class="kw">true</span><span class="op">,</span></span>
<span id="cb2-3"><a href="#cb2-3" aria-hidden="true" tabindex="-1"></a>  <span class="va">elisp</span> <span class="op">=</span> <span class="kw">true</span><span class="op">,</span></span>
<span id="cb2-4"><a href="#cb2-4" aria-hidden="true" tabindex="-1"></a>  <span class="va">html</span> <span class="op">=</span> <span class="kw">true</span><span class="op">,</span></span>
<span id="cb2-5"><a href="#cb2-5" aria-hidden="true" tabindex="-1"></a>  <span class="va">lua</span> <span class="op">=</span> <span class="kw">true</span><span class="op">,</span></span>
<span id="cb2-6"><a href="#cb2-6" aria-hidden="true" tabindex="-1"></a>  <span class="va">markdown</span> <span class="op">=</span> <span class="kw">true</span><span class="op">,</span></span>
<span id="cb2-7"><a href="#cb2-7" aria-hidden="true" tabindex="-1"></a>  <span class="va">ruby</span> <span class="op">=</span> <span class="kw">true</span><span class="op">,</span></span>
<span id="cb2-8"><a href="#cb2-8" aria-hidden="true" tabindex="-1"></a>  <span class="va">swift</span> <span class="op">=</span> <span class="kw">true</span><span class="op">,</span></span>
<span id="cb2-9"><a href="#cb2-9" aria-hidden="true" tabindex="-1"></a><span class="op">}</span></span></code></pre></div>
</body>
</html>

ウェブブラウザーで HTML ファイルを開くと、色が付いているのが分かります。

Prism.js を使ったシンタックス・ハイライト

Pandoc のシンタックス・ハイライトを使っても良いですが、せっかく前記事で Prism.js を導入したので、シンタックス・ハイライトはそちらに任せようと思います。

Prism.js を使う場合、Pandoc のデフォルトで付いてくるシンタック・ハイライトの出力が邪魔です。まず --standalone オプションを 付けない ことで、CSS の挿入を妨げ、続いて --syntax-highlighting=none オプションを付けてシンタックス・ハイライト用の出力も抑制します。

pandoc --from=markdown-auto_identifiers --to=html5 --shift-heading-level-by=3 --syntax-highlighting=none --output=source1.html source1.md

すると、シンプルな HTML 出力を得ました。

<p>Lua のサンプルコードです。</p>
<pre class="lua"><code>local languages = {
  [&#39;emacs-lisp&#39;] = true,
  elisp = true,
  html = true,
  lua = true,
  markdown = true,
  ruby = true,
  swift = true,
}</code></pre>

さて、Prism.js にシンタックス・ハイライトをさせるためには、次の書式で HTML を書く必要がありました。 これは HTML5 で推奨されている書き方だそうです。

<pre><code class="language-XXX">
プログラミング言語 XXX のソースコード
</code></pre>

HTML5 の推奨は code 要素の class 属性に language-lua と書くのですが、Pandoc の出力は pre 要素の class 属性に lua と書なっています。

Pandoc には Lua 言語でフィルターを作成することができて、Pandoc の HTML 出力を Prism.js に合わせることが可能です。

上記サイトを参考にして、Lua フィルタースクリプトを書きました。これを standard_code.lua という名前で保存します。 なお、保存する時に、シンタックスハイライトしたい言語を languages に記述してください。

--- standard-code: ouput code blocks with class="language-*" attributes
-- © 2020 Aman Verma. Distributed under the MIT license.

-- Check keywords at https://prismjs.com/#supported-languages
local languages = {
  ['emacs-lisp'] = true,
  elisp = true,
  html = true,
  lua = true,
  makefile = true,
  markdown = true,
  ruby = true,
  shellsession = true,
  swift = true,
}

-- local languages = {meta = true,markup = true,css = true,clike = true,javascript = true,abap = true,abnf = true,actionscript = true,ada = true,agda = true,al = true,antlr4 = true,apacheconf = true,apl = true,applescript = true,aql = true,arduino = true,arff = true,asciidoc = true,aspnet = true,asm6502 = true,autohotkey = true,autoit = true,bash = true,basic = true,batch = true,bbcode = true,bison = true,bnf = true,brainfuck = true,brightscript = true,bro = true,bsl = true,c = true,csharp = true,cpp = true,cil = true,clojure = true,cmake = true,coffeescript = true,concurnas = true,csp = true,crystal = true,['css-extras'] = true,cypher = true,d = true,dart = true,dax = true,dhall = true,diff = true,django = true,['dns-zone-file'] = true,docker = true,ebnf = true,editorconfig = true,eiffel = true,ejs = true,['emacs-lisp'] = true,elisp = true,elixir = true,elm = true,etlua = true,erb = true,erlang = true,['excel-formula'] = true,fsharp = true,factor = true,['firestore-security-rules'] = true,flow = true,fortran = true,ftl = true,gml = true,gcode = true,gdscript = true,gedcom = true,gherkin = true,git = true,glsl = true,go = true,graphql = true,groovy = true,haml = true,html = true,handlebars = true,haskell = true,haxe = true,hcl = true,hlsl = true,http = true,hpkp = true,hsts = true,ichigojam = true,icon = true,ignore = true,inform7 = true,ini = true,io = true,j = true,java = true,javadoc = true,javadoclike = true,javastacktrace = true,jolie = true,jq = true,jsdoc = true,['js-extras'] = true,json = true,json5 = true,jsonp = true,jsstacktrace = true,['js-templates'] = true,julia = true,keyman = true,kotlin = true,latex = true,latte = true,less = true,lilypond = true,liquid = true,lisp = true,livescript = true,llvm = true,lolcode = true,lua = true,makefile = true,markdown = true,['markup-templating'] = true,matlab = true,mel = true,mizar = true,mongodb = true,monkey = true,moonscript = true,n1ql = true,n4js = true,['nand2tetris-hdl'] = true,naniscript = true,nasm = true,neon = true,nginx = true,nim = true,nix = true,nsis = true,objc = true,objectivec = true,ocaml = true,opencl = true,oz = true,parigp = true,parser = true,pascal = true,pascaligo = true,pcaxis = true,peoplecode = true,perl = true,php = true,phpdoc = true,['php-extras'] = true,plsql = true,powerquery = true,powershell = true,processing = true,prolog = true,properties = true,protobuf = true,pug = true,puppet = true,pure = true,purebasic = true,purescript = true,python = true,q = true,qml = true,qore = true,r = true,racket = true,jsx = true,tsx = true,reason = true,regex = true,renpy = true,rest = true,rip = true,roboconf = true,robotframework = true,ruby = true,rust = true,sas = true,sass = true,scss = true,scala = true,scheme = true,['shell-session'] = true,smali = true,smalltalk = true,smarty = true,solidity = true,['solution-file'] = true,soy = true,sparql = true,['splunk-spl'] = true,sqf = true,sql = true,stan = true,iecst = true,stylus = true,swift = true,['t4-templating'] = true,['t4-cs'] = true,['t4-vb'] = true,tap = true,tcl = true,tt2 = true,textile = true,toml = true,turtle = true,twig = true,typescript = true,typoscript = true,unrealscript = true,vala = true,vbnet = true,velocity = true,verilog = true,vhdl = true,vim = true,['visual-basic'] = true,warpscript = true,wasm = true,wiki = true,xeora = true,['xml-doc'] = true,xojo = true,xquery = true,yaml = true,yang = true,zig = true}


local function escape(s)
  -- Escape according to HTML 5 rules
  return s:gsub(
    [=[[<>&"']]=],
    function(x)
      if x == '<' then
        return '&lt;'
      elseif x == '>' then
        return '&gt;'
      elseif x == '&' then
        return '&amp;'
      elseif x == '"' then
        return '&quot;'
      elseif x == "'" then
        return '&#39;'
      else
        return x
      end
    end
  )
end

local function getCodeClass(classes)
  -- Check if the first element of classes (pandoc.CodeBlock.classes) matches a
  -- programming language name. If it does, it gets removed from classes and a valid
  -- HTML class attribute string (with space at beginning) is returned.

  if languages[classes[1]] then
    return ' class="language-' .. table.remove(classes, 1) .. '"'
  else
    return ''
  end
end

local function makeIdentifier(ident)
  -- Returns a valid HTML id attribute (with space at beginning) OR empty string.

  if #ident ~= 0 then
    return ' id="'.. ident .. '"'
  else
    return ''
  end
end

local function makeClasses(classes)
  -- Returns a valid HTML class attribute with classes separated by spaces (with a space
  -- at the beginning) OR empty string.

  if #classes ~= 0 then
    return ' class="' .. table.concat(classes, ' ') .. '"'
  else
    return ''
  end
end

local function makeAttributes(attrs)
  -- Returns a string of HTML attributes from key-value pairs
  -- (with space at beginning for each attribute) OR empty string.

  if #attrs ~= 0 then
    local result = ''
    for key, value in pairs(attrs) do
      result = result .. ' ' .. key .. '="' .. escape(value) .. '"'
    end
    return result
  else
    return ''
  end
end

return {
  {
    CodeBlock = function(elem)
      if FORMAT ~= 'html5' then
        return nil
      end

      local id = makeIdentifier(elem.identifier)
      local classLang = getCodeClass(elem.classes)
      local classReg = makeClasses(elem.classes)
      local extraAttrs = makeAttributes(elem.attributes)

      local preCode = string.format(
        '<pre%s%s%s><code%s>%s</code></pre>', id, classReg, extraAttrs, classLang, escape(elem.text)
      )
      return pandoc.RawBlock('html', preCode, 'RawBlock')
    end,

  }
}

--lua-filter=standard_code.lua オプションで Lua スプリプトを指定します。

pandoc --from=markdown-auto_identifiers --to=html5 --shift-heading-level-by=3 --syntax-highlighting=none --lua-filter=standard_code.lua --output=source1.html source1.md

出力を見てみましょう。Prism.js 向けの HTML になりました。

<p>Lua のサンプルコードです。</p>
<pre><code class="language-lua">local languages = {
  [&#39;emacs-lisp&#39;] = true,
  elisp = true,
  html = true,
  lua = true,
  markdown = true,
  ruby = true,
  swift = true,
}</code></pre>
行番号を表示する

Prism.js にはプラグインを導入することで、行番号を表示することができます。 そのためには、class="line-numbers" を指定する必要があります。 Pandoc では {.line-numbers} と書くことでクラスを追加できます。

実際に試してみます。 Markdown ファイルに {.line-numbers} を追加した後、

Lua のサンプルコードです。

``` lua {.line-numbers}
local languages = {
  ['emacs-lisp'] = true,
  elisp = true,
  html = true,
  lua = true,
  markdown = true,
  ruby = true,
  swift = true,
}
```

pandoc コマンドを実行します。

pandoc --from=markdown-auto_identifiers --to=html5 --shift-heading-level-by=3 --syntax-highlighting=none --lua-filter=standard_code.lua --output=source2.html source2.md

出力は次の通り。 行番号対応した HTML が得られました。

<p>Lua のサンプルコードです。</p>
<pre class="line-numbers"><code class="language-lua">local languages = {
  [&#39;emacs-lisp&#39;] = true,
  elisp = true,
  html = true,
  lua = true,
  markdown = true,
  ruby = true,
  swift = true,
}</code></pre>
行をハイライトする

行をハイライトする方法も見ておきましょう。 ハイライトしたい行を data-line="行番号" という形で指定するのでした。 Pandoc では {data-line="5"} と指定すれば OK です。

こちらも試してみましょう。 Markdown ファイルに data-line="5" を追加してみます。

Lua のサンプルコードです。

``` lua {.line-numbers data-line="5"}
local languages = {
  ['emacs-lisp'] = true,
  elisp = true,
  html = true,
  lua = true,
  markdown = true,
  ruby = true,
  swift = true,
}
```

これを pandoc で処理します。

pandoc --from=markdown-auto_identifiers --to=html5 --shift-heading-level-by=3 --syntax-highlighting=none --lua-filter=standard_code.lua --output=source3.html source3.md

行ハイライトに対応した HTML が出力されていますね。

<p>Lua のサンプルコードです。</p>
<pre class="line-numbers" data-line="5"><code class="language-lua">local languages = {
  [&#39;emacs-lisp&#39;] = true,
  elisp = true,
  html = true,
  lua = true,
  markdown = true,
  ruby = true,
  swift = true,
}</code></pre>

Makefile の作成

以上の準備を整えれば、Markdown ファイルを pandoc コマンドで HTML ファイルに変換ができます。 しかし、必要な pandoc コマンドはオプションを含めると長くなってしまいました。 これだけのオプションを毎回手打ちするのは避けたいところです。

そこで、Makefile を作って自動化することにしました。

Markdown ファイルと同じディレクトリーに、次のコードを Makefile という名前で保存します。

all: html

PANDOC = pandoc
PANDOC_OPTS = --from=markdown-auto_identifiers --to=html5 --shift-heading-level-by=3 --syntax-highlighting=none --lua-filter=standard_code.lua

MD_FILES := $(wildcard *.md)
HTML_FILES := $(MD_FILES:.md=.html)

.PHONY: html
html: $(HTML_FILES)

%.html: %.md
    $(PANDOC) $(PANDOC_OPTS) --output=$@ $<

この Makefile.md という拡張子のファイルがあったら、pandoc コマンドを PANDOC_OPTS オプションを使って処理し、同名の .html ファイルを作成する、というものです。 Makefile を作っておけば、make と実行するだけで、HTML ファイルが作成されます。

make

あとがき

以上で設定は終わりです。 最近はめっきりブログを書くことが少なくなりました。 Markdown で気軽に書けるシステムを構築できたので、月一くらいで記事を書ければと思います。

0 件のコメント:

コメントを投稿