Class: VivlioStarter::CLI::ReviewMarkdownGenerator

Inherits:
Object
  • Object
show all
Defined in:
lib/vivlio_starter/cli/index/review_markdown_generator.rb

Constant Summary collapse

REVIEW_FILE =

旧ファイル名との互換性のため、両方をチェック

'_index_glossary_review.md'
LEGACY_REVIEW_FILE =
'_index_review.md'
OUT_OF_SCOPE_NOTE =

今回走査しなかった章から拾った抜粋であることを示す注記。表示専用で、 apply のパース時に剥がされる(章名の一部と誤って辞書へ戻さないため)。

2 種類あるのは著者の判断が変わるから。走査対象外は「今回指定しなかっただけ」で 本には載る章。catalog 未登録は本に入らない章なので、その語には索引のページ番号が 付かない——章を catalog へ戻すか、語を索引から外すかを決める必要がある。

'(走査対象外)'
OUTSIDE_CATALOG_NOTE =
'(catalog 未登録)'
CONTEXT_NOTES =

旧版が書いた「(catalog 外)」も剥がす(レビューファイルは版をまたいで残る)

[OUTSIDE_CATALOG_NOTE, OUT_OF_SCOPE_NOTE, '(catalog 外)'].freeze
TERMS_SECTION =

セクションの見出し。走査範囲の境目に使うので綴りを 1 箇所に置く

'## 1. 登録済み用語の確認'
HIGH_SECTION =
'## 2. 推奨候補'
REJECTED_SECTION =
'## 4. 除外済みリスト'
MAIN_REFERENCE_PREFIX =

主要参照の指定を抽出する(- 主要参照: 21, 22 / - main: 21-22)。

著者が触るのはこのレビューファイルであって辞書 YAML ではない。用語集の 説明文と同じく「子ブロックに書く」形に揃えてある——フラグ欄に数字を 入れる案([igm21,22])は、閉じていたフラグの語彙を開いてしまい、 7 つのパーサすべてに切り分け処理が要る(§R3)。

値は章トークン。21 21, 22 21-22 のいずれも書ける(TokenResolver が解釈)。 行が無ければ nil を返す——「指定なし」と「行を消した=解除」を同じ扱いにする。

子行の綴りはここだけで決める。出現箇所行( - 章名: 文脈)と見分けが つかない形なので、値の有無に関わらず弾ける前半を切り出しておく。

Returns:

  • (Hash{String => Array<String>, nil})

    用語 → 章トークンの配列

/^\s*-\s*(?:主要参照|main)\s*[::]/
MAIN_REFERENCE_LINE =
/#{MAIN_REFERENCE_PREFIX}\s*(?:`(?:NEW!|Today)`\s*)?(.+)$/

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initializeReviewMarkdownGenerator

Returns a new instance of ReviewMarkdownGenerator.



63
64
65
66
# File 'lib/vivlio_starter/cli/index/review_markdown_generator.rb', line 63

def initialize
  @content = nil
  @config = load_index_config
end

Class Method Details

.section_index(content, heading) ⇒ Integer?

見出しの位置。行頭に限るのが要点——本文で見出し名に触れただけで 境界がそこへ動き、以降の解釈がまるごとずれる。凡例に「4 節『除外済み リスト』」と書こうとして実際に踏んだ罠で、9 つの読み取りが一斉に壊れた。

Returns:

  • (Integer, nil)

    見出しの開始位置。無ければ nil



59
60
61
# File 'lib/vivlio_starter/cli/index/review_markdown_generator.rb', line 59

def self.section_index(content, heading)
  content.match(/^#{Regexp.escape(heading)}/)&.begin(0)
end

Instance Method Details

#exists?Boolean

レビューファイルが存在するか

Returns:

  • (Boolean)


84
85
86
# File 'lib/vivlio_starter/cli/index/review_markdown_generator.rb', line 84

def exists?
  File.exist?(REVIEW_FILE) || File.exist?(LEGACY_REVIEW_FILE)
end

#generate!(data) ⇒ Object

レビュー用Markdownを生成

Parameters:

  • data (Hash)

    セクション別データ

    • :terms [Array] 登録済み用語
    • :high_candidates [Array] 推奨候補
    • :low_candidates [Array] 一般候補
    • :rejected [Array] 除外済みリスト


74
75
76
77
78
79
80
# File 'lib/vivlio_starter/cli/index/review_markdown_generator.rb', line 74

def generate!(data)
  content = build_markdown(data)
  File.write(REVIEW_FILE, content, encoding: 'utf-8')
  Common.log_success("レビュー用ファイルを生成しました: #{REVIEW_FILE}")
  Common.log_info('ファイルを開いて [ ] を [x] または [r] に変更してください')
  Common.log_info('完了したら: vs index:apply')
end

#parse_approvedArray<Hash>

後方互換性のため parse_approved も維持(索引用)

Returns:

  • (Array<Hash>)

    承認済み候補のリスト



158
159
160
# File 'lib/vivlio_starter/cli/index/review_markdown_generator.rb', line 158

def parse_approved
  parse_index_approved
end

#parse_glossary_approvedArray<Hash>

用語集として承認された候補を抽出([g], [ig], [gi] マーク) 説明文も抽出する

Returns:

  • (Array<Hash>)

    用語集候補のリスト(definition 付き)



134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
# File 'lib/vivlio_starter/cli/index/review_markdown_generator.rb', line 134

def parse_glossary_approved
  return [] unless exists?

  content = File.read(review_file_path, encoding: 'utf-8')
  approved = []

  # [g], [ig], [gi] を用語集として抽出
  parse_terms_with_definitions(content).each do |entry|
    flag = entry[:flag]
    next unless flag.match?(/^(?:g|ig|gi)$/)

    approved << {
      'term' => entry[:term],
      'yomi' => entry[:yomi],
      'definition' => entry[:definition],
      'contexts' => entry[:contexts]
    }
  end

  approved
end

#parse_glossary_rejectedArray<Hash>

用語集のみリジェクト([-g] マーク)を抽出

Returns:

  • (Array<Hash>)

    用語集リジェクト候補のリスト



181
182
183
184
# File 'lib/vivlio_starter/cli/index/review_markdown_generator.rb', line 181

def parse_glossary_rejected
  term_lines_before_rejected_section.select(&:reject_glossary?)
                                    .map { { 'term' => it.term, 'yomi' => it.yomi, 'kind' => 'glossary' } }
end

#parse_index_approvedArray<Hash>

索引として承認された候補を抽出([i], [ig], [gi], [x] マーク)

Returns:

  • (Array<Hash>)

    索引候補のリスト



99
100
101
# File 'lib/vivlio_starter/cli/index/review_markdown_generator.rb', line 99

def parse_index_approved
  term_lines.select(&:index?).map { { 'term' => it.term, 'yomi' => it.yomi } }
end

#parse_index_rejectedArray<Hash>

索引のみリジェクト([-i] マーク)を抽出

Returns:

  • (Array<Hash>)

    索引リジェクト候補のリスト



174
175
176
177
# File 'lib/vivlio_starter/cli/index/review_markdown_generator.rb', line 174

def parse_index_rejected
  term_lines_before_rejected_section.select(&:reject_index?)
                                    .map { { 'term' => it.term, 'yomi' => it.yomi, 'kind' => 'index' } }
end

#parse_main_referencesObject



217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
# File 'lib/vivlio_starter/cli/index/review_markdown_generator.rb', line 217

def parse_main_references
  return {} unless exists?

  content = File.read(review_file_path, encoding: 'utf-8')
  boundary = self.class.section_index(content, REJECTED_SECTION)
  search = boundary ? content[0...boundary] : content

  # まずフラグ欄(`[igm33]`)を読み、子行があればそちらで上書きする。
  # 章名や節指定のような長い値は子行にしか書けないので、後から書き足した
  # 細かい指定が勝つ形にしてある。
  result = IndexCommands::TermLine.scan(search).to_h { [it.term, it.main] }
  term_blocks(search).each do |term, body|
    line = body[MAIN_REFERENCE_LINE, 1]
    result[term] = split_chapter_tokens(line) if line
  end
  result
end

#parse_rejectedArray<Hash>

リジェクト候補を抽出([r], [-ig], [-gi] マーク)

Returns:

  • (Array<Hash>)

    リジェクト候補のリスト



164
165
166
167
168
169
170
# File 'lib/vivlio_starter/cli/index/review_markdown_generator.rb', line 164

def parse_rejected
  term_lines_before_rejected_section.select(&:reject_both?).map do |line|
    entry = { 'term' => line.term, 'yomi' => line.yomi, 'kind' => 'both' }
    entry['score'] = line.score if line.score
    entry
  end
end

#parse_rejected_section_allArray<Hash>

除外済みリストの全項目を抽出(フラグ不問)。 apply 時に index_terms/glossary_terms からの除去と rejected への同期に使う。

Returns:

  • (Array<Hash>)

    全項目のリスト(flag 付き)



197
198
199
# File 'lib/vivlio_starter/cli/index/review_markdown_generator.rb', line 197

def parse_rejected_section_all
  term_lines_in_rejected_section.map { { 'term' => it.term, 'yomi' => it.yomi, 'flag' => it.flags } }
end

#parse_terms_with_definitions(content) ⇒ Array<Hash>

用語と説明文をパース 出現箇所リストと説明文を区別して抽出

Parameters:

  • content (String)

    Markdown内容

Returns:

  • (Array<Hash>)

    パース結果



279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
# File 'lib/vivlio_starter/cli/index/review_markdown_generator.rb', line 279

def parse_terms_with_definitions(content)
  results = []
  lines = content.lines
  i = 0

  while i < lines.size
    line = lines[i]

    # 用語行を検出(綴りの定義元は TermLine)
    if (parsed = IndexCommands::TermLine.parse(line))
      flag = parsed.flags
      term = parsed.term
      yomi = parsed.yomi

      i += 1
      contexts = []
      definition_lines = []
      in_definition = false

      # 次の用語行まで走査
      while i < lines.size && lines[i] !~ /^- \[/
        current_line = lines[i]

        # 主要参照の子行は著者の指定であって出現箇所ではない。綴りが
        # `  - ラベル: 値` で下の出現箇所行と同型なので、先に弾かないと
        # `chapter: 主要参照` という文脈が辞書へ入る。しかもレビューを
        # 往復するたび再出力・再取り込みされ、値が空へ潰れて残り続ける。
        if current_line.match?(MAIN_REFERENCE_PREFIX)
          i += 1
          next
        end

        # 出現箇所行: "  - chapter: context"
        # MatchData を受けてから読む——`Regexp.last_match` のままだと、
        # 章名を整える sub がその場で $~ を上書きし、続けて読む文脈が
        # nil になる。辞書の contexts が軒並み空だったのはこれが原因。
        if (occurrence = current_line.match(/^  - ([^:]+): (.+)/))
          # 表示用の注記は辞書へ戻さない
          chapter = occurrence[1].sub(/#{Regexp.union(CONTEXT_NOTES)}\z/o, '')
          contexts << { 'chapter' => chapter, 'context' => occurrence[2] }
          i += 1
          next
        end

        # 空行で説明文開始を判定
        if current_line.strip.empty?
          in_definition = true
          i += 1
          next
        end

        # インデントされた行は説明文
        definition_lines << Regexp.last_match(1) if in_definition && current_line =~ /^  (.+)/

        i += 1
      end

      results << {
        flag: flag,
        term: term,
        yomi: yomi,
        contexts: contexts,
        definition: definition_lines.join("\n").strip
      }
    else
      i += 1
    end
  end

  results
end

#parse_unrejectArray<Hash>

除外済みリストで復帰マークが付いた候補を抽出(リジェクト解除+直接登録)。 フラグをそのまま持ち帰り、索引・用語集への登録先の判断に使う。

Returns:

  • (Array<Hash>)

    リジェクト解除候補のリスト(flag 付き)



189
190
191
192
# File 'lib/vivlio_starter/cli/index/review_markdown_generator.rb', line 189

def parse_unreject
  term_lines_in_rejected_section.select(&:unrejecting?)
                                .map { { 'term' => it.term, 'yomi' => it.yomi, 'flag' => it.flags } }
end

#parse_yomi_changesArray<Hash>

登録済み用語セクションで読みが変更された用語を抽出

Returns:

  • (Array<Hash>)

    読み変更された用語のリスト



263
264
265
266
267
268
269
270
271
272
273
# File 'lib/vivlio_starter/cli/index/review_markdown_generator.rb', line 263

def parse_yomi_changes
  return [] unless exists?

  content = File.read(review_file_path, encoding: 'utf-8')
  start = self.class.section_index(content, TERMS_SECTION) or return []
  finish = self.class.section_index(content, HIGH_SECTION) || content.length

  IndexCommands::TermLine.scan(content[start...finish])
                         .select { it.index? || it.glossary? }
                         .map { { 'term' => it.term, 'yomi' => it.yomi } }
end

#review_file_pathString

実際のレビューファイルパスを取得

Returns:

  • (String)


90
91
92
93
94
95
# File 'lib/vivlio_starter/cli/index/review_markdown_generator.rb', line 90

def review_file_path
  return REVIEW_FILE if File.exist?(REVIEW_FILE)
  return LEGACY_REVIEW_FILE if File.exist?(LEGACY_REVIEW_FILE)

  REVIEW_FILE
end

#split_chapter_tokens(text) ⇒ Object

21, 22 21-22 21 22 のいずれも章トークンの配列にする。 解決(番号 → basename)は TokenResolver の仕事なので、ここでは分割だけ。



257
258
259
# File 'lib/vivlio_starter/cli/index/review_markdown_generator.rb', line 257

def split_chapter_tokens(text)
  text.split(/[,、\s]+/).map(&:strip).reject(&:empty?)
end

#term_blocks(content) ⇒ Object

用語行とそれに続くインデント行を 1 ブロックとして切り出す。 行単位で独立に scan する他のパーサと違い、用語と子項目の対応が要るため。



237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
# File 'lib/vivlio_starter/cli/index/review_markdown_generator.rb', line 237

def term_blocks(content)
  blocks = []
  current = nil
  content.to_s.lines.each do |line|
    if (parsed = IndexCommands::TermLine.parse(line))
      current = [parsed.term, +'']
      blocks << current
    elsif current && line.match?(/\A[ \t]+\S/)
      current[1] << line
    elsif line.strip.empty?
      next # 空行はブロックを切らない(説明文が続くことがある)
    else
      current = nil
    end
  end
  blocks
end

#term_linesArray<TermLine>

レビューファイル全体の用語行。フラグの綴りは TermLine が唯一の定義元で、 ここから下のパーサはその判定(index? / reject_index? …)を使う。

Returns:

  • (Array<TermLine>)


106
107
108
109
110
# File 'lib/vivlio_starter/cli/index/review_markdown_generator.rb', line 106

def term_lines
  return [] unless exists?

  IndexCommands::TermLine.scan(File.read(review_file_path, encoding: 'utf-8'))
end

#term_lines_before_rejected_sectionObject

除外済みリスト(セクション 4)の手前までの用語行。 あちらは「復帰させるか」を問う別の場なので、承認・棄却の集計には混ぜない。



114
115
116
117
118
119
120
# File 'lib/vivlio_starter/cli/index/review_markdown_generator.rb', line 114

def term_lines_before_rejected_section
  return [] unless exists?

  content = File.read(review_file_path, encoding: 'utf-8')
  boundary = self.class.section_index(content, REJECTED_SECTION)
  IndexCommands::TermLine.scan(boundary ? content[0...boundary] : content)
end

#term_lines_in_rejected_sectionObject

除外済みリスト(セクション 4)の用語行



123
124
125
126
127
128
129
# File 'lib/vivlio_starter/cli/index/review_markdown_generator.rb', line 123

def term_lines_in_rejected_section
  return [] unless exists?

  content = File.read(review_file_path, encoding: 'utf-8')
  boundary = self.class.section_index(content, REJECTED_SECTION)
  boundary ? IndexCommands::TermLine.scan(content[boundary..]) : []
end