🎉

Vivliostyle Flavored Markdownでソースコードを取り込む

2024/12/08に公開

はじめに

Vivliostyle Flavored Markdownでコードに行番号を付ける の続編になります。

Vivliostyle Flavored Markdownでソースコードを取り込む

プログラミングに関する技術書には、ソースコードの掲載が不可欠です。普通は次のようにソースコードを直接Markdown中に記述することとなります。

```ruby:prime.rb
def prime?(num)
  return false if num <= 1
  (2..Math.sqrt(num)).none? { |i| num % i == 0 }
end

primes = (1..100).select { |num| prime?(num) }
puts primes
```

そして、既にソースファイルが存在する場合、コピー&ペーストで貼り付けるのは手間です。そこで、ソースコードを取り込む機能を実装したいと思います。

仕様

include文

まず、ソースコードを取り込む機能 についてですが、includeimport と呼ばれることが多いです。
ここでは、C言語に倣って include を用いることにします。

HonKit

次に、include文の記法についてです。Markdownでの書籍執筆ツールとしてHonKit も有名です。

HonKitでは、次のように書くことで、sourceディレクトリのprime.rbを取り込むことができます。

[include](source/prime.rb)

Block Code

一般的な markdown に倣って、次のように書くことも自然な拡張のように思えます。言語名ruby に代えて、include文であることを示しています。

```include:source/prime.rb
```

One Line

include文に必要なのは、```include: source/prime.rb``` のみですから、前後の``` を外して、以下のように書けるとよりスマートかもしれません。

include: source/prime.rb

ということで、好みもあるかと思いますので、以上の HonKitBlock codeOne Lineに対応した include機能を実装したいと思います。

アルゴリズム

  1. 元のMarkdownファイルを読み込む。
  2. 一行ずつ走査して
    include文が書かれていたら、
      ソースコードを読み込んで、ファイルに書き込む
    書かれていなかったら
      各行をそのままファイルに書き込む

コード

以上の方針を踏まえて、完成したコードは次のようになります。

include.rb
# 取り込み前
# HonKit
# [include](prime.rb)
# Block Code
# ```include:prime.rb
# ```
# One Line
# include: prime.rb

# 取り込み後
# ```ruby:prime.rb
# def prime?(num)
#   return false if num <= 1
#   (2..Math.sqrt(num)).none? { |i| num % i == 0 }
# end
#
# primes = (1..100).select { |num| prime?(num) }
# puts primes
# ```

# 拡張子と言語との対応ハッシュ
hash = { rb:   "ruby",
         html: "html",
         css:  "css",
         js:   "javascript",
         c:    "c",
         java: "java" }

# ソースコードを読み込む
def load_source_file(path)
  begin
    File.read(path)
  rescue Errno::ENOENT => e
    # ソースコードが読み込めなかった場合の例外発生時
    puts "ファイルが開けませんでした: #{e.message}"
    false
  end
end

# 読み込みファイル名
input_file = ARGV[0]

# 主ファイル名と拡張子
basename = File.basename(ARGV[0], ".*")
extname  = File.extname(ARGV[0])
# 書き込みファイル名
output_file = "_i_#{basename}#{extname}"

# 置換後のMarkdownをファイルに書き込む
File.open(output_file, "w") do |file|
  # ファイルを読み込む
  content = File.read(input_file)
  content.each_line do |line|
    # [include](source/prime.rb)
    # ```include:prime.rb
    # include: prime.rb
    # に一致すれば、prime.rb を読み込む
    if match = line.match(/include:\s*([a-zA-Z0-9_\/\.\-]+)/) ||
               line.match(/\[include\]\(([a-zA-Z0-9_\/\.\-]+)\)/)
      path          = match[1]                                # パス名
      directory     = File.dirname(path)                      # ディレクトリ名
      main_filename = File.basename(path, File.extname(path)) # 主ファイル名
      extension     = File.extname(path).delete('.')          # 拡張子を取得
      filename      = "#{main_filename}.#{extension}"         # ファイ名
      language      = hash[extension.to_sym]                  # 言語名
      file.puts "```#{language}:#{filename}"
      if source = load_source_file(path)
        file.puts source
      else
        file.puts "=== Could not open the source file ==="
      end

      # コードブロックの不足があれば補う
      file.puts "```" unless line.match(/```/)
    else
      file.puts line
    end
  end
end

実行結果

sample.rb として、次の Markdownを用意します。

---
lang: ja
link:
  - rel: 'stylesheet'
    href: 'prism.css'
---

# include 練習

```include```の練習です。

## Ruby
```ruby:prime.rb
def prime?(num)
  return false if num <= 1
  (2..Math.sqrt(num)).none? { |i| num % i == 0 }
end

primes = (1..100).select { |num| prime?(num) }
puts primes
```

## HonKit
[include](source/prime.rb)

## Block Code
```include:source/prime.rb
```

## One Line
include: source/prime.rb

次のコマンドで実行します。

$ ruby include.rb sample.md

_i_sample.mdとして、次のようにmarkdownファイルが出力されています。

---
lang: ja
link:
  - rel: 'stylesheet'
    href: 'prism.css'
---

# include 練習

```include```の練習です。

## Ruby
```ruby:prime.rb
def prime?(num)
  return false if num <= 1
  (2..Math.sqrt(num)).none? { |i| num % i == 0 }
end

primes = (1..100).select { |num| prime?(num) }
puts primes
```

## HonKit
```ruby:prime.rb
def prime?(num)
  return false if num <= 1
  (2..Math.sqrt(num)).none? { |i| num % i == 0 }
end

primes = (1..100).select { |num| prime?(num) }
puts primes
```

## Block Code
```ruby:prime.rb
def prime?(num)
  return false if num <= 1
  (2..Math.sqrt(num)).none? { |i| num % i == 0 }
end

primes = (1..100).select { |num| prime?(num) }
puts primes
```

## One Line
```ruby:prime.rb
def prime?(num)
  return false if num <= 1
  (2..Math.sqrt(num)).none? { |i| num % i == 0 }
end

primes = (1..100).select { |num| prime?(num) }
puts primes
```

無事に、sourceディレクトリのprime.rbファイルを取りこめたことが分かります。

出力結果の _i_sample.md は作業用です。不要になったら、次のコマンドで削除できます。

rm _i_sample.md

行番号表示と組み合わせる

ソースコードを読み込むことができましたので、前回行った行番号表示と組み合わせましょう。

前回の最後に登場した vfm2html.rbを次のように少し書き換えます。

vfm2html.rb
#!/usr/bin/env ruby

require "find"

# 変換対象ファイル
files = []

if ARGV.size != 0
  # 変換対象ファイルが指定されているなら当該ファイルを変換
  filename_without_extension = File.basename(ARGV[0], ".md")
  files << filename_without_extension
else
  # カレントディレクトリ直下の全てのMarkdownファイルを対象に変換
  directory = "."
  md_files = Dir.glob("#{directory}/*.md")
  md_files.each do |file|
    # 拡張子を取り除く
    filename_without_extension = File.basename(file, ".md")
    files << filename_without_extension
  end
end

# 変換対象ファイルへの繰り返し処理
files.each do |file|
  # 実行するコマンドを設定
  temporary_file = "_i_#{file}.md"
  command = <<~CMD
    ruby include.rb #{file}.md
    vfm #{temporary_file} > #{file}.html
    ruby line_numbers.rb #{file}.html
  CMD

  # コマンドを実行する
  output = `#{command}`
  # 作業ファイルを削除する
  File.delete(temporary_file)
end

# 結果を表示
puts "Markdownから、行番号付きHTMLへの変換が完了しました。"
$ ./vfm2html.rb sample.md

sample.md から sample.html が作成されましたので、Vivliostyleで表示結果を確認してみましょう。

$ vivliostyle preview sample.html

ソースコードを取り込み、行番号付きで表示することができました。

おまけ

vivliostyle preview の際には、元のmarkdownファイルではなく、加工後の html を参照したいことと思います。 そこで、vivliostyle.config.js の内容を少し更新しましょう。

前回は markdownファイルをプレビューしたので、entryファイルの拡張子が.mdになっていましたが、.htmlに更新します。

vivliostyle.config.js
// @ts-check
/** @type {import('@vivliostyle/cli').VivliostyleConfigSchema} */
const vivliostyleConfig = {
  title: 'Rubyの世界: 初心者のためのプログラミング入門',
  author: 'アトリヱ未來',
  image: 'ghcr.io/vivliostyle/cli:8.17.1',
  entry: [
    "00-introduction.html",
    "01-basics-of-ruby.html",
    "02-setting-up.html",
    "03-control-structures.html",
    "04-using-ethods.html",
    "05-data-structures.html",
    "06-object-oriented-programming.html",
    "07-error-handling.html",
    "08-ruby-standard-library.html",
    "09-simple-projects.html",
    "10-next-steps.html",
    "11-appendix.html"
  ],
};

module.exports = vivliostyleConfig;

これで、以下のコマンドを実行することで、更新した Markdownファイルの内容を確認することができるようになりました。

$ ./vfm2html.rb
$ vivliostyle preview

終わりに

CSS組版として人気のVivliostyleで、ソースコードの行番号表示とソースコードの取り込みができるようになりました。電子書籍や印刷物を簡単に作ることができます。お役に立てば幸いです。

GitHubで編集を提案

Discussion