Blogger の記事作成を Markdown で行う

アイキャッチ画像

2024 年 大晦日

Blogger 記事の管理方法を変更することにした。

これまでは、Blogger の管理機能からエディタで入力していたが、 タグの入力などが面倒なため、Markdown 形式で記事を記述し、 Pandoc で HTML 形式に変換する方法に改めることにした。 画像の配置は、別途行うようにしている。

注意点の一つとして、コードブロック(インラインコードブロックも含む)以外のダブルクォートは、 Pandoc の smart 拡張機能によって、全角の「“”」に置換される。2つのハイフンも一つになったりする。

特に面倒だったのが、コードブロックで、 highlight.js を利用しているので、コードブロック毎に <pre>, <code> を追加する必要があった。

Pandoc で変換すると、自動的にコードブロックを囲ってくれるので楽になる。 変換コマンドは、以下となる。別途 highlight.js を導入しているため、 Pandoc 側でコードハイライトの修飾しないように指定している。 Mermaid も導入しているが、highlight.js と干渉するので、 Pandoc のフィルタ機能で Mermaid のコードブロックに対し、 タグ構成変更と highlight.js 処理対象外にする変更をしている。 さらに、MathJax も導入しているので、そのオプションも追加している。 その他には、Markdown 側の行の折り返しとタブ文字をそのまま残すようにしている。 (Go 言語や Makefile などは、タブインデントなため)。 出力された HTML ソースを Blogger に貼り付ける。

[mermaid-filter.lua]

function CodeBlock(el)
  if el.classes:includes('mermaid') then
    local pre = pandoc.RawBlock('html', '<pre class="mermaid nohighlight">')
    local content = pandoc.RawBlock('html', el.text)
    local pre_end = pandoc.RawBlock('html', '</pre>')
    return {pre, content, pre_end}
  end
end
pandoc --no-highlight --mathjax --wrap=preserve --preserve-tabs --lua-filter=mermaid-filter.lua 入力.md -o 出力.html

Pandoc の Markdown に関する仕様は、 Pandoc User’s Guide 日本語版 - Pandoc’s Markdown を参照。

Markdown は、HTML タグを直接記述できるが、 記述が面倒で、見づらくなるため、あまりやりたくない。 見映えのために、CSS を利用したい場合、 Pandoc では、クラス、スタイルや ID などの指定ができる。 詳細は、 Pandoc User’s Guide 日本語版 - Div と Span を参照。

コロン(:)を3つ以上と続いて {} 内で追加したい属性を記述すると、 Div タグを生成して属性を設定してくれる。 インライン要素の場合は、Span タグを生成する。

以下のような Markdown を記述すると。

# 見出しの場合 {.test}

::: {#identity1 .class1 style="margin: 2em;"}
- この項目が
- Div タグで囲まれる
:::

:::: block1
::: {#block2 .class2 style="margin: 3em;"}
- 入れ子にも
- できる。
:::
::::

属性を付与したい項目を[カギ括弧]{#id1 .csl2 style="color: red;"}で囲んで波カッコ内に属性を記述する。
この[ように[**他の修飾**や[リンク](#identity1)などを含めたり]{.cls3}、入れ子]{style="color: blue;"}にすることもできる。

以下のような HTML を生成する。

<h1 class="test" id="見出しの場合">見出しの場合</h1>
<div id="identity1" class="class1" style="margin: 2em;">
    <ul>
        <li>この項目が</li>
        <li>Div タグで囲まれる</li>
    </ul>
</div>
<div class="block1">
    <div id="block2" class="class2" style="margin: 3em;">
        <ul>
            <li>入れ子にも</li>
            <li>できる。</li>
        </ul>
    </div>
</div>
<p>属性を付与したい項目を<span id="id1" class="csl2" style="color: red;">カギ括弧</span>で囲んで波カッコ内に属性を記述する。
    この<span style="color: blue;">ように<span class="cls3"><strong>他の修飾</strong>や<a href="#identity1">リンク</a>などを含めたり</span>、入れ子</span>にすることもできる。
</p>

画像は、Markdown で管理せず、別途設定する。 画像の生成は、Gemini の自動生成で行っている。 プロンプトは、以下のようにしている。

以下の文章の内容に合った画像を生成してください。
<ここに、Markdown のソースをすべて貼り付ける>

生成された画像のダウンロードボタンを押下する。 拡張子は、.exif となっているが、jpeg ファイルである。 画像サイズは、2048 x 2048 と大きいので、800 x 800 に縮小して利用する。 画像の縮小には、GraphicsMagick を利用する。 変換後の画像フォーマットは、WebP とする。

gm convert -resize 800 入力ファイル.exif 出力ファイル.webp


以下は、各種言語のソースコードを記載したサンプルとなる。

Go 言語ソースの Markdown 記述 (最初のインデント幅が少ないのは、行番号を入れている影響だと思うが、目を瞑る)

```go
package main

import "fmt"

func main() {
	for i := 1; i <= 100; i++ {
		if i%15 == 0 {
			fmt.Println("FizzBuzz")
		} else if i%3 == 0 {
			fmt.Println("Fizz")
		} else if i%5 == 0 {
			fmt.Println("Buzz")
		} else {
			fmt.Println(i)
		}
	}
}
```

Go 言語ソース表示

package main

import "fmt"

func main() {
    for i := 1; i <= 100; i++ {
        if i%15 == 0 {
            fmt.Println("FizzBuzz")
        } else if i%3 == 0 {
            fmt.Println("Fizz")
        } else if i%5 == 0 {
            fmt.Println("Buzz")
        } else {
            fmt.Println(i)
        }
    }
}

Python 言語ソースの Markdown 記述

```python
def fizzbuzz(n):
  for i in range(1, n + 1):
    if i % 15 == 0:
      print("FizzBuzz")
    elif i % 3 == 0:
      print("Fizz")
    elif i % 5 == 0:
      print("Buzz")
    else:
      print(i)


fizzbuzz(100)
```

Python 言語ソースの表示

def fizzbuzz(n):
    for i in range(1, n + 1):
        if i % 15 == 0:
            print("FizzBuzz")
        elif i % 3 == 0:
            print("Fizz")
        elif i % 5 == 0:
            print("Buzz")
        else:
            print(i)


fizzbuzz(100)

IntelliJ の場合は、External Tools に設定をすると変換が楽になる。

設定画面を開く (File | Settings | Tools | External Tools)、 設定画面で Add し、以下のように設定し、OK ボタンを押下。 設定を終了する。

項目
Name: to html
Group: External Tools
Program: pandoc
Arguments: –no-highlight –mathjax –wrap=preserve –preserve-tabs –lua-filter=mermaid-filter.lua $FileName$ -o $FileNameWithoutExtension$.html
Working directory: $FileDir$
項目
Name: to 800 webp
Group: External Tools
Program: gm
Arguments: convert -resize 800 $FileName$ $FileNameWithoutExtension$.webp
Working directory: $FileDir$

変換したいファイルを選択して、 右クリック | External Tools | to html または、 右クリック | External Tools | to 800 webp を選択すると、選択したファイルのディレクトリに、変換後のファイルが生成される。

上記のようなテーブルの場合、Blogger のテーマによっては、 なにもスタイルが設定されていないので、 別途 Blogger のテーマをカスタマイズして、スタイルシートを追加設定する必要がある。 テーマ | カスタマイズ | 詳細設定 | CSS を追加 でテーブルのスタイルを追加する。

table {
    border-top: 2px solid darkgray;
    border-bottom: 2px solid darkgray;
    border-collapse: separate;
    border-spacing: 0.5rem;
    margin-top: 3em;
    margin-bottom: 3em;
}

th {
    border-bottom: 1px solid darkgray;
    font-weight: bold;
    padding-left: 0.5em;
    padding-right: 0.5em;
}

td {
    padding-left: 0.5em;
    padding-right: 0.5em;
}

tbody tr:nth-child(2n+1) {
    background-color: #080420;
}

自分用の備忘録として Blogger のテーマをカスタマイズした部分を記載しておく。

テーマ | カスタム | 詳細 | CSS を追加

.mermaid p {
    border-left: unset;
    padding: unset;
}

table {
    border-top: 2px solid darkgray;
    border-bottom: 2px solid darkgray;
    border-collapse: separate;
    border-spacing: 0.5rem;
    margin-top: 3em;
    margin-bottom: 3em;
}

th {
    border-bottom: 1px solid darkgray;
    font-weight: bold;
    padding-left: 0.5em;
    padding-right: 0.5em;
}

td {
    padding-left: 0.5em;
    padding-right: 0.5em;
}

tbody tr:nth-child(2n+1) {
    background-color: #080420;
}

.ichili-top-link {
    text-align: right;
}

.ichili-top-link p {
    border-left: unset;
    padding: unset;
}

.copy-button {
    opacity: 0.8;
    color: #888;
    font-size: .6rem;
    display: inline-block;
    height: 20px;
    line-height: 17px;
    padding: 0 8px;
    box-shadow: 0px 2px 5px 0px rgba(0, 0, 0, 0.16), 0px 2px 6px 0px rgba(0, 0, 0, 0.12);
    border: 2px solid #666;
    cursor: pointer;
    background-color: #fff;
    transition: all .2s ease;
    position: absolute;
    right: 0;
    top: 0;
    -webkit-user-select: none;
    -moz-user-select: none;
    -ms-user-select: none;
    user-select: none;
}

.copy-button.success {
    border-color: #00c851;
    background-color: #c8e6c9;
    color: #007e33;
}

.copy-button.failed {
    border-color: #ff4444;
    background-color: #ffcdd2;
    color: #cc0000;
}

.copy-button:hover {
    opacity: 1;
    box-shadow: 0px 2px 5px 0px rgba(0, 0, 0, 0.3), 0px 2px 10px 0px rgba(0, 0, 0, 0.12);
}

pre {
    counter-reset: rowNumber;
}

pre code {
    line-height: 1.5em;
    tab-size: 4;
}

pre span.row-number {
    counter-increment: rowNumber;
}

pre:not(.nonum) span.row-number::before {
    content: counter(rowNumber);
    width: 2rem;
    display: inline-block;
    color: #a0a0a0;
    padding-left: 10px;
    margin-right: 10px;
    background: #505050;
}

レイアウト | フッター | HTML/JavaScript (タイトルは空欄)

  • highlight.js をインポート
  • コードの行番号表示と、コピーボタンの追加
<!-- highlight.js -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.10.0/styles/monokai.min.css" integrity="sha512-pU8Ny4jS7Uq58y/K4YLD+jF/74zC3R5SDco/Ln143vyLEmGQY7MV8p+Z5q7big/mNimhvQsfQpprGvUa2QzihQ==" crossorigin="anonymous"/>

<!-- importmap は、module (ここで定義していないものも含む)よりも、先に定義する必要がある -->
<script type="importmap">
{
  "imports": {
    "highlightjs": "https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.10.0/es/highlight.min.js",
    "groovy": "https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.10.0/es/languages/groovy.min.js",
    "dockerfile": "https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.10.0/es/languages/dockerfile.min.js"
  },
  "integrity": {
    "https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.10.0/es/highlight.min.js": "sha512-nlc+OFebuN78mhTZDqa8Md8mOLDJ5lKi1mN3q3eoS7uVeFOkpHD+kiaQhV+1nWHRiKwuWPhMrXl4CD/thL1Xsw==",
    "https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.10.0/es/languages/groovy.min.js": "sha512-9I2VvFgixVk7CD2euE3ujw4pYEB0yw6BrgQ25UMsK6kKGch9dnA0S4hLsU/uJkl71LnzCx+l000cvxtiCwIHCQ==",
    "https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.10.0/es/languages/dockerfile.min.js": "sha512-RVgu/sdw9iWocj3Fz2gY5FlZOK3gKRg7RI9pXL1JE7K/WLsDi8kVHbz2J7BBwNSYPICc8B1+hgym/9XiTW5JFw=="
  }
}
</script>

<script type="module">
  // Default Languages: bash, c, cpp, csharp, css, diff, go, graphql, ini, java, javascript, json, kotlin,
  //   less, lua, makefile, markdown, objectivec, perl, php-template, php, plaintext, python-repl, python,
  //   r, ruby, rust, scss, shell, sql, swift, typescript, vbnet, wasm xml, yaml
  import hljs from 'highlightjs';
  import groovy from 'groovy';
  import dockerfile from 'dockerfile';
  hljs.registerLanguage('groovy', groovy);
  hljs.registerLanguage('dockerfile', dockerfile);

// Line Number & Copy Button Plugin

const copyToClipboard = async (element) => {
  const ranges = [];
  const selection = window.getSelection();
  const range = document.createRange();
  let result = false;
  for (let i = 0; i < selection.rangeCount; i += 1) {
    ranges[i] = selection.getRangeAt(i);
  }
  range.selectNodeContents(element);
  selection.removeAllRanges();
  selection.addRange(range);
  try {
    await navigator.clipboard.writeText(selection);
    result = true;
  } catch (e) {
    console.error(e);
  }
  selection.removeAllRanges();
  for (let i = 0; i < ranges.length; i += 1) {
    selection.addRange(ranges[i]);
  }
  return result;
};

const body = document.querySelector("body");
const active = [];

body.addEventListener("click", async (e) => {
  try {
    const target = e.target;
    if (!target.classList.contains('copy-button')) return;

    const pre = target.closest("pre");
    let result = false;
    if (active.indexOf(target) !== -1) return;
    if (pre) {
      result = await copyToClipboard(pre);
      target.innerText = (result ? "COPIED!" : "FAILED!");
      target.classList.add((result ? "success" : "failed"));
      active.push(target);
      setTimeout(() => {
        let index = active.indexOf(target);
        target.className = "copy-button";
        target.innerText = "COPY";
        if (index !== -1) active.splice(index, 1);
      }, 2000);
    }
  } catch (e) {
    console.error(e);
  }
});

hljs.addPlugin({
  'after:highlightElement': ({el, result}) => {
    let lines = result.value;
    if (lines.split(/\n/).length >= 2) {
      lines = result.value.replace(/^/gm, '<span class="row-number"></span>');
    }
    el.innerHTML = `<button class="copy-button" style="border-radius: 6px;">COPY</button>${lines}`;
  }
});

hljs.highlightAll();
</script>

<!-- Mermaid -->
<script type="module">
  import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs';
  mermaid.initialize({ startOnLoad: true,  theme: 'dark'});
</script>

<!-- MathJax -->
<script id="MathJax-script" async src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"></script>