読者です 読者をやめる 読者になる 読者になる

ほんじゃら堂

めんどくさい仕事をラクにする作業自動化レシピ集

MarkdownファイルをreStructuredTextに変換する

IT系・技術系 データ変換 clojure

f:id:piro_suke:20160827004244j:plain

新しいプロジェクトを開始するにあたり、

ExcelやめてSphinx + reStructuredTextでドキュメント作るようにしません?

という提案をしてみたら、意外とすんなり通った。

概要 — Sphinx 1.4.6 ドキュメント

reStructuredText入門 — Sphinx 1.4.6 ドキュメント

これでドキュメントのバージョン管理とか、

複数人での共同作成とかがしやすくなりそうで嬉しい。

この提案の前に、

チーム内でのちょっとした技術情報とか議事メモの共有に

Markdown形式のファイルを使うようにしていて、

テキスト形式の文書管理に抵抗がなかったのが良かったのかもしれない。

で、どうせならこれまでMarkdownで作成したファイル(結構作ってた)も

reStructuredTextに変換して、SphinxでHTML化して閲覧できるようにしたい!

ということで今回のテーマにつながった。

実はSphinxはプラグインだか設定だかツールだかで

Markdown形式のファイルも使えるらしいのだけど、

www.slideshare.net

これから作るファイルは

全部reStructuredText形式になるのだから揃えたい、

ということでやはり既存Markdownファイルを変換することにした。

オーソドックスなのはPandocで変換する方法

PandocというHaskell製の文書変換ツールで、

多様な入力、出力フォーマットに対応してるらしい。

Pandoc ユーザーズガイド 日本語版 - Japanese Pandoc User's Association

qiita.com

おそらくこのツールを使うのが無難で良いと思われる。

変換ツールを作ってみたい

変換といっても、

作成したMarkdownをreStructuredTextに変換するのに必要なのは

セクション見出しの変換ぐらいだし...

Clojureでファイル形式変換ってちょっと書いてみたいし...

ということでどうしても自分で書いてみたくなったので、

Markdownファイルの見出し部分を

reStructuredText形式に変換するプログラムを作成する。

Clojureで変換プログラムを作る

指定したフォルダ内の「.md」ファイルを変換して

「.rst」ファイルとして同じフォルダ内に出力する

プログラムを作る。

まずは指定したフォルダ内の「.md」ファイル一覧を取得する処理を書く:

ns md2rst.core
  (:gen-class)
  (:require [clojure.java.io :as io]))

...

(defn convert-files
  [dir-path]
  (let [dir (io/file dir-path)
        fs (filter #(re-find #"\.md$" (.getName %)) (.listFiles dir))]
    (doseq [f fs] 
      (println (.getPath f)) 
      (convert-file f))))

(defn -main
  [& args]
  (let [dir-path (first args)]
    (convert-files dir-path)))

続いて、「.md」ファイルを行単位で読み込んで

変換処理を経由して

「.rst」ファイルとして出力する関数を追加する:

(defn convert-file
  [f]
  (let [out-file-path (clojure.string/replace (.getPath f) #"\.md$" ".rst")]
    (with-open [rdr (io/reader f)
                wtr (io/writer out-file-path :encoding "UTF-8")]
      (doseq [line (line-seq rdr)]
        (.write wtr (str (convert-line line) "\n"))))))

多分ブロックの変換にも対応しようと思ったら、

行単位での処理ではダメなんだろうけど、

見出し変換だけならこれでOK。

Clojureのwith-openで複数のファイルをcloseしてくれるのいいな。

次に行のフォーマットをチェックして対応する変換処理を

行う関数を追加する:

(defn convert-line
  [line]
  (cond
    (not= nil (re-find #"^# " line)) (conv-h1 line)
    (not= nil (re-find #"^## " line)) (conv-h2 line)
    (not= nil (re-find #"^### " line)) (conv-h3 line)
    (not= nil (re-find #"^#### " line)) (conv-h4 line)
    (not= nil (re-find #"^##### " line)) (conv-h5 line)
    :else line))

最後に、各変換用関数を追加して、

全体としては下記のようなスクリプトになった:

(ns md2rst.core
  (:gen-class)
  (:require [clojure.java.io :as io]))

(defn count-hankaku
  [content]
  (count (.getBytes content "windows-31j")))

(defn conv-h1
  [line]
  (let [content (clojure.string/replace line #"^# " "")
        content-count (+ 1 (count-hankaku content))
        marks (clojure.string/join (repeat content-count "#"))]
    (str marks "\n" content "\n" marks)))

(defn conv-h2
  [line]
  (let [content (clojure.string/replace line #"^## " "")
        content-count (+ 1 (count-hankaku content))
        marks (clojure.string/join (repeat content-count "*"))]
    (str marks "\n" content "\n" marks)))
    
(defn conv-h3
  [line]
  (let [content (clojure.string/replace line #"^### " "")
        content-count (+ 1 (count-hankaku content))
        marks (clojure.string/join (repeat content-count "="))]
    (str content "\n" marks)))
    
(defn conv-h4
  [line]
  (let [content (clojure.string/replace line #"^#### " "")
        content-count (+ 1 (count-hankaku content))
        marks (clojure.string/join (repeat content-count "-"))]
    (str content "\n" marks)))
    
(defn conv-h5
  [line]
  (let [content (clojure.string/replace line #"^##### " "")
        content-count (+ 1 (count-hankaku content))
        marks (clojure.string/join (repeat content-count "^"))]
    (str content "\n" marks)))
    
(defn convert-line
  [line]
  (cond
    (not= nil (re-find #"^# " line)) (conv-h1 line)
    (not= nil (re-find #"^## " line)) (conv-h2 line)
    (not= nil (re-find #"^### " line)) (conv-h3 line)
    (not= nil (re-find #"^#### " line)) (conv-h4 line)
    (not= nil (re-find #"^##### " line)) (conv-h5 line)
    :else line))

(defn convert-file
  [f]
  (let [out-file-path (clojure.string/replace (.getPath f) #"\.md$" ".rst")]
    (with-open [rdr (io/reader f)
                wtr (io/writer out-file-path :encoding "UTF-8")]
      (doseq [line (line-seq rdr)]
        (.write wtr (str (convert-line line) "\n"))))))

(defn convert-files
  [dir-path]
  (let [dir (io/file dir-path)
        fs (filter #(re-find #"\.md$" (.getName %)) (.listFiles dir))]
    (doseq [f fs]
      (println (.getPath f))
      (convert-file f))))

(defn -main
  "I don't do a whole lot ... yet."
  [& args]
  (let [dir-path (first args)]
    (convert-files dir-path)))

count-hankaku関数は、

見出しの長さを半角計算で算出するための関数。

見出しの記号が見出しの半角文字数よりも短いと

Sphinxが警告を出すので、countの代わりに使用している。

このスクリプトを実行すると、

# 見出し1

## 見出し2

### 見出し3

#### 見出し4

##### 見出し5

########
見出し1
########

********
見出し2
********

見出し3
========

見出し4
--------

見出し5
^^^^^^^^

のように変換して出力される。

おわり

Pandocがあるのにわざわざ自分で書いた割には、

特にClojureの良さを生かせてない気がする...。

もう少し洗練された書き方がありそうだけど、

ひとまずこれで変換処理を追加していけば

強調表示とか1行内のフォーマット変換には対応できるようにはなったので、

一旦終了。

また思いついたら改良したい。

考える技術・書く技術―問題解決力を伸ばすピラミッド原則

考える技術・書く技術―問題解決力を伸ばすピラミッド原則