bitclust-core 1.3.0 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (152) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +3 -0
  3. data/README.md +65 -0
  4. data/Rakefile +32 -0
  5. data/data/bitclust/catalog/ja_JP.UTF-8 +19 -1
  6. data/data/bitclust/searchpage/index.html +90 -0
  7. data/data/bitclust/template/class +69 -8
  8. data/data/bitclust/template/function +2 -2
  9. data/data/bitclust/template/layout +11 -3
  10. data/data/bitclust/template/method +2 -2
  11. data/data/bitclust/template/search +1 -1
  12. data/data/bitclust/template.epub/class +22 -1
  13. data/data/bitclust/template.epub/function +2 -2
  14. data/data/bitclust/template.epub/layout +1 -3
  15. data/data/bitclust/template.epub/method +2 -2
  16. data/data/bitclust/template.lillia/class +26 -5
  17. data/data/bitclust/template.lillia/layout +1 -3
  18. data/data/bitclust/template.lillia/method +2 -2
  19. data/data/bitclust/template.offline/class +48 -4
  20. data/data/bitclust/template.offline/function +2 -2
  21. data/data/bitclust/template.offline/layout +27 -3
  22. data/data/bitclust/template.offline/method +2 -2
  23. data/lib/bitclust/app.rb +16 -17
  24. data/lib/bitclust/capi_converter.rb +20 -0
  25. data/lib/bitclust/classentry.rb +67 -17
  26. data/lib/bitclust/compat.rb +2 -2
  27. data/lib/bitclust/completion.rb +58 -27
  28. data/lib/bitclust/crossrubyutils.rb +8 -5
  29. data/lib/bitclust/database.rb +28 -8
  30. data/lib/bitclust/doc_converter.rb +63 -0
  31. data/lib/bitclust/docentry.rb +1 -1
  32. data/lib/bitclust/entity_splitter.rb +221 -0
  33. data/lib/bitclust/entry.rb +47 -7
  34. data/lib/bitclust/functiondatabase.rb +13 -1
  35. data/lib/bitclust/functionentry.rb +5 -1
  36. data/lib/bitclust/functionreferenceparser.rb +4 -4
  37. data/lib/bitclust/generators/epub.rb +10 -3
  38. data/lib/bitclust/htmlutils.rb +1 -1
  39. data/lib/bitclust/include_graph.rb +375 -0
  40. data/lib/bitclust/include_pruner.rb +144 -0
  41. data/lib/bitclust/interface.rb +1 -1
  42. data/lib/bitclust/libraryentry.rb +8 -7
  43. data/lib/bitclust/lineinput.rb +7 -5
  44. data/lib/bitclust/link_checker.rb +179 -0
  45. data/lib/bitclust/markdown_bridge.rb +186 -0
  46. data/lib/bitclust/markdown_orchestrator.rb +267 -0
  47. data/lib/bitclust/markdown_to_rrd.rb +824 -0
  48. data/lib/bitclust/markdown_tree.rb +217 -0
  49. data/lib/bitclust/mdcompiler.rb +883 -0
  50. data/lib/bitclust/mdparser.rb +347 -0
  51. data/lib/bitclust/messagecatalog.rb +3 -3
  52. data/lib/bitclust/method_since_calculator.rb +117 -0
  53. data/lib/bitclust/methoddatabase.rb +124 -14
  54. data/lib/bitclust/methodentry.rb +125 -7
  55. data/lib/bitclust/methodid.rb +12 -11
  56. data/lib/bitclust/methodsignature.rb +10 -4
  57. data/lib/bitclust/nameutils.rb +41 -22
  58. data/lib/bitclust/preprocessor.rb +66 -29
  59. data/lib/bitclust/rdcompiler.rb +195 -95
  60. data/lib/bitclust/refsdatabase.rb +23 -8
  61. data/lib/bitclust/reloadable_request_handler.rb +72 -0
  62. data/lib/bitclust/requesthandler.rb +21 -16
  63. data/lib/bitclust/ridatabase.rb +10 -4
  64. data/lib/bitclust/rrd_to_markdown.rb +925 -0
  65. data/lib/bitclust/rrdparser.rb +196 -30
  66. data/lib/bitclust/runner.rb +17 -2
  67. data/lib/bitclust/screen.rb +80 -11
  68. data/lib/bitclust/search_index_generator.rb +288 -0
  69. data/lib/bitclust/searcher.rb +69 -20
  70. data/lib/bitclust/simplesearcher.rb +24 -12
  71. data/lib/bitclust/subcommand.rb +7 -1
  72. data/lib/bitclust/subcommands/ancestors_command.rb +9 -2
  73. data/lib/bitclust/subcommands/checklink_command.rb +54 -0
  74. data/lib/bitclust/subcommands/chm_command.rb +15 -7
  75. data/lib/bitclust/subcommands/classes_command.rb +5 -0
  76. data/lib/bitclust/subcommands/extract_command.rb +5 -0
  77. data/lib/bitclust/subcommands/htmlfile_command.rb +21 -7
  78. data/lib/bitclust/subcommands/init_command.rb +4 -1
  79. data/lib/bitclust/subcommands/list_command.rb +12 -4
  80. data/lib/bitclust/subcommands/lookup_command.rb +28 -9
  81. data/lib/bitclust/subcommands/methods_command.rb +10 -2
  82. data/lib/bitclust/subcommands/methodsince_command.rb +80 -0
  83. data/lib/bitclust/subcommands/preproc_command.rb +5 -0
  84. data/lib/bitclust/subcommands/property_command.rb +2 -1
  85. data/lib/bitclust/subcommands/searchpage_command.rb +102 -0
  86. data/lib/bitclust/subcommands/server_command.rb +10 -8
  87. data/lib/bitclust/subcommands/setup_command.rb +24 -10
  88. data/lib/bitclust/subcommands/statichtml_command.rb +129 -6
  89. data/lib/bitclust/subcommands/update_command.rb +110 -2
  90. data/lib/bitclust/syntax_highlighter.rb +48 -4
  91. data/lib/bitclust/textutils.rb +3 -3
  92. data/lib/bitclust/version.rb +1 -1
  93. data/lib/bitclust/version_badges.rb +90 -0
  94. data/lib/bitclust/whole_file_gate.rb +137 -0
  95. data/test/test_app.rb +269 -0
  96. data/test/test_bitclust.rb +2 -0
  97. data/test/test_capi_converter.rb +91 -0
  98. data/test/test_class_screen.rb +413 -0
  99. data/test/test_copy_doc_md.rb +53 -0
  100. data/test/test_doc_converter.rb +78 -0
  101. data/test/test_entity_splitter.rb +231 -0
  102. data/test/test_entry.rb +207 -2
  103. data/test/test_eol_warning.rb +43 -0
  104. data/test/test_functionreferenceparser.rb +11 -0
  105. data/test/test_include_graph.rb +640 -0
  106. data/test/test_include_pruner.rb +133 -0
  107. data/test/test_link_checker.rb +114 -0
  108. data/test/test_lookup_command.rb +135 -0
  109. data/test/test_markdown_bridge.rb +217 -0
  110. data/test/test_markdown_orchestrator.rb +478 -0
  111. data/test/test_markdown_to_rrd.rb +757 -0
  112. data/test/test_markdown_tree.rb +369 -0
  113. data/test/test_mdcompiler.rb +1276 -0
  114. data/test/test_mdparser.rb +538 -0
  115. data/test/test_method_screen.rb +166 -0
  116. data/test/test_method_since_calculator.rb +290 -0
  117. data/test/test_methoddatabase.rb +16 -0
  118. data/test/test_methodentry.rb +328 -0
  119. data/test/test_methodsignature.rb +3 -1
  120. data/test/test_methodsince_command.rb +168 -0
  121. data/test/test_nameutils.rb +22 -0
  122. data/test/test_preprocessor.rb +215 -0
  123. data/test/test_rdcompiler.rb +401 -30
  124. data/test/test_refsdatabase.rb +32 -0
  125. data/test/test_rrd_to_markdown.rb +968 -0
  126. data/test/test_rrdparser.rb +295 -2
  127. data/test/test_run_ruby_wasm.rb +54 -0
  128. data/test/test_run_worker_prelude.rb +83 -0
  129. data/test/test_runner.rb +51 -0
  130. data/test/test_search_index_generator.rb +513 -0
  131. data/test/test_search_screen.rb +48 -0
  132. data/test/test_searcher.rb +132 -0
  133. data/test/test_searchpage_command.rb +167 -0
  134. data/test/test_simplesearcher.rb +28 -0
  135. data/test/test_statichtml_command.rb +204 -0
  136. data/test/test_syntax_highlighter.rb +60 -0
  137. data/test/test_whole_file_gate.rb +175 -0
  138. data/theme/default/js/NOTICE +42 -0
  139. data/theme/default/js/run-worker.js +111 -0
  140. data/theme/default/js/run.js +350 -0
  141. data/theme/default/js/search_controller.js +134 -0
  142. data/theme/default/js/search_init.js +176 -0
  143. data/theme/default/js/search_navigation.js +110 -0
  144. data/theme/default/js/search_page.js +202 -0
  145. data/theme/default/js/search_ranker.js +244 -0
  146. data/theme/default/script.js +91 -18
  147. data/theme/default/search.css +149 -0
  148. data/theme/default/style.css +193 -21
  149. data/theme/default/syntax-highlight.css +8 -0
  150. data/theme/lillia/style.css +45 -1
  151. metadata +161 -20
  152. data/README +0 -32
@@ -0,0 +1,347 @@
1
+ # frozen_string_literal: true
2
+ #
3
+ # bitclust/mdparser.rb
4
+ #
5
+ # Markdown ソースを直接パースして DB エントリを作るパーサ(フェーズ3 M3)。
6
+ #
7
+ # RRDParser のサブクラスとして、行レベルのディスパッチだけを Markdown 記法
8
+ # (MARKUP_SPEC)に差し替える。構造の組み立て(Context・Signature・Chunk)は
9
+ # すべて RRDParser から継承する。エントリの source には md 断片がそのまま入る。
10
+ #
11
+ # - `= class X < Y` → `# class X < Y`
12
+ # - `== Class Methods` 等 → `## Class Methods`(名称は同一)
13
+ # - `--- sig` → `### def sig` / `### module_function def` / `### const` / `### gvar`
14
+ # - category/require/sublibrary・include/extend/alias → front matter
15
+ # - `#@since` 等の指令は md でも同一記法のため Preprocessor は無変更で使う
16
+ # (front matter 内の `#@` 行も先に版解決される)
17
+
18
+ require 'bitclust/rrdparser'
19
+ require 'bitclust/functionreferenceparser'
20
+ require 'bitclust/methodsignature'
21
+
22
+ module BitClust
23
+
24
+ # C API リファレンス(manual/capi)の md を直接パースする。
25
+ # capi の md は本文見出しを持たず、### 全行がシグネチャ
26
+ # (--- <C sig> 相当、キーワード無し)。source は本文の md がそのまま入る
27
+ class MDFunctionParser < FunctionReferenceParser
28
+ private
29
+
30
+ def file_entries(f)
31
+ f.skip_blank_lines
32
+ f.while_match(/\A### /) do |header|
33
+ entry header.sub(/\A###/, '').strip, f.break(/\A### /)
34
+ f.skip_blank_lines
35
+ end
36
+ end
37
+ end
38
+
39
+ class MDParser < RRDParser
40
+
41
+ # doc ページの md からタイトル(# 見出し、アンカー付き含む)と本文を分ける
42
+ # (RRDParser.split_doc の md 版)
43
+ def MDParser.split_doc(source)
44
+ if m = /^#( +(.*?))(?: \{#[^}]+\})?\r?\n/.match(source)
45
+ return ($2 || raise), m.post_match
46
+ end
47
+ return ["", source]
48
+ end
49
+
50
+ # メソッド系シグネチャ(キーワード付き h3)
51
+ SIG_RE = /\A### (?:module_function def |def |const |gvar )/
52
+ # H1(エンティティ見出し)。エスケープされた行頭リテラル \# は除外
53
+ H1_RE = /\A#[^#]/
54
+ # H2(レベル2セクション見出し)
55
+ H2_RE = /\A##[^#]/
56
+ # klass.source / library.source の終端(H1・H2・シグネチャ)
57
+ BREAK_RE = /\A##?[^#]|\A### (?:module_function def |def |const |gvar )/
58
+
59
+ def parse(f, libname, params = {})
60
+ @context = Context.new(@db, libname)
61
+ f = LineInput.new(Preprocessor.wrap(f, params))
62
+ @front_matter = read_front_matter(f)
63
+ do_parse f
64
+ @context.library
65
+ end
66
+
67
+ private
68
+
69
+ # BREAK_RE まで行を収集する(f.break のフェンス対応版)。
70
+ # コードフェンス内の行頭 `# コメント` 等が H1/H2 と衝突しないよう、
71
+ # フェンス内は無条件に本文として消費する
72
+ def md_break(f)
73
+ lines = [] #: Array[String?]
74
+ fence_len = nil
75
+ while (line = f.peek)
76
+ if fence_len
77
+ lines.push(f.gets)
78
+ fence_len = nil if line =~ /\A`{#{fence_len}}\s*$/
79
+ elsif line =~ /\A(`{3,})/
80
+ fence_len = ($1 || raise).length
81
+ lines.push(f.gets)
82
+ elsif line =~ BREAK_RE
83
+ break
84
+ else
85
+ lines.push(f.gets)
86
+ end
87
+ end
88
+ lines
89
+ end
90
+
91
+ # 断片の front matter を @front_matter へマージ(リストは連結、
92
+ # スカラーは後勝ち)
93
+ def merge_front_matter(fm)
94
+ fm.each do |k, v|
95
+ if v.is_a?(Array) && @front_matter[k].is_a?(Array)
96
+ @front_matter[k] |= v
97
+ else
98
+ @front_matter[k] = v
99
+ end
100
+ end
101
+ end
102
+
103
+ # front matter(--- ... ---)を読む。YAML のサブセット:
104
+ # スカラー(type/library/category/since/until)とリスト
105
+ # (include/extend/alias/require/sublibrary)のみ。#@/#% 行は
106
+ # Preprocessor で版解決済みの残り(#@#/#%# コメント等)なので読み飛ばす
107
+ def read_front_matter(f)
108
+ fm = {} #: front_matter
109
+ return fm unless f.peek && f.peek =~ /\A---\s*$/
110
+ f.gets
111
+ key = nil
112
+ while (line = f.gets)
113
+ break if line =~ /\A---\s*$/
114
+ case line
115
+ when /\A\#[@%]/
116
+ next
117
+ when /\A(\w+):\s*$/
118
+ key = $1
119
+ fm[key] = []
120
+ when /\A(\w+):\s*(.+?)\s*$/
121
+ fm[$1] = ($2 || raise).sub(/\A"(.*)"\z/, '\1')
122
+ key = nil
123
+ when /\A\s+-\s+(\S.*?)\s*$/
124
+ list = fm[key]
125
+ list << $1 if key && list.is_a?(Array)
126
+ end
127
+ end
128
+ fm
129
+ end
130
+
131
+ def do_parse(f)
132
+ f.skip_blank_lines
133
+ # #@include 展開で断片の front matter がストリーム先頭に現れる
134
+ # (rdoc/parser/simple → parsers/parse_simple の require リスト等)。
135
+ # 連続するブロックを読んでメタデータをマージする
136
+ while f.peek && f.peek =~ /\A---\s*$/
137
+ merge_front_matter read_front_matter(f)
138
+ f.skip_blank_lines
139
+ end
140
+ # RRDParser と同じく未指定でも呼ぶ(category=nil を明示的に設定)
141
+ @context.categorize @front_matter['category'] if @front_matter['type'] == 'library'
142
+ Array(@front_matter['require']).each { |r| @context.require r }
143
+ Array(@front_matter['sublibrary']).each { |s| @context.sublibrary s }
144
+ prose = md_break(f).join('').rstrip
145
+ # メンバーファイル(library: 参照のみ)のパースで lib 本文を
146
+ # 上書きしないよう、lib ファイル(type: library)のときだけ設定する
147
+ @context.library.source = prose if @front_matter['type'] == 'library'
148
+ read_classes f
149
+ if line = f.gets # error
150
+ case line
151
+ when H2_RE
152
+ parse_error "met level-2 header in library document; maybe you forgot level-1 header", line
153
+ when SIG_RE
154
+ parse_error "met bare method entry in library document; maybe you forgot reopen/redefine level-1 header", line
155
+ else
156
+ parse_error "unexpected line in library document", line
157
+ end
158
+ end
159
+ end
160
+
161
+ RELATION_KEYS = %w[include extend alias].freeze
162
+
163
+ def read_classes(f)
164
+ entity_count = 0
165
+ f.while_match(H1_RE) do |line|
166
+ entity_count += 1
167
+ if entity_count > 1 && RELATION_KEYS.any? { |k| @front_matter.key?(k) }
168
+ # 案B: 関係を持つファイルは単一エンティティ(front matter の
169
+ # include/extend/alias の帰属が曖昧になるため)
170
+ parse_error "multiple entities in a file with front matter relations", line
171
+ end
172
+ type, name, superclass = *parse_level1_header(line)
173
+ case type
174
+ when 'class'
175
+ @context.define_class name, (superclass || 'Object'), location: line.location
176
+ read_class_body f
177
+ when 'module'
178
+ parse_error "superclass given for module", line if superclass
179
+ @context.define_module name, location: line.location
180
+ read_class_body f
181
+ when 'object'
182
+ @context.define_object name, superclass, location: line.location
183
+ read_object_body f
184
+ when 'reopen'
185
+ @context.reopen_class name
186
+ read_reopen_body f
187
+ when 'redefine'
188
+ @context.redefine_class name
189
+ read_reopen_body f
190
+ else
191
+ parse_error "wrong level-1 header", line
192
+ end
193
+ end
194
+ end
195
+
196
+ def parse_level1_header(line)
197
+ m = /\A(\S+)\s*([^\s<]+)(?:\s*<\s*(\S+))?\z/.match(line.sub(/\A#/, '').strip)
198
+ unless m
199
+ parse_error "level-1 header syntax error", line
200
+ end
201
+ return m[1], m[2], m[3]
202
+ end
203
+
204
+ # 関係(include/extend/alias)は front matter が置き場(案B)。
205
+ # ただし #@include 断片(rdoc/parsers/parse_rb 等)は rd 形式の
206
+ # 関係行を本文に保持しているため、RRDParser 同様ストリームからも読む
207
+ def read_aliases(f)
208
+ Array(@front_matter['alias']).each { |name| @context.alias name }
209
+ f.while_match(/\Aalias\s/) do |line|
210
+ @context.alias line.split[1]
211
+ end
212
+ end
213
+
214
+ def read_includes(f, reopen = false)
215
+ Array(@front_matter['include']).each do |name|
216
+ reopen ? @context.dynamic_include(name) : @context.include(name)
217
+ end
218
+ f.while_match(/\Ainclude\s/) do |line|
219
+ name = line.split[1]
220
+ reopen ? @context.dynamic_include(name) : @context.include(name)
221
+ end
222
+ end
223
+
224
+ def read_extends(f, reopen = false)
225
+ Array(@front_matter['extend']).each do |name|
226
+ reopen ? @context.dynamic_extend(name) : @context.extend(name)
227
+ end
228
+ f.while_match(/\Aextend\s/) do |line|
229
+ name = line.split[1]
230
+ reopen ? @context.dynamic_extend(name) : @context.extend(name)
231
+ end
232
+ end
233
+
234
+ def read_class_body(f)
235
+ f.skip_blank_lines
236
+ read_aliases f
237
+ read_extends f
238
+ read_includes f
239
+ f.skip_blank_lines
240
+ @context.klass&.source = md_break(f).join('').rstrip
241
+ read_level2_blocks f
242
+ end
243
+
244
+ def read_reopen_body(f)
245
+ f.skip_blank_lines
246
+ read_extends f, true
247
+ read_includes f, true
248
+ f.skip_blank_lines
249
+ read_level2_blocks f
250
+ end
251
+
252
+ def read_object_body(f)
253
+ f.skip_blank_lines
254
+ read_aliases f
255
+ read_extends f
256
+ f.skip_blank_lines
257
+ @context.klass&.source = md_break(f).join('').rstrip
258
+ @context.visibility = :public
259
+ @context.type = :singleton_method
260
+ read_level2_blocks f
261
+ end
262
+
263
+ def read_level2_blocks(f)
264
+ read_entries f
265
+ f.skip_blank_lines
266
+ f.while_match(H2_RE) do |line|
267
+ case line.sub(/\A##/, '').strip
268
+ when /\A((?:public|private|protected)\s+)?(?:(class|singleton|instance)\s+)?methods?\z/i
269
+ # @type var visibility: :public | :private | :protected
270
+ visibility = ($1 || 'public').downcase.strip.intern
271
+ @context.visibility = visibility
272
+ t = ($2 || 'instance').downcase.sub(/class/, 'singleton')
273
+ @context.type = _ = "#{t}_method".intern
274
+ when /\AModule\s+Functions?\z/i
275
+ @context.module_function
276
+ when /\AConstants?\z/i
277
+ @context.constant
278
+ when /\ASpecial\s+Variables?\z/i
279
+ @context.special_variable
280
+ else
281
+ parse_error "unknown level-2 header", line
282
+ end
283
+ read_entries f
284
+ end
285
+ end
286
+
287
+ def read_chunks(f)
288
+ f.skip_blank_lines
289
+ result = [] #: Array[Chunk]
290
+ f.while_match(SIG_RE) do |line|
291
+ f.ungets line
292
+ result.push read_chunk(f)
293
+ end
294
+ result
295
+ end
296
+
297
+ def read_chunk(f)
298
+ # シグネチャ行の直後には {: ...} 属性行を置ける(rd 側と同じ)。
299
+ # 属性行を挟んでもひとつのチャンク(別名グループ)として読む
300
+ header = [] #: Array[String]
301
+ sig_lines = [] #: Array[String]
302
+ while f.next?
303
+ if SIG_RE =~ f.peek
304
+ line = f.gets or raise
305
+ header.push line
306
+ sig_lines.push line
307
+ elsif !sig_lines.empty? && METHOD_ATTRIBUTE_LINE_RE =~ f.peek
308
+ header.push(f.gets || raise)
309
+ else
310
+ break
311
+ end
312
+ end
313
+ body = md_break(f)
314
+ src = (header + body).join('')
315
+ src.location = sig_lines[0].location
316
+ sigs = sig_lines.map {|line| method_signature(line) }
317
+ mainsig = check_chunk_signatures(sigs, sig_lines[0])
318
+ names = sigs.map {|s| s.name }.compact.uniq.sort
319
+ Chunk.new(mainsig, names, src)
320
+ end
321
+
322
+ # md シグネチャ行を rd 形式(--- ...)へ落とし、既存の
323
+ # Signature パースを継承する。エラー時は元の行を報告する
324
+ def method_signature(line)
325
+ rd_line = line.sub(SIG_RE, '--- ')
326
+ begin
327
+ # 描画(MethodSignature.parse)が受理できないシグネチャは
328
+ # statichtml まで進んでから落ちる(か、誤った名前で DB に入る)ので
329
+ # パース時に拒否する。RBS 形式(def name:)や self. プレフィクスが該当
330
+ MethodSignature.parse(rd_line)
331
+ rescue ParseError
332
+ parse_error "unsupported method signature", line
333
+ end
334
+ case
335
+ when m = SIGNATURE.match(rd_line)
336
+ klass, typemark_, name = m.captures
337
+ typemark = _ = typemark_
338
+ Signature.new(klass, typemark, name)
339
+ when m = GVAR.match(rd_line)
340
+ Signature.new(nil, '$', (m[1] || raise)[1..-1])
341
+ else
342
+ parse_error "wrong method signature", line
343
+ end
344
+ end
345
+ end
346
+
347
+ end
@@ -53,7 +53,7 @@ module BitClust
53
53
 
54
54
  def MessageCatalog.load_with_locales(prefix, locales)
55
55
  path, loc = find_catalog(prefix, locales)
56
- path ? load_file(path, loc) : new({}, 'C')
56
+ path ? load_file(path, loc || raise) : new({}, 'C')
57
57
  end
58
58
 
59
59
  def MessageCatalog.env_locales
@@ -72,10 +72,10 @@ module BitClust
72
72
  private_class_method :find_catalog
73
73
 
74
74
  def MessageCatalog.load_file(path, locale)
75
- h = {}
75
+ h = {} #: Hash[String, String]
76
76
  fopen(path, 'r:UTF-8') {|f|
77
77
  f.each do |key|
78
- h[key.chomp] = f.gets.chomp
78
+ h[key.chomp] = (f.gets || raise).chomp
79
79
  end
80
80
  }
81
81
  new(h, locale)
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+ #
3
+ # bitclust/method_since_calculator.rb
4
+ #
5
+
6
+ require 'bitclust/exception'
7
+
8
+ module BitClust
9
+
10
+ # 複数バージョンの DB(バージョンラダー)からメソッド名別の初出/削除
11
+ # バージョンを算出し、対象 DB のメソッドエントリへ書き込む(bitclust#132 P2)。
12
+ #
13
+ # ライブラリは意図的に無視する: 同じメソッドが版によって別ライブラリの
14
+ # 下でドキュメント化されていても、同一メソッドとして union で扱う。
15
+ #
16
+ # 算出値は「著者が明示した値(将来 P4 で {: since ...} 等から記録される
17
+ # 想定)」より優先度が低い。#apply は MethodEntry#fill_since/fill_until を
18
+ # 使うため、既に値がある名前は上書きしない。
19
+ class MethodSinceCalculator
20
+
21
+ def initialize(dbs)
22
+ versions = dbs.map {|db| db.propget('version') }
23
+ versions.each do |v|
24
+ if v.nil? || v.empty?
25
+ raise UserError, "database has no version property"
26
+ end
27
+ end
28
+ dup = versions.tally.select {|_, n| n > 1 }.keys
29
+ unless dup.empty?
30
+ raise UserError, "duplicate version(s) in ladder: #{dup.join(', ')}"
31
+ end
32
+ sorted = dbs.sort_by {|db| Gem::Version.new(db.propget('version') || raise) }
33
+ @versions = sorted.map {|db| db.propget('version') || raise }
34
+ @ladder = sorted
35
+ @first = {} #: Hash[key, String]
36
+ @last = {} #: Hash[key, String]
37
+ end
38
+
39
+ # ラダーの各 DB を1つずつ走査して [クラス名, typechar, 生名] ごとの
40
+ # 初出/最終出現バージョンを記録する。走査済みの DB への参照は
41
+ # メモリを膨らませないようその場で捨てる
42
+ def scan
43
+ ladder = @ladder or raise "scan was already called"
44
+ @ladder = nil
45
+ until ladder.empty?
46
+ db = ladder.shift
47
+ version = db.propget('version') || raise
48
+ db.classes.each do |c|
49
+ c.entries.each do |m|
50
+ next if m.kind == :undefined
51
+ m.names.each do |name|
52
+ key = [c.name, m.typechar, name] #: key
53
+ @first[key] ||= version
54
+ @last[key] = version
55
+ end
56
+ end
57
+ end
58
+ end
59
+ self
60
+ end
61
+
62
+ # key = [クラス名, typechar, 生名]。ラダー最古版から存在する(フロア)
63
+ # 場合は「不明」を意味するので nil を返す
64
+ def since_for(key)
65
+ first = @first[key] or return nil
66
+ first == (@versions.first || raise) ? nil : first
67
+ end
68
+
69
+ # ラダー最新版でも存在する場合は「まだ削除されていない」ので nil。
70
+ # そうでなければ最終出現バージョンの次のラダーバージョン(削除された版)
71
+ def until_for(key)
72
+ last = @last[key] or return nil
73
+ return nil if last == (@versions.last || raise)
74
+ idx = @versions.index(last) or raise "must not happen: #{last.inspect}"
75
+ @versions[idx + 1] || raise
76
+ end
77
+
78
+ # 対象 DB の各メソッドエントリへ算出済みの since/until を書き込む。
79
+ # target_db のバージョンはラダーに含まれていなければならない。
80
+ # 既に値がある名前は fill_since/fill_until の仕様により上書きされない
81
+ # (著者による明示値が算出値より優先される)。冪等: 2回目の apply は
82
+ # 何も変更しない(floor_skipped を除く)
83
+ def apply(target_db)
84
+ version = target_db.propget('version')
85
+ unless @versions.include?(version)
86
+ raise UserError, "#{version.inspect} is not one of the ladder versions: #{@versions.join(', ')}"
87
+ end
88
+ stats = {entries_updated: 0, since_filled: 0, until_filled: 0, floor_skipped: 0} #: stats
89
+ target_db.classes.each do |c|
90
+ c.entries.each do |m|
91
+ next if m.kind == :undefined
92
+ changed = false
93
+ m.names.each do |name|
94
+ key = [c.name, m.typechar, name] #: key
95
+ if since_v = since_for(key)
96
+ if m.fill_since(name, since_v)
97
+ stats[:since_filled] += 1
98
+ changed = true
99
+ end
100
+ else
101
+ stats[:floor_skipped] += 1
102
+ end
103
+ if (until_v = until_for(key)) && m.fill_until(name, until_v)
104
+ stats[:until_filled] += 1
105
+ changed = true
106
+ end
107
+ end
108
+ if changed
109
+ m.save
110
+ stats[:entries_updated] += 1
111
+ end
112
+ end
113
+ end
114
+ stats
115
+ end
116
+ end
117
+ end
@@ -18,6 +18,7 @@ require 'bitclust/refsdatabase'
18
18
  require 'bitclust/rrdparser'
19
19
  require 'bitclust/exception'
20
20
  require 'fileutils'
21
+ require 'pathname'
21
22
 
22
23
  module BitClust
23
24
 
@@ -47,14 +48,15 @@ module BitClust
47
48
  attr_writer :refs
48
49
 
49
50
  def init
50
- FileUtils.rm_rf @prefix
51
- FileUtils.mkdir_p @prefix
52
- Dir.mkdir "#{@prefix}/library"
53
- Dir.mkdir "#{@prefix}/class"
54
- Dir.mkdir "#{@prefix}/method"
55
- Dir.mkdir "#{@prefix}/doc"
56
- FileUtils.touch "#{@prefix}/properties"
57
- FileUtils.touch "#{@prefix}/refs"
51
+ prefix = @prefix || raise
52
+ FileUtils.rm_rf prefix
53
+ FileUtils.mkdir_p prefix
54
+ Dir.mkdir "#{prefix}/library"
55
+ Dir.mkdir "#{prefix}/class"
56
+ Dir.mkdir "#{prefix}/method"
57
+ Dir.mkdir "#{prefix}/doc"
58
+ FileUtils.touch "#{prefix}/properties"
59
+ FileUtils.touch "#{prefix}/refs"
58
60
  end
59
61
 
60
62
  #
@@ -162,6 +164,56 @@ module BitClust
162
164
  RRDParser.new(self).parse_file(path, libname, properties())
163
165
  end
164
166
 
167
+ # Markdown ツリー(manual/api)を直接パースして DB を更新する(M3)。
168
+ # front matter の library: が所属を表すため、ライブラリごとに
169
+ # lib ファイル → メンバーファイル(reopen/redefine のみのファイルは後置。
170
+ # dynamic include の対象 module が先に定義されている必要があるため)の
171
+ # 順でパースする。版ゲート(since/until)外のライブラリ/メンバーはスキップ。
172
+ # エントリの source には md 断片が入り、source_location は md の実パスを指す
173
+ def update_by_markdowntree(md_root)
174
+ require 'bitclust/markdown_tree'
175
+ require 'bitclust/mdparser'
176
+ @md_root = md_root
177
+ # 描画層(screen.rb)が MDCompiler を選択するためのマーカー
178
+ propset 'source_format', 'markdown'
179
+ tree = MarkdownTree.scan(md_root)
180
+ version = properties()["version"]
181
+ tree.libraries.sort.each do |libname, lib|
182
+ next unless md_version_covers?(version, lib[:since], lib[:until])
183
+ lib_entry = MDParser.new(self).parse_file(File.join(md_root, lib[:path]), libname, properties())
184
+ lib_location = lib_entry.source_location
185
+ # 多重所属(ゲート付き library リスト)は、この版でゲートが生きている
186
+ # membership を持つライブラリだけがメンバーとして取り込む
187
+ members = tree.entities.select { |_, e|
188
+ e[:memberships].any? { |m|
189
+ m[:library] == libname && md_version_covers?(version, m[:since], m[:until])
190
+ }
191
+ }
192
+ sorted = members.keys.sort_by { |path|
193
+ reopen_only = members[path][:kinds].all? { |kind, _| %w[reopen redefine].include?(kind) }
194
+ [reopen_only ? 1 : 0, path]
195
+ }
196
+ sorted.each do |path|
197
+ entity = members[path]
198
+ next unless md_version_covers?(version, entity[:since], entity[:until])
199
+ MDParser.new(self).parse_file(File.join(md_root, path), libname, properties())
200
+ end
201
+ # parse_file は毎回 library の source_location を上書きするため
202
+ # lib ファイルの値へ戻す
203
+ lib_entry.source_location = lib_location
204
+ end
205
+ end
206
+
207
+ # since は「その版以降」、until は「その版未満」(ブリッジの
208
+ # #@since/#@until ラッパーと同じ意味論)
209
+ def md_version_covers?(version, since_version, until_version)
210
+ v = Gem::Version.new(version)
211
+ return false if since_version && v < Gem::Version.new(since_version)
212
+ return false if until_version && v >= Gem::Version.new(until_version)
213
+ true
214
+ end
215
+ private :md_version_covers?
216
+
165
217
  def refs
166
218
  @refs ||= RefsDatabase.load(realpath('refs'))
167
219
  end
@@ -176,10 +228,12 @@ module BitClust
176
228
  end
177
229
 
178
230
  def copy_doc
231
+ return copy_doc_md if @md_root
232
+ return unless @root
179
233
  root_path = Pathname.new(@root).expand_path
180
234
  Dir.glob("#{@root}/../../doc/**/*.rd").each do |f|
181
235
  if %r!\A#{Regexp.escape(@root)}/\.\./\.\./doc/([-\./\w]+)\.rd\z! =~ f
182
- id = libname2id($1)
236
+ id = libname2id($1 || raise)
183
237
  se = DocEntry.new(self, id)
184
238
  s = Preprocessor.read(f, properties)
185
239
  title, source = RRDParser.split_doc(s)
@@ -192,6 +246,65 @@ module BitClust
192
246
  end
193
247
  end
194
248
 
249
+ # ネイティブ md ツリーの doc ページ登録(manual/api の隣の manual/doc)。
250
+ # ページ = 他の doc ファイルから #@include 参照されていない .md。
251
+ # source には md がそのまま入り、source_location は md の実パスを指す
252
+ def copy_doc_md
253
+ require 'bitclust/mdparser'
254
+ # source_location は md_root と同じ形で格納する(api 側と同じ慣例。
255
+ # Rakefile の相対 manual/api なら manual/doc/... になる)。絶対化すると
256
+ # Windows のドライブレターのコロンで file:line 分割が壊れ、
257
+ # 編集リンクも実行環境のパス依存になる
258
+ doc_root = Pathname.new(File.join(@md_root, '..', 'doc')).cleanpath.to_s
259
+ return unless File.directory?(doc_root)
260
+ files = Dir.glob("#{doc_root}/**/*.md").sort
261
+ referenced = files.flat_map { |f|
262
+ base = File.dirname(f)
263
+ File.read(f).scan(/^\#[@%]include\((.*?)\)/).map { |t|
264
+ p = File.expand_path(t[0] || raise, base)
265
+ [p, "#{p}.md", p.sub(/\.rd\z/, '.md')]
266
+ }.flatten
267
+ }.to_set
268
+ version = properties()["version"]
269
+ files.each do |f|
270
+ next if referenced.include?(File.expand_path(f))
271
+ # front matter の since/until でページ自体をバージョンで出し分ける
272
+ # (ライブラリ側 update_by_markdowntree と同じ意味論)。範囲外の版では
273
+ # ページを登録しない(例: doc/spec/safelevel は until: "3.2" で 3.2 以降
274
+ # 非生成)
275
+ since_version, until_version = doc_md_version_range(f)
276
+ next unless md_version_covers?(version, since_version, until_version)
277
+ id = libname2id(f.delete_prefix("#{doc_root}/").sub(/\.md\z/, ''))
278
+ se = DocEntry.new(self, id)
279
+ s = Preprocessor.read(f, properties)
280
+ title, source = MDParser.split_doc(s)
281
+ se.title = title
282
+ se.source = source
283
+ se.source_location = Location.new(f, 1)
284
+ se.save
285
+ end
286
+ end
287
+ private :copy_doc_md
288
+
289
+ # doc ページの先頭 front matter から since/until を読む
290
+ # (MarkdownTree.scan がライブラリ/エンティティで読むのと同じ書式)。
291
+ # front matter が無い、または since/until を持たなければ [nil, nil]。
292
+ def doc_md_version_range(path)
293
+ since_version = nil
294
+ until_version = nil
295
+ File.open(path, 'r:UTF-8') do |io|
296
+ first = io.gets
297
+ return [nil, nil] unless first && first =~ /\A---\s*$/
298
+ while (line = io.gets)
299
+ break if line =~ /\A---\s*$/
300
+ since_version = $1 if line =~ /\Asince:\s*"?([^"\s]+)"?/
301
+ until_version = $1 if line =~ /\Auntil:\s*"?([^"\s]+)"?/
302
+ end
303
+ end
304
+ [since_version, until_version]
305
+ end
306
+ private :doc_md_version_range
307
+
195
308
  #
196
309
  # Doc Entry
197
310
  #
@@ -311,7 +424,7 @@ module BitClust
311
424
  check_transaction
312
425
  id = classname2id(name)
313
426
  if exist?("class/#{id}")
314
- c = load_class(id)
427
+ c = load_class(id) || raise
315
428
  c.clear
316
429
  else
317
430
  c = (@classmap[id] ||= ClassEntry.new(self, id))
@@ -331,6 +444,7 @@ module BitClust
331
444
  private :load_class
332
445
 
333
446
  def load_extent(entry_class)
447
+ # @type var h: Hash[String, untyped]
334
448
  h = {}
335
449
  id_extent(entry_class).each do |id|
336
450
  h[id] = entry_class.new(self, id)
@@ -382,10 +496,6 @@ module BitClust
382
496
  fetch_class(spec.klass).fetch_method(spec)
383
497
  end
384
498
 
385
- def search_method(pattern)
386
- search_methods(pattern).first
387
- end
388
-
389
499
  def search_methods(pattern)
390
500
  result = _search_methods(pattern)
391
501
  if result.fail?