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,288 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
#
|
|
3
|
+
# bitclust/search_index_generator.rb
|
|
4
|
+
#
|
|
5
|
+
# Builds a client-side search index compatible with RDoc's Aliki theme.
|
|
6
|
+
#
|
|
7
|
+
# The index is a flat array of entries:
|
|
8
|
+
#
|
|
9
|
+
# { name:, full_name:, type:, path: }
|
|
10
|
+
#
|
|
11
|
+
# Singleton methods and module functions (full_name holding a literal "."/
|
|
12
|
+
# ".#"/"?.", e.g. "File.open", "Kernel.#open", "Kernel?.open" -- see #250)
|
|
13
|
+
# additionally carry a +match_name+: full_name with "?." folded to ".#" and
|
|
14
|
+
# every "." turned into "::". This mirrors the "." -> "::" rewrite the
|
|
15
|
+
# vendored Aliki ranker's parseQuery() applies to the *query* text (RDoc's
|
|
16
|
+
# own full_names use "::" for class methods), so search_init.js/search_page.js
|
|
17
|
+
# can feature-detect-wrap computeScore() to compare like-for-like without
|
|
18
|
+
# ever touching the displayed full_name. See bitclust#279.
|
|
19
|
+
#
|
|
20
|
+
# { name:, full_name:, type:, path:, match_name: }
|
|
21
|
+
#
|
|
22
|
+
# serialized as a JavaScript assignment so it can be loaded over file://
|
|
23
|
+
# without tripping the browser's CORS check on JSON files:
|
|
24
|
+
#
|
|
25
|
+
# var search_data = { "index": [ ... ] };
|
|
26
|
+
#
|
|
27
|
+
# This mirrors the static layout produced by StatichtmlCommand so that the
|
|
28
|
+
# +path+ of each entry points at the file actually generated there.
|
|
29
|
+
#
|
|
30
|
+
|
|
31
|
+
require 'json'
|
|
32
|
+
require 'bitclust/nameutils'
|
|
33
|
+
|
|
34
|
+
module BitClust
|
|
35
|
+
class SearchIndexGenerator
|
|
36
|
+
include NameUtils
|
|
37
|
+
|
|
38
|
+
# Maps BitClust method typenames onto the type vocabulary understood by
|
|
39
|
+
# Aliki's searcher (class/module/constant/class_method/instance_method).
|
|
40
|
+
# Module functions are callable as singleton methods, so they are indexed
|
|
41
|
+
# as class methods; special variables have no Aliki equivalent and keep
|
|
42
|
+
# their own label (the searcher only uses the type for ranking).
|
|
43
|
+
TYPENAME_TO_SEARCH_TYPE = {
|
|
44
|
+
singleton_method: 'class_method',
|
|
45
|
+
module_function: 'class_method',
|
|
46
|
+
instance_method: 'instance_method',
|
|
47
|
+
constant: 'constant',
|
|
48
|
+
special_variable: 'variable',
|
|
49
|
+
}.freeze
|
|
50
|
+
|
|
51
|
+
# Doc pages (manual/doc/**/*.md) are prose, so language keywords such as
|
|
52
|
+
# "defined?", "undef" or "alias" (see rurema/doctree#2352) are never
|
|
53
|
+
# indexed as a class/method/library. They do, however, live at their own
|
|
54
|
+
# {#id}-anchored heading within a doc page (e.g. spec/def.md's "defined?"
|
|
55
|
+
# section). Rather than hardcoding a keyword list, scan every doc page for
|
|
56
|
+
# ATX headings carrying an explicit {#id} anchor -- the same anchors
|
|
57
|
+
# [ref:d:...] cross references already rely on -- and index each one
|
|
58
|
+
# individually so a keyword search can jump straight to its section.
|
|
59
|
+
# Headings without an explicit anchor aren't linkable, so they are
|
|
60
|
+
# skipped; a "# comment" that happens to open a line inside a fenced code
|
|
61
|
+
# block is not a heading at all, so fenced regions are skipped wholesale.
|
|
62
|
+
ANCHORED_HEADING_RE = /\A(\#{1,6})[ \t]+(.*?)(?:[ \t]+\{#([\w-]+)\})?[ \t]*\z/.freeze
|
|
63
|
+
FENCE_START_RE = /\A`{3,}/.freeze
|
|
64
|
+
|
|
65
|
+
def initialize(suffix: '.html', fs_casesensitive: false)
|
|
66
|
+
@suffix = suffix
|
|
67
|
+
@fs_casesensitive = fs_casesensitive
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Builds the search index as an array of entry hashes.
|
|
71
|
+
# +db+ is a MethodDatabase; +fdb+ is an optional FunctionDatabase (C API).
|
|
72
|
+
def build_index(db, fdb = nil)
|
|
73
|
+
index = [] #: Array[entry]
|
|
74
|
+
index.concat(class_entries(db))
|
|
75
|
+
index.concat(method_entries(db))
|
|
76
|
+
index.concat(library_entries(db))
|
|
77
|
+
index.concat(document_entries(db))
|
|
78
|
+
index.concat(document_heading_entries(db))
|
|
79
|
+
index.concat(function_entries(fdb)) if fdb
|
|
80
|
+
index
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Serializes the index as an Aliki-compatible +search_data.js+ body.
|
|
84
|
+
def to_js(db, fdb = nil)
|
|
85
|
+
"var search_data = #{JSON.generate(index: build_index(db, fdb))};"
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Merges per-version indexes (an array of [version, index] pairs) into a
|
|
89
|
+
# single index whose entries carry a +versions+ array, for the standalone
|
|
90
|
+
# cross-version search page. Entries are considered the same when all of
|
|
91
|
+
# name/full_name/type/path match. Versions are sorted numerically ("3.10"
|
|
92
|
+
# sorts after "3.4"), and entry order is the first appearance while
|
|
93
|
+
# scanning versions in ascending order — independent of the caller's
|
|
94
|
+
# argument order, so the generated file diffs stay stable.
|
|
95
|
+
#
|
|
96
|
+
# A module function's full_name is spelled ".#" by pre-4.0 databases and
|
|
97
|
+
# "?." by 4.0+ ones (bitclust#250), which would split it into two rows —
|
|
98
|
+
# confusing on a page that lists every version side by side. Fold the
|
|
99
|
+
# display to the current "?." spelling here, so both spellings collapse
|
|
100
|
+
# into one row with a unified versions list (name/type/path/match_name
|
|
101
|
+
# are spelling-independent already). Only entries carrying a match_name
|
|
102
|
+
# (dot-qualified methods) are folded: a prose heading whose *text*
|
|
103
|
+
# happens to contain ".#" must keep matching its page verbatim. The
|
|
104
|
+
# single-version in-page search keeps each version's own spelling —
|
|
105
|
+
# build_index is not affected. See the report in bitclust#279.
|
|
106
|
+
def self.merge(version_indexes)
|
|
107
|
+
merged = {} #: Hash[Array[String?], merged_entry]
|
|
108
|
+
sorted = version_indexes.sort_by { |version, _| Gem::Version.new(version) }
|
|
109
|
+
sorted.each do |version, index|
|
|
110
|
+
index.each do |e|
|
|
111
|
+
if e[:match_name] && e[:full_name].include?('.#')
|
|
112
|
+
e = e.merge(full_name: e[:full_name].gsub('.#', '?.'))
|
|
113
|
+
end
|
|
114
|
+
key = e.values_at(:name, :full_name, :type, :path)
|
|
115
|
+
entry = (merged[key] ||= e.merge(versions: []))
|
|
116
|
+
entry[:versions] << version
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
merged.values
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Serializes a merged multi-version index as a +search_data.js+ body.
|
|
123
|
+
def self.merged_js(version_indexes)
|
|
124
|
+
"var search_data = #{JSON.generate(index: merge(version_indexes))};"
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
private
|
|
128
|
+
|
|
129
|
+
def class_entries(db)
|
|
130
|
+
db.classes.sort.reject(&:dummy?).map do |c|
|
|
131
|
+
{
|
|
132
|
+
name: c.name,
|
|
133
|
+
full_name: c.name,
|
|
134
|
+
type: c.type.to_s,
|
|
135
|
+
path: "class/#{encode(c.name)}#{@suffix}",
|
|
136
|
+
}
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def method_entries(db)
|
|
141
|
+
seen = {} #: Hash[String, bool]
|
|
142
|
+
result = [] #: Array[entry]
|
|
143
|
+
db.methods.each do |entry|
|
|
144
|
+
next if entry.undefined?
|
|
145
|
+
|
|
146
|
+
type = TYPENAME_TO_SEARCH_TYPE[entry.typename]
|
|
147
|
+
next unless type
|
|
148
|
+
|
|
149
|
+
cname = entry.klass.name
|
|
150
|
+
# bitclust#250: 検索結果に出る表示ラベル(name/full_name)は 4.0
|
|
151
|
+
# 以降のドキュメントでは module function を "?." で表示する。path
|
|
152
|
+
# は識別子なので typechar(常に不変)のまま
|
|
153
|
+
tmark = entry.display_typemark
|
|
154
|
+
tchar = entry.typechar
|
|
155
|
+
entry.names.each do |mname|
|
|
156
|
+
path = "method/#{encode(cname)}/#{tchar}/#{encode(mname)}#{@suffix}"
|
|
157
|
+
next if seen[path]
|
|
158
|
+
|
|
159
|
+
seen[path] = true
|
|
160
|
+
if tmark == '$'
|
|
161
|
+
# A special variable has no owning class to qualify it; its "$"
|
|
162
|
+
# sigil is the only thing distinguishing it, so keep it in +name+
|
|
163
|
+
# too (not just +full_name+) or a "$;"-style query can't match it.
|
|
164
|
+
name = full_name = tmark + mname
|
|
165
|
+
result << { name: name, full_name: full_name, type: type, path: path }
|
|
166
|
+
else
|
|
167
|
+
name = mname
|
|
168
|
+
full_name = cname + tmark + mname
|
|
169
|
+
item = { name: name, full_name: full_name, type: type, path: path } #: entry
|
|
170
|
+
if tmark.include?('.')
|
|
171
|
+
# bitclust#279: singleton methods (".") and module functions
|
|
172
|
+
# (".#"/"?.") keep a literal "." in full_name for display, but
|
|
173
|
+
# the vendored ranker's parseQuery() rewrites every "." in the
|
|
174
|
+
# *query* to "::". match_name is the same rewrite applied to
|
|
175
|
+
# this entry, so the search_init.js/search_page.js computeScore
|
|
176
|
+
# wrap can compare like-for-like. See the file header comment.
|
|
177
|
+
item[:match_name] = full_name.gsub('?.', '.#').gsub('.', '::')
|
|
178
|
+
end
|
|
179
|
+
result << item
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
result
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def library_entries(db)
|
|
187
|
+
db.libraries.sort.map do |lib|
|
|
188
|
+
{
|
|
189
|
+
name: lib.name,
|
|
190
|
+
full_name: lib.name,
|
|
191
|
+
type: 'library',
|
|
192
|
+
path: "library/#{encode(lib.name)}#{@suffix}",
|
|
193
|
+
}
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def document_entries(db)
|
|
198
|
+
db.docs.map do |doc|
|
|
199
|
+
slug = doc.name
|
|
200
|
+
title = doc.title
|
|
201
|
+
# Prose pages are identified by a Japanese title, so index the title
|
|
202
|
+
# (for human queries) together with the slug (for identifier queries).
|
|
203
|
+
label = (title && !title.empty? && title != slug) ? "#{title} (#{slug})" : slug
|
|
204
|
+
{
|
|
205
|
+
name: label,
|
|
206
|
+
full_name: label,
|
|
207
|
+
type: 'document',
|
|
208
|
+
path: "doc/#{encode(slug)}#{@suffix}",
|
|
209
|
+
}
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
# One entry per {#id}-anchored heading found in a doc page's body, so
|
|
214
|
+
# that e.g. "defined?" or "alias" (headings inside spec/def.md) surface
|
|
215
|
+
# directly instead of only the whole "クラス/メソッドの定義" page. See
|
|
216
|
+
# ANCHORED_HEADING_RE above for why this only looks at doc pages (prose,
|
|
217
|
+
# not method/class references) and only at explicitly anchored headings.
|
|
218
|
+
def document_heading_entries(db)
|
|
219
|
+
seen = {} #: Hash[String, bool]
|
|
220
|
+
result = [] #: Array[entry]
|
|
221
|
+
db.docs.each do |doc|
|
|
222
|
+
source = doc.source
|
|
223
|
+
next unless source
|
|
224
|
+
page_title = (doc.title && !doc.title.empty?) ? doc.title : doc.name
|
|
225
|
+
base_path = "doc/#{encode(doc.name)}#{@suffix}"
|
|
226
|
+
each_anchored_heading(source) do |label, id|
|
|
227
|
+
path = "#{base_path}##{id}"
|
|
228
|
+
next if seen[path]
|
|
229
|
+
|
|
230
|
+
seen[path] = true
|
|
231
|
+
result << {
|
|
232
|
+
name: label,
|
|
233
|
+
full_name: "#{label} (#{page_title})",
|
|
234
|
+
type: 'heading',
|
|
235
|
+
path: path,
|
|
236
|
+
}
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
result
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
# Yields [label, id] for each ATX heading in +source+ that carries an
|
|
243
|
+
# explicit {#id} anchor, skipping the contents of fenced code blocks
|
|
244
|
+
# (```lang ... ```) so an in-sample "# comment" line can never be
|
|
245
|
+
# mistaken for a heading.
|
|
246
|
+
def each_anchored_heading(source)
|
|
247
|
+
fence = nil #: Integer?
|
|
248
|
+
source.each_line do |raw|
|
|
249
|
+
line = raw.chomp
|
|
250
|
+
if fence
|
|
251
|
+
# Matches MDCompiler#code_fence's own terminator: the closing fence
|
|
252
|
+
# must be exactly as long as the opening one.
|
|
253
|
+
fence = nil if /\A`{#{fence}}[ \t]*\z/ =~ line
|
|
254
|
+
next
|
|
255
|
+
end
|
|
256
|
+
if m = FENCE_START_RE.match(line)
|
|
257
|
+
fence = (m[0] || raise).size
|
|
258
|
+
next
|
|
259
|
+
end
|
|
260
|
+
next unless m = ANCHORED_HEADING_RE.match(line)
|
|
261
|
+
|
|
262
|
+
id = m[3] or next
|
|
263
|
+
yield clean_heading_label(m[2] || raise), id
|
|
264
|
+
end
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
# Headings may carry inline Markdown (code spans, escaped punctuation);
|
|
268
|
+
# strip that down to plain text for the index entry.
|
|
269
|
+
def clean_heading_label(label)
|
|
270
|
+
label.gsub(/`([^`]*)`/, '\1').gsub(/\\(.)/, '\1')
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
def function_entries(fdb)
|
|
274
|
+
fdb.functions.sort.map do |func|
|
|
275
|
+
{
|
|
276
|
+
name: func.name,
|
|
277
|
+
full_name: func.name,
|
|
278
|
+
type: 'function',
|
|
279
|
+
path: "function/#{func.name}#{@suffix}",
|
|
280
|
+
}
|
|
281
|
+
end
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
def encode(str)
|
|
285
|
+
@fs_casesensitive ? encodename_url(str) : encodename_fs(str)
|
|
286
|
+
end
|
|
287
|
+
end
|
|
288
|
+
end
|
data/lib/bitclust/searcher.rb
CHANGED
|
@@ -40,8 +40,18 @@ module BitClust
|
|
|
40
40
|
parser.banner = "Usage: #{@name} <pattern>"
|
|
41
41
|
unless cmd == 'bitclust'
|
|
42
42
|
parser.on('-d', '--database=URL', "Database location (default: #{dblocation_name()})") {|loc|
|
|
43
|
-
|
|
44
|
-
|
|
43
|
+
@dblocation =
|
|
44
|
+
if windows_drive_path?(loc)
|
|
45
|
+
# "G:/Users/foo/.bitclust/db-2.2.0" のような Windows の
|
|
46
|
+
# ドライブレター付きパスは "/:/ =~ loc" が真になってしまい
|
|
47
|
+
# (":" がドライブレターの区切りに含まれるため)、既に URL
|
|
48
|
+
# 形式だと誤認されて file:// を付けずに URI.parse に渡って
|
|
49
|
+
# しまう。drive_path_uri で組み立てる
|
|
50
|
+
drive_path_uri(loc)
|
|
51
|
+
else
|
|
52
|
+
url = (/:/ =~ loc) ? loc : "file://#{File.expand_path(loc)}"
|
|
53
|
+
URI.parse(url)
|
|
54
|
+
end
|
|
45
55
|
}
|
|
46
56
|
parser.on('--server=URL', 'Spawns BitClust database server and listen URL. Requires --database option with local path.') {|url|
|
|
47
57
|
require 'bitclust/server' # require here for speed
|
|
@@ -92,6 +102,14 @@ module BitClust
|
|
|
92
102
|
|
|
93
103
|
def exec(argv, options = {})
|
|
94
104
|
db = nil
|
|
105
|
+
prefix = options[:prefix]
|
|
106
|
+
if prefix
|
|
107
|
+
if options[:capi]
|
|
108
|
+
db = BitClust::FunctionDatabase.new(prefix)
|
|
109
|
+
else
|
|
110
|
+
db = BitClust::MethodDatabase.new(prefix)
|
|
111
|
+
end
|
|
112
|
+
end
|
|
95
113
|
if @listen_url
|
|
96
114
|
spawn_server db
|
|
97
115
|
else
|
|
@@ -107,7 +125,7 @@ module BitClust
|
|
|
107
125
|
|
|
108
126
|
def server_mode_check(argv)
|
|
109
127
|
if @dblocation
|
|
110
|
-
unless @dblocation
|
|
128
|
+
unless @dblocation&.scheme == 'file'
|
|
111
129
|
$stderr.puts "Give local path to --database option on server mode"
|
|
112
130
|
exit 1
|
|
113
131
|
end
|
|
@@ -140,19 +158,19 @@ module BitClust
|
|
|
140
158
|
#compiler = RDCompiler::Text.new
|
|
141
159
|
compiler = Plain.new
|
|
142
160
|
@view = TerminalView.new(compiler,
|
|
143
|
-
{:describe_all => @describe_all,
|
|
161
|
+
{:describe_all => @describe_all,
|
|
144
162
|
:line => @linep,
|
|
145
163
|
:encoding => @encoding})
|
|
146
164
|
end
|
|
147
165
|
|
|
148
166
|
def spawn_server(db)
|
|
149
|
-
Server.new(new_local_database(db)).listen @listen_url, @foreground
|
|
167
|
+
Server.new(new_local_database(db)).listen (@listen_url || raise), @foreground
|
|
150
168
|
end
|
|
151
169
|
|
|
152
170
|
def new_local_database(db)
|
|
153
171
|
return db if db
|
|
154
|
-
path = @dblocation ? @dblocation
|
|
155
|
-
MethodDatabase.new(path)
|
|
172
|
+
path = @dblocation ? @dblocation&.path : dbpath()
|
|
173
|
+
MethodDatabase.new(path || raise)
|
|
156
174
|
end
|
|
157
175
|
|
|
158
176
|
def new_database
|
|
@@ -172,15 +190,38 @@ module BitClust
|
|
|
172
190
|
|
|
173
191
|
def find_dblocation
|
|
174
192
|
%w( REFE2_SERVER BITCLUST_SERVER ).each do |key|
|
|
175
|
-
return URI.parse(ENV
|
|
193
|
+
return URI.parse(ENV.fetch(key)) if ENV[key]
|
|
176
194
|
end
|
|
177
195
|
if path = dbpath()
|
|
178
|
-
|
|
196
|
+
# Windows で "G:/Users/foo/.bitclust/db-2.2.0" のようなドライブ
|
|
197
|
+
# レター付きの絶対パスを URI.parse("file://#{path}") に渡すと、
|
|
198
|
+
# "G" が URI のホスト部分として解釈されてしまい、path からドライブ
|
|
199
|
+
# レターが失われる(URI.parse("file://G:/foo").path は "/foo" に
|
|
200
|
+
# なってしまう)。ドライブレター付きパスのときは URI 化せず、
|
|
201
|
+
# drive_path_uri でドライブレターを保持したまま file: URI を作る
|
|
202
|
+
windows_drive_path?(path) ? drive_path_uri(path) : URI.parse("file://#{path}")
|
|
179
203
|
else
|
|
180
204
|
nil
|
|
181
205
|
end
|
|
182
206
|
end
|
|
183
207
|
|
|
208
|
+
# "G:/foo" や "C:\\foo" のような、Windows のドライブレターで始まる
|
|
209
|
+
# 絶対パスかどうかを判定する
|
|
210
|
+
def windows_drive_path?(path)
|
|
211
|
+
/\A[A-Za-z]:/.match?(path)
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
# ドライブレター付きの絶対パスを、ドライブレターを失わずに保持した
|
|
215
|
+
# ままの file: URI (URI::File) にする。通常の URI.parse("file://...")
|
|
216
|
+
# はホスト部分を認識してしまい先頭のドライブレターを取りこぼすため、
|
|
217
|
+
# ここでは URI::Generic#initialize の arg_check を無効にして、
|
|
218
|
+
# "先頭は / から始まる絶対パスであること" という通常の URI のパス
|
|
219
|
+
# 検証を素通りさせ、path にドライブレターを含む文字列をそのまま保持
|
|
220
|
+
# させる
|
|
221
|
+
def drive_path_uri(path)
|
|
222
|
+
URI::File.new('file', nil, '', nil, nil, path, nil, nil, nil, nil, false)
|
|
223
|
+
end
|
|
224
|
+
|
|
184
225
|
def dbpath
|
|
185
226
|
env_dbpath() || default_dbpath()
|
|
186
227
|
end
|
|
@@ -188,7 +229,7 @@ module BitClust
|
|
|
188
229
|
def env_dbpath
|
|
189
230
|
[ 'REFE2_DATADIR', 'BITCLUST_DATADIR' ].each do |key|
|
|
190
231
|
if ENV.key?(key)
|
|
191
|
-
unless MethodDatabase.datadir?(ENV
|
|
232
|
+
unless MethodDatabase.datadir?(ENV.fetch(key))
|
|
192
233
|
raise InvalidDatabase, "environment variable #{key} given but #{ENV[key]} is not a valid BitClust database"
|
|
193
234
|
end
|
|
194
235
|
return ENV[key]
|
|
@@ -202,7 +243,7 @@ module BitClust
|
|
|
202
243
|
[ "#{datadir}/refe2", "#{datadir}/bitclust" ].each do |path|
|
|
203
244
|
return path if MethodDatabase.datadir?(path)
|
|
204
245
|
end
|
|
205
|
-
config_path = Pathname(ENV
|
|
246
|
+
config_path = Pathname(ENV.fetch('HOME')) + ".bitclust" + "config"
|
|
206
247
|
if config_path.exist?
|
|
207
248
|
config = YAML.load_file(config_path)
|
|
208
249
|
return "#{config[:database_prefix]}-#{config[:default_version]}"
|
|
@@ -213,10 +254,14 @@ module BitClust
|
|
|
213
254
|
def search_pattern(db, argv)
|
|
214
255
|
db ||= new_database()
|
|
215
256
|
@view.database ||= db if @view
|
|
257
|
+
# FIXME なぜか else 節にきかないのでここに書いておく
|
|
258
|
+
# @type var db: MethodDatabase
|
|
216
259
|
case @target_type || db
|
|
217
260
|
when :class
|
|
261
|
+
# @type var db: MethodDatabase
|
|
218
262
|
find_class db, argv[0]
|
|
219
263
|
when FunctionDatabase
|
|
264
|
+
# @type var db: FunctionDatabase
|
|
220
265
|
case argv.size
|
|
221
266
|
when 0
|
|
222
267
|
show_all_functions db
|
|
@@ -226,6 +271,7 @@ module BitClust
|
|
|
226
271
|
raise "must not happen: #{argv.size}"
|
|
227
272
|
end
|
|
228
273
|
else
|
|
274
|
+
# @type var db: MethodDatabase
|
|
229
275
|
case argv.size
|
|
230
276
|
when 0
|
|
231
277
|
show_all_classes db
|
|
@@ -236,7 +282,7 @@ module BitClust
|
|
|
236
282
|
find_method db, c, nil, m
|
|
237
283
|
when 3
|
|
238
284
|
c, t, m = *argv
|
|
239
|
-
check_method_type t
|
|
285
|
+
check_method_type t || raise
|
|
240
286
|
find_method db, c, t, m
|
|
241
287
|
else
|
|
242
288
|
raise "must not happen: #{argv.size}"
|
|
@@ -278,7 +324,8 @@ module BitClust
|
|
|
278
324
|
when /\A\$/ # Special variable.
|
|
279
325
|
find_method db, 'Kernel', '$', pattern.sub(/\A\$/, '')
|
|
280
326
|
when /[\#,]\.|\.[\#,]|[\#\.\,]/ # method spec
|
|
281
|
-
|
|
327
|
+
c, t, m = parse_method_spec_pattern(pattern)
|
|
328
|
+
find_method db, c, t, m
|
|
282
329
|
when /::/ # Class name or constant name.
|
|
283
330
|
find_constant db, pattern
|
|
284
331
|
when /\A[A-Z]/ # Method name or class name, but class name is better.
|
|
@@ -305,11 +352,13 @@ module BitClust
|
|
|
305
352
|
find_method db, cnames.join('::'), '::', name
|
|
306
353
|
end
|
|
307
354
|
|
|
355
|
+
# 区切りは反転文字列上で探す。"?." は 4.0 以降の module function の
|
|
356
|
+
# 表示形式(反転すると ".?")で、内部表記の ".#" に正規化する
|
|
308
357
|
def parse_method_spec_pattern(pat)
|
|
309
|
-
_m, _t, _c = pat.reverse.split(/([\#,]\.|\.[\#,]
|
|
310
|
-
c = _c.reverse
|
|
311
|
-
t = _t.tr(',', '#').sub(
|
|
312
|
-
m = _m.reverse
|
|
358
|
+
_m, _t, _c = pat.reverse.split(/([\#,]\.|\.[\#,]|\.\?|[\#\.\,])/, 2)
|
|
359
|
+
c = (_c || raise).reverse
|
|
360
|
+
t = (_t || raise).tr(',', '#').sub(/\#\.|\.\?/, '.#')
|
|
361
|
+
m = (_m || raise).reverse
|
|
313
362
|
return c, t, m
|
|
314
363
|
end
|
|
315
364
|
|
|
@@ -465,7 +514,7 @@ module BitClust
|
|
|
465
514
|
puts title
|
|
466
515
|
print_names methods.map{|m| m.name }
|
|
467
516
|
end
|
|
468
|
-
|
|
517
|
+
|
|
469
518
|
def describe_method(rec)
|
|
470
519
|
unless rec.entry.library.name == '_builtin'
|
|
471
520
|
puts "require '#{rec.entry.library.name}'"
|
|
@@ -501,10 +550,10 @@ module BitClust
|
|
|
501
550
|
end
|
|
502
551
|
|
|
503
552
|
def convert(string)
|
|
504
|
-
return string if
|
|
553
|
+
return string if database.nil?
|
|
505
554
|
_output_encoding = output_encoding
|
|
506
555
|
return string if _output_encoding.nil?
|
|
507
|
-
input_nkf_option = encoding_to_nkf_option(
|
|
556
|
+
input_nkf_option = encoding_to_nkf_option(database.encoding)
|
|
508
557
|
output_nkf_option = encoding_to_nkf_option(_output_encoding)
|
|
509
558
|
if input_nkf_option and output_nkf_option
|
|
510
559
|
NKF.nkf("-#{input_nkf_option.upcase}#{output_nkf_option}", string)
|
|
@@ -9,12 +9,14 @@ module BitClust
|
|
|
9
9
|
include NameUtils
|
|
10
10
|
|
|
11
11
|
module_function
|
|
12
|
-
|
|
12
|
+
|
|
13
13
|
def search_pattern(db, pat)
|
|
14
14
|
pat = to_pattern(pat)
|
|
15
15
|
return [] if pat.empty? or /\A\s+\z/ =~ pat
|
|
16
16
|
cname, type, mname = parse_method_spec_pattern(pat)
|
|
17
|
-
|
|
17
|
+
# @type var cs: Array[ClassEntry]
|
|
18
|
+
# @type var ms: Array[MethodEntry]
|
|
19
|
+
cs = []; ms = []
|
|
18
20
|
if cname and not cname.empty?
|
|
19
21
|
if mname
|
|
20
22
|
ms = find_class_method(db, cname, type, mname)
|
|
@@ -23,9 +25,9 @@ module BitClust
|
|
|
23
25
|
cs = find_class(db, cname)
|
|
24
26
|
end
|
|
25
27
|
elsif type == '$'
|
|
26
|
-
ms = find_special_vars(db, mname)
|
|
28
|
+
ms = find_special_vars(db, mname || raise)
|
|
27
29
|
else
|
|
28
|
-
ms = find_methods(db, mname)
|
|
30
|
+
ms = find_methods(db, mname || raise)
|
|
29
31
|
end
|
|
30
32
|
ms = ms.sort_by{|e| [e.library.name, e.klass.name] }
|
|
31
33
|
cs = cs.sort_by{|e| [e.library.name] }
|
|
@@ -35,9 +37,9 @@ module BitClust
|
|
|
35
37
|
def find_class(db, cname)
|
|
36
38
|
db.classes.find_all{|c| /\b#{Regexp.escape(cname)}\w*\z/ =~ c.name }
|
|
37
39
|
end
|
|
38
|
-
|
|
40
|
+
|
|
39
41
|
def find_class_method(db, cname, type, mname)
|
|
40
|
-
ret = []
|
|
42
|
+
ret = [] #: Array[MethodEntry]
|
|
41
43
|
db.classes.each{|c|
|
|
42
44
|
if /\b#{Regexp.escape(cname)}/ =~ c.name
|
|
43
45
|
ret += c.methods.find_all{|m|
|
|
@@ -62,24 +64,34 @@ module BitClust
|
|
|
62
64
|
|
|
63
65
|
def to_pattern(pat)
|
|
64
66
|
pat = pat.to_str
|
|
65
|
-
pat = pat[/\A\s*(.*?)\s*\z/, 1]
|
|
67
|
+
pat = pat[/\A\s*(.*?)\s*\z/, 1] || raise
|
|
66
68
|
end
|
|
67
|
-
|
|
69
|
+
|
|
68
70
|
def parse_method_spec_pattern(pat)
|
|
69
71
|
if /\s/ =~ pat
|
|
70
72
|
return parse_method_spec_pattern0(pat)
|
|
71
73
|
end
|
|
72
74
|
return pat, nil, nil if /\A[A-Z]\w*\z/ =~ pat
|
|
73
75
|
return nil, '$', $1 if /\$(\S*)/ =~ pat
|
|
74
|
-
|
|
76
|
+
# bitclust#250: "?." is an alternate spelling of the module-function
|
|
77
|
+
# separator ".#" (Ruby >=4.0 docs display it that way since #277; see
|
|
78
|
+
# NameUtils.display_typemark). Accept it here regardless of which
|
|
79
|
+
# Ruby version the query is about -- this is query *input*, not
|
|
80
|
+
# display, so both notations are always accepted. "?" is added to the
|
|
81
|
+
# same reversed-pattern alternative that already matches "#." and ",."
|
|
82
|
+
# (a marker character immediately followed by ".", read in the
|
|
83
|
+
# reversed pattern), so "?." matches the same way "#." already does.
|
|
84
|
+
_m, _t, _c = pat.reverse.split(/(::|[\#,]\.|\.[\#,\?]|[\#\.\,])/, 2)
|
|
85
|
+
_m || raise
|
|
75
86
|
c = _c.reverse if _c
|
|
76
|
-
t = _t.tr('
|
|
87
|
+
t = _t.tr(',?', '##').sub(/\#\./, '.#') if _t
|
|
77
88
|
m = _m.reverse
|
|
78
89
|
return c, t, m
|
|
79
90
|
end
|
|
80
91
|
|
|
81
|
-
def parse_method_spec_pattern0(
|
|
82
|
-
|
|
92
|
+
def parse_method_spec_pattern0(pat)
|
|
93
|
+
# @type var q: Array[String]
|
|
94
|
+
q = _ = pat.scan(/\S+/)[0..1]
|
|
83
95
|
q = q.reverse unless /\A[A-Z]/ =~ q[0]
|
|
84
96
|
return q[0], nil, q[1]
|
|
85
97
|
end
|
data/lib/bitclust/subcommand.rb
CHANGED
|
@@ -24,6 +24,12 @@ module BitClust
|
|
|
24
24
|
@parser.parse! argv
|
|
25
25
|
end
|
|
26
26
|
|
|
27
|
+
# グローバル --database(options[:prefix])を必要とするか。
|
|
28
|
+
# DB パスを自前の引数で受けるサブコマンド(searchpage)は false を返す
|
|
29
|
+
def needs_database?
|
|
30
|
+
true
|
|
31
|
+
end
|
|
32
|
+
|
|
27
33
|
def help
|
|
28
34
|
@parser.help
|
|
29
35
|
end
|
|
@@ -57,7 +63,7 @@ module BitClust
|
|
|
57
63
|
def align_progress_bar_title(title)
|
|
58
64
|
size = title.size
|
|
59
65
|
if size > 14
|
|
60
|
-
title[0..13]
|
|
66
|
+
title[0..13] || raise
|
|
61
67
|
else
|
|
62
68
|
title + ' ' * (14 - size)
|
|
63
69
|
end
|
|
@@ -45,10 +45,15 @@ Options:
|
|
|
45
45
|
}
|
|
46
46
|
end
|
|
47
47
|
|
|
48
|
+
# DB はサブコマンド自身の -d/--database オプションで受ける
|
|
49
|
+
def needs_database?
|
|
50
|
+
false
|
|
51
|
+
end
|
|
52
|
+
|
|
48
53
|
def exec(argv, options)
|
|
49
54
|
classname = argv[0]
|
|
50
|
-
db = MethodDatabase.new(@prefix)
|
|
51
|
-
ruby = get_ruby(@version)
|
|
55
|
+
db = MethodDatabase.new(@prefix || raise)
|
|
56
|
+
ruby = get_ruby(@version) || raise
|
|
52
57
|
if classname && !@all
|
|
53
58
|
check_ancestors(db, ruby, @requires, classname)
|
|
54
59
|
else
|
|
@@ -83,6 +88,7 @@ Options:
|
|
|
83
88
|
rescue ClassNotFound => ex
|
|
84
89
|
$stderr.puts ex.backtrace
|
|
85
90
|
$stderr.puts "class not found in database : #{classname}"
|
|
91
|
+
# @type var b: Array[String]
|
|
86
92
|
b = []
|
|
87
93
|
end
|
|
88
94
|
unless a.to_set == b.to_set
|
|
@@ -95,6 +101,7 @@ Options:
|
|
|
95
101
|
end
|
|
96
102
|
|
|
97
103
|
def check_all_ancestors(db, ruby, requires)
|
|
104
|
+
# @type var classnames: Array[String]
|
|
98
105
|
classnames = []
|
|
99
106
|
requires.each do |lib|
|
|
100
107
|
classnames.push(*defined_classes(ruby, lib, []))
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
#
|
|
3
|
+
# bitclust/subcommands/checklink_command.rb
|
|
4
|
+
#
|
|
5
|
+
# DB 中の全ソースをコンパイル経路で走査し、[[c:]]/[[m:]]/[[lib:]]/[[d:]]/
|
|
6
|
+
# [[f:]] 参照のリンク切れを報告する:
|
|
7
|
+
#
|
|
8
|
+
# bitclust -d db-3.4 checklink
|
|
9
|
+
# bitclust -d db-3.4 checklink --capi-database=db-3.4-capi
|
|
10
|
+
#
|
|
11
|
+
# リンク切れが1件でもあれば exit 1(CI 組み込み用)。
|
|
12
|
+
# [[f:]](C API 関数)は --capi-database 指定時のみ検証し、
|
|
13
|
+
# 未指定ならスキップ数だけ報告する。
|
|
14
|
+
|
|
15
|
+
require 'bitclust'
|
|
16
|
+
require 'bitclust/subcommand'
|
|
17
|
+
require 'bitclust/link_checker'
|
|
18
|
+
|
|
19
|
+
module BitClust
|
|
20
|
+
module Subcommands
|
|
21
|
+
class ChecklinkCommand < Subcommand
|
|
22
|
+
def initialize
|
|
23
|
+
super
|
|
24
|
+
@capi_prefix = nil
|
|
25
|
+
@parser.banner = "Usage: #{File.basename($0, '.*')} --database=PATH checklink [options]"
|
|
26
|
+
@parser.on('--capi-database=PATH', 'C API (function) database for [[f:]] refs.') {|path|
|
|
27
|
+
@capi_prefix = path
|
|
28
|
+
}
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def exec(argv, options)
|
|
32
|
+
super
|
|
33
|
+
db = @db
|
|
34
|
+
unless db.is_a?(MethodDatabase)
|
|
35
|
+
error 'checklink is for method databases (do not use --capi; pass --capi-database instead)'
|
|
36
|
+
end
|
|
37
|
+
capi_prefix = @capi_prefix
|
|
38
|
+
fdb = capi_prefix ? BitClust::FunctionDatabase.new(capi_prefix) : nil
|
|
39
|
+
checker = LinkChecker.new(db, function_database: fdb)
|
|
40
|
+
checker.check_all
|
|
41
|
+
# 同一エントリ内の同一参照の繰り返しは1行にまとめる
|
|
42
|
+
checker.findings.map {|f| "#{f.location}: [[#{f.ref}]] #{f.message}" }.uniq.each do |line|
|
|
43
|
+
puts line
|
|
44
|
+
end
|
|
45
|
+
if fdb.nil? && checker.skipped_function_refs > 0
|
|
46
|
+
puts "note: #{checker.skipped_function_refs} [[f:]] refs skipped (no --capi-database)"
|
|
47
|
+
end
|
|
48
|
+
version = @db.properties['version']
|
|
49
|
+
puts "#{checker.broken_count} broken link(s) in #{version ? "db (version #{version})" : 'db'}"
|
|
50
|
+
exit 1 unless checker.broken_count.zero?
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|