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.
- checksums.yaml +4 -4
- data/Gemfile +3 -0
- data/README.md +65 -0
- data/Rakefile +32 -0
- data/data/bitclust/catalog/ja_JP.UTF-8 +19 -1
- data/data/bitclust/searchpage/index.html +90 -0
- data/data/bitclust/template/class +69 -8
- data/data/bitclust/template/function +2 -2
- data/data/bitclust/template/layout +11 -3
- data/data/bitclust/template/method +2 -2
- data/data/bitclust/template/search +1 -1
- data/data/bitclust/template.epub/class +22 -1
- data/data/bitclust/template.epub/function +2 -2
- data/data/bitclust/template.epub/layout +1 -3
- data/data/bitclust/template.epub/method +2 -2
- data/data/bitclust/template.lillia/class +26 -5
- data/data/bitclust/template.lillia/layout +1 -3
- data/data/bitclust/template.lillia/method +2 -2
- data/data/bitclust/template.offline/class +48 -4
- data/data/bitclust/template.offline/function +2 -2
- data/data/bitclust/template.offline/layout +27 -3
- data/data/bitclust/template.offline/method +2 -2
- data/lib/bitclust/app.rb +16 -17
- data/lib/bitclust/capi_converter.rb +20 -0
- data/lib/bitclust/classentry.rb +67 -17
- data/lib/bitclust/compat.rb +2 -2
- data/lib/bitclust/completion.rb +58 -27
- data/lib/bitclust/crossrubyutils.rb +8 -5
- data/lib/bitclust/database.rb +28 -8
- data/lib/bitclust/doc_converter.rb +63 -0
- data/lib/bitclust/docentry.rb +1 -1
- data/lib/bitclust/entity_splitter.rb +221 -0
- data/lib/bitclust/entry.rb +47 -7
- data/lib/bitclust/functiondatabase.rb +13 -1
- data/lib/bitclust/functionentry.rb +5 -1
- data/lib/bitclust/functionreferenceparser.rb +4 -4
- data/lib/bitclust/generators/epub.rb +10 -3
- data/lib/bitclust/htmlutils.rb +1 -1
- data/lib/bitclust/include_graph.rb +375 -0
- data/lib/bitclust/include_pruner.rb +144 -0
- data/lib/bitclust/interface.rb +1 -1
- data/lib/bitclust/libraryentry.rb +8 -7
- data/lib/bitclust/lineinput.rb +7 -5
- data/lib/bitclust/link_checker.rb +179 -0
- data/lib/bitclust/markdown_bridge.rb +186 -0
- data/lib/bitclust/markdown_orchestrator.rb +267 -0
- data/lib/bitclust/markdown_to_rrd.rb +824 -0
- data/lib/bitclust/markdown_tree.rb +217 -0
- data/lib/bitclust/mdcompiler.rb +883 -0
- data/lib/bitclust/mdparser.rb +347 -0
- data/lib/bitclust/messagecatalog.rb +3 -3
- data/lib/bitclust/method_since_calculator.rb +117 -0
- data/lib/bitclust/methoddatabase.rb +124 -14
- data/lib/bitclust/methodentry.rb +125 -7
- data/lib/bitclust/methodid.rb +12 -11
- data/lib/bitclust/methodsignature.rb +10 -4
- data/lib/bitclust/nameutils.rb +41 -22
- data/lib/bitclust/preprocessor.rb +66 -29
- data/lib/bitclust/rdcompiler.rb +195 -95
- data/lib/bitclust/refsdatabase.rb +23 -8
- data/lib/bitclust/reloadable_request_handler.rb +72 -0
- data/lib/bitclust/requesthandler.rb +21 -16
- data/lib/bitclust/ridatabase.rb +10 -4
- data/lib/bitclust/rrd_to_markdown.rb +925 -0
- data/lib/bitclust/rrdparser.rb +196 -30
- data/lib/bitclust/runner.rb +17 -2
- data/lib/bitclust/screen.rb +80 -11
- data/lib/bitclust/search_index_generator.rb +288 -0
- data/lib/bitclust/searcher.rb +69 -20
- data/lib/bitclust/simplesearcher.rb +24 -12
- data/lib/bitclust/subcommand.rb +7 -1
- data/lib/bitclust/subcommands/ancestors_command.rb +9 -2
- data/lib/bitclust/subcommands/checklink_command.rb +54 -0
- data/lib/bitclust/subcommands/chm_command.rb +15 -7
- data/lib/bitclust/subcommands/classes_command.rb +5 -0
- data/lib/bitclust/subcommands/extract_command.rb +5 -0
- data/lib/bitclust/subcommands/htmlfile_command.rb +21 -7
- data/lib/bitclust/subcommands/init_command.rb +4 -1
- data/lib/bitclust/subcommands/list_command.rb +12 -4
- data/lib/bitclust/subcommands/lookup_command.rb +28 -9
- data/lib/bitclust/subcommands/methods_command.rb +10 -2
- data/lib/bitclust/subcommands/methodsince_command.rb +80 -0
- data/lib/bitclust/subcommands/preproc_command.rb +5 -0
- data/lib/bitclust/subcommands/property_command.rb +2 -1
- data/lib/bitclust/subcommands/searchpage_command.rb +102 -0
- data/lib/bitclust/subcommands/server_command.rb +10 -8
- data/lib/bitclust/subcommands/setup_command.rb +24 -10
- data/lib/bitclust/subcommands/statichtml_command.rb +129 -6
- data/lib/bitclust/subcommands/update_command.rb +110 -2
- data/lib/bitclust/syntax_highlighter.rb +48 -4
- data/lib/bitclust/textutils.rb +3 -3
- data/lib/bitclust/version.rb +1 -1
- data/lib/bitclust/version_badges.rb +90 -0
- data/lib/bitclust/whole_file_gate.rb +137 -0
- data/test/test_app.rb +269 -0
- data/test/test_bitclust.rb +2 -0
- data/test/test_capi_converter.rb +91 -0
- data/test/test_class_screen.rb +413 -0
- data/test/test_copy_doc_md.rb +53 -0
- data/test/test_doc_converter.rb +78 -0
- data/test/test_entity_splitter.rb +231 -0
- data/test/test_entry.rb +207 -2
- data/test/test_eol_warning.rb +43 -0
- data/test/test_functionreferenceparser.rb +11 -0
- data/test/test_include_graph.rb +640 -0
- data/test/test_include_pruner.rb +133 -0
- data/test/test_link_checker.rb +114 -0
- data/test/test_lookup_command.rb +135 -0
- data/test/test_markdown_bridge.rb +217 -0
- data/test/test_markdown_orchestrator.rb +478 -0
- data/test/test_markdown_to_rrd.rb +757 -0
- data/test/test_markdown_tree.rb +369 -0
- data/test/test_mdcompiler.rb +1276 -0
- data/test/test_mdparser.rb +538 -0
- data/test/test_method_screen.rb +166 -0
- data/test/test_method_since_calculator.rb +290 -0
- data/test/test_methoddatabase.rb +16 -0
- data/test/test_methodentry.rb +328 -0
- data/test/test_methodsignature.rb +3 -1
- data/test/test_methodsince_command.rb +168 -0
- data/test/test_nameutils.rb +22 -0
- data/test/test_preprocessor.rb +215 -0
- data/test/test_rdcompiler.rb +401 -30
- data/test/test_refsdatabase.rb +32 -0
- data/test/test_rrd_to_markdown.rb +968 -0
- data/test/test_rrdparser.rb +295 -2
- data/test/test_run_ruby_wasm.rb +54 -0
- data/test/test_run_worker_prelude.rb +83 -0
- data/test/test_runner.rb +51 -0
- data/test/test_search_index_generator.rb +513 -0
- data/test/test_search_screen.rb +48 -0
- data/test/test_searcher.rb +132 -0
- data/test/test_searchpage_command.rb +167 -0
- data/test/test_simplesearcher.rb +28 -0
- data/test/test_statichtml_command.rb +204 -0
- data/test/test_syntax_highlighter.rb +60 -0
- data/test/test_whole_file_gate.rb +175 -0
- data/theme/default/js/NOTICE +42 -0
- data/theme/default/js/run-worker.js +111 -0
- data/theme/default/js/run.js +350 -0
- data/theme/default/js/search_controller.js +134 -0
- data/theme/default/js/search_init.js +176 -0
- data/theme/default/js/search_navigation.js +110 -0
- data/theme/default/js/search_page.js +202 -0
- data/theme/default/js/search_ranker.js +244 -0
- data/theme/default/script.js +91 -18
- data/theme/default/search.css +149 -0
- data/theme/default/style.css +193 -21
- data/theme/default/syntax-highlight.css +8 -0
- data/theme/lillia/style.css +45 -1
- metadata +161 -20
- 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
|
data/lib/bitclust/interface.rb
CHANGED
|
@@ -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]'
|
|
72
|
-
property :methods, '[MethodEntry]'
|
|
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
|
data/lib/bitclust/lineinput.rb
CHANGED
|
@@ -36,7 +36,9 @@ class LineInput
|
|
|
36
36
|
end
|
|
37
37
|
|
|
38
38
|
def path
|
|
39
|
-
@
|
|
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
|
|
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
|