Class: VivlioStarter::CLI::IndexCommands::IndexMatchScanner

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

Overview

索引語スキャン・タグ付けクラス

Defined Under Namespace

Classes: LineMask

Constant Summary collapse

HEADING_LINE =

見出し行とそのレベル(### 見出し → 3)

/\A(\#{1,6})[ \t]+\S/
INDEX_TERM_PATTERN =

索引語マッチの正規表現。綴りの定義元は IndexMarkup(唯一の定義元)。 [用語|読み] または [用語] 形式を検出し、リンク記法 text と インライン脚注 ^[本文] は除外される。

IndexMarkup::TERM_PATTERN

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(defer_warnings: false) ⇒ IndexMatchScanner

Returns a new instance of IndexMatchScanner.



81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
# File 'lib/vivlio_starter/cli/index/index_match_scanner.rb', line 81

def initialize(defer_warnings: false)
  @seen_terms = Set[]
  @term_occurrence = Hash.new(0)
  @index_data = Hash.new { |h, k| h[k] = Set[] }
  @matches = []
  @yomi_inferrer = YomiInferrer.new
  @config_missing = false
  @no_matches = false
  @defer_warnings = defer_warnings
  @unified_terms = load_unified_terms
  @config_terms = longest_first(@unified_terms.select { it['flags'].to_s.include?('i') })
  @glossary_terms = @unified_terms.select { it['flags'].to_s.include?('g') }.to_h { [it['term'], it] }
  @glossary_backlinks = Hash.new { |h, k| h[k] = [] }
  # 用語集のみの用語(索引対象外だがバックリンクは必要)
  @glossary_only_terms = longest_first(@unified_terms.select do |t|
    flags = t['flags'].to_s
    flags.include?('g') && !flags.include?('i')
  end)
  # 主要参照(説明箇所)の指定。辞書の main: を 用語 → 章名の集合に畳む。
  # 単一章とリストの両方を受ける(index-main-reference-spec.md §1.3)。
  # 主要参照の指定。`21#Markdown とは` のような節指定も受ける(R2)
  @main_refs = @unified_terms.to_h do |t|
    [t['term'], Array(t['main']).map { MainReference.parse(it) }]
  end
  @main_chapters = @main_refs.transform_values { it.map(&:chapter).to_set }
  # 主要参照の落とし先(R1)。章を読んだ時点で下見した結果を持つ。
  @has_section_heading = {}
  @has_prose_occurrence = {}
  @main_section_range = {}
  @main_decided = Set[]
  @section_warned = Set[]
  @current_heading_level = nil
  @current_lineno = nil
  # 用語ごとに不変な導出物のキャッシュ(1 行ごとに作り直さない)
  @index_patterns = {}
  @term_yomis = {}
end

Instance Attribute Details

#config_missingObject (readonly)

Returns the value of attribute config_missing.



78
79
80
# File 'lib/vivlio_starter/cli/index/index_match_scanner.rb', line 78

def config_missing
  @config_missing
end

Returns the value of attribute glossary_backlinks.



78
79
80
# File 'lib/vivlio_starter/cli/index/index_match_scanner.rb', line 78

def glossary_backlinks
  @glossary_backlinks
end

#index_dataObject (readonly)

Returns the value of attribute index_data.



78
79
80
# File 'lib/vivlio_starter/cli/index/index_match_scanner.rb', line 78

def index_data
  @index_data
end

#matchesObject (readonly)

Returns the value of attribute matches.



78
79
80
# File 'lib/vivlio_starter/cli/index/index_match_scanner.rb', line 78

def matches
  @matches
end

#no_matchesObject (readonly)

Returns the value of attribute no_matches.



78
79
80
# File 'lib/vivlio_starter/cli/index/index_match_scanner.rb', line 78

def no_matches
  @no_matches
end

#seen_termsObject (readonly)

Returns the value of attribute seen_terms.



78
79
80
# File 'lib/vivlio_starter/cli/index/index_match_scanner.rb', line 78

def seen_terms
  @seen_terms
end

#term_occurrenceObject (readonly)

Returns the value of attribute term_occurrence.



78
79
80
# File 'lib/vivlio_starter/cli/index/index_match_scanner.rb', line 78

def term_occurrence
  @term_occurrence
end

Instance Method Details

#find_chapter_file(chapter, prefer_contents: false) ⇒ String?

章ファイルを探す 前処理済み中間 .md はワークスペースの html/ に置かれる(P4 §3.4-1)

Parameters:

  • chapter (String)

    章名

  • prefer_contents (Boolean) (defaults to: false)

    contents/ ディレクトリを優先するか

Returns:

  • (String, nil)

    ファイルパス



171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
# File 'lib/vivlio_starter/cli/index/index_match_scanner.rb', line 171

def find_chapter_file(chapter, prefer_contents: false)
  workspace_file = File.join(Common::BUILD_HTML_DIR, "#{chapter}.md")
  contents_file = File.join(Common::CONTENTS_DIR, "#{chapter}.md")

  if prefer_contents
    # contents/ を優先
    return contents_file if File.exist?(contents_file)
    return workspace_file if File.exist?(workspace_file)
  else
    # 前処理済み(ワークスペース)を優先
    return workspace_file if File.exist?(workspace_file)
    return contents_file if File.exist?(contents_file)
  end

  nil
end

#load_unified_termsObject

統合用語辞書(config/index_glossary_terms.yml)を読み込む



120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
# File 'lib/vivlio_starter/cli/index/index_match_scanner.rb', line 120

def load_unified_terms
  config_file = 'config/index_glossary_terms.yml'
  unless File.exist?(config_file)
    if @defer_warnings
      @config_missing = true
    else
      IndexCommands.emit_index_message(INDEX_TERMS_MISSING_MESSAGE)
    end
    return []
  end

  begin
    data = YAML.load_file(config_file)
    terms = data['terms'] || []
    Common.log_info("統合用語辞書から #{terms.size} 件の語句をロードしました")
    terms
  rescue StandardError => e
    Common.log_warn("config/index_glossary_terms.yml の読み込みに失敗: #{e.message}")
    []
  end
end

#scan_all_chapters!(chapters, read_only: false) ⇒ Object

全章ファイルをスキャンして索引語をタグ付け

Parameters:

  • chapters (Array<String>)

    対象章のファイル名リスト(例: ['11-basics', '12-advanced'])

  • read_only (Boolean) (defaults to: false)

    読み取り専用モード(ファイルを書き換えない、contents/ を優先)



145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
# File 'lib/vivlio_starter/cli/index/index_match_scanner.rb', line 145

def scan_all_chapters!(chapters, read_only: false)
  Common.log_action("索引語のスキャンを開始します... (対象: #{chapters.size} 章)")

  chapters.each do |chapter|
    # ファイルを探す
    # read_only モードでは contents/ を優先(原稿のマークアップを検出するため)
    # 通常モードではルート直下を優先(pre_process 後のファイルを更新するため)
    md_file = find_chapter_file(chapter, prefer_contents: read_only)

    unless md_file
      Common.log_warn("スキップ (Markdown が見つかりません): #{chapter}")
      next
    end

    scan_and_tag_file!(md_file, read_only: read_only)
  end

  save_matches!
  Common.log_success("索引語スキャン完了: #{@matches.size} 件の索引語を検出")
end

#scan_and_tag_file!(md_file, read_only: false) ⇒ Object

単一ファイルをスキャンしてタグ付け

Parameters:

  • md_file (String)

    Markdown ファイルパス

  • read_only (Boolean) (defaults to: false)

    読み取り専用モード(ファイルを書き換えない)



191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
# File 'lib/vivlio_starter/cli/index/index_match_scanner.rb', line 191

def scan_and_tag_file!(md_file, read_only: false)
  content = File.read(md_file, encoding: 'utf-8')
  file_basename = File.basename(md_file, '.md')

  # contents/ ディレクトリ内のファイルは常に読み取り専用(原稿保護)
  effective_read_only = read_only || md_file.start_with?(Common::CONTENTS_DIR)

  match_count_before = @matches.size
  Common.log_info("スキャン中: #{md_file} ...")

  # コードブロック内を除外してスキャン
  new_content = process_content_with_code_block_exclusion(content, file_basename)

  match_count_after = @matches.size
  index_diff = match_count_after - match_count_before
  content_changed = new_content != content

  if content_changed
    # read_only モードでない場合のみファイルを書き換え
    if effective_read_only
      Common.log_success("#{md_file}: #{index_diff} 件の索引語を検出しました(読み取り専用)")
    else
      File.write(md_file, new_content, encoding: 'utf-8')
      Common.log_success("#{md_file}: #{index_diff} 件の索引語をタグ付けしました")
    end
  else
    Common.log_info("#{md_file}: 索引語は見つかりませんでした")
  end
end