ごあいさつ
ご機嫌よう、みやたるてぃです。
情報系の学部に通うしがないB2をやってます。
技術記事を書きたくて
大学入学を機に始めたプログラミングも一年が経とうとしてる頃、そろそろ私も技術記事なるものを書いてみたいと思い、筆をとってみた次第です。
Markdown...
気づけばそれは、インターネットのリンガフランカとも称せるマークアップ言語の一つ。例にもれずQiita、そしてZennでも提供されている記法なわけでございますが、実を言うと私はこのMarkdownが好きではありません。
だって、サービスによって微妙に記法が異なるんですよ 微妙に。
普段の生活では、私用の連絡でDiscord, インターン先ではSlackを使っておりますが、いざ太字を書きたいと思い「*」で囲めば、それはSlackでは太字になりDiscordでは斜体になる...とてもではないが付き合っていられません。
そして何より逆張りたい。
人生の夏休みたる大学生活、些かの便利と引き換えに、世の王道から抜け出して、逆張る自己満足に浸りたいのです。
そんな事情もあいまって Markdown から離れ、とある記法にいきつきました。
Org-modeで技術記事を書きたくて
Org-mode とは、小指を鍛えるテキストエディタ Emacs に標準搭載されている機能のひとつ。そのOrg-modeが独自の軽量マークアップ記法を提供してくれているのですが、おこがましくもEmacserを名乗る者としてはその記法を使わないわけにはいかず、気付いたら私はその虜になっておりました。
なればこそ、太陽は東から昇るが如く、Org-modeで記事を書きたいと思うのは自然の運び。
そのような訳もあり少し奮戦してみました。
その過程を記すのがこの記事の目的となります。
目指した投稿フロー
まず目指したのは、できるだけ普段の執筆環境から離れずに記事を書き、そのまま投稿まで持っていける流れでございます。
私がやりたかったことは、ざっくり言えば以下の通り。
- Emacs で Org ファイルを書く
- Org ファイル内にタイトルやタグなどの記事情報も書く
- その Org ファイルを Zenn 用 Markdown と Qiita 用 Markdown に変換する
- 変換されたファイルを、作成した GitHub repo に push する
- GitHub repo と連携した Zenn と Qiita に記事が反映され投稿される(Qiitaについては、GitHub Actions で Qiita CLI を動かしている)
これを叶えるためには、Zenn, Qiita それぞれのMarkdown記法に変換してくれるコンバータの作成が必要でした。
作ってみました ox-hub
ということでコンバータを作ってみました。
仕組みの全体像
ox-hub の役割は、正本となる Org ファイルから Zenn 用と Qiita 用の Markdown を生成すること。
想定しているディレクトリ構成は、まとめると以下の通りです。
.
├── org/ # Org-mode で書いた元記事
├── articles/ # Zenn 用 Markdown
└── public/ # Qiita 用 Markdown
org/ に置いた Org ファイルをこのパッケージが提供する機能を使い変換すると、 articles/ には Zenn 用 Markdown、 public/ には Qiita 用 Markdown が出力されます。
Orgファイルに記事情報を書く
記事のタイトルやタグ、公開状態などのメタデータは、Org ファイルの先頭に #+OXHUB_* という形で記載。
#+OXHUB_TITLE: 記事タイトル
#+OXHUB_TAGS: Emacs 個人開発
#+OXHUB_STATUS: draft
#+OXHUB_ZENN_EMOJI: 🦄
#+OXHUB_ZENN_TYPE: tech
#+OXHUB_QIITA_PRIVATE: false
#+OXHUB_QIITA_SLIDE: false
こうすることで、本文だけでなく記事情報も Org ファイル内にまとめられるようにしました。
Zenn用MarkdownとQiita用Markdownに変換する
変換は Emacs 上で次のインタラクティブ関数を評価。
M-x ox-hub-export-current-buffer
すると、現在開いている Org ファイルから、Zenn 用と Qiita 用の Markdown がそれぞれ生成されます、
本文の Org 記法を Markdown に変換するだけでなく、 #+OXHUB_* に書いた情報をもとに、Zenn と Qiita それぞれのサービスが要求するメタデータも作成するようにしました、
実装でこだわったこと
今回 ox-hub を作るにあたり、以下のポイントをこだわってみました。
-
.orgファイルを唯一の原稿として扱うこと - Zenn と Qiita の差分を ox-hub 側で抽象化すること
- 不正なメタデータを早い段階で弾くこと
- 本文レンダラーをできるだけ共通化すること
- Zenn / Qiita 固有の処理だけを分岐する
- 後から拡張しやすい形にすること
1. .org ファイルを唯一の原稿として扱う
.org ファイルを唯一の原稿として扱うこと、これは一番こだわりました。
なんといっても、Org-mode だけで記事を書きたいという思いで開発したわけですから、これは最優先に叶える要件でした。
結果、私が編集するのはあくまで org/ 配下の Org ファイルだけというようにあいなったわけでございます。
org/example.org
├── articles/example.md # Zenn 用
└── public/example.md # Qiita 用
「正本は常に Org ファイルに」
これを金科玉条として、何か修正する際はOrgファイルだけをいじって生成しなおす、というようにしました。
2. Zenn と Qiita の差分を ox-hub 側で抽象化する
Zenn と Qiita はどちらも Markdown で記事を書けますが、実際には front matter や投稿設定に差があります。
たとえば Zenn では emoji や type が必要になり、Qiita では private や slide といった項目を扱うといった具合にですね。
そこで ox-hub では、Org ファイル側には #+OXHUB_* という共通のメタデータを書き、それを Zenn 用、Qiita 用に変換するようにしました。
#+OXHUB_TITLE: 記事タイトル
#+OXHUB_TAGS: Emacs 個人開発
#+OXHUB_STATUS: draft
#+OXHUB_ZENN_EMOJI: 🦄
#+OXHUB_ZENN_TYPE: tech
#+OXHUB_QIITA_PRIVATE: false
#+OXHUB_QIITA_SLIDE: false
このようにしておけば、Org ファイルを書く側は「この記事の情報」を書くだけで済みます。
その後、Zenn では Zenn が期待する front matter に、Qiita では Qiita が期待する front matter に ox-hub が変換します。
そのサービスが期待しないメタデータについてはコンバータ側で出力をしないように制御してみました。
3. 不正なメタデータを早い段階で弾く
また、メタデータのバリデーションについても実装してみました。
たとえば OXHUB_STATUS には draft または published だけを許可し、 OXHUB_ZENN_TYPE には tech または idea だけを許可するようにしました。
また、Qiita 用の OXHUB_QIITA_PRIVATE や OXHUB_QIITA_SLIDE についても、真偽値として解釈できる値だけを受け入れるようにしています。
これは、間違った値をなんとなく受け入れてしまうと、後の段階で原因の分かりにくい不具合になるからですね。
たとえば draft と書くべきところを darft と書いてしまった場合、変換自体は成功してしまうが投稿先で意図しない挙動になる、という状態は避けたいと考えました。
そのため ox-hub では、変換時点でおかしい値を見つけたらそこで止める方針にしています。
多少厳しくても、壊れた Markdown を静かに生成するより、早い段階で「ここが違う」と分かった方が安心です。
4. 本文レンダラーをできるだけ共通化する
本文の変換については、Zenn 用と Qiita 用で完全に別々の変換器を作るのではなく、できるだけ共通のレンダラーで処理するようにしました。
Org ファイルを解析し、見出し、段落、リスト、コードブロック、リンクなどの要素を Markdown に変換していきます。
基本的な Markdown 記法は Zenn と Qiita で共通している部分も多いため、そこを別々に実装してしまうと、同じような処理が二重に存在してしまいます。
そうなると、片方だけ修正し忘れる可能性が高くなります。
そこで、共通で扱える Org 記法は共通の処理に寄せ、Zenn と Qiita で差が出る部分だけを分岐させる設計にしました。
イメージとしては、本文変換の大きな流れはひとつにしておき、必要な箇所だけ target を見て処理を切り替える形です。
Org AST
-> 共通レンダラー
-> 共通 Markdown
-> Zenn 固有処理
-> Qiita 固有処理
この形にしておくことで、本文変換の見通しを保ちながら、投稿先ごとの差分にも対応しやすいようにしました。
5. Zenn / Qiita 固有の処理だけを分岐する
Zenn と Qiita には、それぞれ独自の記法や仕様があります。
そのため、すべてを完全に共通化するのではなく、サービスごとに違う部分は明示的に分岐させることにしました。
たとえばフロントマターの生成は、Zenn と Qiita で明確に異なります。
Zenn では次のような項目が必要になります。
title: "記事タイトル"
emoji: "🦄"
type: "tech"
topics: ["Emacs", "個人開発"]
published: false
一方で Qiita では、別の形式のメタデータが必要になります。
このような違いまで無理に共通化しようとすると、かえって分かりにくい実装になります。
なので、共通化する部分と分岐する部分を分けることを意識しました。
共通化できるところは共通化する。
しかし、Zenn らしさ、Qiita らしさが出るところは、それぞれ専用の処理に任せる。
この切り分けは、実装するうえで意識したところです。
6. 後から拡張しやすい形にする
現時点の ox-hub は、あくまで自分が技術記事を書くために必要な範囲を中心に作っています。
ただし、将来的には対応したいことが増やそうと考えております。
たとえば、Zenn や Qiita の独自記法にもう少し対応したくなるかもしれませんし、Org の書き方が ox-hub の想定に合っているかを事前に確認する lint のような機能も欲しくなるかもしれません。
そのため、最初からすべての記法に対応するのではなく、処理の責務を分けておくことを意識しました。
大きく分けると、ox-hub の処理は次のような流れになります。
Org ファイル
-> メタデータ抽出
-> メタデータ検証
-> フロントマター生成
-> 本文変換
-> Zenn / Qiita 用 Markdown として出力
このように段階を分けておけば、あとから機能を足すときにも、どこに処理を追加すればよいかが分かりやすくなるはずです。
たとえば、メタデータの項目を増やしたいならメタデータ処理に手を入れればよく、本文記法を増やしたいなら本文レンダラーに処理を追加すればよい。
責務をできる限り分けてあげる。
以上がこだわったポイントでございました。
作ってみて感じたこと
今回 ox-hub を作ってみて一番強く感じたのは 「自分が Emacs package を作る側になるなんて」 という驚きでした。
これまでの私にとって Emacs の設定をするということ、それ即ち他の人の設定をコピペする、あるいはChatGPTのいわれるがままに外部パッケージを入れる。そうしてどうにか動かしたりすることがほとんどでした。
その度に私は「守破離の常道に倣っている」と自己弁護しつつ、どこかこの Emacs が自分のものでないという疎外感がついて離れませんでした。
けれど今回、「Org ファイルから Zenn 用と Qiita 用の Markdown を生成する」という機能を、自分だけが欲する機能を自分から実装してみました。
だからこそ、ひとまずの完成を迎えられたことに驚嘆と感動を覚えています。
Org ファイルを書き、メタデータもそこにまとめ、コマンドひとつで Zenn 用と Qiita 用の Markdown が生成される。
自分の普段いる場所から、そのまま記事投稿の流れにつながっていく感覚は、私の想像以上に心地のよいものでした。
今まではどこかよそよそしかったこの Emacs が、溶け込むように自分のものになりつつある。
今回の開発では、そのような体験を得られたことが自分にとって一番の成果だったと感じております。
今後の展望を結びに
とはいえこのパッケージ、ほとんどリファクタリングできておりません。
生成 AI と一緒に作成しましたが、MVP 完成を優先したため、雑な実装になっているところも沢山ございまして、時間をとりつつ改修していきたいと考えています。
また、現状では生成した記事のプレビュー確認をブラウザ上で行っています。
しかし、できることならこの確認作業も Emacs 上で完結させたいところです。
Emacs で書き、Emacs で変換し、Emacs で確認する。
そんなパラダイスみたいな Emacs 環境を構築してぬくぬくしていきたいわけでございます。
さらに、ox-hub とは別に、Zenn CLI や Qiita CLI のコマンドを Emacs のインタラクティブ関数として呼び出せるようなラッパーも作ってみたいと考えています。
Org から Markdown を生成するだけでなく、プレビューや投稿まわりの操作までまとめて扱える、より包括的な Emacs package に育てられたら楽しそうです。
今後も少しずつ手を入れながら、自分の執筆環境を自分の手で育てていきたいと思います。