Class: Vivlio::Starter::CLI::IndexCommands::IndexMatchScanner

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

Overview

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

Constant Summary collapse

INDEX_TERM_PATTERN =

索引語マッチの正規表現

用語|読み

または [用語] 形式を検出

ただし、(url) 形式のリンクは除外(後ろに ( が続く場合はスキップ)

/\[([^\[\]\n]+)\](?!\()/

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(defer_warnings: false) ⇒ IndexMatchScanner

Returns a new instance of IndexMatchScanner.



71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
# File 'lib/vivlio/starter/cli/index/index_match_scanner.rb', line 71

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 = @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 = @unified_terms.select do |t|
    flags = t['flags'].to_s
    flags.include?('g') && !flags.include?('i')
  end
end

Instance Attribute Details

#config_missingObject (readonly)

Returns the value of attribute config_missing.



69
70
71
# File 'lib/vivlio/starter/cli/index/index_match_scanner.rb', line 69

def config_missing
  @config_missing
end

#index_dataObject (readonly)

Returns the value of attribute index_data.



69
70
71
# File 'lib/vivlio/starter/cli/index/index_match_scanner.rb', line 69

def index_data
  @index_data
end

#matchesObject (readonly)

Returns the value of attribute matches.



69
70
71
# File 'lib/vivlio/starter/cli/index/index_match_scanner.rb', line 69

def matches
  @matches
end

#no_matchesObject (readonly)

Returns the value of attribute no_matches.



69
70
71
# File 'lib/vivlio/starter/cli/index/index_match_scanner.rb', line 69

def no_matches
  @no_matches
end

#seen_termsObject (readonly)

Returns the value of attribute seen_terms.



69
70
71
# File 'lib/vivlio/starter/cli/index/index_match_scanner.rb', line 69

def seen_terms
  @seen_terms
end

#term_occurrenceObject (readonly)

Returns the value of attribute term_occurrence.



69
70
71
# File 'lib/vivlio/starter/cli/index/index_match_scanner.rb', line 69

def term_occurrence
  @term_occurrence
end

Instance Method Details

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

章ファイルを探す

Parameters:

  • chapter (String)

    章名

  • prefer_contents (Boolean) (defaults to: false)

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

Returns:

  • (String, nil)

    ファイルパス



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

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

  if prefer_contents
    # contents/ を優先
    return contents_file if File.exist?(contents_file)
    return root_file if File.exist?(root_file)
  else
    # ルート直下を優先
    return root_file if File.exist?(root_file)
    return contents_file if File.exist?(contents_file)
  end

  nil
end

#load_unified_termsObject

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



92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
# File 'lib/vivlio/starter/cli/index/index_match_scanner.rb', line 92

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

#save_glossary_backlinks!Object

用語集のバックリンクを index_glossary_terms.yml に保存



115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
# File 'lib/vivlio/starter/cli/index/index_match_scanner.rb', line 115

def save_glossary_backlinks!
  return if @glossary_backlinks.empty?

  config_file = 'config/index_glossary_terms.yml'
  return unless File.exist?(config_file)

  begin
    data = YAML.load_file(config_file)
    terms = data['terms'] || []

    terms.each do |term|
      term_name = term['term']
      next unless @glossary_backlinks.key?(term_name)

      term['backlink_sources'] = @glossary_backlinks[term_name]
    end

    data['terms'] = terms
    data['updated_at'] = Time.now.strftime('%Y-%m-%d %H:%M:%S')
    File.write(config_file, data.to_yaml, encoding: 'utf-8')
    Common.log_info('用語集のバックリンクを更新しました')
  rescue StandardError => e
    Common.log_warn("用語集のバックリンク保存に失敗しました: #{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/ を優先)



144
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 144

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!
  save_glossary_backlinks!
  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)

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



190
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
# File 'lib/vivlio/starter/cli/index/index_match_scanner.rb', line 190

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