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,375 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BitClust
4
+ # doctree の #@include グラフを版ゲート付きで解析する。
5
+ #
6
+ # LIBRARIES の各エントリ <name> をルートファイル <name>.rd として、
7
+ # #@include(target) を include 元ディレクトリ相対で解決しながら再帰的に走査し、
8
+ # 各ファイルの分類(grouping = エンティティ / fragment = 共有断片)と、
9
+ # grouping ファイルの全所属(ライブラリ + 経路上の版条件)を生のまま収集する。
10
+ #
11
+ # 特定の版範囲([3.0, 4.2) 等)への絞り込みは行わない faithful 層。
12
+ # スコープ適用は IncludeGraph::Scope が担い、範囲はパラメータ化されている
13
+ # (旧版サルベージ時に別範囲で再利用するため)。
14
+ class IncludeGraph
15
+ # 版条件。kind は :since / :until / :if。
16
+ # version は :since/:until ではバージョン文字列、:if では条件式文字列。
17
+ Condition = Struct.new(:kind, :version)
18
+
19
+ # grouping ファイルの1所属。conditions は LIBRARIES のゲート+include 経路上の
20
+ # 条件スタックのスナップショット(faithful、スコープ未適用)。
21
+ Membership = Struct.new(:library, :conditions)
22
+
23
+ # RRDParser は /\A=[^=]/ で H1 行を認識し `=` 直後の空白は必須でない
24
+ # (実データ: _builtin/Encoding は「=class Encoding」)
25
+ ENTITY_H1_RE = /\A=(?!=)\s*(?:class|module|object|reopen|redefine)\b/
26
+
27
+ def self.analyze(src_root)
28
+ new(src_root).analyze
29
+ end
30
+
31
+ # conditions が表す版区間 [lo, hi)。lo/hi は Gem::Version、制約なしは nil。
32
+ # :since は max、:until は min を取る。
33
+ def self.interval(conditions)
34
+ lo, hi = bounds(conditions)
35
+ [lo&.first, hi&.first]
36
+ end
37
+
38
+ # conditions から下限・上限を [Gem::Version, 原文字列] の組で集める。
39
+ # :if の連言(ubygems の "1.9.1" <= version and version < "2.5.0" 等)も
40
+ # 分解して境界に寄与させる。分解できない連言子は寄与しない(保守的に広く扱う。
41
+ # 恒偽の証明は Scope#never? が連言子単位で別途行う)
42
+ def self.bounds(conditions)
43
+ sinces = [] #: Array[bound]
44
+ untils = [] #: Array[bound]
45
+ conditions.each do |c|
46
+ case c.kind
47
+ when :since then sinces << [Gem::Version.new(c.version), c.version]
48
+ when :until then untils << [Gem::Version.new(c.version), c.version]
49
+ when :if
50
+ if_bounds(c.version).each do |kind, v|
51
+ (kind == :since ? sinces : untils) << [Gem::Version.new(v), v]
52
+ end
53
+ end
54
+ end
55
+ [sinces.max_by(&:first), untils.min_by(&:first)]
56
+ end
57
+
58
+ # 連言(and 区切り)の #@if 条件式を since/until 境界に分解する。
59
+ # 対応形: "X" <= version / version >= "X" → since X、version < "X" → until X。
60
+ # <= や否定(#@else 反転)は正確に表現できないので分解しない
61
+ def self.if_bounds(expr)
62
+ return [] if expr.lstrip.start_with?('!')
63
+ bounds = [] #: Array[[Symbol, String?]]
64
+ expr.split(/\band\b/).each do |c|
65
+ case c
66
+ when /version\s*<\s*"([\d.]+)"/ then bounds << [:until, $1]
67
+ when /version\s*>=\s*"([\d.]+)"/, /"([\d.]+)"\s*<=\s*version/ then bounds << [:since, $1]
68
+ end
69
+ end
70
+ bounds
71
+ end
72
+
73
+ # 対象版範囲 [lo, hi)。範囲はパラメータであり、[3.0, 4.2) 以外
74
+ # (旧版サルベージ等)でも同じ解析結果に対して再利用できる。
75
+ class Scope
76
+ attr_reader :lo, :hi
77
+
78
+ def initialize(lo, hi)
79
+ @lo = Gem::Version.new(lo)
80
+ @hi = Gem::Version.new(hi)
81
+ end
82
+
83
+ # conditions の版区間がスコープと交差するか。
84
+ # 恒偽と証明できる :if 条件が含まれる場合も交差しない
85
+ def cover?(conditions)
86
+ return false if conditions.any? { |c| c.kind == :if && never?(c) }
87
+ lo, hi = IncludeGraph.interval(conditions)
88
+ return false if lo && hi && lo >= hi # 空区間
89
+ (lo.nil? || lo < @hi) && (hi.nil? || hi > @lo)
90
+ end
91
+
92
+ # 条件がスコープ内の全バージョンで真か(:if・不正な版文字列は判定不能なので false)
93
+ def always?(cond)
94
+ case cond.kind
95
+ when :since then Gem::Version.new(cond.version) <= @lo
96
+ when :until then Gem::Version.new(cond.version) >= @hi
97
+ else false
98
+ end
99
+ rescue ArgumentError
100
+ false
101
+ end
102
+
103
+ # 条件がスコープ内の全バージョンで偽か(不正な版文字列は判定不能なので false)。
104
+ # :if は連言子(and 区切り)のいずれかが恒偽と証明できる場合のみ true
105
+ # (LIBRARIES の #@if("1.9.1" <= version and version < "2.5.0") 等)
106
+ def never?(cond)
107
+ case cond.kind
108
+ when :since then Gem::Version.new(cond.version) >= @hi
109
+ when :until then Gem::Version.new(cond.version) <= @lo
110
+ when :if
111
+ cond.version.split(/\band\b/).any? { |c| never_conjunct?(c) }
112
+ else false
113
+ end
114
+ rescue ArgumentError
115
+ false
116
+ end
117
+
118
+ # #@if の連言子が恒偽と証明できるか。
119
+ # 対応形: version < "X" / version <= "X" / version >= "X" /
120
+ # "X" <= version / version == "X"(判定できない形は false)
121
+ def never_conjunct?(conjunct)
122
+ case conjunct
123
+ when /version\s*<=?\s*"([\d.]+)"/
124
+ op_lt = !conjunct.include?('<=')
125
+ v = Gem::Version.new($1)
126
+ op_lt ? v <= @lo : v < @lo
127
+ when /version\s*>=\s*"([\d.]+)"/, /"([\d.]+)"\s*<=\s*version/
128
+ Gem::Version.new($1) >= @hi
129
+ when /version\s*==\s*"([\d.]+)"/
130
+ v = Gem::Version.new($1)
131
+ v < @lo || v >= @hi
132
+ else
133
+ false
134
+ end
135
+ rescue ArgumentError
136
+ false
137
+ end
138
+
139
+ # front matter に書く構造ゲート。スコープ内で効く境界のみ残す
140
+ # (下限以下の since・上限以上の until は省略)。スコープ外なら nil。
141
+ # バージョン文字列は原文の表記を保持する。
142
+ def gate(conditions)
143
+ return nil unless cover?(conditions)
144
+ lo, hi = IncludeGraph.bounds(conditions)
145
+ gate = {} #: IncludeGraph::gate
146
+ gate[:since] = lo[1] if lo && lo[0] > @lo
147
+ gate[:until] = hi[1] if hi && hi[0] < @hi
148
+ gate
149
+ end
150
+ end
151
+
152
+ attr_reader :warnings
153
+
154
+ def initialize(src_root)
155
+ @src_root = src_root
156
+ @memberships = {} # relpath => [Membership]
157
+ @kinds = {} # relpath => :grouping | :fragment
158
+ @grouping_sites = {} # relpath => [include target(記載どおりの文字列)]
159
+ @library_gates = {} # library name => [Condition](LIBRARIES 由来)
160
+ @warnings = []
161
+ end
162
+
163
+ def analyze
164
+ read_libraries.each do |name, gate|
165
+ root = "#{name}.rd"
166
+ unless File.file?(File.join(@src_root, root))
167
+ @warnings << "library root not found: #{root}"
168
+ next
169
+ end
170
+ @library_gates[name] = gate
171
+ walk(root, name, gate, [root])
172
+ end
173
+ self
174
+ end
175
+
176
+ # grouping ファイル relpath の全 membership(生・スコープ未適用)
177
+ def memberships(relpath)
178
+ @memberships.fetch(relpath, [])
179
+ end
180
+
181
+ def groupings
182
+ @kinds.select { |_, kind| kind == :grouping }.keys.sort
183
+ .to_h { |path| [path, @memberships.fetch(path, [])] }
184
+ end
185
+
186
+ def fragments
187
+ @kinds.select { |_, kind| kind == :fragment }.keys.sort
188
+ end
189
+
190
+ # grouping include のサイト一覧(prune 対象)。
191
+ # { relpath => [#@include に記載どおりの target 文字列] }
192
+ # fragment include は含まない(transclusion として温存する)。
193
+ def grouping_include_sites
194
+ @grouping_sites
195
+ end
196
+
197
+ # 各ライブラリ概要ファイル(LIBRARIES のルート)へ注入する front matter。
198
+ # { relpath => { "type" => "library", "since" => v, "until" => v } }
199
+ # LIBRARIES 内の版ゲート(fiber: until 3.1 等)をスコープ適用して付与する。
200
+ # スコープ外のライブラリ(cmath/scanf/sync 等)は含まない。
201
+ def library_front_matter_map(scope)
202
+ result = {} #: Hash[String, Hash[String, String]]
203
+ @library_gates.each do |name, gate|
204
+ root = "#{name}.rd"
205
+ scoped = scope.gate(gate)
206
+ next unless scoped
207
+ fm = { 'type' => 'library' }
208
+ fm['since'] = scoped[:since] if scoped[:since]
209
+ fm['until'] = scoped[:until] if scoped[:until]
210
+ result[root] = fm
211
+ end
212
+ result
213
+ end
214
+
215
+ # 各 grouping メンバーへ注入する front matter(スコープ適用済み)。
216
+ # 単一所属: { relpath => { "library" => name, "since" => v, "until" => v } }
217
+ # 複数所属: { relpath => { "library" => [{ "name" => n, "since" => v,
218
+ # "until" => v }, ...], ... } }
219
+ # RRDToMarkdown の extra_front_matter: にそのまま渡せる形。
220
+ #
221
+ # - スコープ外のメンバーは含まない(旧版サルベージは別スコープで再実行)
222
+ # - 複数ライブラリへの所属(旧 rdoc の同時所属、thread 系の版切替所属)は
223
+ # ゲート付きリストで表現する(MARKUP_SPEC §1.2)。並び順は
224
+ # 「現在まで存在する側(until なし)→ 名前順」で決定的にする。
225
+ # このときトップレベルの since/until はライブラリ横断の hull
226
+ # (エンティティ自体の存在ゲート)
227
+ # - 同一ライブラリ内の複数 include サイトは、いずれかが有効なら
228
+ # エンティティが存在するため、ゲートは区間の hull(弱い方)を取る
229
+ def front_matter_map(scope)
230
+ result = {} #: Hash[String, front_matter]
231
+ groupings.each do |path, ms|
232
+ covered = ms.select { |m| scope.cover?(m.conditions) }
233
+ next if covered.empty?
234
+ libraries = covered.map(&:library).uniq
235
+ per_lib = libraries.to_h { |lib|
236
+ [lib, hull(covered.select { |m| m.library == lib }
237
+ .map { |m| scope.gate(m.conditions) })]
238
+ }
239
+ if libraries.size == 1
240
+ fm = { 'library' => libraries.first }
241
+ gate = per_lib.values.first
242
+ else
243
+ entries = per_lib.map { |lib, g|
244
+ e = { 'name' => lib }
245
+ e['since'] = g[:since] if g[:since]
246
+ e['until'] = g[:until] if g[:until]
247
+ e
248
+ }.sort_by { |e| [e['until'] ? 1 : 0, e['name']] }
249
+ fm = { 'library' => entries }
250
+ gate = hull(per_lib.values)
251
+ end
252
+ fm['since'] = gate[:since] if gate[:since]
253
+ fm['until'] = gate[:until] if gate[:until]
254
+ result[path] = fm
255
+ end
256
+ result
257
+ end
258
+
259
+ private
260
+
261
+ # 複数サイトのゲートの hull。片方でも無条件({})なら無条件。
262
+ # since は最小、until は最大を取る(全サイトに揃っている境界のみ残る)。
263
+ def hull(gates)
264
+ return {} if gates.any?(&:empty?)
265
+ sinces = gates.map { |g| g[:since] }
266
+ untils = gates.map { |g| g[:until] }
267
+ result = {} #: gate
268
+ result[:since] = sinces.min_by { |v| Gem::Version.new(v) } if sinces.all?
269
+ result[:until] = untils.max_by { |v| Gem::Version.new(v) } if untils.all?
270
+ result
271
+ end
272
+
273
+ # LIBRARIES を版ゲート付きで読む。 [[name, [Condition]], ...]
274
+ def read_libraries
275
+ entries = {} #: Hash[String, Array[Condition]]
276
+ stack = [] #: Array[Condition]
277
+ File.foreach(File.join(@src_root, 'LIBRARIES')) do |line|
278
+ line = line.chomp
279
+ if line.start_with?('#@#') || apply_directive(stack, line)
280
+ next
281
+ elsif line !~ /\A\s*\z/
282
+ entries[line] ||= stack.dup
283
+ end
284
+ end
285
+ entries
286
+ end
287
+
288
+ # #@ ディレクティブなら条件スタックを更新して true を返す。
289
+ # #@samplecode もブロック(#@end で閉じる)なので、pop の対応を
290
+ # 取るために :samplecode を積む(版条件ではないので snapshot では除外する)。
291
+ def apply_directive(stack, line)
292
+ case line
293
+ when /\A\#@since\s+(\S+)/ then stack.push(Condition.new(:since, $1))
294
+ when /\A\#@until\s+(\S+)/ then stack.push(Condition.new(:until, $1))
295
+ when /\A\#@if\s*(.*)/ then stack.push(Condition.new(:if, ($1 || raise).strip))
296
+ when /\A\#@samplecode\b/ then stack.push(Condition.new(:samplecode, nil))
297
+ when /\A\#@else\b/ then (cond = stack.pop) && stack.push(invert(cond))
298
+ when /\A\#@end\b/ then stack.pop
299
+ else return false
300
+ end
301
+ true
302
+ end
303
+
304
+ def invert(cond)
305
+ case cond.kind
306
+ when :since then Condition.new(:until, cond.version)
307
+ when :until then Condition.new(:since, cond.version)
308
+ when :if then Condition.new(:if, "!(#{cond.version})")
309
+ else cond
310
+ end
311
+ end
312
+
313
+ # relpath のファイル内の #@include を条件スタック付きで走査する。
314
+ # base_conditions は LIBRARIES ゲート+ここまでの include 経路の条件。
315
+ def walk(relpath, library, base_conditions, path_stack)
316
+ stack = [] #: Array[Condition]
317
+ File.foreach(File.join(@src_root, relpath)) do |line|
318
+ line = line.chomp
319
+ if line =~ /\A\#@include\s*\((.*?)\)/
320
+ conditions = (base_conditions + stack).reject { |c| c.kind == :samplecode }
321
+ add_include(relpath, $1 || raise, library, conditions, path_stack)
322
+ else
323
+ apply_directive(stack, line)
324
+ end
325
+ end
326
+ end
327
+
328
+ def add_include(from, target, library, conditions, path_stack)
329
+ relpath = resolve(from, target)
330
+ unless relpath
331
+ @warnings << "include target not found: #{target} (from #{from})"
332
+ return
333
+ end
334
+ kind = classify(relpath)
335
+ @kinds[relpath] ||= kind
336
+
337
+ if kind == :grouping
338
+ m = Membership.new(library, conditions)
339
+ list = (@memberships[relpath] ||= [])
340
+ list << m unless list.include?(m)
341
+ sites = (@grouping_sites[from] ||= [])
342
+ sites << target unless sites.include?(target)
343
+ end
344
+
345
+ if path_stack.include?(relpath)
346
+ @warnings << "include cycle: #{(path_stack + [relpath]).join(' -> ')}"
347
+ return
348
+ end
349
+ # fragment も走査する: fragment を経由して grouping へ至る transclusion
350
+ # チェーンがある(fiddle.rd → fiddle/2.0/fiddle.lib.rd → Fiddle 等)
351
+ walk(relpath, library, conditions, path_stack + [relpath])
352
+ end
353
+
354
+ # include 元ディレクトリ相対で target → target.rd の順に解決。
355
+ # `../` を含む参照があるため正規化する(同一ファイルの二重登録防止)
356
+ def resolve(from, target)
357
+ base = File.dirname(from)
358
+ [target, "#{target}.rd"].each do |cand|
359
+ rel = base == '.' ? cand : File.join(base, cand)
360
+ rel = File.expand_path(rel, '/').delete_prefix('/')
361
+ return rel if File.file?(File.join(@src_root, rel))
362
+ end
363
+ nil
364
+ end
365
+
366
+ # 最初の非空・非 #@ 行がエンティティ H1 なら grouping、そうでなければ fragment
367
+ def classify(relpath)
368
+ File.foreach(File.join(@src_root, relpath)) do |line|
369
+ next if line =~ /\A\s*\z/ || line.start_with?('#@')
370
+ return line =~ ENTITY_H1_RE ? :grouping : :fragment
371
+ end
372
+ :fragment
373
+ end
374
+ end
375
+ end
@@ -0,0 +1,144 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BitClust
4
+ # RD テキストから指定 target の #@include 行を除去する純変換(rd→rd)。
5
+ #
6
+ # 新パイプラインでは grouping include(エンティティの取り込み)は
7
+ # glob + front matter による発見に置き換わるため、ライブラリ概要ファイル等から
8
+ # 除去する(二重取り込み防止)。fragment include(共有断片の transclusion)は
9
+ # target に含めなければそのまま残る。
10
+ #
11
+ # 除去に伴う後始末:
12
+ # - 除去で空になった版ゲートブロック(#@since/#@until/#@if ... #@end、
13
+ # #@else の両枝が空の場合を含む)はブロックごと除去する
14
+ # - 除去痕の前後が空行同士なら1つに畳む。ファイル先頭に残る空行は削る
15
+ # (末尾の空行は保持する: メタデータ領域再生成の空行仕様と整合させるため)
16
+ # - もともと空だったブロックや、target を含まない入力はバイト単位で変更しない
17
+ module IncludePruner
18
+ INCLUDE_RE = /\A\#@include\s*\((.*?)\)/
19
+ GATE_OPEN_RE = /\A\#@(?:since|until|if)\b/
20
+ CODE_OPEN_RE = /\A\#@samplecode\b/
21
+ ELSE_RE = /\A\#@else\b/
22
+ END_RE = /\A\#@end\b/
23
+ BLANK_RE = /\A\s*\z/
24
+
25
+ # ブロック除去・空行畳み込みの位置を示す番兵
26
+ REMOVED = Object.new.freeze
27
+ private_constant :REMOVED
28
+
29
+ Block = Struct.new(:open, :body, :else_line, :else_body, :end_line, :gate)
30
+ private_constant :Block
31
+
32
+ module_function
33
+
34
+ def prune(src, targets)
35
+ return src if targets.empty?
36
+ lookup = {} #: Hash[String?, bool]
37
+ targets.each { |t| lookup[t] = true }
38
+
39
+ lines = src.lines
40
+ nodes = catch(:unbalanced) { parse(lines) }
41
+ return src if nodes.nil?
42
+
43
+ stream = [] #: stream
44
+ changed = emit(nodes, lookup, stream)
45
+ return src unless changed
46
+ tidy(stream).join
47
+ end
48
+
49
+ # lines をブロック構造にパースする。対応の取れないファイルは throw で nil を返す
50
+ def parse(lines)
51
+ nodes, i = parse_nodes(lines, 0)
52
+ throw :unbalanced, nil if i < lines.length # トップレベルに #@else / #@end
53
+ nodes
54
+ end
55
+
56
+ def parse_nodes(lines, i)
57
+ nodes = [] #: Array[node]
58
+ while i < lines.length
59
+ line = lines[i]
60
+ if line =~ ELSE_RE || line =~ END_RE
61
+ return [nodes, i]
62
+ elsif line =~ GATE_OPEN_RE || line =~ CODE_OPEN_RE
63
+ gate = line !~ CODE_OPEN_RE
64
+ body, j = parse_nodes(lines, i + 1)
65
+ else_line = nil
66
+ else_body = [] #: Array[node]
67
+ if j < lines.length && lines[j] =~ ELSE_RE
68
+ else_line = lines[j]
69
+ else_body, j = parse_nodes(lines, j + 1)
70
+ end
71
+ throw :unbalanced, nil unless j < lines.length && lines[j] =~ END_RE
72
+ nodes << Block.new(line, body, else_line, else_body, lines[j], gate)
73
+ i = j + 1
74
+ else
75
+ nodes << line
76
+ i += 1
77
+ end
78
+ end
79
+ [nodes, i]
80
+ end
81
+
82
+ # nodes を stream(行と REMOVED 番兵の列)へ展開し、変更の有無を返す
83
+ def emit(nodes, lookup, stream)
84
+ changed = false
85
+ nodes.each do |node|
86
+ if node.is_a?(Block)
87
+ changed |= emit_block(node, lookup, stream)
88
+ elsif node =~ INCLUDE_RE && lookup[$1]
89
+ stream << REMOVED
90
+ changed = true
91
+ else
92
+ stream << node
93
+ end
94
+ end
95
+ changed
96
+ end
97
+
98
+ def emit_block(block, lookup, stream)
99
+ body = [] #: stream
100
+ body_changed = emit(block.body, lookup, body)
101
+ else_body = [] #: stream
102
+ else_changed = block.else_line ? emit(block.else_body, lookup, else_body) : false
103
+ changed = body_changed || else_changed
104
+
105
+ if block.gate && changed && blank_only?(body) && blank_only?(else_body)
106
+ stream << REMOVED
107
+ return true
108
+ end
109
+
110
+ stream << block.open
111
+ stream.concat(body)
112
+ if block.else_line
113
+ stream << block.else_line
114
+ stream.concat(else_body)
115
+ end
116
+ stream << block.end_line
117
+ changed
118
+ end
119
+
120
+ def blank_only?(stream)
121
+ stream.all? { |item| item.equal?(REMOVED) || item =~ BLANK_RE }
122
+ end
123
+
124
+ # REMOVED 番兵を取り除きつつ、除去痕の前後が空行同士なら1つに畳み、
125
+ # 先頭に残った空行を削る
126
+ def tidy(stream)
127
+ result = [] #: Array[String]
128
+ pending = false
129
+ stream.each do |item|
130
+ if item.equal?(REMOVED)
131
+ pending = true
132
+ next
133
+ end
134
+ if pending && item =~ BLANK_RE && (result.empty? || result.last =~ BLANK_RE)
135
+ pending = false
136
+ next
137
+ end
138
+ pending = false
139
+ result << item
140
+ end
141
+ result
142
+ end
143
+ end
144
+ end
@@ -83,7 +83,7 @@ module BitClust
83
83
 
84
84
  alias do_POST do_GET
85
85
  end
86
-
86
+
87
87
  end
88
88
 
89
89
  end
@@ -16,8 +16,6 @@ module BitClust
16
16
  # Entry for libraries ("_builtin", "yaml", etc.)
17
17
  class LibraryEntry < Entry
18
18
 
19
- include Enumerable
20
-
21
19
  def LibraryEntry.type_id
22
20
  :library
23
21
  end
@@ -68,8 +66,8 @@ module BitClust
68
66
 
69
67
  persistent_properties {
70
68
  property :requires, '[LibraryEntry]'
71
- property :classes, '[ClassEntry]' # :defined classes
72
- property :methods, '[MethodEntry]' # :added/:redefined entries
69
+ property :classes, '[ClassEntry]' ## :defined classes
70
+ property :methods, '[MethodEntry]' ## :added/:redefined entries
73
71
  property :source, 'String'
74
72
  property :sublibraries, '[LibraryEntry]'
75
73
  property :is_sublibrary, 'bool'
@@ -82,7 +80,7 @@ module BitClust
82
80
  end
83
81
 
84
82
  def description
85
- source.split(/\n\n+/, 2)[0]
83
+ description_text(source.split(/\n\n+/, 2)[0])
86
84
  end
87
85
 
88
86
  def check_link(path = [])
@@ -108,7 +106,7 @@ module BitClust
108
106
  end
109
107
 
110
108
  def all_classes
111
- return @all_classes if @all_classes
109
+ return (@all_classes || raise) if @all_classes
112
110
  required_classes = (sublibraries & requires).map{|l| l.classes }.flatten
113
111
  @all_classes = (classes() + required_classes).uniq.sort
114
112
  end
@@ -160,6 +158,7 @@ module BitClust
160
158
  def classmap
161
159
  @classmap ||=
162
160
  begin
161
+ # @type var h: Hash[String, ClassEntry]
163
162
  h = {}
164
163
  classes().each do |c|
165
164
  h[c.name] = c
@@ -170,9 +169,10 @@ module BitClust
170
169
  private :classmap
171
170
 
172
171
  def fetch_methods(spec)
172
+ # @type var ms: Array[MethodEntry]
173
173
  ms = if c = get_class(spec.klass)
174
174
  then c.fetch_methods(spec)
175
- else []
175
+ else [] #: Array[MethodEntry]
176
176
  end +
177
177
  methods().select {|m| spec.match?(m) }
178
178
  if ms.empty?
@@ -197,6 +197,7 @@ module BitClust
197
197
  def methodmap
198
198
  @methodmap ||=
199
199
  begin
200
+ # @type var h: Hash[MethodEntry, MethodEntry]
200
201
  h = {}
201
202
  methods().each do |m|
202
203
  h[m] = m
@@ -36,7 +36,9 @@ class LineInput
36
36
  end
37
37
 
38
38
  def path
39
- @input.path if @input.respond_to?(:path)
39
+ # @type var input: IO
40
+ input = _ = @input
41
+ input.path if input.respond_to?(:path)
40
42
  end
41
43
 
42
44
  def name
@@ -101,7 +103,7 @@ class LineInput
101
103
  ungets line
102
104
  return nil
103
105
  end
104
- return $~[index] if index
106
+ return $~&.[](index) if index
105
107
  line
106
108
  end
107
109
 
@@ -132,7 +134,7 @@ class LineInput
132
134
  end
133
135
 
134
136
  def getlines_while(re)
135
- buf = []
137
+ buf = [] #: Array[String]
136
138
  while_match(re) do |line|
137
139
  buf.push line
138
140
  end
@@ -153,7 +155,7 @@ class LineInput
153
155
  end
154
156
 
155
157
  def getlines_until(re)
156
- buf = []
158
+ buf = [] #: Array[String]
157
159
  until_match(re) do |line|
158
160
  buf.push line
159
161
  end
@@ -171,7 +173,7 @@ class LineInput
171
173
  end
172
174
 
173
175
  def getblock(term_re)
174
- buf = []
176
+ buf = [] #: Array[String]
175
177
  until_terminator(term_re) do |line|
176
178
  buf.push line
177
179
  end