bitclust-core 1.6.1 → 1.8.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/Rakefile +1 -0
- data/data/bitclust/catalog/ja_JP.UTF-8 +4 -2
- data/data/bitclust/template/class-index +0 -1
- data/data/bitclust/template/function-index +1 -0
- data/data/bitclust/template/layout +1 -0
- data/data/bitclust/template.epub/class +5 -5
- data/data/bitclust/template.epub/class-index +0 -1
- data/data/bitclust/template.epub/contents +7 -4
- data/data/bitclust/template.epub/layout +4 -4
- data/data/bitclust/template.offline/class-index +0 -1
- data/data/bitclust/template.offline/function-index +1 -0
- data/data/bitclust/template.offline/layout +3 -0
- data/lib/bitclust/classentry.rb +1 -1
- data/lib/bitclust/docentry.rb +1 -1
- data/lib/bitclust/functionentry.rb +1 -1
- data/lib/bitclust/generators/epub.rb +69 -5
- data/lib/bitclust/irb.rb +74 -0
- data/lib/bitclust/markdown_bridge.rb +11 -6
- data/lib/bitclust/markdown_exporter.rb +311 -0
- data/lib/bitclust/markdown_tree.rb +118 -11
- data/lib/bitclust/mdcompiler.rb +15 -1
- data/lib/bitclust/mdparser.rb +42 -1
- data/lib/bitclust/methoddatabase.rb +17 -4
- data/lib/bitclust/methodentry.rb +24 -0
- data/lib/bitclust/methodsignature.rb +1 -1
- data/lib/bitclust/nameutils.rb +13 -0
- data/lib/bitclust/preprocessor.rb +4 -37
- data/lib/bitclust/rbs_overload_matcher.rb +179 -0
- data/lib/bitclust/rbs_sig_importer.rb +276 -0
- data/lib/bitclust/rbs_signatures.rb +125 -0
- data/lib/bitclust/rdcompiler.rb +12 -1
- data/lib/bitclust/remote.rb +405 -0
- data/lib/bitclust/runner.rb +7 -10
- data/lib/bitclust/screen.rb +6 -1
- data/lib/bitclust/search_index_generator.rb +28 -10
- data/lib/bitclust/searcher.rb +54 -25
- data/lib/bitclust/subcommands/chm_command.rb +1 -1
- data/lib/bitclust/subcommands/epub_command.rb +2 -2
- data/lib/bitclust/subcommands/extract_command.rb +7 -0
- data/lib/bitclust/subcommands/htmlfile_command.rb +7 -0
- data/lib/bitclust/subcommands/methods_command.rb +5 -28
- data/lib/bitclust/subcommands/query_command.rb +1 -0
- data/lib/bitclust/subcommands/rbssig_command.rb +99 -0
- data/lib/bitclust/subcommands/server_command.rb +21 -10
- data/lib/bitclust/subcommands/setup_command.rb +32 -11
- data/lib/bitclust/subcommands/statichtml_command.rb +59 -0
- data/lib/bitclust/syntax_highlighter.rb +65 -4
- data/lib/bitclust/user_dirs.rb +62 -0
- data/lib/bitclust/version.rb +1 -1
- data/lib/bitclust-irb.rb +2 -0
- data/test/test_chm_command.rb +22 -0
- data/test/test_epub_command.rb +60 -0
- data/test/test_epub_generator.rb +330 -0
- data/test/test_extract_command.rb +45 -0
- data/test/test_irb_plugin.rb +212 -0
- data/test/test_link_checker.rb +1 -1
- data/test/test_markdown_bridge.rb +35 -0
- data/test/test_markdown_exporter.rb +260 -0
- data/test/test_markdown_tree.rb +144 -0
- data/test/test_mdcompiler.rb +47 -5
- data/test/test_mdparser.rb +223 -4
- data/test/test_methodentry.rb +111 -0
- data/test/test_methods_command.rb +32 -0
- data/test/test_methodsignature.rb +3 -1
- data/test/test_opensearchdescription_screen.rb +39 -0
- data/test/test_preprocessor.rb +26 -0
- data/test/test_query_command.rb +30 -0
- data/test/test_rbs_overload_matcher.rb +170 -0
- data/test/test_rbs_sig_importer.rb +237 -0
- data/test/test_rbssig_command.rb +186 -0
- data/test/test_rdcompiler.rb +99 -1
- data/test/test_remote.rb +374 -0
- data/test/test_runner.rb +61 -1
- data/test/test_search_index_generator.rb +81 -0
- data/test/test_searcher.rb +63 -0
- data/test/test_server_command.rb +41 -0
- data/test/test_setup_command.rb +164 -0
- data/test/test_statichtml_command.rb +137 -0
- data/test/test_syntax_highlighter.rb +124 -0
- data/test/test_user_dirs.rb +62 -0
- data/theme/default/js/run.js +14 -0
- data/theme/default/js/search_init.js +3 -1
- data/theme/default/js/search_page.js +7 -1
- data/theme/default/js/version_switcher.js +147 -0
- data/theme/default/search.css +21 -0
- data/theme/default/style.css +83 -7
- data/theme/lillia/style.css +26 -0
- metadata +71 -2
- data/lib/bitclust/ridatabase.rb +0 -359
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'bitclust/nameutils'
|
|
4
|
+
|
|
5
|
+
module BitClust
|
|
6
|
+
|
|
7
|
+
# statichtml の --markdown-output 用に、HTML の各ページと対になる Markdown を
|
|
8
|
+
# エントリの前処理済みソース(Markdown ツリー由来の md)から組み立てる。
|
|
9
|
+
# 参照記法(単一ブラケットの [m:...] 等と、参照 scheme を宛先にした
|
|
10
|
+
# ラベル付きリンク)は相対 .md リンクへ解決する。解決できない参照
|
|
11
|
+
# (未対応の種別・不正な指定)はそのまま残し、エラーにしない。
|
|
12
|
+
# コードスパンとコードフェンスの中は変換しない。
|
|
13
|
+
class MarkdownExporter
|
|
14
|
+
|
|
15
|
+
# クラスページの メソッド種別 → セクション見出し(doctree の md と同じ名称)
|
|
16
|
+
CLASS_SECTIONS = [
|
|
17
|
+
["Class Methods", :public, :singleton_method],
|
|
18
|
+
["Instance Methods", :public, :instance_method],
|
|
19
|
+
["Private Class Methods", :private, :singleton_method],
|
|
20
|
+
["Private Instance Methods", :private, :instance_method],
|
|
21
|
+
["Protected Instance Methods", :protected, :instance_method],
|
|
22
|
+
["Module Functions", :public, :module_function],
|
|
23
|
+
["Constants", :public, :constant],
|
|
24
|
+
["Special Variables", :public, :special_variable],
|
|
25
|
+
].freeze
|
|
26
|
+
|
|
27
|
+
def initialize(urlmapper, html_suffix)
|
|
28
|
+
@urlmapper = urlmapper
|
|
29
|
+
@html_suffix = html_suffix
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def library_page(entry)
|
|
33
|
+
page("# library #{entry.name}", entry.source)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# クラスページは HTML と同じく本文+メソッドリンク一覧(本文は載せない)
|
|
37
|
+
def class_page(entry)
|
|
38
|
+
out = +""
|
|
39
|
+
CLASS_SECTIONS.each do |section, visibility, type|
|
|
40
|
+
methods = entry.entries.select { |m|
|
|
41
|
+
!m.undefined? && m.type == type && m.visibility == visibility
|
|
42
|
+
}
|
|
43
|
+
next if methods.empty?
|
|
44
|
+
out << "\n## #{section}\n\n"
|
|
45
|
+
methods.each do |m|
|
|
46
|
+
m.names.each do |name|
|
|
47
|
+
spec = m.klass.name + m.typemark + name
|
|
48
|
+
url = ref_url('m', spec)
|
|
49
|
+
out << (url ? "- [#{escape_label(name)}](#{url})\n" : "- #{escape_label(name)}\n")
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
page(class_heading(entry), entry.source, out)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def method_page(method_name, entries)
|
|
57
|
+
out = +"# #{method_name.sub('.#', '?.')}\n"
|
|
58
|
+
entries.each do |m|
|
|
59
|
+
body = m.source.strip
|
|
60
|
+
out << "\n" << convert_text(body) << "\n" unless body.empty?
|
|
61
|
+
end
|
|
62
|
+
out
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def doc_page(entry)
|
|
66
|
+
page("# #{entry.title}", entry.source)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def function_page(entry)
|
|
70
|
+
page("# #{entry.name}", "### #{entry.header}\n#{entry.source}")
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# 本文中の参照をコードスパン・コードフェンスを避けて相対 .md リンクへ
|
|
74
|
+
# 変換する
|
|
75
|
+
def convert_text(str)
|
|
76
|
+
out = +''
|
|
77
|
+
fence = nil
|
|
78
|
+
str.each_line do |line|
|
|
79
|
+
if fence
|
|
80
|
+
out << line
|
|
81
|
+
stripped = line.strip
|
|
82
|
+
fence = nil if stripped.match?(/\A`+\z/) && stripped.length >= (fence || 0)
|
|
83
|
+
elsif (m = /\A {0,3}(`{3,})/.match(line))
|
|
84
|
+
fence = (m[1] || raise).length
|
|
85
|
+
out << line
|
|
86
|
+
else
|
|
87
|
+
out << convert_line(line)
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
out
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
private
|
|
94
|
+
|
|
95
|
+
def page(heading, source, trailer = "")
|
|
96
|
+
out = +"#{heading}\n"
|
|
97
|
+
body = source.strip
|
|
98
|
+
out << "\n" << convert_text(body) << "\n" unless body.empty?
|
|
99
|
+
out << trailer
|
|
100
|
+
out
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def class_heading(entry)
|
|
104
|
+
case entry.type
|
|
105
|
+
when :class
|
|
106
|
+
sc = entry.superclass
|
|
107
|
+
sc ? "# class #{entry.name} < #{sc.name}" : "# class #{entry.name}"
|
|
108
|
+
when :module
|
|
109
|
+
"# module #{entry.name}"
|
|
110
|
+
when :object
|
|
111
|
+
"# object #{entry.name}"
|
|
112
|
+
else
|
|
113
|
+
"# #{entry.name}"
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def convert_line(line)
|
|
118
|
+
spans = [] #: Array[String]
|
|
119
|
+
converted = convert_refs(extract_code_spans(line, spans))
|
|
120
|
+
converted.gsub(/\x00(\d+)\x00/) { spans[($1 || raise).to_i] || raise }
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# インラインコードスパン(CommonMark 6.1、N 連バッククォートの同長
|
|
124
|
+
# ペアリング)を書かれたままプレースホルダへ退避する
|
|
125
|
+
def extract_code_spans(str, saved)
|
|
126
|
+
result = +''
|
|
127
|
+
i = 0
|
|
128
|
+
len = str.length
|
|
129
|
+
while i < len
|
|
130
|
+
c = str[i] or raise
|
|
131
|
+
if c == '\\' && i + 1 < len
|
|
132
|
+
result << (str[i, 2] || raise)
|
|
133
|
+
i += 2
|
|
134
|
+
next
|
|
135
|
+
end
|
|
136
|
+
if c == '`'
|
|
137
|
+
run = 1
|
|
138
|
+
run += 1 while str[i + run] == '`'
|
|
139
|
+
close = find_code_span_close(str, i + run, run)
|
|
140
|
+
if close
|
|
141
|
+
saved << (str[i...(close + run)] || raise)
|
|
142
|
+
result << "\x00#{saved.size - 1}\x00"
|
|
143
|
+
i = close + run
|
|
144
|
+
next
|
|
145
|
+
end
|
|
146
|
+
result << ('`' * run)
|
|
147
|
+
i += run
|
|
148
|
+
next
|
|
149
|
+
end
|
|
150
|
+
result << c
|
|
151
|
+
i += 1
|
|
152
|
+
end
|
|
153
|
+
result
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def find_code_span_close(str, from, run_len)
|
|
157
|
+
i = from
|
|
158
|
+
len = str.length
|
|
159
|
+
while i < len
|
|
160
|
+
case str[i]
|
|
161
|
+
when "\n"
|
|
162
|
+
return nil
|
|
163
|
+
when '`'
|
|
164
|
+
j = i
|
|
165
|
+
j += 1 while str[j] == '`'
|
|
166
|
+
return i if j - i == run_len
|
|
167
|
+
i = j
|
|
168
|
+
else
|
|
169
|
+
i += 1
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
nil
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
REF_SCHEME_RE = /\A([a-zA-Z][a-zA-Z-]*):(.*)\z/m
|
|
176
|
+
|
|
177
|
+
def convert_refs(str)
|
|
178
|
+
result = +''
|
|
179
|
+
i = 0
|
|
180
|
+
while i < str.length
|
|
181
|
+
if str[i] == '\\' && i + 1 < str.length
|
|
182
|
+
result << (str[i, 2] || raise)
|
|
183
|
+
i += 2
|
|
184
|
+
next
|
|
185
|
+
end
|
|
186
|
+
if str[i] == '['
|
|
187
|
+
close = matching_delimiter(str, i, '[', ']')
|
|
188
|
+
if close
|
|
189
|
+
if str[close + 1] == '(' &&
|
|
190
|
+
(dest_end = matching_delimiter(str, close + 1, '(', ')', space_ends: true)) &&
|
|
191
|
+
(dest = str[(close + 2)...dest_end])
|
|
192
|
+
# ラベル付きリンク [text](dest)
|
|
193
|
+
if (m = REF_SCHEME_RE.match(dest)) && (url = ref_url(m[1] || raise, m[2] || raise))
|
|
194
|
+
result << "[#{str[(i + 1)...close]}](#{url})"
|
|
195
|
+
i = dest_end + 1
|
|
196
|
+
next
|
|
197
|
+
end
|
|
198
|
+
if dest.start_with?('#') || dest.match?(%r{\Ahttps?://})
|
|
199
|
+
# 通常の Markdown リンクはそのまま(ラベル内は再解釈しない)
|
|
200
|
+
result << (str[i..dest_end] || raise)
|
|
201
|
+
i = dest_end + 1
|
|
202
|
+
next
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
# 単一ブラケット参照 [type:target]([[ は除外)
|
|
206
|
+
inner = str[(i + 1)...close] || raise
|
|
207
|
+
if str[i + 1] != '[' && (m = REF_SCHEME_RE.match(inner)) &&
|
|
208
|
+
(converted = bare_ref(m[1] || raise, m[2] || raise, inner))
|
|
209
|
+
result << converted
|
|
210
|
+
i = close + 1
|
|
211
|
+
next
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
result << (str[i] || raise)
|
|
216
|
+
i += 1
|
|
217
|
+
end
|
|
218
|
+
result
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
# open 位置の括弧に対応する閉じ括弧の位置(\ エスケープ対応・ネスト可)。
|
|
222
|
+
# space_ends: リンク宛先用。空白が現れたらリンクではない(nil)
|
|
223
|
+
def matching_delimiter(str, open, open_char, close_char, space_ends: false)
|
|
224
|
+
depth = 0
|
|
225
|
+
i = open
|
|
226
|
+
while i < str.length
|
|
227
|
+
c = str[i]
|
|
228
|
+
if c == '\\'
|
|
229
|
+
i += 1
|
|
230
|
+
elsif space_ends && c =~ /\s/
|
|
231
|
+
return nil
|
|
232
|
+
elsif c == open_char
|
|
233
|
+
depth += 1
|
|
234
|
+
elsif c == close_char
|
|
235
|
+
depth -= 1
|
|
236
|
+
return i if depth == 0
|
|
237
|
+
end
|
|
238
|
+
i += 1
|
|
239
|
+
end
|
|
240
|
+
nil
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
# 単一ブラケット参照 1 個を Markdown リンクにする。ラベルは書かれた
|
|
244
|
+
# ままの形(エスケープ込み)から type: を落としたもの
|
|
245
|
+
def bare_ref(type, arg, written)
|
|
246
|
+
label =
|
|
247
|
+
case type
|
|
248
|
+
when 'm', 'c', 'lib', 'd', 'f'
|
|
249
|
+
written.split(':', 2)[1]
|
|
250
|
+
when 'url'
|
|
251
|
+
arg
|
|
252
|
+
when 'ref'
|
|
253
|
+
case arg
|
|
254
|
+
when /\A(\w+):(.*)\#([-\w]+)\z/
|
|
255
|
+
"#{$2}##{$3}"
|
|
256
|
+
when /\A[-\w]+\z/
|
|
257
|
+
arg
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
return nil unless label
|
|
261
|
+
url = ref_url(type, arg)
|
|
262
|
+
return nil unless url
|
|
263
|
+
"[#{label}](#{url})"
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
# 参照 scheme の宛先 URL(.md への相対リンク)。解決できなければ nil
|
|
267
|
+
def ref_url(type, arg)
|
|
268
|
+
arg = unescape_md_brackets(arg)
|
|
269
|
+
case type
|
|
270
|
+
when 'm'
|
|
271
|
+
# md 表記の module function ?. は内部表記 .# に正規化して引く
|
|
272
|
+
md_suffix(@urlmapper.method_url(arg.sub('?.', '.#')))
|
|
273
|
+
when 'c'
|
|
274
|
+
md_suffix(@urlmapper.class_url(arg))
|
|
275
|
+
when 'lib'
|
|
276
|
+
md_suffix(@urlmapper.library_url(arg))
|
|
277
|
+
when 'd'
|
|
278
|
+
md_suffix(@urlmapper.document_url(arg))
|
|
279
|
+
when 'f'
|
|
280
|
+
md_suffix(@urlmapper.function_url(arg))
|
|
281
|
+
when 'url'
|
|
282
|
+
arg
|
|
283
|
+
when 'ref'
|
|
284
|
+
case arg
|
|
285
|
+
when /\A(\w+):(.*)\#([-\w]+)\z/
|
|
286
|
+
url = ref_url($1 || raise, $2 || raise)
|
|
287
|
+
url ? "#{url}##{$3}" : nil
|
|
288
|
+
when /\A[-\w]+\z/
|
|
289
|
+
"##{arg}"
|
|
290
|
+
end
|
|
291
|
+
end
|
|
292
|
+
rescue StandardError
|
|
293
|
+
nil
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
def md_suffix(html_url)
|
|
297
|
+
return html_url unless html_url.end_with?(@html_suffix)
|
|
298
|
+
"#{html_url[0, html_url.length - @html_suffix.length]}.md"
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
def unescape_md_brackets(str)
|
|
302
|
+
str.gsub(/\\([\[\]\\])/, '\1')
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
def escape_label(text)
|
|
306
|
+
text.gsub(/[\\\[\]]/) { "\\#{$&}" }
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
end
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'bitclust/exception'
|
|
4
|
+
require 'bitclust/preprocessor'
|
|
5
|
+
|
|
3
6
|
module BitClust
|
|
4
7
|
# 新パイプラインのファイル発見(MARKUP_SPEC §1.1)。
|
|
5
8
|
#
|
|
@@ -116,36 +119,45 @@ module BitClust
|
|
|
116
119
|
if lines[0] =~ /\A---\s*\z/
|
|
117
120
|
i = 1
|
|
118
121
|
in_library_block = false
|
|
119
|
-
gate_stack = [] #: Array[
|
|
122
|
+
gate_stack = [] #: Array[list_gate]
|
|
120
123
|
while i < lines.length && lines[i] !~ /\A---\s*\z/
|
|
121
124
|
line = lines[i]
|
|
122
125
|
if in_library_block
|
|
123
|
-
# 多重所属のゲート付きリスト(MARKUP_SPEC §1.2)。
|
|
126
|
+
# 多重所属のゲート付きリスト(MARKUP_SPEC §1.2.1)。
|
|
124
127
|
# 項目は積まれているゲートをすべてまとって membership になる
|
|
125
128
|
case line
|
|
126
129
|
when /\A\s+- (\S+)/
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
+
info[:memberships].concat gated_memberships($1 || raise, gate_stack)
|
|
131
|
+
i += 1; next
|
|
132
|
+
when /\A\#[@%]\#/ # 前処理コメント(Preprocessor と同じく無視)
|
|
133
|
+
i += 1; next
|
|
134
|
+
when /\A\#[@%]/
|
|
135
|
+
list_directive(line, gate_stack, path, i + 1)
|
|
136
|
+
i += 1; next
|
|
137
|
+
when /\A\s+\#[@%]/
|
|
138
|
+
# 列 0 でない #% 行は Preprocessor でも指令にならない(YAML の
|
|
139
|
+
# コメント扱い)。ゲートが無音で無視されないようエラーにする
|
|
140
|
+
raise ParseError, "#{path}:#{i + 1}: preprocessor directive must start at column 0: #{line.strip}"
|
|
141
|
+
when /\A\s+\#/ # YAML コメント
|
|
130
142
|
i += 1; next
|
|
131
|
-
when /\A\#[@%]since\s+(\S+)/ then gate_stack.push([:since, $1]); i += 1; next
|
|
132
|
-
when /\A\#[@%]until\s+(\S+)/ then gate_stack.push([:until, $1]); i += 1; next
|
|
133
|
-
when /\A\#[@%]end\s*\z/ then gate_stack.pop; i += 1; next
|
|
134
143
|
else
|
|
135
|
-
|
|
144
|
+
# ブロック終端。この行は通常キーとして処理
|
|
145
|
+
list_end(gate_stack, path, i + 1)
|
|
146
|
+
in_library_block = false
|
|
136
147
|
end
|
|
137
148
|
end
|
|
138
149
|
case line
|
|
139
150
|
when /\Atype:\s*library\s*\z/ then info[:library_file] = true
|
|
140
151
|
when /\Aname:\s*(\S+)/ then info[:name] = $1
|
|
141
|
-
when /\Alibrary:\s*\z/ then in_library_block = true; gate_stack = [] #: Array[
|
|
142
|
-
when /\Alibrary:\s*(\S+)/ then info[:memberships] << { library: $1 }
|
|
152
|
+
when /\Alibrary:\s*\z/ then in_library_block = true; gate_stack = [] #: Array[list_gate]
|
|
153
|
+
when /\Alibrary:\s*(\S+)/ then info[:memberships] << { library: $1 || raise }
|
|
143
154
|
when /\Asince:\s*"?([^"\s]+)"?/ then info[:since] = $1
|
|
144
155
|
when /\Auntil:\s*"?([^"\s]+)"?/ then info[:until] = $1
|
|
145
156
|
when RELATION_KEY_RE then info[:front_matter_relations] = true
|
|
146
157
|
end
|
|
147
158
|
i += 1
|
|
148
159
|
end
|
|
160
|
+
list_end(gate_stack, path, i + 1) if in_library_block
|
|
149
161
|
i += 1
|
|
150
162
|
end
|
|
151
163
|
info[:library] = info[:memberships].dig(0, :library)
|
|
@@ -190,6 +202,101 @@ module BitClust
|
|
|
190
202
|
info
|
|
191
203
|
end
|
|
192
204
|
|
|
205
|
+
# --- front matter ゲート付きリストのディレクティブ ---
|
|
206
|
+
# 本文側 Preprocessor と同じ集合のうち #%since/#%until/#%version/#%else/#%end
|
|
207
|
+
# を解釈する。#%if(任意の条件式)は非対応。解釈できない行は無音で
|
|
208
|
+
# リストを打ち切らず ParseError にする(本文側の unknown directive と同じ扱い)。
|
|
209
|
+
#
|
|
210
|
+
# ゲート1段は clause の選言。clause は since(以上)/until(未満)/
|
|
211
|
+
# version(その版のみ)/except(その版以外)の交差で、#%else は直前ゲートの
|
|
212
|
+
# 補集合(#%version A...B なら「A 未満」または「B 以上」の 2 clause)
|
|
213
|
+
|
|
214
|
+
def list_directive(line, stack, path, lineno)
|
|
215
|
+
err = ->(msg) { raise ParseError, "#{path}:#{lineno}: #{msg}: #{line.strip}" }
|
|
216
|
+
case line
|
|
217
|
+
when /\A\#[@%](since|until)\b(.*)/
|
|
218
|
+
kind, raw = $1, $2 || raise
|
|
219
|
+
ver = version_literal(raw.strip) or err.("wrong conditional expr")
|
|
220
|
+
clause = kind == 'since' ? { since: ver } : { until: ver } #: gate_clause
|
|
221
|
+
stack.push({ clauses: [clause], elsed: false })
|
|
222
|
+
when /\A\#[@%]version\b(.*)/
|
|
223
|
+
clause = version_range(($1 || raise).strip) or
|
|
224
|
+
err.("wrong version range (expected V / A...B / A... / ...B)")
|
|
225
|
+
stack.push({ clauses: [clause], elsed: false })
|
|
226
|
+
when /\A\#[@%]if\b/
|
|
227
|
+
err.("#%if is not supported in front matter library list (use #%since/#%until/#%version/#%else)")
|
|
228
|
+
when /\A\#[@%]else\s*\z/
|
|
229
|
+
gate = stack.last or err.("no matching #%since/#%until/#%version")
|
|
230
|
+
err.("duplicate #%else") if gate[:elsed]
|
|
231
|
+
stack[-1] = { clauses: gate[:clauses].flat_map { |c| complement(c) }, elsed: true }
|
|
232
|
+
when /\A\#[@%]end\s*\z/
|
|
233
|
+
stack.pop or err.("no matching #%since/#%until/#%version")
|
|
234
|
+
else
|
|
235
|
+
err.("unknown preprocessor directive")
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
def list_end(stack, path, lineno)
|
|
240
|
+
return if stack.empty?
|
|
241
|
+
raise ParseError, "#{path}:#{lineno}: unterminated #%since/#%until/#%version in front matter library list"
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
VERSION_LITERAL = Preprocessor::VERSION_LITERAL
|
|
245
|
+
|
|
246
|
+
def version_literal(raw)
|
|
247
|
+
raw =~ /\A#{VERSION_LITERAL}\z/o ? ($1 || $2) : nil
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
# #%version の版範囲(Preprocessor#build_cond_by_range と同じ記法)
|
|
251
|
+
def version_range(raw)
|
|
252
|
+
case raw
|
|
253
|
+
when /\A#{VERSION_LITERAL}\.\.\.#{VERSION_LITERAL}\z/o
|
|
254
|
+
{ since: $1 || $2 || raise, until: $3 || $4 || raise }
|
|
255
|
+
when /\A#{VERSION_LITERAL}\.\.\.\z/o then { since: $1 || $2 || raise }
|
|
256
|
+
when /\A\.\.\.#{VERSION_LITERAL}\z/o then { until: $1 || $2 || raise }
|
|
257
|
+
when /\A#{VERSION_LITERAL}\z/o then { version: $1 || $2 || raise }
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
def complement(clause)
|
|
262
|
+
return [{ except: [clause[:version] || raise] }] if clause[:version]
|
|
263
|
+
alts = [] #: Array[gate_clause]
|
|
264
|
+
alts << { until: clause[:since] || raise } if clause[:since]
|
|
265
|
+
alts << { since: clause[:until] || raise } if clause[:until]
|
|
266
|
+
alts
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
# 積まれた全ゲートの交差。各段が選言なので組合せごとに1つの membership
|
|
270
|
+
# (矛盾する組合せ = 異なる単一版どうしは捨てる)
|
|
271
|
+
def gated_memberships(library, stack)
|
|
272
|
+
seed = [{}] #: Array[gate_clause]
|
|
273
|
+
combos = stack.inject(seed) { |acc, gate|
|
|
274
|
+
acc.product(gate[:clauses]).filter_map { |a, b| intersect(a, b) }
|
|
275
|
+
}
|
|
276
|
+
combos.map { |c|
|
|
277
|
+
m = { library: library } #: membership
|
|
278
|
+
m[:since] = c[:since] || raise if c[:since]
|
|
279
|
+
m[:until] = c[:until] || raise if c[:until]
|
|
280
|
+
m[:version] = c[:version] || raise if c[:version]
|
|
281
|
+
m[:except] = c[:except] || raise if c[:except]
|
|
282
|
+
m
|
|
283
|
+
}
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
def intersect(a, b)
|
|
287
|
+
r = {} #: gate_clause
|
|
288
|
+
sinces = [a[:since], b[:since]].compact
|
|
289
|
+
r[:since] = sinces.max_by { |v| Gem::Version.new(v) } || raise unless sinces.empty?
|
|
290
|
+
untils = [a[:until], b[:until]].compact
|
|
291
|
+
r[:until] = untils.min_by { |v| Gem::Version.new(v) } || raise unless untils.empty?
|
|
292
|
+
versions = [a[:version], b[:version]].compact.uniq
|
|
293
|
+
return nil if versions.size > 1
|
|
294
|
+
r[:version] = versions.first if versions.size == 1
|
|
295
|
+
excepts = (a[:except] || []) + (b[:except] || [])
|
|
296
|
+
r[:except] = excepts.uniq unless excepts.empty?
|
|
297
|
+
r
|
|
298
|
+
end
|
|
299
|
+
|
|
193
300
|
# 大文字小文字のみが異なる名前は macOS/Windows の case-insensitive FS で
|
|
194
301
|
# チェックアウト不能になるため、衝突を警告する(途中のディレクトリ名も見る)
|
|
195
302
|
def warn_case_collisions(files)
|
data/lib/bitclust/mdcompiler.rb
CHANGED
|
@@ -126,6 +126,12 @@ module BitClust
|
|
|
126
126
|
break
|
|
127
127
|
end
|
|
128
128
|
end
|
|
129
|
+
# RBS シグネチャは見出し <dt> 群の直後・説明 <dd> の直前に、この
|
|
130
|
+
# チャンクへ振り分けられたぶんだけ <dt> 行として出す(rd 側
|
|
131
|
+
# entry_chunk と同じ。rbs_sig property が無ければ何も出ない)
|
|
132
|
+
if (rbs_dts = rbs_signature_dts_for_chunk())
|
|
133
|
+
@out.puts rbs_dts
|
|
134
|
+
end
|
|
129
135
|
@out.puts %Q(<dd class="#{@type.to_s}-description">)
|
|
130
136
|
undef_message if attrs.include?('undef')
|
|
131
137
|
while @f.next?
|
|
@@ -184,6 +190,12 @@ module BitClust
|
|
|
184
190
|
super(sig_line.sub(signature_re, '--- '), first)
|
|
185
191
|
end
|
|
186
192
|
|
|
193
|
+
# RBS シグネチャのチャンク走査(RbsSignatures)用: md のシグネチャ行を
|
|
194
|
+
# rd の正規形にして返す(method_signature と同じ変換)
|
|
195
|
+
def rbs_signature_line(line)
|
|
196
|
+
line.sub(signature_re, '--- ') if signature_re =~ line
|
|
197
|
+
end
|
|
198
|
+
|
|
187
199
|
def headline(line)
|
|
188
200
|
hashes = line[/\A#+/] || raise
|
|
189
201
|
label = line.sub(/\A#+\s*/, '').strip
|
|
@@ -405,11 +417,13 @@ module BitClust
|
|
|
405
417
|
}
|
|
406
418
|
end
|
|
407
419
|
|
|
420
|
+
# 列揃えはインラインスタイルで出す。align 属性は presentational hint
|
|
421
|
+
# としての扱いになり、テーマ CSS の text-align 指定に負けて効かない
|
|
408
422
|
def table_row(row, tag, aligns)
|
|
409
423
|
cells = split_table_row(row)
|
|
410
424
|
string '<tr>'
|
|
411
425
|
cells.each_with_index do |cell, i|
|
|
412
|
-
align = aligns[i] ? %Q(
|
|
426
|
+
align = aligns[i] ? %Q( style="text-align:#{aligns[i]}") : ''
|
|
413
427
|
string "<#{tag}#{align}>#{compile_text(cell)}</#{tag}>"
|
|
414
428
|
end
|
|
415
429
|
line '</tr>'
|
data/lib/bitclust/mdparser.rb
CHANGED
|
@@ -314,11 +314,50 @@ module BitClust
|
|
|
314
314
|
src = (header + body).join('')
|
|
315
315
|
src.location = sig_lines[0].location
|
|
316
316
|
sigs = sig_lines.map {|line| method_signature(line) }
|
|
317
|
+
# キーワード不一致はより具体的なメッセージになるよう
|
|
318
|
+
# check_chunk_signatures(signature crash)より先に検査する
|
|
319
|
+
check_entry_style(sig_lines, sigs)
|
|
317
320
|
mainsig = check_chunk_signatures(sigs, sig_lines[0])
|
|
318
321
|
names = sigs.map {|s| s.name }.compact.uniq.sort
|
|
319
322
|
Chunk.new(mainsig, names, src)
|
|
320
323
|
end
|
|
321
324
|
|
|
325
|
+
# セクション(h2)の型ごとに期待するエントリキーワード。
|
|
326
|
+
# キーワードは型決定には使われない(型はセクションで決まる)ため、
|
|
327
|
+
# 食い違ったまま書けてしまう分類間違い(rurema/doctree#3291 の
|
|
328
|
+
# 「Class Methods 配下の const」等)をパース時に検出する
|
|
329
|
+
ENTRY_KEYWORD_RE = /\A### (module_function def|def|const|gvar) /
|
|
330
|
+
EXPECTED_ENTRY_KEYWORD = {
|
|
331
|
+
'#' => 'def',
|
|
332
|
+
'.' => 'def',
|
|
333
|
+
'.#' => 'module_function def',
|
|
334
|
+
'::' => 'const',
|
|
335
|
+
'$' => 'gvar',
|
|
336
|
+
}.freeze
|
|
337
|
+
|
|
338
|
+
# エントリの書式とセクションの一致検査。
|
|
339
|
+
# - キーワードがセクションの種別と一致すること
|
|
340
|
+
# - 特異メソッドは `def Klass.name` / `def self.name` のプレフィクス
|
|
341
|
+
# 必須(MARKUP_SPEC §3.1。def 行だけでメソッド種別が分かるようにする)
|
|
342
|
+
def check_entry_style(sig_lines, sigs)
|
|
343
|
+
cxt = @context.signature
|
|
344
|
+
return unless cxt && cxt.type
|
|
345
|
+
expected = EXPECTED_ENTRY_KEYWORD[cxt.type] or return
|
|
346
|
+
sig_lines.zip(sigs).each do |line, sig|
|
|
347
|
+
keyword = (line || raise)[ENTRY_KEYWORD_RE, 1]
|
|
348
|
+
unless keyword == expected
|
|
349
|
+
parse_error "entry keyword `#{keyword}' does not match this section (expected `#{expected}')", line
|
|
350
|
+
end
|
|
351
|
+
if cxt.type == '.' && sig && sig.type.nil?
|
|
352
|
+
parse_error "singleton method entry must be written as `def #{cxt.klass || 'Klass'}.name' or `def self.name'", line
|
|
353
|
+
end
|
|
354
|
+
end
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
# `def self.name` 形式。type は singleton に確定し、klass は
|
|
358
|
+
# 見出しコンテキスト(H1/H2)から補完される
|
|
359
|
+
SELF_SIGNATURE = /\A---\s*self\.(#{METHOD_NAME_RE})/
|
|
360
|
+
|
|
322
361
|
# md シグネチャ行を rd 形式(--- ...)へ落とし、既存の
|
|
323
362
|
# Signature パースを継承する。エラー時は元の行を報告する
|
|
324
363
|
def method_signature(line)
|
|
@@ -326,12 +365,14 @@ module BitClust
|
|
|
326
365
|
begin
|
|
327
366
|
# 描画(MethodSignature.parse)が受理できないシグネチャは
|
|
328
367
|
# statichtml まで進んでから落ちる(か、誤った名前で DB に入る)ので
|
|
329
|
-
# パース時に拒否する。RBS 形式(def name
|
|
368
|
+
# パース時に拒否する。RBS 形式(def name:)が該当
|
|
330
369
|
MethodSignature.parse(rd_line)
|
|
331
370
|
rescue ParseError
|
|
332
371
|
parse_error "unsupported method signature", line
|
|
333
372
|
end
|
|
334
373
|
case
|
|
374
|
+
when m = SELF_SIGNATURE.match(rd_line)
|
|
375
|
+
Signature.new(nil, '.', m[1])
|
|
335
376
|
when m = SIGNATURE.match(rd_line)
|
|
336
377
|
klass, typemark_, name = m.captures
|
|
337
378
|
typemark = _ = typemark_
|
|
@@ -186,7 +186,7 @@ module BitClust
|
|
|
186
186
|
# membership を持つライブラリだけがメンバーとして取り込む
|
|
187
187
|
members = tree.entities.select { |_, e|
|
|
188
188
|
e[:memberships].any? { |m|
|
|
189
|
-
m[:library] == libname &&
|
|
189
|
+
m[:library] == libname && md_gate_covers?(version, m)
|
|
190
190
|
}
|
|
191
191
|
}
|
|
192
192
|
sorted = members.keys.sort_by { |path|
|
|
@@ -211,12 +211,25 @@ module BitClust
|
|
|
211
211
|
# since は「その版以降」、until は「その版未満」(ブリッジの
|
|
212
212
|
# #@since/#@until ラッパーと同じ意味論)
|
|
213
213
|
def md_version_covers?(version, since_version, until_version)
|
|
214
|
+
gate = {} #: MarkdownTree::gate_clause
|
|
215
|
+
gate[:since] = since_version if since_version
|
|
216
|
+
gate[:until] = until_version if until_version
|
|
217
|
+
md_gate_covers?(version, gate)
|
|
218
|
+
end
|
|
219
|
+
private :md_version_covers?
|
|
220
|
+
|
|
221
|
+
# membership のゲート(MarkdownTree の since/until/version/except)がこの版を
|
|
222
|
+
# 含むか。version は「その版のみ」、except は「その版以外」(#%version V と
|
|
223
|
+
# その #%else)。比較は Gem::Version なので 3.4 と 3.4.0 は同じ版
|
|
224
|
+
def md_gate_covers?(version, gate)
|
|
214
225
|
v = Gem::Version.new(version)
|
|
215
|
-
return false if
|
|
216
|
-
return false if
|
|
226
|
+
return false if (s = gate[:since]) && v < Gem::Version.new(s)
|
|
227
|
+
return false if (u = gate[:until]) && v >= Gem::Version.new(u)
|
|
228
|
+
return false if (only = gate[:version]) && v != Gem::Version.new(only)
|
|
229
|
+
return false if (except = gate[:except]) && except.any? { |x| v == Gem::Version.new(x) }
|
|
217
230
|
true
|
|
218
231
|
end
|
|
219
|
-
private :
|
|
232
|
+
private :md_gate_covers?
|
|
220
233
|
|
|
221
234
|
def refs
|
|
222
235
|
@refs ||= RefsDatabase.load(realpath('refs'))
|
data/lib/bitclust/methodentry.rb
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
require 'bitclust/entry'
|
|
12
12
|
require 'bitclust/exception'
|
|
13
|
+
require 'json'
|
|
13
14
|
|
|
14
15
|
module BitClust
|
|
15
16
|
|
|
@@ -118,6 +119,11 @@ module BitClust
|
|
|
118
119
|
# 現れない前提。fill_since/fill_until で検査する)
|
|
119
120
|
property :since_by_name, '[String]'
|
|
120
121
|
property :until_by_name, '[String]'
|
|
122
|
+
# RBS シグネチャ表示。オーバーロード配列(RbsSigImporter 参照)の
|
|
123
|
+
# JSON 1 行を持つ。型シグネチャは ',' を普通に含むので [String] 型
|
|
124
|
+
# (',' 区切り)は使えない。JSON は改行を含まず、property ファイルの
|
|
125
|
+
# key=value 1 行形式は値中の '=' を許す(split('=', 2))ので安全
|
|
126
|
+
property :rbs_sig, 'String'
|
|
121
127
|
}
|
|
122
128
|
|
|
123
129
|
def inspect
|
|
@@ -166,6 +172,24 @@ module BitClust
|
|
|
166
172
|
"#{methodid2typechar(@id)}_#{encodename_fs(name).gsub(/=/, '--')}".upcase
|
|
167
173
|
end
|
|
168
174
|
|
|
175
|
+
# rbs_sig(JSON)をオーバーロード配列に戻す。現行形式は
|
|
176
|
+
# {"overloads":[{"segments":..., "params":..., "arity":..., "block":...}]}
|
|
177
|
+
# で、セグメント行列そのものの配列だった旧形式の DB は segments だけの
|
|
178
|
+
# オーバーロード列に読み替える。property が無い古い DB(nil)・
|
|
179
|
+
# シグネチャ無しで保存された空文字列・未保存エントリの "(uninitialized)"
|
|
180
|
+
# センチネル・壊れた JSON はすべて nil(= 表示しない)に落として
|
|
181
|
+
# 描画側を守る
|
|
182
|
+
def rbs_signature_overloads
|
|
183
|
+
json = rbs_sig()
|
|
184
|
+
return nil unless json && (json.start_with?('{') || json.start_with?('['))
|
|
185
|
+
data = JSON.parse(json)
|
|
186
|
+
overloads = data.is_a?(Hash) ? data['overloads']
|
|
187
|
+
: data.map {|segments| {'segments' => segments} }
|
|
188
|
+
overloads.is_a?(Array) && !overloads.empty? ? overloads : nil
|
|
189
|
+
rescue JSON::ParserError
|
|
190
|
+
nil
|
|
191
|
+
end
|
|
192
|
+
|
|
169
193
|
def labels
|
|
170
194
|
c, t, _m, _lib = methodid2specparts(@id)
|
|
171
195
|
names().map {|name| "#{c}#{t}#{name}" }
|
|
@@ -21,7 +21,7 @@ module BitClust
|
|
|
21
21
|
|
|
22
22
|
METHOD_SIGNATURE_RE = /\A
|
|
23
23
|
--- \s*
|
|
24
|
-
(?: (?:#{CLASS_PATH_RE} #{TYPEMARK_RE})? (#{METHOD_NAME_RE})
|
|
24
|
+
(?: (?:#{CLASS_PATH_RE} #{TYPEMARK_RE} | self\.)? (#{METHOD_NAME_RE})
|
|
25
25
|
| (#{GVAR_RE})
|
|
26
26
|
) \s* # method name ($1) or gvar name ($2)
|
|
27
27
|
(?: \( (.*?) \) \s* )? # parameters (optional); $3=parameter_list
|