bitclust-core 1.7.0 → 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 (49) hide show
  1. checksums.yaml +4 -4
  2. data/data/bitclust/template/class-index +0 -1
  3. data/data/bitclust/template.epub/class +5 -5
  4. data/data/bitclust/template.epub/class-index +0 -1
  5. data/data/bitclust/template.epub/contents +7 -4
  6. data/data/bitclust/template.epub/layout +4 -4
  7. data/data/bitclust/template.offline/class-index +0 -1
  8. data/lib/bitclust/generators/epub.rb +69 -5
  9. data/lib/bitclust/irb.rb +14 -8
  10. data/lib/bitclust/markdown_bridge.rb +11 -6
  11. data/lib/bitclust/markdown_tree.rb +118 -11
  12. data/lib/bitclust/methoddatabase.rb +17 -4
  13. data/lib/bitclust/nameutils.rb +13 -0
  14. data/lib/bitclust/preprocessor.rb +0 -36
  15. data/lib/bitclust/remote.rb +405 -0
  16. data/lib/bitclust/runner.rb +5 -10
  17. data/lib/bitclust/screen.rb +2 -1
  18. data/lib/bitclust/searcher.rb +43 -24
  19. data/lib/bitclust/subcommands/chm_command.rb +1 -1
  20. data/lib/bitclust/subcommands/epub_command.rb +2 -2
  21. data/lib/bitclust/subcommands/extract_command.rb +7 -0
  22. data/lib/bitclust/subcommands/htmlfile_command.rb +7 -0
  23. data/lib/bitclust/subcommands/methods_command.rb +5 -28
  24. data/lib/bitclust/subcommands/query_command.rb +1 -0
  25. data/lib/bitclust/subcommands/server_command.rb +21 -10
  26. data/lib/bitclust/subcommands/setup_command.rb +28 -10
  27. data/lib/bitclust/syntax_highlighter.rb +65 -4
  28. data/lib/bitclust/user_dirs.rb +62 -0
  29. data/lib/bitclust/version.rb +1 -1
  30. data/test/test_chm_command.rb +22 -0
  31. data/test/test_epub_command.rb +60 -0
  32. data/test/test_epub_generator.rb +330 -0
  33. data/test/test_extract_command.rb +45 -0
  34. data/test/test_irb_plugin.rb +100 -6
  35. data/test/test_markdown_bridge.rb +35 -0
  36. data/test/test_markdown_tree.rb +144 -0
  37. data/test/test_mdparser.rb +39 -0
  38. data/test/test_methods_command.rb +32 -0
  39. data/test/test_opensearchdescription_screen.rb +39 -0
  40. data/test/test_query_command.rb +30 -0
  41. data/test/test_remote.rb +374 -0
  42. data/test/test_runner.rb +61 -1
  43. data/test/test_searcher.rb +63 -0
  44. data/test/test_server_command.rb +41 -0
  45. data/test/test_setup_command.rb +143 -0
  46. data/test/test_syntax_highlighter.rb +124 -0
  47. data/test/test_user_dirs.rb +62 -0
  48. metadata +37 -2
  49. data/lib/bitclust/ridatabase.rb +0 -359
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9f02fb9cdddf44a8c30df3e0e9d2fa51159db37abb39220e090a5bc94b61c9cf
4
- data.tar.gz: ceffcab8cd2c8bdd34d555e104cd8ac0d774f89b4e42296ff26b64f16570ccbd
3
+ metadata.gz: 299762c07571e6e3b72eebe00c9d1e1d520ef9f5c3b68de759adc0c173d2ee18
4
+ data.tar.gz: beab538e8caec8fb1f7265808adceaa5574b22793dfb11a521651687204416d1
5
5
  SHA512:
6
- metadata.gz: 66e1b74b7651e3c12be4d9f55facd88ab6a29def01fae1deb67670191d861a070d95a05c21309e61edf7f43ee7a6f3a948aff410a3bc400eb4b96b5ec6822634
7
- data.tar.gz: 680e2945c4225e31b0417495c72945a90824105a7af7d9151a006a30d690fc7b36bd04b9fd4427fad46ab6b1513517c7c052ce49c9ec7913ed3b155402f9fa20
6
+ metadata.gz: a4d23713bd97acaa6cb98de51e95766c87f09329c0dba4c2b42152838023ec4ce681f1c7f38db582b7e3e20809a2749152da84a74399741dc113ad9251f0875d
7
+ data.tar.gz: ce441c5b5a4abc9cbef8a46bf9efa2a57a5cae8bdd827c9edf7af095e60bea9c95a85ceee54baa73e850b0a7a4682b1d5afd41ce41e386040e78761ca88038fd
@@ -29,5 +29,4 @@
29
29
  end
30
30
  headline_pop
31
31
  %>
32
- </ul>
33
32
 
@@ -24,20 +24,20 @@
24
24
  <% end %>
25
25
 
26
26
  <% unless @entry.extended.empty? %>
27
- <br>extend: <%= @entry.extended.map {|c| class_link(c.name) }.join(', ') %>
27
+ <br />extend: <%= @entry.extended.map {|c| class_link(c.name) }.join(', ') %>
28
28
  <% end %>
29
29
  <% unless @entry.aliases.empty? %>
30
- <br>aliases: <%=h @entry.aliases.map{|c| c.name}.join(', ') %>
30
+ <br />aliases: <%=h @entry.aliases.map{|c| c.name}.join(', ') %>
31
31
  <% end %>
32
32
  <% unless @entry.dynamically_included.empty? %>
33
- <br> dynamic include:
33
+ <br /> dynamic include:
34
34
  <%= @entry.dynamically_included.map{|m|
35
35
  class_link(m.name) + " (by " + library_link(m.library.name) + ")"
36
36
  }.join(", ")
37
37
  %>
38
38
  <% end %>
39
39
  <% unless @entry.dynamically_extended.empty? %>
40
- <br> dynamic extend:
40
+ <br /> dynamic extend:
41
41
  <%= @entry.dynamically_extended.map{|m|
42
42
  class_link(m.name) + " (by " + library_link(m.library.name) + ")"
43
43
  }.join(", ")
@@ -72,7 +72,7 @@
72
72
  entries.each do |m|
73
73
  m.names.each do |mname|
74
74
  %>
75
- <a href="#<%= m.index_id %>"><%= "#{prefix}#{mname}" %></a>
75
+ <a href="#<%= m.index_id %>"><%= escape_html "#{prefix}#{mname}" %></a>
76
76
  <%
77
77
  end
78
78
  end
@@ -25,4 +25,3 @@
25
25
  end
26
26
  headline_pop
27
27
  %>
28
- </ul>
@@ -8,15 +8,18 @@
8
8
  </metadata>
9
9
  <manifest>
10
10
  <item id="toc" properties="nav" href="nav.xhtml" media-type="application/xhtml+xml" />
11
- <item id="index" href="OEBPS/doc/index.html" media-type="application/xhtml+xml" />
12
- <% items.each do |item| -%>
13
- <item id="<%= item[:id] %>" href="<%= item[:path] %>" media-type="application/xhtml+xml" />
11
+ <item id="index" href="OEBPS/doc/index.xhtml" media-type="application/xhtml+xml" />
12
+ <% class_items.each do |item| -%>
13
+ <item id="<%= item[:id] %>" href="<%= item[:path] %>" media-type="<%= item[:media_type] %>" />
14
+ <% end -%>
15
+ <% other_items.each do |item| -%>
16
+ <item id="<%= item[:id] %>" href="<%= item[:path] %>" media-type="<%= item[:media_type] %>" />
14
17
  <% end -%>
15
18
  </manifest>
16
19
  <spine>
17
20
  <itemref idref="toc" />
18
21
  <itemref idref="index" />
19
- <% items.each do |item| -%>
22
+ <% class_items.each do |item| -%>
20
23
  <itemref idref="<%= item[:id] %>" />
21
24
  <% end -%>
22
25
  </spine>
@@ -2,10 +2,10 @@
2
2
  <!DOCTYPE html>
3
3
  <html lang="ja-JP">
4
4
  <head>
5
- <meta http-equiv="Content-Type" content="text/html; charset=<%=h charset() %>">
6
- <meta http-equiv="Content-Language" content="ja-JP">
7
- <link rel="stylesheet" type="text/css" href="<%=h css_url() %>">
8
- <link rel="icon" type="image/png" href="<%=h favicon_url() %>">
5
+ <meta http-equiv="Content-Type" content="text/html; charset=<%=h charset() %>" />
6
+ <meta http-equiv="Content-Language" content="ja-JP" />
7
+ <link rel="stylesheet" type="text/css" href="<%=h css_url() %>" />
8
+ <link rel="icon" type="image/png" href="<%=h favicon_url() %>" />
9
9
  <title><%=h @title %> (Ruby <%=h ruby_version %>)</title>
10
10
  </head>
11
11
  <body>
@@ -54,5 +54,4 @@
54
54
  end
55
55
  headline_pop
56
56
  %>
57
- </ul>
58
57
  </main>
@@ -24,11 +24,17 @@ module BitClust
24
24
 
25
25
  CONTENTS_DIR_NAME = 'OEBPS'
26
26
 
27
+ # StatichtmlCommand が出力ルートに書く 2 行のリダイレクト用スタブ
28
+ # (<meta http-equiv="refresh"> と <a>)。ルート要素が無く XHTML として
29
+ # 整形式でない。EPUB の入口は nav.xhtml なのでスタブは同梱せず削除する
30
+ INDEX_STUB_NAME = 'index.xhtml'
31
+
27
32
  def generate
28
33
  make_epub_directory do |epub_directory|
29
34
  contents_directory = epub_directory + CONTENTS_DIR_NAME
30
35
  copy_static_files(epub_directory)
31
36
  generate_xhtml_files(contents_directory)
37
+ remove_index_stub(contents_directory)
32
38
  generate_contents_opf(epub_directory)
33
39
  pack_epub(epub_directory)
34
40
  end
@@ -71,15 +77,45 @@ module BitClust
71
77
  cmd.exec(argv, options)
72
78
  end
73
79
 
80
+ # manifest に id="index" で固定的に載せる doc/index ページの href
81
+ # (epub_directory 相対)。other_items からは除いて二重掲載を避ける
82
+ INDEX_PATH = Pathname.new("#{CONTENTS_DIR_NAME}/doc/index.xhtml")
83
+
84
+ MEDIA_TYPES = {
85
+ '.xhtml' => 'application/xhtml+xml',
86
+ '.css' => 'text/css',
87
+ '.js' => 'application/javascript',
88
+ '.png' => 'image/png',
89
+ '.jpg' => 'image/jpeg',
90
+ '.jpeg' => 'image/jpeg',
91
+ '.gif' => 'image/gif',
92
+ '.svg' => 'image/svg+xml',
93
+ }.freeze
94
+
74
95
  def generate_contents_opf(epub_directory)
75
- items = [] #: Array[{:id => String, :path => Pathname}]
96
+ class_items = [] #: Array[{:id => String, :path => Pathname, :media_type => String}]
76
97
  glob_relative_path(epub_directory, "#{CONTENTS_DIR_NAME}/class/*.xhtml").each do |path|
77
- items << {
78
- :id => decodename_package(path.basename(".*").to_s),
79
- :path => path
98
+ class_items << {
99
+ :id => "class-#{decodename_package(path.basename(".*").to_s)}",
100
+ :path => path,
101
+ :media_type => media_type_for(path),
80
102
  }
81
103
  end
82
- items.sort_by!{|item| item[:path] }
104
+ class_items.sort_by!{|item| item[:path] }
105
+
106
+ excluded_paths = class_items.map{|item| item[:path] } + [INDEX_PATH]
107
+ other_items = [] #: Array[{:id => String, :path => Pathname, :media_type => String}]
108
+ oebps_file_paths(epub_directory).each do |path|
109
+ next if excluded_paths.include?(path)
110
+
111
+ other_items << {
112
+ :id => manifest_id_for(path),
113
+ :path => path,
114
+ :media_type => media_type_for(path),
115
+ }
116
+ end
117
+ other_items.sort_by!{|item| item[:path] }
118
+
83
119
  template = (@templatedir + "contents").read
84
120
  if ::ERB.instance_method(:initialize).parameters.last.first == :key
85
121
  erb = ::ERB.new(template, trim_mode: '-')
@@ -92,6 +128,34 @@ module BitClust
92
128
  end
93
129
  end
94
130
 
131
+ # OEBPS 配下の通常ファイル全部(method/library/doc/function ページと、
132
+ # statichtml がコピーした CSS・画像)を epub_directory 相対で返す。
133
+ # class ページも含む(呼び出し側で class_items と重複排除する)
134
+ def oebps_file_paths(epub_directory)
135
+ glob_relative_path(epub_directory, "#{CONTENTS_DIR_NAME}/**/*").select do |relative_path|
136
+ (epub_directory + relative_path).file?
137
+ end
138
+ end
139
+
140
+ def media_type_for(path)
141
+ MEDIA_TYPES[path.extname.downcase] || 'application/octet-stream'
142
+ end
143
+
144
+ # class ページ以外("OEBPS/method/Array/i/each.xhtml" 等)の manifest
145
+ # item id。XML の NCName として妥当で一意になるよう、使えない文字は "-" に
146
+ # 置き換え、ディレクトリ境界は "--" にして encodename 由来の "-" と
147
+ # 区別する
148
+ def manifest_id_for(path)
149
+ relative = path.to_s.sub(%r{\A#{Regexp.escape(CONTENTS_DIR_NAME)}/}, "")
150
+ segments = relative.split("/").map{|segment| segment.gsub(/[^A-Za-z0-9_.-]/, "-") }
151
+ "item-#{segments.join('--')}"
152
+ end
153
+
154
+ def remove_index_stub(contents_directory)
155
+ stub_path = contents_directory + INDEX_STUB_NAME
156
+ FileUtils.rm_f(stub_path.to_s, :verbose => @verbose) if stub_path.file?
157
+ end
158
+
95
159
  def pack_epub(epub_directory)
96
160
  epub_filename = @outputdir + @filename
97
161
  Dir.chdir(epub_directory.to_s) do
data/lib/bitclust/irb.rb CHANGED
@@ -2,8 +2,9 @@
2
2
  #
3
3
  # irb からるりま(Ruby リファレンスマニュアル)を引く refe コマンド。
4
4
  # ~/.irbrc に `require "bitclust/irb"` と書くと irb に `refe` コマンドが
5
- # 登録される。検索対象の DB は refe コマンドと同じ場所
6
- # (bitclust setup が作る ~/.bitclust/config)から探す。
5
+ # 登録される。検索対象の DB は refe コマンドと同じ場所(bitclust setup が
6
+ # 作る設定ファイル)から探し、DB が無ければ docs.ruby-lang.org の検索索引と
7
+ # Markdown 配信から取得する(BitClust::Remote)。
7
8
 
8
9
  require 'stringio'
9
10
  require 'bitclust'
@@ -18,15 +19,18 @@ module BitClust
18
19
  refe String#gsub インスタンスメソッド
19
20
  refe Array.new 特異メソッド
20
21
  refe Comparable クラス・モジュール
21
- refe printf 名前だけでの検索
22
+ refe each 名前だけでの検索(該当が複数なら一覧)
22
23
 
23
- DB が無い場合は `bitclust setup` で作成してください。
24
+ DB が無い場合は docs.ruby-lang.org から取得します(1 日キャッシュ)。
25
+ `bitclust setup` で DB を作ると手元で検索できます。
24
26
  USAGE
25
27
 
26
28
  # pattern を検索して整形済みテキストを io へ書く。db が nil なら
27
- # 既定の場所から探す。検索の失敗は例外にせず io へメッセージを書く
28
- # (irb セッションを止めないため)
29
- def self.lookup(pattern, io: $stdout, db: nil)
29
+ # 既定の場所から探し、そこにも無ければ remote(省略時は
30
+ # Remote.default。nil ならフォールバックしない)で検索する。
31
+ # 検索の失敗は例外にせず io へメッセージを書く(irb セッションを
32
+ # 止めないため)
33
+ def self.lookup(pattern, io: $stdout, db: nil, remote: Remote.default)
30
34
  words = pattern.to_s.split
31
35
  if words.empty?
32
36
  io.puts USAGE
@@ -35,7 +39,9 @@ module BitClust
35
39
  view = TerminalView.new(Plain.new,
36
40
  { describe_all: false, line: false, encoding: nil },
37
41
  io: io)
38
- Searcher.new.run_query(db, words, view)
42
+ searcher = Searcher.new
43
+ searcher.remote = remote
44
+ searcher.run_query(db, words, view)
39
45
  rescue BitClust::UserError => err
40
46
  io.puts err.message
41
47
  end
@@ -134,12 +134,17 @@ module BitClust
134
134
  reopen_only = members[path][:kinds].all? { |kind, _| %w[reopen redefine].include?(kind) }
135
135
  [reopen_only ? 1 : 0, path]
136
136
  end
137
- "\n" + sorted.map { |path|
138
- m = members[path][:memberships].find { |mm| mm[:library] == libname } || raise
139
- inc = "\#@include(#{relative(emitted[path], base)})\n"
140
- inc = "\#@since #{m[:since]}\n#{inc}\#@end\n" if m[:since]
141
- inc = "\#@until #{m[:until]}\n#{inc}\#@end\n" if m[:until]
142
- inc
137
+ # 同じライブラリへの membership が複数ある(#%version A...B の #%else =
138
+ # A 未満と B 以上)ときは、それぞれのゲートで include サイトを繰り返す
139
+ "\n" + sorted.flat_map { |path|
140
+ members[path][:memberships].select { |mm| mm[:library] == libname }.map { |m|
141
+ inc = "\#@include(#{relative(emitted[path], base)})\n"
142
+ inc = "\#@since #{m[:since]}\n#{inc}\#@end\n" if m[:since]
143
+ inc = "\#@until #{m[:until]}\n#{inc}\#@end\n" if m[:until]
144
+ inc = "\#@if (version == \"#{m[:version]}\")\n#{inc}\#@end\n" if m[:version]
145
+ (m[:except] || []).each { |x| inc = "\#@if (version != \"#{x}\")\n#{inc}\#@end\n" }
146
+ inc
147
+ }
143
148
  }.join
144
149
  end
145
150
 
@@ -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)
@@ -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'))
@@ -222,6 +222,19 @@ module BitClust
222
222
  Gem::Version.new(version) >= Gem::Version.new('4.0') ? '?.' : mark
223
223
  end
224
224
 
225
+ # refe の検索パターン("String#gsub"・"Array.new"・"Kernel.#printf" や
226
+ # "," を "#" の代用にした "String,gsub" など)を [クラス名, typemark,
227
+ # メソッド名] に分ける。区切りは反転文字列上で探す。"?." は 4.0 以降の
228
+ # module function の表示形式(反転すると ".?")で、内部表記の ".#" に
229
+ # 正規化する
230
+ def parse_method_spec_pattern(pat)
231
+ _m, _t, _c = pat.reverse.split(/([\#,]\.|\.[\#,]|\.\?|[\#\.\,])/, 2)
232
+ c = (_c || raise).reverse
233
+ t = (_t || raise).tr(',', '#').sub(/\#\.|\.\?/, '.#')
234
+ m = (_m || raise).reverse
235
+ return c, t, m
236
+ end
237
+
225
238
  def functionname?(n)
226
239
  /\A\w+\z/ =~ n ? true : false
227
240
  end
@@ -343,40 +343,4 @@ module BitClust
343
343
  end
344
344
  end
345
345
  end
346
-
347
- # Used by tools/stattodo.rb
348
- class LineCollector < LineFilter
349
-
350
- def LineCollector.process(path)
351
- fopen(path) {|f|
352
- return wrap(f).to_a
353
- }
354
- end
355
-
356
- def LineCollector.wrap(f)
357
- new(LineStream.new(f))
358
- end
359
-
360
- private
361
-
362
- def next_line(f)
363
- while line = f.gets
364
- if /\A\#[@%]include\s*\((.*?)\)/ =~ line
365
- begin
366
- file = ($1 || raise).strip
367
- basedir = File.dirname(line.location.file || raise)
368
- @buf.concat LineCollector.process("#{basedir}/#{file}")
369
- rescue Errno::ENOENT => _err
370
- raise WrongInclude, "#{line.location}: \#@include'ed file not exist: #{file}"
371
- end
372
- else
373
- @buf.push line
374
- end
375
- break unless @buf.empty?
376
- end
377
- @buf.shift
378
- end
379
-
380
- end
381
-
382
346
  end