bitclust-core 1.2.6 → 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 (160) 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 +49 -5
  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 +17 -17
  24. data/lib/bitclust/capi_converter.rb +20 -0
  25. data/lib/bitclust/classentry.rb +68 -17
  26. data/lib/bitclust/compat.rb +3 -2
  27. data/lib/bitclust/completion.rb +59 -27
  28. data/lib/bitclust/crossrubyutils.rb +9 -5
  29. data/lib/bitclust/database.rb +29 -8
  30. data/lib/bitclust/doc_converter.rb +63 -0
  31. data/lib/bitclust/docentry.rb +2 -1
  32. data/lib/bitclust/entity_splitter.rb +221 -0
  33. data/lib/bitclust/entry.rb +48 -7
  34. data/lib/bitclust/exception.rb +1 -0
  35. data/lib/bitclust/functiondatabase.rb +14 -1
  36. data/lib/bitclust/functionentry.rb +6 -1
  37. data/lib/bitclust/functionreferenceparser.rb +5 -4
  38. data/lib/bitclust/generators/epub.rb +11 -3
  39. data/lib/bitclust/htmlutils.rb +2 -1
  40. data/lib/bitclust/include_graph.rb +375 -0
  41. data/lib/bitclust/include_pruner.rb +144 -0
  42. data/lib/bitclust/interface.rb +2 -1
  43. data/lib/bitclust/libraryentry.rb +9 -7
  44. data/lib/bitclust/lineinput.rb +8 -5
  45. data/lib/bitclust/link_checker.rb +179 -0
  46. data/lib/bitclust/markdown_bridge.rb +186 -0
  47. data/lib/bitclust/markdown_orchestrator.rb +267 -0
  48. data/lib/bitclust/markdown_to_rrd.rb +824 -0
  49. data/lib/bitclust/markdown_tree.rb +217 -0
  50. data/lib/bitclust/mdcompiler.rb +883 -0
  51. data/lib/bitclust/mdparser.rb +347 -0
  52. data/lib/bitclust/messagecatalog.rb +4 -3
  53. data/lib/bitclust/method_since_calculator.rb +117 -0
  54. data/lib/bitclust/methoddatabase.rb +125 -14
  55. data/lib/bitclust/methodentry.rb +130 -7
  56. data/lib/bitclust/methodid.rb +13 -11
  57. data/lib/bitclust/methodsignature.rb +11 -4
  58. data/lib/bitclust/nameutils.rb +42 -22
  59. data/lib/bitclust/parseutils.rb +1 -0
  60. data/lib/bitclust/preprocessor.rb +70 -32
  61. data/lib/bitclust/progress_bar.rb +1 -0
  62. data/lib/bitclust/rdcompiler.rb +197 -96
  63. data/lib/bitclust/refsdatabase.rb +24 -8
  64. data/lib/bitclust/reloadable_request_handler.rb +72 -0
  65. data/lib/bitclust/requesthandler.rb +22 -16
  66. data/lib/bitclust/ridatabase.rb +11 -4
  67. data/lib/bitclust/rrd_to_markdown.rb +925 -0
  68. data/lib/bitclust/rrdparser.rb +197 -30
  69. data/lib/bitclust/runner.rb +18 -2
  70. data/lib/bitclust/screen.rb +81 -11
  71. data/lib/bitclust/search_index_generator.rb +288 -0
  72. data/lib/bitclust/searcher.rb +72 -22
  73. data/lib/bitclust/server.rb +6 -1
  74. data/lib/bitclust/silent_progress_bar.rb +1 -0
  75. data/lib/bitclust/simplesearcher.rb +25 -12
  76. data/lib/bitclust/subcommand.rb +8 -1
  77. data/lib/bitclust/subcommands/ancestors_command.rb +10 -2
  78. data/lib/bitclust/subcommands/checklink_command.rb +54 -0
  79. data/lib/bitclust/subcommands/chm_command.rb +16 -7
  80. data/lib/bitclust/subcommands/classes_command.rb +6 -0
  81. data/lib/bitclust/subcommands/epub_command.rb +1 -0
  82. data/lib/bitclust/subcommands/extract_command.rb +6 -0
  83. data/lib/bitclust/subcommands/htmlfile_command.rb +22 -7
  84. data/lib/bitclust/subcommands/init_command.rb +5 -1
  85. data/lib/bitclust/subcommands/list_command.rb +13 -4
  86. data/lib/bitclust/subcommands/lookup_command.rb +29 -9
  87. data/lib/bitclust/subcommands/methods_command.rb +11 -2
  88. data/lib/bitclust/subcommands/methodsince_command.rb +80 -0
  89. data/lib/bitclust/subcommands/preproc_command.rb +6 -0
  90. data/lib/bitclust/subcommands/property_command.rb +3 -1
  91. data/lib/bitclust/subcommands/query_command.rb +1 -0
  92. data/lib/bitclust/subcommands/searchpage_command.rb +102 -0
  93. data/lib/bitclust/subcommands/server_command.rb +16 -10
  94. data/lib/bitclust/subcommands/setup_command.rb +25 -10
  95. data/lib/bitclust/subcommands/statichtml_command.rb +132 -6
  96. data/lib/bitclust/subcommands/update_command.rb +111 -2
  97. data/lib/bitclust/syntax_highlighter.rb +52 -5
  98. data/lib/bitclust/textutils.rb +4 -3
  99. data/lib/bitclust/version.rb +2 -1
  100. data/lib/bitclust/version_badges.rb +90 -0
  101. data/lib/bitclust/whole_file_gate.rb +137 -0
  102. data/lib/bitclust.rb +1 -0
  103. data/test/test_app.rb +269 -0
  104. data/test/test_bitclust.rb +2 -0
  105. data/test/test_capi_converter.rb +91 -0
  106. data/test/test_class_screen.rb +413 -0
  107. data/test/test_copy_doc_md.rb +53 -0
  108. data/test/test_doc_converter.rb +78 -0
  109. data/test/test_entity_splitter.rb +231 -0
  110. data/test/test_entry.rb +207 -2
  111. data/test/test_eol_warning.rb +43 -0
  112. data/test/test_functionreferenceparser.rb +11 -0
  113. data/test/test_include_graph.rb +640 -0
  114. data/test/test_include_pruner.rb +133 -0
  115. data/test/test_link_checker.rb +114 -0
  116. data/test/test_lookup_command.rb +135 -0
  117. data/test/test_markdown_bridge.rb +217 -0
  118. data/test/test_markdown_orchestrator.rb +478 -0
  119. data/test/test_markdown_to_rrd.rb +757 -0
  120. data/test/test_markdown_tree.rb +369 -0
  121. data/test/test_mdcompiler.rb +1276 -0
  122. data/test/test_mdparser.rb +538 -0
  123. data/test/test_method_screen.rb +166 -0
  124. data/test/test_method_since_calculator.rb +290 -0
  125. data/test/test_methoddatabase.rb +16 -0
  126. data/test/test_methodentry.rb +328 -0
  127. data/test/test_methodsignature.rb +3 -1
  128. data/test/test_methodsince_command.rb +168 -0
  129. data/test/test_nameutils.rb +22 -0
  130. data/test/test_preprocessor.rb +215 -0
  131. data/test/test_rdcompiler.rb +429 -27
  132. data/test/test_refsdatabase.rb +32 -0
  133. data/test/test_rrd_to_markdown.rb +968 -0
  134. data/test/test_rrdparser.rb +295 -2
  135. data/test/test_run_ruby_wasm.rb +54 -0
  136. data/test/test_run_worker_prelude.rb +83 -0
  137. data/test/test_runner.rb +51 -0
  138. data/test/test_search_index_generator.rb +513 -0
  139. data/test/test_search_screen.rb +48 -0
  140. data/test/test_searcher.rb +153 -0
  141. data/test/test_searchpage_command.rb +167 -0
  142. data/test/test_simplesearcher.rb +28 -0
  143. data/test/test_statichtml_command.rb +204 -0
  144. data/test/test_syntax_highlighter.rb +60 -0
  145. data/test/test_whole_file_gate.rb +175 -0
  146. data/theme/default/js/NOTICE +42 -0
  147. data/theme/default/js/run-worker.js +111 -0
  148. data/theme/default/js/run.js +350 -0
  149. data/theme/default/js/search_controller.js +134 -0
  150. data/theme/default/js/search_init.js +176 -0
  151. data/theme/default/js/search_navigation.js +110 -0
  152. data/theme/default/js/search_page.js +202 -0
  153. data/theme/default/js/search_ranker.js +244 -0
  154. data/theme/default/script.js +91 -18
  155. data/theme/default/search.css +149 -0
  156. data/theme/default/style.css +193 -21
  157. data/theme/default/syntax-highlight.css +8 -0
  158. data/theme/lillia/style.css +45 -1
  159. metadata +178 -21
  160. data/README +0 -32
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require "ripper"
2
3
  require "bitclust/htmlutils"
3
4
 
@@ -55,6 +56,7 @@ module BitClust
55
56
  nl: nil, # \n
56
57
  op: "o", # operator
57
58
  period: "p", # .
59
+ qsymbols_beg: "ss", # %i(
58
60
  qwords_beg: "sx", # %w(
59
61
  rbrace: "p", # }
60
62
  rbracket: "p", # ]
@@ -64,6 +66,7 @@ module BitClust
64
66
  semicolon: nil, # ;
65
67
  sp: nil, # space
66
68
  symbeg: "ss", # :
69
+ symbols_beg: "ss", # %I(
67
70
  tlambda: "o", # ->
68
71
  tlambeg: "p", # (->) {
69
72
  tstring_beg: nil, # " (string")
@@ -130,7 +133,7 @@ module BitClust
130
133
  end
131
134
 
132
135
  def on_default(event, token, data)
133
- event_name = event.to_s.sub(/\Aon_/, "") # :on_event --> "event"
136
+ event_name = event.to_s.sub(/\Aon_/, "") ## :on_event --> "event"
134
137
  style = COLORS[event_name.to_sym]
135
138
  escaped_token = escape_html(token)
136
139
  data << (style ? "<span class=\"#{style}\">#{escaped_token}</span>" : escaped_token)
@@ -164,6 +167,8 @@ module BitClust
164
167
  when @stack.last == :method_call
165
168
  data << "<span class=\"nf\">#{token}</span>"
166
169
  @stack.pop
170
+ when @stack.last == :class
171
+ @name_buffer << token
167
172
  when BUILTINS_G.include?(token)
168
173
  data << "<span class=\"nb\">#{token}</span>"
169
174
  else
@@ -178,6 +183,9 @@ module BitClust
178
183
  @name_buffer << token
179
184
  when @stack.last == :module
180
185
  @name_buffer << token
186
+ when @stack.last == :symbol
187
+ data << "#{token}</span>"
188
+ @stack.pop
181
189
  else
182
190
  on_default(:on_const, token, data)
183
191
  end
@@ -213,8 +221,11 @@ module BitClust
213
221
 
214
222
  def on_op(token, data)
215
223
  case
216
- when token == "::" && [:class, :module].include?(@stack.last)
224
+ when token == "::" && [:class, :module].include?(_ = @stack.last)
217
225
  @name_buffer << token
226
+ when token == "<<" && @stack.last == :class
227
+ @stack.pop
228
+ on_default(:on_op, token, data)
218
229
  else
219
230
  @stack.pop if @stack.last == :method_call
220
231
  on_default(:on_op, token, data)
@@ -266,6 +277,28 @@ module BitClust
266
277
  on_default(:on_nl, token, data)
267
278
  end
268
279
 
280
+ def on_semicolon(token, data)
281
+ case
282
+ when @name_buffer.empty?
283
+ return on_default(:on_semicolon, token, data)
284
+ when @stack.last == :module
285
+ name = @name_buffer.join
286
+ data << "<span class=\"nn\">#{name}</span>"
287
+ @stack.pop
288
+ @name_buffer.clear
289
+ when @stack.last == :class
290
+ namespace = @name_buffer.values_at(0..-3).join
291
+ operator = @name_buffer[-2]
292
+ name = @name_buffer.last
293
+ data << "<span class=\"nn\">#{namespace}</span>"
294
+ data << "<span class=\"o\">#{operator}</span>"
295
+ data << "<span class=\"nc\">#{name}</span>"
296
+ @stack.pop
297
+ @name_buffer.clear
298
+ end
299
+ on_default(:on_semicolon, token, data)
300
+ end
301
+
269
302
  def on_regexp_beg(token, data)
270
303
  style = COLORS[:regexp_beg]
271
304
  data << "<span class=\"#{style}\">#{token}"
@@ -313,7 +346,7 @@ module BitClust
313
346
  case
314
347
  when token == "'"
315
348
  data << "#{token}</span>"
316
- when [:qwords, :words].include?(@stack.last)
349
+ when %i[qwords words qsymbols symbols].include?(_ = @stack.last)
317
350
  @stack.pop
318
351
  data << "#{token}</span>"
319
352
  else
@@ -337,6 +370,20 @@ module BitClust
337
370
  data
338
371
  end
339
372
 
373
+ def on_qsymbols_beg(token, data)
374
+ @stack.push(:qsymbols)
375
+ style = COLORS[:qsymbols_beg]
376
+ data << "<span class=\"#{style}\">#{token}"
377
+ data
378
+ end
379
+
380
+ def on_symbols_beg(token, data)
381
+ @stack.push(:symbols)
382
+ style = COLORS[:symbols_beg]
383
+ data << "<span class=\"#{style}\">#{token}"
384
+ data
385
+ end
386
+
340
387
  def on_heredoc_beg(token, data)
341
388
  @stack.push(:heredoc)
342
389
  on_default(:on_heredoc_beg, token, data)
@@ -360,12 +407,12 @@ module BitClust
360
407
  def on___end__(token, data)
361
408
  on_default(:on___end__, token, data)
362
409
  style = COLORS[:comment]
363
- data << "<span class=\"#{style}\">#{escape_html(@src.lines[lineno..-1].join)}</span>"
410
+ data << "<span class=\"#{style}\">#{escape_html((@src.lines[lineno..-1] || raise).join)}</span>"
364
411
  data
365
412
  end
366
413
 
367
414
  def highlight
368
- parse("")
415
+ parse(+"")
369
416
  end
370
417
  end
371
418
  end
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  #
2
3
  # bitclust/textutils
3
4
  #
@@ -17,7 +18,7 @@ module BitClust
17
18
  def detab(str, ts = 8)
18
19
  add = 0
19
20
  str.gsub(/\t/) {
20
- len = ts - ($~.begin(0) + add) % ts
21
+ len = ts - ((($~ || raise).begin(0) || raise) + add) % ts
21
22
  add += len - 1
22
23
  ' ' * len
23
24
  }
@@ -29,11 +30,11 @@ module BitClust
29
30
  end
30
31
 
31
32
  def n_minimum_indent(lines)
32
- lines.reject {|line| line.strip.empty? }.map {|line| n_indent(line) }.min
33
+ lines.reject {|line| line.strip.empty? }.map {|line| n_indent(line) }.min || raise
33
34
  end
34
35
 
35
36
  def n_indent(line)
36
- line.slice(/\A\s*/).size
37
+ (line.slice(/\A\s*/) || raise).size
37
38
  end
38
39
 
39
40
  INDENT_RE = {
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module BitClust
2
- VERSION = "1.2.6"
3
+ VERSION = "1.6.0"
3
4
  end
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+ #
3
+ # bitclust/version_badges.rb
4
+ #
5
+ # This program is free software.
6
+ # You can distribute/modify this program under the Ruby License.
7
+ #
8
+
9
+ module BitClust
10
+
11
+ # Renders the small "since"/"until" version badges shown next to a
12
+ # method's signature (bitclust#132 phase P3). The version data comes from
13
+ # MethodEntry's per-name since_map/until_map (bitclust#132 phase P2); an
14
+ # empty map means "no badge", which keeps the output byte-identical to
15
+ # what it was before this feature existed.
16
+ #
17
+ # Included by both RDCompiler (the <dt class="method-heading"> emitted
18
+ # for RD/Markdown method pages) and TemplateScreen (the default server
19
+ # template's method table rows), so it only relies on the `_`
20
+ # (Translatable) and escape_html (HTMLUtils) methods that both hosts
21
+ # already provide.
22
+ module VersionBadges
23
+ SINCE_CSS_CLASS = 'method-since-badge'
24
+ UNTIL_CSS_CLASS = 'method-until-badge'
25
+ SINCE_CATALOG_KEY = 'since Ruby %s'
26
+ UNTIL_CATALOG_KEY = 'removed in Ruby %s'
27
+
28
+ # HTML for the badges attached to the <dt> heading of one of +entry+'s
29
+ # signature lines (RDCompiler#method_signature). since/until are
30
+ # decided independently: when every one of entry's names maps to the
31
+ # very same version, the badge is rendered once, on the first
32
+ # signature's <dt> (+first+ true), instead of being repeated on every
33
+ # alias's <dt>; otherwise (mixed/partial) it is looked up per +name+.
34
+ def heading_version_badges(entry, name, first)
35
+ name = badge_lookup_name(name)
36
+ join_badge_spans(
37
+ heading_badge_span(entry, name, first, entry.since_map,
38
+ SINCE_CSS_CLASS, SINCE_CATALOG_KEY),
39
+ heading_badge_span(entry, name, first, entry.until_map,
40
+ UNTIL_CSS_CLASS, UNTIL_CATALOG_KEY)
41
+ )
42
+ end
43
+
44
+ # HTML for the badges attached to a single signature line in the
45
+ # default (server) template's method table
46
+ # (data/bitclust/template/class). Each row already lists every alias on
47
+ # its own line, so this always looks the version up by +name+ directly
48
+ # -- no uniform-across-all-names aggregation, unlike heading_version_badges.
49
+ def row_version_badges(entry, name)
50
+ name = badge_lookup_name(name)
51
+ join_badge_spans(
52
+ badge_span(entry.since_map[name], SINCE_CSS_CLASS, SINCE_CATALOG_KEY),
53
+ badge_span(entry.until_map[name], UNTIL_CSS_CLASS, UNTIL_CATALOG_KEY)
54
+ )
55
+ end
56
+
57
+ private
58
+
59
+ # シグネチャ行から取り出した名前を since_map/until_map のキーに合わせる。
60
+ # 特殊変数のシグネチャ名は "$SAFE" のように $ 付きだが、エントリの names
61
+ # (= マップのキー)は先頭の $ を除いた形("SAFE"。"$$" なら "$")で
62
+ # 格納されている(rrdparser の method_signature と同じ規約)
63
+ def badge_lookup_name(name)
64
+ name.sub(/\A\$/, '')
65
+ end
66
+
67
+ def heading_badge_span(entry, name, first, map, css_class, catalog_key)
68
+ return nil if map.empty?
69
+ version = uniform_map?(entry, map) ? (first ? map.values.first : nil) : map[name]
70
+ badge_span(version, css_class, catalog_key)
71
+ end
72
+
73
+ # Whether +map+ assigns the very same version to every one of entry's
74
+ # names, i.e. the badge applies uniformly and doesn't need to be
75
+ # repeated per alias.
76
+ def uniform_map?(entry, map)
77
+ map.size == entry.names.size && map.values.uniq.size == 1
78
+ end
79
+
80
+ def badge_span(version, css_class, catalog_key)
81
+ # 空文字は「明示的に不明」({: since=""})= バッジ非表示(bitclust#132)
82
+ return nil if version.nil? || version.empty?
83
+ %Q(<span class="#{css_class}">#{escape_html(_(catalog_key, version))}</span>)
84
+ end
85
+
86
+ def join_badge_spans(*spans)
87
+ spans.compact.join(' ')
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,137 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bitclust/include_graph'
4
+
5
+ module BitClust
6
+ # ファイル全体を包む単一の版ゲート(#@else 無し)の検出と解除(O4)。
7
+ #
8
+ # 旧 RD ではライブラリ/エンティティ自体の版ゲートをファイル全体の
9
+ # #@since/#@until/#@if ラップで表現していた。新パイプラインでは
10
+ # front matter の since/until(構造ゲート)で表現するため、
11
+ # スコープの下で「常に真」または「since/until で表現できる」ラップは外す。
12
+ #
13
+ # 据え置き(nil を返す):
14
+ # - #@else 付き(fiddle.rd。枝の選択は本文の版分岐として温存)
15
+ # - スコープ外のゲート(profile.rd 等。旧版サルベージは別スコープで再実行)
16
+ # - 常に真と証明できない #@if 条件
17
+ module WholeFileGate
18
+ GATE_OPEN_RE = /\A\#@(since|until|if)\s*(.*)$/
19
+ BLOCK_OPEN_RE = /\A\#@(?:since|until|if|samplecode)\b/
20
+ BLANK_RE = /\A\s*\z/
21
+
22
+ # 常に真と証明できる #@if 条件の形(rss.rd の (version >= "1.8.2"))
23
+ VERSION_GE_RE = /\A\(?\s*version\s*>=\s*"([\d.]+)"\s*\)?\z/
24
+
25
+ module_function
26
+
27
+ # ファイル全体を包むゲートの Condition を返す。該当しなければ nil
28
+ def detect(src)
29
+ open_idx, close_idx, cond, has_else = parse(src.lines)
30
+ return nil if open_idx.nil? || has_else
31
+ cond if close_idx
32
+ end
33
+
34
+ # 据え置きゲート(unwrap できない library ルート等)のメタデータ領域
35
+ # (require/sublibrary/category)を、本文と分けてそれぞれ同じゲートで包んだ
36
+ # 意味等価な正規形に書き換える。native パースはメタデータを front matter から
37
+ # しか読まないため、ゲート内に埋もれた rd 形式のメタ行を、既存のゲート対応
38
+ # メタデータ収集器が front matter 化できる位置(ファイル先頭のブロック)へ出す。
39
+ # 全体ゲートでない・メタデータが無い・本文が無い場合は nil
40
+ def regate_metadata(src)
41
+ lines = src.lines
42
+ open_idx, close_idx, cond, has_else = parse(lines)
43
+ return nil if open_idx.nil? || close_idx.nil? || has_else || cond.nil?
44
+ content = lines[(open_idx + 1)...close_idx] || raise
45
+ i = 0
46
+ i += 1 while content[i] && content[i] =~ BLANK_RE
47
+ meta_start = i
48
+ meta_end = nil
49
+ j = meta_start
50
+ while content[j]
51
+ case content[j]
52
+ when /\A(?:require|sublibrary|category)\s+\S/ then meta_end = j + 1
53
+ when BLANK_RE then nil
54
+ else break
55
+ end
56
+ j += 1
57
+ end
58
+ return nil unless meta_end
59
+ rest = content[meta_end..] || raise
60
+ rest.shift while rest.first && rest.first =~ BLANK_RE
61
+ return nil if rest.empty?
62
+ gate_line = lines[open_idx]
63
+ # 種別(category/require/sublibrary)ごとに独立ブロックへ分ける
64
+ # (メタデータ収集器の「#@ ブロックは単一種のみ」の制約に合わせる)
65
+ runs = [] #: Array[[String?, Array[String]]]
66
+ (content[meta_start...meta_end] || raise).each do |l|
67
+ next if l =~ BLANK_RE
68
+ kind = l[/\A(category|require|sublibrary)/, 1]
69
+ if runs.last && runs.last[0] == kind
70
+ runs.last[1] << l
71
+ else
72
+ runs << [kind, [l]]
73
+ end
74
+ end
75
+ meta_blocks = runs.flat_map { |_, ls| [gate_line] + ls + ["\#@end\n", "\n"] }
76
+ ((lines[0...open_idx] || raise) +
77
+ meta_blocks +
78
+ [gate_line] + rest + ["\#@end\n"] +
79
+ (lines[(close_idx + 1)..] || raise)).join
80
+ end
81
+
82
+ # スコープの下で解除できる全体ゲートなら [解除後の src, front matter に
83
+ # 書く gate({} は不要の意)] を返す。据え置きなら nil
84
+ def unwrap_for_scope(src, scope)
85
+ lines = src.lines
86
+ open_idx, close_idx, cond, has_else = parse(lines)
87
+ return nil if open_idx.nil? || close_idx.nil? || has_else || cond.nil?
88
+
89
+ gate =
90
+ case cond.kind
91
+ when :if
92
+ return nil unless provably_true_if?(cond.version, scope)
93
+ {} #: IncludeGraph::gate
94
+ else
95
+ scoped = scope.gate([cond])
96
+ return nil unless scoped # スコープ外 → 据え置き
97
+ scoped
98
+ end
99
+
100
+ body = (lines[0...open_idx] || raise) + (lines[(open_idx + 1)...close_idx] || raise) + (lines[(close_idx + 1)..] || raise)
101
+ body.shift while body.first && body.first =~ BLANK_RE
102
+ [body.join, gate]
103
+ end
104
+
105
+ # [開き行 idx, 閉じ行 idx, Condition, トップレベル #@else の有無] を返す。
106
+ # 全体ゲートでなければ open_idx が nil
107
+ def parse(lines)
108
+ open_idx = lines.index { |l| l !~ BLANK_RE }
109
+ return [nil] unless open_idx && lines[open_idx] =~ GATE_OPEN_RE
110
+ cond = IncludeGraph::Condition.new(($1 || raise).to_sym, ($2 || raise).strip)
111
+
112
+ nest = 0
113
+ has_else = false
114
+ close_idx = nil #: Integer?
115
+ (open_idx...lines.length).each do |i|
116
+ case lines[i]
117
+ when BLOCK_OPEN_RE then nest += 1
118
+ when /\A\#@else\b/ then has_else = true if nest == 1
119
+ when /\A\#@end\b/
120
+ nest -= 1
121
+ if nest.zero?
122
+ close_idx = i
123
+ break
124
+ end
125
+ end
126
+ end
127
+ return [nil] if close_idx.nil?
128
+ # 閉じ行の後に非空行があれば全体ゲートではない
129
+ return [nil] if (lines[(close_idx + 1)..] || raise).any? { |l| l !~ BLANK_RE }
130
+ [open_idx, close_idx, cond, has_else]
131
+ end
132
+
133
+ def provably_true_if?(condition, scope)
134
+ condition =~ VERSION_GE_RE && Gem::Version.new($1) <= scope.lo
135
+ end
136
+ end
137
+ end
data/lib/bitclust.rb CHANGED
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require 'bitclust/requesthandler'
2
3
  require 'bitclust/screen'
3
4
  require 'bitclust/server'
data/test/test_app.rb ADDED
@@ -0,0 +1,269 @@
1
+ # frozen_string_literal: true
2
+ require 'test/unit'
3
+ require 'bitclust'
4
+ require 'bitclust/app'
5
+ require 'rack'
6
+ require 'rack/mock'
7
+ require 'webrick'
8
+ require 'tmpdir'
9
+ require 'fileutils'
10
+ require 'stringio'
11
+
12
+ # bitclust#275: `rake generate` で DB を再生成しても `bitclust server` を
13
+ # 再起動しなくてよいようにする。
14
+ #
15
+ # テストリスト:
16
+ # [x] DB を再生成(rm_rf+init+update)した後、同じ App/Interface への同じ
17
+ # リクエストで新しい内容が返り、古い内容(メモ化されていたはず)が消えて
18
+ # いる(Rack ハンドラ、dbpath が String の単一 DB)
19
+ # [x] 同じことが WEBrick ハンドラ(options[:rack] を渡さない既定経路)でも
20
+ # 成り立つ
21
+ # [x] dbpath が Array(複数バージョン)のときも、各バージョンの Interface が
22
+ # 独立に再生成を検知する。片方だけ再生成しても、もう片方の内容は無関係
23
+ # のまま
24
+ # [x] DB 再生成の途中(rm_rf 直後、properties が一時的に存在しない)に
25
+ # リクエストが来ても例外にならず、直前の内容を返し続ける(次のリクエスト
26
+ # で改めて判定する)
27
+ # [x] --capi 有効時は FunctionDatabase 側の再生成も反映される
28
+ class TestApp < Test::Unit::TestCase
29
+ def setup
30
+ $bitclust_context_cache = nil
31
+ @tmpdir = Dir.mktmpdir('bitclust-app-reload')
32
+ end
33
+
34
+ def teardown
35
+ FileUtils.rm_rf(@tmpdir)
36
+ $bitclust_context_cache = nil
37
+ end
38
+
39
+ def test_rack_handler_reflects_regenerated_db_without_restart
40
+ dbdir = "#{@tmpdir}/db"
41
+ srcroot = "#{@tmpdir}/src"
42
+ build_method_db(dbdir, srcroot, 'ORIGINAL_MARKER')
43
+
44
+ interface = build_app(dbdir, :rack => true).interfaces['/view/']
45
+
46
+ assert_include(rack_body(interface, '/class/Target'), 'ORIGINAL_MARKER')
47
+
48
+ regenerate(dbdir) { build_method_db(dbdir, srcroot, 'UPDATED_MARKER') }
49
+
50
+ after = rack_body(interface, '/class/Target')
51
+ assert_include(after, 'UPDATED_MARKER')
52
+ assert_not_include(after, 'ORIGINAL_MARKER')
53
+ end
54
+
55
+ def test_webrick_handler_reflects_regenerated_db_without_restart
56
+ dbdir = "#{@tmpdir}/db"
57
+ srcroot = "#{@tmpdir}/src"
58
+ build_method_db(dbdir, srcroot, 'ORIGINAL_MARKER')
59
+
60
+ servlet = build_app(dbdir).interfaces['/view/'].get_instance(WEBrick::Config::HTTP)
61
+
62
+ assert_include(webrick_body(servlet, '/class/Target'), 'ORIGINAL_MARKER')
63
+
64
+ regenerate(dbdir) { build_method_db(dbdir, srcroot, 'UPDATED_MARKER') }
65
+
66
+ after = webrick_body(servlet, '/class/Target')
67
+ assert_include(after, 'UPDATED_MARKER')
68
+ assert_not_include(after, 'ORIGINAL_MARKER')
69
+ end
70
+
71
+ def test_each_version_reloads_independently_for_array_dbpath
72
+ dbdir_30 = "#{@tmpdir}/db-3.0"
73
+ dbdir_40 = "#{@tmpdir}/db-4.0"
74
+ src_30 = "#{@tmpdir}/src-3.0"
75
+ src_40 = "#{@tmpdir}/src-4.0"
76
+ build_method_db(dbdir_30, src_30, 'V30_ORIGINAL')
77
+ build_method_db(dbdir_40, src_40, 'V40_ORIGINAL')
78
+
79
+ datadir = File.expand_path('../data/bitclust', __dir__)
80
+ app = BitClust::App.new(
81
+ :dbpath => [dbdir_30, dbdir_40],
82
+ :rack => true,
83
+ :baseurl => '',
84
+ :capi => false,
85
+ :encoding => 'utf-8',
86
+ :datadir => datadir,
87
+ :templatedir => "#{datadir}/template.offline"
88
+ )
89
+ interface_30 = app.interfaces['3.0']
90
+ interface_40 = app.interfaces['4.0']
91
+
92
+ assert_include(rack_body(interface_30, '/class/Target'), 'V30_ORIGINAL')
93
+ assert_include(rack_body(interface_40, '/class/Target'), 'V40_ORIGINAL')
94
+
95
+ regenerate(dbdir_30) { build_method_db(dbdir_30, src_30, 'V30_UPDATED') }
96
+
97
+ v30_after = rack_body(interface_30, '/class/Target')
98
+ assert_include(v30_after, 'V30_UPDATED')
99
+ assert_not_include(v30_after, 'V30_ORIGINAL')
100
+ # 触っていない 4.0 側は無関係のまま
101
+ assert_include(rack_body(interface_40, '/class/Target'), 'V40_ORIGINAL')
102
+ end
103
+
104
+ def test_stale_content_served_without_raising_while_properties_missing
105
+ dbdir = "#{@tmpdir}/db"
106
+ srcroot = "#{@tmpdir}/src"
107
+ build_method_db(dbdir, srcroot, 'ORIGINAL_MARKER')
108
+
109
+ interface = build_app(dbdir, :rack => true).interfaces['/view/']
110
+ assert_include(rack_body(interface, '/class/Target'), 'ORIGINAL_MARKER')
111
+
112
+ # rake generate の rm_rf 直後(init/update が終わる前)を再現する: properties
113
+ # が一時的に存在しない
114
+ FileUtils.rm_rf(dbdir)
115
+
116
+ assert_nothing_raised do
117
+ during = rack_body(interface, '/class/Target')
118
+ assert_include(during, 'ORIGINAL_MARKER')
119
+ end
120
+
121
+ regenerate(dbdir) { build_method_db(dbdir, srcroot, 'UPDATED_MARKER') }
122
+
123
+ assert_include(rack_body(interface, '/class/Target'), 'UPDATED_MARKER')
124
+ end
125
+
126
+ def test_capi_function_database_also_reloads
127
+ dbdir = "#{@tmpdir}/db"
128
+ srcroot = "#{@tmpdir}/src"
129
+ build_method_db(dbdir, srcroot, 'ORIGINAL_MARKER')
130
+ build_function(dbdir, 'This is the original function doc.')
131
+
132
+ interface = build_app(dbdir, :rack => true, :capi => true).interfaces['/view/']
133
+
134
+ assert_include(rack_body(interface, '/function/public_func'), 'the original function doc')
135
+
136
+ regenerate(dbdir) do
137
+ build_method_db(dbdir, srcroot, 'UPDATED_MARKER')
138
+ build_function(dbdir, 'This is the updated function doc.')
139
+ end
140
+
141
+ after = rack_body(interface, '/function/public_func')
142
+ assert_include(after, 'the updated function doc')
143
+ assert_not_include(after, 'the original function doc')
144
+ end
145
+
146
+ # rake generate の途中(init 後〜update 完了前)にリクエストが来ると、
147
+ # init 時点の properties mtime で途中状態の DB を掴んで再構築してしまう。
148
+ # update 完了(transaction の commit)時に properties の mtime が進むこと
149
+ # により、次のリクエストで完成した DB を読み直して収束すること。
150
+ # (utime による人工的な mtime 操作をせず、commit 側の touch だけに頼る)
151
+ def test_request_during_regeneration_converges_after_update_completes
152
+ dbdir = "#{@tmpdir}/db"
153
+ srcroot = "#{@tmpdir}/src"
154
+ build_method_db(dbdir, srcroot, 'ORIGINAL_MARKER')
155
+ interface = build_app(dbdir, :rack => true).interfaces['/view/']
156
+ assert_include(rack_body(interface, '/class/Target'), 'ORIGINAL_MARKER')
157
+
158
+ # 再生成の途中状態を再現: rm_rf → init+propset まで(update はまだ)
159
+ FileUtils.rm_rf(dbdir)
160
+ db = BitClust::MethodDatabase.new(dbdir)
161
+ db.init
162
+ db.transaction do
163
+ db.propset('version', '3.4')
164
+ db.propset('encoding', 'utf-8')
165
+ end
166
+ # 途中状態のリクエストは(内容はともかく)例外にならないこと
167
+ assert_nothing_raised { rack_body(interface, '/class/Target') }
168
+
169
+ # update 完了 → 次のリクエストで新しい内容に収束すること
170
+ File.write("#{srcroot}/testlib.rd", <<~RD)
171
+ = class Target < Object
172
+ UPDATED_MARKER
173
+
174
+ == Instance Methods
175
+ --- foo
176
+ foo body
177
+ RD
178
+ db.transaction do
179
+ db.update_by_stdlibtree(srcroot)
180
+ end
181
+ after = rack_body(interface, '/class/Target')
182
+ assert_include(after, 'UPDATED_MARKER')
183
+ assert_not_include(after, 'ORIGINAL_MARKER')
184
+ end
185
+
186
+ private
187
+
188
+ # Screen#run_template は互換 ERB を self.class(ClassScreen 等)にキャッシュ
189
+ # するため、同一プロセス内で異なる templatedir を混ぜて使うと「最初に
190
+ # コンパイルされた方が以後ずっと勝つ」(test_method_screen.rb 冒頭のコメント
191
+ # 参照)。このテストファイルはスイート内の他の Screen 系テストと同じ
192
+ # template.offline に統一し、新たな衝突を持ち込まない
193
+ def build_app(dbdir, extra = {})
194
+ datadir = File.expand_path('../data/bitclust', __dir__)
195
+ BitClust::App.new({
196
+ :dbpath => dbdir,
197
+ :viewpath => '/view/',
198
+ :baseurl => '',
199
+ :capi => false,
200
+ :encoding => 'utf-8',
201
+ :datadir => datadir,
202
+ :templatedir => "#{datadir}/template.offline"
203
+ }.merge(extra))
204
+ end
205
+
206
+ def rack_body(interface, path)
207
+ env = Rack::MockRequest.env_for(path)
208
+ _status, _headers, body = interface.call(env)
209
+ body.join
210
+ end
211
+
212
+ def webrick_body(servlet, path)
213
+ config = WEBrick::Config::HTTP
214
+ wreq = WEBrick::HTTPRequest.new(config)
215
+ wreq.parse(StringIO.new("GET #{path} HTTP/1.1\r\nHost: example.com\r\n\r\n"))
216
+ wres = WEBrick::HTTPResponse.new(config)
217
+ servlet.do_GET(wreq, wres)
218
+ wres.body
219
+ end
220
+
221
+ # DB 再生成の前後で properties の mtime が必ず変わるようにする。
222
+ # ファイルシステムの mtime 解像度(秒単位のものもある)に依存しないよう、
223
+ # 再生成後に明示的に前より先の時刻へ File.utime で進める
224
+ def regenerate(dbdir)
225
+ properties_path = "#{dbdir}/properties"
226
+ before = File.exist?(properties_path) ? File.mtime(properties_path) : nil
227
+ yield
228
+ forced = (before || Time.now) + 5
229
+ File.utime(forced, forced, properties_path)
230
+ end
231
+
232
+ def build_method_db(dbdir, srcroot, marker)
233
+ FileUtils.mkdir_p(srcroot)
234
+ File.write("#{srcroot}/LIBRARIES", "testlib\n")
235
+ File.write("#{srcroot}/testlib.rd", <<~RD)
236
+ = class Target < Object
237
+ #{marker}
238
+
239
+ == Instance Methods
240
+ --- foo
241
+ foo body
242
+ RD
243
+ db = BitClust::MethodDatabase.new(dbdir)
244
+ db.init
245
+ db.transaction do
246
+ db.propset('version', '3.4')
247
+ db.propset('encoding', 'utf-8')
248
+ end
249
+ db.transaction do
250
+ db.update_by_stdlibtree(srcroot)
251
+ end
252
+ db
253
+ end
254
+
255
+ def build_function(dbdir, doc)
256
+ FileUtils.mkdir_p("#{dbdir}/function")
257
+ File.write("#{dbdir}/function/public_func", <<~HERE)
258
+ filename=test.c
259
+ macro=false
260
+ private=false
261
+ type=VALUE
262
+ name=public_func
263
+ params=()
264
+
265
+
266
+ #{doc}
267
+ HERE
268
+ end
269
+ end
@@ -73,9 +73,11 @@ This is public function.
73
73
  <dt>header</dt><dd>VALUE public_func()</dd>
74
74
  <dt>filename</dt><dd>test.c</dd>
75
75
  </dl>
76
+ <dd class="function-description">
76
77
  <p>
77
78
  This is public function.
78
79
  </p>
80
+ </dd>
79
81
  EOS
80
82
  end
81
83
  end