Class: VivlioStarter::CLI::ReviewMarkdownGenerator
- Inherits:
-
Object
- Object
- VivlioStarter::CLI::ReviewMarkdownGenerator
- 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)。値は章トークン。
2121, 2221-22のいずれも書ける(TokenResolver が解釈)。 行が無ければ nil を返す——「指定なし」と「行を消した=解除」を同じ扱いにする。子行の綴りはここだけで決める。出現箇所行(
- 章名: 文脈)と見分けが つかない形なので、値の有無に関わらず弾ける前半を切り出しておく。 /^\s*-\s*(?:主要参照|main)\s*[::]/- MAIN_REFERENCE_LINE =
/#{MAIN_REFERENCE_PREFIX}\s*(?:`(?:NEW!|Today)`\s*)?(.+)$/
Class Method Summary collapse
-
.section_index(content, heading) ⇒ Integer?
見出しの位置。行頭に限るのが要点——本文で見出し名に触れただけで 境界がそこへ動き、以降の解釈がまるごとずれる。凡例に「4 節『除外済み リスト』」と書こうとして実際に踏んだ罠で、9 つの読み取りが一斉に壊れた。.
Instance Method Summary collapse
-
#exists? ⇒ Boolean
レビューファイルが存在するか.
-
#generate!(data) ⇒ Object
レビュー用Markdownを生成.
-
#initialize ⇒ ReviewMarkdownGenerator
constructor
A new instance of ReviewMarkdownGenerator.
-
#parse_approved ⇒ Array<Hash>
後方互換性のため parse_approved も維持(索引用).
-
#parse_glossary_approved ⇒ Array<Hash>
用語集として承認された候補を抽出([g], [ig], [gi] マーク) 説明文も抽出する.
-
#parse_glossary_rejected ⇒ Array<Hash>
用語集のみリジェクト([-g] マーク)を抽出.
-
#parse_index_approved ⇒ Array<Hash>
索引として承認された候補を抽出([i], [ig], [gi], [x] マーク).
-
#parse_index_rejected ⇒ Array<Hash>
索引のみリジェクト([-i] マーク)を抽出.
- #parse_main_references ⇒ Object
-
#parse_rejected ⇒ Array<Hash>
リジェクト候補を抽出([r], [-ig], [-gi] マーク).
-
#parse_rejected_section_all ⇒ Array<Hash>
除外済みリストの全項目を抽出(フラグ不問)。 apply 時に index_terms/glossary_terms からの除去と rejected への同期に使う。.
-
#parse_terms_with_definitions(content) ⇒ Array<Hash>
用語と説明文をパース 出現箇所リストと説明文を区別して抽出.
-
#parse_unreject ⇒ Array<Hash>
除外済みリストで復帰マークが付いた候補を抽出(リジェクト解除+直接登録)。 フラグをそのまま持ち帰り、索引・用語集への登録先の判断に使う。.
-
#parse_yomi_changes ⇒ Array<Hash>
登録済み用語セクションで読みが変更された用語を抽出.
-
#review_file_path ⇒ String
実際のレビューファイルパスを取得.
-
#split_chapter_tokens(text) ⇒ Object
21, 2221-2221 22のいずれも章トークンの配列にする。 解決(番号 → basename)は TokenResolver の仕事なので、ここでは分割だけ。. -
#term_blocks(content) ⇒ Object
用語行とそれに続くインデント行を 1 ブロックとして切り出す。 行単位で独立に scan する他のパーサと違い、用語と子項目の対応が要るため。.
-
#term_lines ⇒ Array<TermLine>
レビューファイル全体の用語行。フラグの綴りは TermLine が唯一の定義元で、 ここから下のパーサはその判定(index? / reject_index? …)を使う。.
-
#term_lines_before_rejected_section ⇒ Object
除外済みリスト(セクション 4)の手前までの用語行。 あちらは「復帰させるか」を問う別の場なので、承認・棄却の集計には混ぜない。.
-
#term_lines_in_rejected_section ⇒ Object
除外済みリスト(セクション 4)の用語行.
Constructor Details
#initialize ⇒ ReviewMarkdownGenerator
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 つの読み取りが一斉に壊れた。
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
レビューファイルが存在するか
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を生成
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_approved ⇒ Array<Hash>
後方互換性のため parse_approved も維持(索引用)
158 159 160 |
# File 'lib/vivlio_starter/cli/index/review_markdown_generator.rb', line 158 def parse_approved parse_index_approved end |
#parse_glossary_approved ⇒ Array<Hash>
用語集として承認された候補を抽出([g], [ig], [gi] マーク) 説明文も抽出する
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_rejected ⇒ Array<Hash>
用語集のみリジェクト([-g] マーク)を抽出
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_approved ⇒ Array<Hash>
索引として承認された候補を抽出([i], [ig], [gi], [x] マーク)
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_rejected ⇒ Array<Hash>
索引のみリジェクト([-i] マーク)を抽出
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_references ⇒ Object
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_rejected ⇒ Array<Hash>
リジェクト候補を抽出([r], [-ig], [-gi] マーク)
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_all ⇒ Array<Hash>
除外済みリストの全項目を抽出(フラグ不問)。 apply 時に index_terms/glossary_terms からの除去と rejected への同期に使う。
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>
用語と説明文をパース 出現箇所リストと説明文を区別して抽出
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_unreject ⇒ Array<Hash>
除外済みリストで復帰マークが付いた候補を抽出(リジェクト解除+直接登録)。 フラグをそのまま持ち帰り、索引・用語集への登録先の判断に使う。
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_changes ⇒ 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_path ⇒ 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_lines ⇒ Array<TermLine>
レビューファイル全体の用語行。フラグの綴りは TermLine が唯一の定義元で、 ここから下のパーサはその判定(index? / reject_index? …)を使う。
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_section ⇒ Object
除外済みリスト(セクション 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_section ⇒ Object
除外済みリスト(セクション 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 |