Pandocは文書形式の相互変換が便利ななツールです。 私も普段から利用していますが、先日、Word(.docx)ファイルからorg-mode(.org)ファイルへ変換する際に面倒な問題に遭遇しました。

この記事では、その試行錯誤の道のりと解決策をまとめます。

課題 : 箇条書きが、意図せず#+begin_quote…#+end_quoteブロックで囲まれてしまう

docx形式で作成した箇条書きリストを、Pandocを使ってorgファイルに変換したところ、2つの大きな問題が発生しました。

  1. ネストした箇条書きが、意図せず#+begin_quote…#+end_quoteブロックで囲まれてしまう。
  2. 各リスト項目の間に、不要な空行が入ってしまい、間延びした表示になる。

問題の出力例:

コード スニペット

  • #+begin_quote

    親項目1

  • #+begin_quote

    子項目A

    #+end_quote

  • #+begin_quote

    子項目B

    #+end_quote

#+end_quote

このような出力は、見た目が崩れるだけでなく、構造的にも意図しない「引用」となってしまい、org-modeでの再利用性を著しく損ないます。

最終結論

この問題は、PandocのLuaフィルタを使い、変換プロセス中にリストのネストを引用としないことで解決しました。

原因は、以下の2点でした。

  1. Word文書のインデント構造が、PandocによってリストのネストではなくBlockQuote(引用ブロック)として解釈されていた。
  2. リストの各項目がPara(段落)要素で構成されており、これがorg-mode出力時に項目間の空行を生んでいた。

最終的に完成したLuaフィルタと実行コマンドは記事の最後にまとめてあります。

試行錯誤1:単純なBlockQuoteの解除

最初に考えたのは、「問題となっているBlockQuoteを単純に解除すればよいのでは?」というアプローチでした。

しかし、単純な条件のフィルタではうまくいかず、原因を特定する必要に迫られました。

まず、 pandoc -t native コマンドで文書の内部構造をダンプしてみると、問題の箇所は以下のような構造になっていることがわかりました。

, BulletList [ [ BlockQuote [ Para [ Str “\12456\12450\12525\12496\12452\12463\12434\36092\20837\12377\12427” ] ] , BulletList [ [ BlockQuote [ Para [ Str “\12489\12524\12483\12469\12540\12434\25448\12390\12427” ] ] ] , [ BlockQuote [ Para [ Str “\12362\12418\12385\12419\12384\12394\12434\37096\23627\12398\30495\12435\20013\12395\25345\12387\12390\12367\12427” ] ] ] , [ BlockQuote [ Para [ Str

BlockQuoteの中にPara(段落)とBulletList(リスト)が同居しており、これでは生半可な条件で太刀打ちできません。

試しに、「すべてのBlockQuoteを無条件に解除する」という乱暴なフィルタを適用してみたところ、問題の箇条書きはきれいになりました。 以下のようなlua filterです。

function BlockQuote(quote)
  return quote.content
end

しかし、当然ながら文書中の正規の引用まで全て消えてしまいました。 これではすべてのQuoteが解除されてしまいます。

試行錯誤2:BulletListのQuoteをターゲットにするアプローチに変換

前回の失敗を踏まえ、アプローチを変更しました。

  • やめたアプローチ:「文書全体からBlockQuoteを探す」
  • 改善後のアプローチ:「*BulletList(箇条書きリスト)の内部のBlockQuoteだけを解除する*」

この方針でフィルタを書き直し、実行しました。

この段階での出力結果:

コード スニペット

  • エアロバイクを購入する

  • ドレッサーを捨てる

  • おもちゃだなを部屋の真ん中に持ってくる

  • おもちゃだなが会った場所に冷凍庫を配置する。

BlockQuoteはほぼ解消されましたが、今度は項目間にくっきりと残る*空行*が気になる結果となりました。 もう少しだけ変更を加えることにしました。

試行錯誤3(最終ソリューション):更に見た目をきれいに。空行を排除

空行問題の根本原因は、リストの各項目がPara(段落)要素で構成されていることでした。 もともとの pandoc -t native コマンドで文書の内部構造を振り返ります。

, BulletList [ [ BlockQuote [ Para [ Str “\12456\12450\12525\12496\12452\12463\12434\36092\20837\12377\12427” ] ]

BlockQuotePara を含んでいることがわかります。

Org Modeの仕様では、項目が「段落」だと、項目間に自動的に空行が入ってしまうのです。

そこで、最終的な解決策として、リスト項目の中にあるParaをPlain(プレーンテキスト)に変換する処理を加えました。

これより、ついにBlockQuoteと不要な空行の両方が除去され、意図通りのクリーンなorg-modeリストを手に入れることができました。

参考(作成したLuaフィルタとPandocのコマンド例)

以下が、今回の試行錯誤の末に完成した最終版のLuaフィルタと、その使い方です。

1. Luaフィルタの作成

以下の内容を、cleanup-list.luaのようなファイル名で保存します。

-- list-final-solution.lua (最終解決版)
-- リスト項目を処理する共通関数
-- リスト項目を処理する共通関数
-- 1. BlockQuoteを解除
-- 2. 空の要素を削除
-- 3. Para を Plain に変換して空行を防ぐ
local function process_list_items(items)
  local new_items = {}
  -- 各リスト項目 (item_blocks) をループ
  for _, item_blocks in ipairs(items) do

    local temp_blocks = {} -- BlockQuote解除と空要素削除後の一時的なブロック配列

    -- STEP 1 & 2: BlockQuote解除と空要素削除
    for _, block in ipairs(item_blocks) do
      -- 空の要素(Para/Plain)かどうかを判定
      local is_empty = (block.t == 'Para' or block.t == 'Plain') and #block.content == 0

      if not is_empty then
        if block.t == 'BlockQuote' then
          for _, inner_block in ipairs(block.content) do
            local is_inner_empty = (inner_block.t == 'Para' or inner_block.t == 'Plain') and #inner_block.content == 0
            if not is_inner_empty then
              table.insert(temp_blocks, inner_block)
            end
          end
        else
          table.insert(temp_blocks, block)
        end
      end
    end

    -- STEP 3: Para を Plain に変換
    local final_blocks = {} -- 最終的なブロック配列
    for _, block in ipairs(temp_blocks) do
      if block.t == 'Para' then
        -- ParaをPlainに変換して挿入
        table.insert(final_blocks, pandoc.Plain(block.content))
      else
        table.insert(final_blocks, block)
      end
    end

    if #final_blocks > 0 then
      table.insert(new_items, final_blocks)
    end
  end
  return new_items
end

-- 箇条書きリスト(BulletList)を処理する関数
function BulletList(blist)
  blist.content = process_list_items(blist.content)
  return blist
end

-- 番号付きリスト(OrderedList)にも同じ処理を適用する関数
function OrderedList(olist)
  olist.content = process_list_items(olist.content)
  return olist
end

2. Pandocコマンドの実行

ターミナルで、以下のコマンドを実行します。

pandoc "変換したいファイル.docx" -o "出力ファイル.org" --lua-filter=cleanup-list.lua
  • “変換したいファイル.docx"と"出力ファイル.org"は、ご自身のファイル名に置き換えてください。
  • –lua-filter=オプションで、先ほど作成したLuaフィルタファイルを指定します。

この記事が、同じ問題で悩む誰かの助けになれば幸いです。