Module: VivlioStarter::CLI::Import::SideimageRestorer

Defined in:
lib/vivlio_starter/cli/import/sideimage_restorer.rb

Overview

//sideimage の復元

Constant Summary collapse

SIDEIMAGE_OPEN =

//sideimage[画像][幅][オプション]{

%r{\A//sideimage\[([^\]]*)\]\[([^\]]*)\]\[([^\]]*)\]\{\s*\z}
BLOCK_CLOSE =

Re:VIEW のブロック終端 //}

%r{\A//\}\s*\z}
STOP_LINE =

画像の脇に置く本文を数えるとき、ここに来たら必ず打ち切る (見出し・別の囲み・水平線・次の画像)

/\A(?:\#{1,6}\s|:::|---\s*\z|!\[)/
DEFAULT_TEXT_WIDTH_MM =

判型が読めないときの版面幅(mm)。B5 標準相当

137.0
WIDTH_PERCENT_RANGE =

画像列の比率の下限・上限。極端な指定で本文の列が潰れるのを防ぐ

(10..60).freeze
PAGE_PRESETS_FILE =
'config/page_presets.yml'

Class Method Summary collapse

Class Method Details

.body_end(lines, image_idx, blocks) ⇒ Object

画像行のうしろから、原稿にあったぶんの本文ブロックを数えて終端の行を返す



161
162
163
164
165
166
167
168
169
170
171
172
173
174
# File 'lib/vivlio_starter/cli/import/sideimage_restorer.rb', line 161

def body_end(lines, image_idx, blocks)
  i = image_idx + 1
  last = image_idx

  blocks.times do
    i += 1 while i < lines.size && lines[i].strip.empty?
    break if i >= lines.size || STOP_LINE.match?(lines[i])

    i += 1 while i < lines.size && !lines[i].strip.empty?
    last = i - 1
  end

  last
end

.build_directive(match, body) ⇒ Object



133
134
135
136
137
138
139
140
# File 'lib/vivlio_starter/cli/import/sideimage_restorer.rb', line 133

def build_directive(match, body)
  {
    image: ImageFilenameSanitizer.sanitize(match[1].to_s.strip),
    width_mm: Units.length_to_mm(match[2].to_s.strip),
    klass: match[3].to_s.include?('side=R') ? 'sideimage-right' : 'sideimage',
    blocks: text_block_count(body)
  }
end

.find_image_line(lines, image, from) ⇒ Object

画像行を探す。取り込み時と同じ正規化を通した名前で照合する



155
156
157
158
# File 'lib/vivlio_starter/cli/import/sideimage_restorer.rb', line 155

def find_image_line(lines, image, from)
  needle = "](#{image}.webp)"
  (from...lines.size).find { lines[it].include?(needle) && lines[it].lstrip.start_with?('![') }
end

.image_line_with_width(line, directive, text_mm) ⇒ Object

画像行へ {width=NN%} を添える。Vivlio ではこの値がそのまま画像列の比率になる



177
178
179
180
181
182
# File 'lib/vivlio_starter/cli/import/sideimage_restorer.rb', line 177

def image_line_with_width(line, directive, text_mm)
  percent = width_percent(directive[:width_mm], text_mm)
  return line unless percent

  "#{line.chomp}{width=#{percent}%}\n"
end

.locate_spans(lines, directives, md_path) ⇒ Object

.re の並び順に、Markdown 側の [画像行, 本文の最終行, 指示] を求める



98
99
100
101
102
103
104
105
106
107
108
109
# File 'lib/vivlio_starter/cli/import/sideimage_restorer.rb', line 98

def locate_spans(lines, directives, md_path)
  cursor = 0

  directives.filter_map do |directive|
    image_idx = find_image_line(lines, directive[:image], cursor)
    next warn_unplaced(md_path, directive) unless image_idx

    last_idx = body_end(lines, image_idx, directive[:blocks])
    cursor = last_idx + 1
    [image_idx, last_idx, directive]
  end
end

.parse_directives(re_lines) ⇒ Object

.re から sideimage の指示を出現順に取り出す



112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
# File 'lib/vivlio_starter/cli/import/sideimage_restorer.rb', line 112

def parse_directives(re_lines)
  directives = []
  i = 0

  while i < re_lines.size
    match = SIDEIMAGE_OPEN.match(re_lines[i].to_s.chomp)
    if match
      body = []
      i += 1
      while i < re_lines.size && !BLOCK_CLOSE.match?(re_lines[i].to_s.chomp)
        body << re_lines[i].to_s.chomp
        i += 1
      end
      directives << build_directive(match, body)
    end
    i += 1
  end

  directives
end

.restore!(temp_dir, starter_dir) ⇒ void

This method returns an undefined value.

temp/ の Markdown に対して sideimage の囲みを復元する

Parameters:

  • temp_dir (String)

    変換済み Markdown の置き場

  • starter_dir (String)

    Re:VIEW Starter プロジェクトのディレクトリ



63
64
65
66
67
68
69
70
71
72
73
74
75
# File 'lib/vivlio_starter/cli/import/sideimage_restorer.rb', line 63

def restore!(temp_dir, starter_dir)
  re_dir = review_contents_dir(starter_dir)
  return unless Dir.exist?(re_dir)

  text_mm = text_area_width_mm(starter_dir)

  restored = Dir.glob(File.join(temp_dir, '*.md')).sum do |md_path|
    re_path = File.join(re_dir, "#{File.basename(md_path, '.md')}.re")
    File.exist?(re_path) ? restore_chapter!(md_path, re_path, text_mm) : 0
  end

  Common.log_info("  sideimage の囲みを #{restored} 件復元しました") if restored.positive?
end

.restore_chapter!(md_path, re_path, text_mm) ⇒ Object

1 章ぶんを復元し、復元できた件数を返す



78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
# File 'lib/vivlio_starter/cli/import/sideimage_restorer.rb', line 78

def restore_chapter!(md_path, re_path, text_mm)
  directives = parse_directives(File.readlines(re_path, encoding: 'utf-8'))
  return 0 if directives.empty?

  lines = File.readlines(md_path, encoding: 'utf-8')
  spans = locate_spans(lines, directives, md_path)
  return 0 if spans.empty?

  # 行番号がずれないよう後ろから書き換える
  spans.reverse_each do |image_idx, last_idx, directive|
    lines[last_idx] = "#{lines[last_idx].chomp}\n:::\n"
    lines[image_idx] = image_line_with_width(lines[image_idx], directive, text_mm)
    lines.insert(image_idx, ":::{.#{directive[:klass]}}\n")
  end

  File.write(md_path, lines.join, encoding: 'utf-8')
  spans.size
end

.review_contents_dir(starter_dir) ⇒ Object

Re:VIEW の原稿ディレクトリ(config.yml の contentdir。既定は contents)



217
218
219
220
221
# File 'lib/vivlio_starter/cli/import/sideimage_restorer.rb', line 217

def review_contents_dir(starter_dir)
  config = File.join(starter_dir, 'config.yml')
  dir = (YAML.safe_load_file(config, permitted_classes: [Symbol])['contentdir'] if File.exist?(config))
  File.join(starter_dir, dir.to_s.strip.empty? ? 'contents' : dir.to_s.strip)
end

.target_page_preset(starter_dir) ⇒ Object

取り込み先で使う判型プリセットの中身



204
205
206
207
208
209
210
211
212
213
214
# File 'lib/vivlio_starter/cli/import/sideimage_restorer.rb', line 204

def target_page_preset(starter_dir)
  starter_config = File.join(starter_dir, 'config-starter.yml')
  return nil unless File.exist?(starter_config) && File.exist?(PAGE_PRESETS_FILE)

  pagesize = YAML.safe_load_file(starter_config, permitted_classes: [Symbol]).dig('starter', 'pagesize')
  name = YamlProcessor::PAGE_PRESETS[pagesize.to_s.strip.upcase]
  return nil unless name

  # プリセットは YAML アンカー(`<<: *b5_std`)で共通部を引くため aliases が要る
  YAML.safe_load_file(PAGE_PRESETS_FILE, aliases: true)[name]
end

.text_area_width_mm(starter_dir) ⇒ Object

版面幅(mm)= 紙幅 − ノド − 小口。Re:VIEW の mm 指定を比率へ直す基準



191
192
193
194
195
196
197
198
199
200
201
# File 'lib/vivlio_starter/cli/import/sideimage_restorer.rb', line 191

def text_area_width_mm(starter_dir)
  preset = target_page_preset(starter_dir)
  return DEFAULT_TEXT_WIDTH_MM unless preset

  width = Units.length_to_mm(Common::PAGE_SIZES.dig(preset['size'].to_s.upcase, :width))
  inner = Units.length_to_mm(preset['margin_inner'])
  outer = Units.length_to_mm(preset['margin_outer'])
  return DEFAULT_TEXT_WIDTH_MM unless width&.positive? && inner && outer

  [width - inner - outer, 1.0].max
end

.text_block_count(body) ⇒ Object

本文のブロック数。//blankline のように「Markdown では空行にしかならない」 命令だけのブロックは数えない——数えると本文を 1 つ多く巻き込む。



144
145
146
147
# File 'lib/vivlio_starter/cli/import/sideimage_restorer.rb', line 144

def text_block_count(body)
  body.join("\n").split(/\n[ \t]*\n/)
      .count { |block| block.lines.any? { text_line?(it) } }
end

.text_line?(line) ⇒ Boolean

Returns:

  • (Boolean)


149
150
151
152
# File 'lib/vivlio_starter/cli/import/sideimage_restorer.rb', line 149

def text_line?(line)
  stripped = line.strip
  !stripped.empty? && !stripped.start_with?('//')
end

.warn_unplaced(md_path, directive) ⇒ Object

置き場所が決まらなかった指示は、著者が手で直せるように名指しで知らせる



224
225
226
227
228
229
230
231
# File 'lib/vivlio_starter/cli/import/sideimage_restorer.rb', line 224

def warn_unplaced(md_path, directive)
  Common.log_warn(
    "  #{File.basename(md_path)}: sideimage の画像 #{directive[:image]} が見つからず、囲みを復元できませんでした。",
    detail: "対処: 該当箇所を :::{.#{directive[:klass]}} … ::: で囲んでください" \
            '(22 章「サイドイメージ」)。'
  )
  nil
end

.width_percent(width_mm, text_mm) ⇒ Object



184
185
186
187
188
# File 'lib/vivlio_starter/cli/import/sideimage_restorer.rb', line 184

def width_percent(width_mm, text_mm)
  return nil unless width_mm&.positive? && text_mm.positive?

  ((width_mm / text_mm) * 100).round.clamp(WIDTH_PERCENT_RANGE.begin, WIDTH_PERCENT_RANGE.end)
end