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,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/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
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'test/unit'
4
+
5
+ require 'bitclust/capi_converter'
6
+ require 'bitclust/markdown_to_rrd'
7
+
8
+ # CapiConverter: refm/capi(C API リファレンス)の md 変換。
9
+ # 構造は「--- シグネチャ + 本文」の列のみ(見出し・include・emlist なし、
10
+ # library 等のクロスファイル情報も無い)。front matter 不使用。
11
+ #
12
+ # テストリスト:
13
+ # [x] シグネチャ: --- <C sig> ↔ ### <C sig>(def 等のキーワードは付けない)
14
+ # [x] MACRO / static プレフィクスも素通し
15
+ # [x] 本文の [[f:...]] 参照 ↔ [f:...]
16
+ # [x] インデントコード例(api と同じフェンス化)
17
+ # [x] #@since/#@until ディレクティブのパススルー
18
+ # [x] 定義リスト・ネストリスト(api と同じ変換)
19
+ # [x] convert(rrd) を md→rd すると元に戻る(roundtrip)
20
+ class TestCapiConverter < Test::Unit::TestCase
21
+ def convert(rrd)
22
+ BitClust::CapiConverter.convert(rrd)
23
+ end
24
+
25
+ def roundtrip(rrd)
26
+ BitClust::MarkdownToRRD.convert(convert(rrd), capi: true)
27
+ end
28
+
29
+ def test_signature
30
+ assert_equal "### VALUE rb_ary_new()\n\n空の Ruby の配列を作成し返します。\n",
31
+ convert("--- VALUE rb_ary_new()\n\n空の Ruby の配列を作成し返します。\n")
32
+ end
33
+
34
+ def test_macro_and_static_signatures
35
+ assert_equal "### MACRO type* ALLOC(type)\n", convert("--- MACRO type* ALLOC(type)\n")
36
+ assert_equal "### static VALUE assign(VALUE self, NODE *lhs)\n",
37
+ convert("--- static VALUE assign(VALUE self, NODE *lhs)\n")
38
+ end
39
+
40
+ def test_function_reference
41
+ assert_equal "[f:SPECIAL_CONST_P](obj) が真のとき落ちます。\n",
42
+ convert("[[f:SPECIAL_CONST_P]](obj) が真のとき落ちます。\n")
43
+ end
44
+
45
+ def test_indented_code_block
46
+ # フェンス個数はベースインデント幅をエンコードする(3+4=7個。api と同仕様)
47
+ rrd = "使用例\n\n VALUE ary;\n ary = rb_ary_new();\n"
48
+ md = convert(rrd)
49
+ assert_match(/^`{7}\nVALUE ary;\nary = rb_ary_new\(\);\n`{7}$/, md)
50
+ assert_equal rrd, roundtrip(rrd)
51
+ end
52
+
53
+ def test_directives_pass_through
54
+ rrd = "\#@until 2.2.0\n--- void Check_SafeStr(VALUE v)\n\n古い API です。\n\#@end\n"
55
+ assert_equal "\#@until 2.2.0\n### void Check_SafeStr(VALUE v)\n\n古い API です。\n\#@end\n",
56
+ convert(rrd)
57
+ end
58
+
59
+ def test_roundtrip_composite
60
+ rrd = <<~RRD
61
+ --- MACRO int BUILTIN_TYPE(VALUE obj)
62
+
63
+ obj の構造体型 ID を返します。
64
+ [[f:SPECIAL_CONST_P]](obj) が真のオブジェクトに対して使うと落ちます。
65
+
66
+ : argcが0以上の時
67
+ その関数がとる引数の数を意味します。
68
+
69
+ * 必須引数の数 (省略可能な引数があるなら省略不可)
70
+ * 省略可能な引数の数 (ゼロ個ならば省略可)
71
+
72
+ --- VALUE rb_ary_new2(long len)
73
+
74
+ 使用例
75
+
76
+ VALUE ary;
77
+ ary = rb_ary_new2(len);
78
+ RRD
79
+ assert_equal rrd, roundtrip(rrd)
80
+ end
81
+
82
+ def test_body_text_starting_with_require_is_not_metadata
83
+ # eval.c.rd の rb_require 型: 「require の C 版です。」のような本文が
84
+ # フラグメント先頭に来ても library メタデータと誤認しない
85
+ # (capi には require/category ヘッダは存在しない)
86
+ rrd = "\nrequire の C 版です。feature「fname」をロードします。\n"
87
+ md = BitClust::CapiConverter.convert(rrd)
88
+ assert_equal rrd, md
89
+ assert_equal rrd, roundtrip(rrd)
90
+ end
91
+ end