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.
Files changed (90) hide show
  1. checksums.yaml +4 -4
  2. data/Rakefile +1 -0
  3. data/data/bitclust/catalog/ja_JP.UTF-8 +4 -2
  4. data/data/bitclust/template/class-index +0 -1
  5. data/data/bitclust/template/function-index +1 -0
  6. data/data/bitclust/template/layout +1 -0
  7. data/data/bitclust/template.epub/class +5 -5
  8. data/data/bitclust/template.epub/class-index +0 -1
  9. data/data/bitclust/template.epub/contents +7 -4
  10. data/data/bitclust/template.epub/layout +4 -4
  11. data/data/bitclust/template.offline/class-index +0 -1
  12. data/data/bitclust/template.offline/function-index +1 -0
  13. data/data/bitclust/template.offline/layout +3 -0
  14. data/lib/bitclust/classentry.rb +1 -1
  15. data/lib/bitclust/docentry.rb +1 -1
  16. data/lib/bitclust/functionentry.rb +1 -1
  17. data/lib/bitclust/generators/epub.rb +69 -5
  18. data/lib/bitclust/irb.rb +74 -0
  19. data/lib/bitclust/markdown_bridge.rb +11 -6
  20. data/lib/bitclust/markdown_exporter.rb +311 -0
  21. data/lib/bitclust/markdown_tree.rb +118 -11
  22. data/lib/bitclust/mdcompiler.rb +15 -1
  23. data/lib/bitclust/mdparser.rb +42 -1
  24. data/lib/bitclust/methoddatabase.rb +17 -4
  25. data/lib/bitclust/methodentry.rb +24 -0
  26. data/lib/bitclust/methodsignature.rb +1 -1
  27. data/lib/bitclust/nameutils.rb +13 -0
  28. data/lib/bitclust/preprocessor.rb +4 -37
  29. data/lib/bitclust/rbs_overload_matcher.rb +179 -0
  30. data/lib/bitclust/rbs_sig_importer.rb +276 -0
  31. data/lib/bitclust/rbs_signatures.rb +125 -0
  32. data/lib/bitclust/rdcompiler.rb +12 -1
  33. data/lib/bitclust/remote.rb +405 -0
  34. data/lib/bitclust/runner.rb +7 -10
  35. data/lib/bitclust/screen.rb +6 -1
  36. data/lib/bitclust/search_index_generator.rb +28 -10
  37. data/lib/bitclust/searcher.rb +54 -25
  38. data/lib/bitclust/subcommands/chm_command.rb +1 -1
  39. data/lib/bitclust/subcommands/epub_command.rb +2 -2
  40. data/lib/bitclust/subcommands/extract_command.rb +7 -0
  41. data/lib/bitclust/subcommands/htmlfile_command.rb +7 -0
  42. data/lib/bitclust/subcommands/methods_command.rb +5 -28
  43. data/lib/bitclust/subcommands/query_command.rb +1 -0
  44. data/lib/bitclust/subcommands/rbssig_command.rb +99 -0
  45. data/lib/bitclust/subcommands/server_command.rb +21 -10
  46. data/lib/bitclust/subcommands/setup_command.rb +32 -11
  47. data/lib/bitclust/subcommands/statichtml_command.rb +59 -0
  48. data/lib/bitclust/syntax_highlighter.rb +65 -4
  49. data/lib/bitclust/user_dirs.rb +62 -0
  50. data/lib/bitclust/version.rb +1 -1
  51. data/lib/bitclust-irb.rb +2 -0
  52. data/test/test_chm_command.rb +22 -0
  53. data/test/test_epub_command.rb +60 -0
  54. data/test/test_epub_generator.rb +330 -0
  55. data/test/test_extract_command.rb +45 -0
  56. data/test/test_irb_plugin.rb +212 -0
  57. data/test/test_link_checker.rb +1 -1
  58. data/test/test_markdown_bridge.rb +35 -0
  59. data/test/test_markdown_exporter.rb +260 -0
  60. data/test/test_markdown_tree.rb +144 -0
  61. data/test/test_mdcompiler.rb +47 -5
  62. data/test/test_mdparser.rb +223 -4
  63. data/test/test_methodentry.rb +111 -0
  64. data/test/test_methods_command.rb +32 -0
  65. data/test/test_methodsignature.rb +3 -1
  66. data/test/test_opensearchdescription_screen.rb +39 -0
  67. data/test/test_preprocessor.rb +26 -0
  68. data/test/test_query_command.rb +30 -0
  69. data/test/test_rbs_overload_matcher.rb +170 -0
  70. data/test/test_rbs_sig_importer.rb +237 -0
  71. data/test/test_rbssig_command.rb +186 -0
  72. data/test/test_rdcompiler.rb +99 -1
  73. data/test/test_remote.rb +374 -0
  74. data/test/test_runner.rb +61 -1
  75. data/test/test_search_index_generator.rb +81 -0
  76. data/test/test_searcher.rb +63 -0
  77. data/test/test_server_command.rb +41 -0
  78. data/test/test_setup_command.rb +164 -0
  79. data/test/test_statichtml_command.rb +137 -0
  80. data/test/test_syntax_highlighter.rb +124 -0
  81. data/test/test_user_dirs.rb +62 -0
  82. data/theme/default/js/run.js +14 -0
  83. data/theme/default/js/search_init.js +3 -1
  84. data/theme/default/js/search_page.js +7 -1
  85. data/theme/default/js/version_switcher.js +147 -0
  86. data/theme/default/search.css +21 -0
  87. data/theme/default/style.css +83 -7
  88. data/theme/lillia/style.css +26 -0
  89. metadata +71 -2
  90. 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[[Symbol, String?]]
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
- m = { library: $1 }
128
- gate_stack.each { |kind, ver| m[kind] = ver }
129
- info[:memberships] << m
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
- in_library_block = false # ブロック終端。この行は通常キーとして処理
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[[Symbol, String?]]
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)
@@ -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( align="#{aligns[i]}") : ''
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>'
@@ -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:)や self. プレフィクスが該当
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 && md_version_covers?(version, m[:since], m[:until])
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 since_version && v < Gem::Version.new(since_version)
216
- return false if until_version && v >= Gem::Version.new(until_version)
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 :md_version_covers?
232
+ private :md_gate_covers?
220
233
 
221
234
  def refs
222
235
  @refs ||= RefsDatabase.load(realpath('refs'))
@@ -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