bitclust-core 1.2.6 → 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 +49 -5
- 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 +17 -17
- data/lib/bitclust/capi_converter.rb +20 -0
- data/lib/bitclust/classentry.rb +68 -17
- data/lib/bitclust/compat.rb +3 -2
- data/lib/bitclust/completion.rb +59 -27
- data/lib/bitclust/crossrubyutils.rb +9 -5
- data/lib/bitclust/database.rb +29 -8
- data/lib/bitclust/doc_converter.rb +63 -0
- data/lib/bitclust/docentry.rb +2 -1
- data/lib/bitclust/entity_splitter.rb +221 -0
- data/lib/bitclust/entry.rb +48 -7
- data/lib/bitclust/exception.rb +1 -0
- data/lib/bitclust/functiondatabase.rb +14 -1
- data/lib/bitclust/functionentry.rb +6 -1
- data/lib/bitclust/functionreferenceparser.rb +5 -4
- data/lib/bitclust/generators/epub.rb +11 -3
- data/lib/bitclust/htmlutils.rb +2 -1
- data/lib/bitclust/include_graph.rb +375 -0
- data/lib/bitclust/include_pruner.rb +144 -0
- data/lib/bitclust/interface.rb +2 -1
- data/lib/bitclust/libraryentry.rb +9 -7
- data/lib/bitclust/lineinput.rb +8 -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 +4 -3
- data/lib/bitclust/method_since_calculator.rb +117 -0
- data/lib/bitclust/methoddatabase.rb +125 -14
- data/lib/bitclust/methodentry.rb +130 -7
- data/lib/bitclust/methodid.rb +13 -11
- data/lib/bitclust/methodsignature.rb +11 -4
- data/lib/bitclust/nameutils.rb +42 -22
- data/lib/bitclust/parseutils.rb +1 -0
- data/lib/bitclust/preprocessor.rb +70 -32
- data/lib/bitclust/progress_bar.rb +1 -0
- data/lib/bitclust/rdcompiler.rb +197 -96
- data/lib/bitclust/refsdatabase.rb +24 -8
- data/lib/bitclust/reloadable_request_handler.rb +72 -0
- data/lib/bitclust/requesthandler.rb +22 -16
- data/lib/bitclust/ridatabase.rb +11 -4
- data/lib/bitclust/rrd_to_markdown.rb +925 -0
- data/lib/bitclust/rrdparser.rb +197 -30
- data/lib/bitclust/runner.rb +18 -2
- data/lib/bitclust/screen.rb +81 -11
- data/lib/bitclust/search_index_generator.rb +288 -0
- data/lib/bitclust/searcher.rb +72 -22
- data/lib/bitclust/server.rb +6 -1
- data/lib/bitclust/silent_progress_bar.rb +1 -0
- data/lib/bitclust/simplesearcher.rb +25 -12
- data/lib/bitclust/subcommand.rb +8 -1
- data/lib/bitclust/subcommands/ancestors_command.rb +10 -2
- data/lib/bitclust/subcommands/checklink_command.rb +54 -0
- data/lib/bitclust/subcommands/chm_command.rb +16 -7
- data/lib/bitclust/subcommands/classes_command.rb +6 -0
- data/lib/bitclust/subcommands/epub_command.rb +1 -0
- data/lib/bitclust/subcommands/extract_command.rb +6 -0
- data/lib/bitclust/subcommands/htmlfile_command.rb +22 -7
- data/lib/bitclust/subcommands/init_command.rb +5 -1
- data/lib/bitclust/subcommands/list_command.rb +13 -4
- data/lib/bitclust/subcommands/lookup_command.rb +29 -9
- data/lib/bitclust/subcommands/methods_command.rb +11 -2
- data/lib/bitclust/subcommands/methodsince_command.rb +80 -0
- data/lib/bitclust/subcommands/preproc_command.rb +6 -0
- data/lib/bitclust/subcommands/property_command.rb +3 -1
- data/lib/bitclust/subcommands/query_command.rb +1 -0
- data/lib/bitclust/subcommands/searchpage_command.rb +102 -0
- data/lib/bitclust/subcommands/server_command.rb +16 -10
- data/lib/bitclust/subcommands/setup_command.rb +25 -10
- data/lib/bitclust/subcommands/statichtml_command.rb +132 -6
- data/lib/bitclust/subcommands/update_command.rb +111 -2
- data/lib/bitclust/syntax_highlighter.rb +52 -5
- data/lib/bitclust/textutils.rb +4 -3
- data/lib/bitclust/version.rb +2 -1
- data/lib/bitclust/version_badges.rb +90 -0
- data/lib/bitclust/whole_file_gate.rb +137 -0
- data/lib/bitclust.rb +1 -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 +429 -27
- 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 +153 -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 +178 -21
- 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
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
1
2
|
#
|
|
2
3
|
# bitclust/searcher.rb
|
|
3
4
|
#
|
|
@@ -39,8 +40,18 @@ module BitClust
|
|
|
39
40
|
parser.banner = "Usage: #{@name} <pattern>"
|
|
40
41
|
unless cmd == 'bitclust'
|
|
41
42
|
parser.on('-d', '--database=URL', "Database location (default: #{dblocation_name()})") {|loc|
|
|
42
|
-
|
|
43
|
-
|
|
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
|
|
44
55
|
}
|
|
45
56
|
parser.on('--server=URL', 'Spawns BitClust database server and listen URL. Requires --database option with local path.') {|url|
|
|
46
57
|
require 'bitclust/server' # require here for speed
|
|
@@ -91,6 +102,14 @@ module BitClust
|
|
|
91
102
|
|
|
92
103
|
def exec(argv, options = {})
|
|
93
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
|
|
94
113
|
if @listen_url
|
|
95
114
|
spawn_server db
|
|
96
115
|
else
|
|
@@ -106,7 +125,7 @@ module BitClust
|
|
|
106
125
|
|
|
107
126
|
def server_mode_check(argv)
|
|
108
127
|
if @dblocation
|
|
109
|
-
unless @dblocation
|
|
128
|
+
unless @dblocation&.scheme == 'file'
|
|
110
129
|
$stderr.puts "Give local path to --database option on server mode"
|
|
111
130
|
exit 1
|
|
112
131
|
end
|
|
@@ -139,19 +158,19 @@ module BitClust
|
|
|
139
158
|
#compiler = RDCompiler::Text.new
|
|
140
159
|
compiler = Plain.new
|
|
141
160
|
@view = TerminalView.new(compiler,
|
|
142
|
-
{:describe_all => @describe_all,
|
|
161
|
+
{:describe_all => @describe_all,
|
|
143
162
|
:line => @linep,
|
|
144
163
|
:encoding => @encoding})
|
|
145
164
|
end
|
|
146
165
|
|
|
147
166
|
def spawn_server(db)
|
|
148
|
-
Server.new(new_local_database(db)).listen @listen_url, @foreground
|
|
167
|
+
Server.new(new_local_database(db)).listen (@listen_url || raise), @foreground
|
|
149
168
|
end
|
|
150
169
|
|
|
151
170
|
def new_local_database(db)
|
|
152
171
|
return db if db
|
|
153
|
-
path = @dblocation ? @dblocation
|
|
154
|
-
MethodDatabase.new(path)
|
|
172
|
+
path = @dblocation ? @dblocation&.path : dbpath()
|
|
173
|
+
MethodDatabase.new(path || raise)
|
|
155
174
|
end
|
|
156
175
|
|
|
157
176
|
def new_database
|
|
@@ -171,15 +190,38 @@ module BitClust
|
|
|
171
190
|
|
|
172
191
|
def find_dblocation
|
|
173
192
|
%w( REFE2_SERVER BITCLUST_SERVER ).each do |key|
|
|
174
|
-
return URI.parse(ENV
|
|
193
|
+
return URI.parse(ENV.fetch(key)) if ENV[key]
|
|
175
194
|
end
|
|
176
195
|
if path = dbpath()
|
|
177
|
-
|
|
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}")
|
|
178
203
|
else
|
|
179
204
|
nil
|
|
180
205
|
end
|
|
181
206
|
end
|
|
182
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
|
+
|
|
183
225
|
def dbpath
|
|
184
226
|
env_dbpath() || default_dbpath()
|
|
185
227
|
end
|
|
@@ -187,7 +229,7 @@ module BitClust
|
|
|
187
229
|
def env_dbpath
|
|
188
230
|
[ 'REFE2_DATADIR', 'BITCLUST_DATADIR' ].each do |key|
|
|
189
231
|
if ENV.key?(key)
|
|
190
|
-
unless MethodDatabase.datadir?(ENV
|
|
232
|
+
unless MethodDatabase.datadir?(ENV.fetch(key))
|
|
191
233
|
raise InvalidDatabase, "environment variable #{key} given but #{ENV[key]} is not a valid BitClust database"
|
|
192
234
|
end
|
|
193
235
|
return ENV[key]
|
|
@@ -201,7 +243,7 @@ module BitClust
|
|
|
201
243
|
[ "#{datadir}/refe2", "#{datadir}/bitclust" ].each do |path|
|
|
202
244
|
return path if MethodDatabase.datadir?(path)
|
|
203
245
|
end
|
|
204
|
-
config_path = Pathname(ENV
|
|
246
|
+
config_path = Pathname(ENV.fetch('HOME')) + ".bitclust" + "config"
|
|
205
247
|
if config_path.exist?
|
|
206
248
|
config = YAML.load_file(config_path)
|
|
207
249
|
return "#{config[:database_prefix]}-#{config[:default_version]}"
|
|
@@ -212,10 +254,14 @@ module BitClust
|
|
|
212
254
|
def search_pattern(db, argv)
|
|
213
255
|
db ||= new_database()
|
|
214
256
|
@view.database ||= db if @view
|
|
257
|
+
# FIXME なぜか else 節にきかないのでここに書いておく
|
|
258
|
+
# @type var db: MethodDatabase
|
|
215
259
|
case @target_type || db
|
|
216
260
|
when :class
|
|
261
|
+
# @type var db: MethodDatabase
|
|
217
262
|
find_class db, argv[0]
|
|
218
263
|
when FunctionDatabase
|
|
264
|
+
# @type var db: FunctionDatabase
|
|
219
265
|
case argv.size
|
|
220
266
|
when 0
|
|
221
267
|
show_all_functions db
|
|
@@ -225,6 +271,7 @@ module BitClust
|
|
|
225
271
|
raise "must not happen: #{argv.size}"
|
|
226
272
|
end
|
|
227
273
|
else
|
|
274
|
+
# @type var db: MethodDatabase
|
|
228
275
|
case argv.size
|
|
229
276
|
when 0
|
|
230
277
|
show_all_classes db
|
|
@@ -235,7 +282,7 @@ module BitClust
|
|
|
235
282
|
find_method db, c, nil, m
|
|
236
283
|
when 3
|
|
237
284
|
c, t, m = *argv
|
|
238
|
-
check_method_type t
|
|
285
|
+
check_method_type t || raise
|
|
239
286
|
find_method db, c, t, m
|
|
240
287
|
else
|
|
241
288
|
raise "must not happen: #{argv.size}"
|
|
@@ -277,7 +324,8 @@ module BitClust
|
|
|
277
324
|
when /\A\$/ # Special variable.
|
|
278
325
|
find_method db, 'Kernel', '$', pattern.sub(/\A\$/, '')
|
|
279
326
|
when /[\#,]\.|\.[\#,]|[\#\.\,]/ # method spec
|
|
280
|
-
|
|
327
|
+
c, t, m = parse_method_spec_pattern(pattern)
|
|
328
|
+
find_method db, c, t, m
|
|
281
329
|
when /::/ # Class name or constant name.
|
|
282
330
|
find_constant db, pattern
|
|
283
331
|
when /\A[A-Z]/ # Method name or class name, but class name is better.
|
|
@@ -304,11 +352,13 @@ module BitClust
|
|
|
304
352
|
find_method db, cnames.join('::'), '::', name
|
|
305
353
|
end
|
|
306
354
|
|
|
355
|
+
# 区切りは反転文字列上で探す。"?." は 4.0 以降の module function の
|
|
356
|
+
# 表示形式(反転すると ".?")で、内部表記の ".#" に正規化する
|
|
307
357
|
def parse_method_spec_pattern(pat)
|
|
308
|
-
_m, _t, _c = pat.reverse.split(/([\#,]\.|\.[\#,]
|
|
309
|
-
c = _c.reverse
|
|
310
|
-
t = _t.tr(',', '#').sub(
|
|
311
|
-
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
|
|
312
362
|
return c, t, m
|
|
313
363
|
end
|
|
314
364
|
|
|
@@ -402,7 +452,7 @@ module BitClust
|
|
|
402
452
|
|
|
403
453
|
def print_packed_names(names)
|
|
404
454
|
max = terminal_column()
|
|
405
|
-
buf = ''
|
|
455
|
+
buf = +''
|
|
406
456
|
names.each do |name|
|
|
407
457
|
if buf.size + name.size + 1 > max
|
|
408
458
|
if buf.empty?
|
|
@@ -410,7 +460,7 @@ module BitClust
|
|
|
410
460
|
next
|
|
411
461
|
end
|
|
412
462
|
puts buf
|
|
413
|
-
buf = ''
|
|
463
|
+
buf = +''
|
|
414
464
|
end
|
|
415
465
|
buf << name << ' '
|
|
416
466
|
end
|
|
@@ -464,7 +514,7 @@ module BitClust
|
|
|
464
514
|
puts title
|
|
465
515
|
print_names methods.map{|m| m.name }
|
|
466
516
|
end
|
|
467
|
-
|
|
517
|
+
|
|
468
518
|
def describe_method(rec)
|
|
469
519
|
unless rec.entry.library.name == '_builtin'
|
|
470
520
|
puts "require '#{rec.entry.library.name}'"
|
|
@@ -500,10 +550,10 @@ module BitClust
|
|
|
500
550
|
end
|
|
501
551
|
|
|
502
552
|
def convert(string)
|
|
503
|
-
return string if
|
|
553
|
+
return string if database.nil?
|
|
504
554
|
_output_encoding = output_encoding
|
|
505
555
|
return string if _output_encoding.nil?
|
|
506
|
-
input_nkf_option = encoding_to_nkf_option(
|
|
556
|
+
input_nkf_option = encoding_to_nkf_option(database.encoding)
|
|
507
557
|
output_nkf_option = encoding_to_nkf_option(_output_encoding)
|
|
508
558
|
if input_nkf_option and output_nkf_option
|
|
509
559
|
NKF.nkf("-#{input_nkf_option.upcase}#{output_nkf_option}", string)
|
data/lib/bitclust/server.rb
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
1
2
|
#
|
|
2
3
|
# bitclust/server.rb
|
|
3
4
|
#
|
|
@@ -14,7 +15,6 @@ require 'bitclust/classentry'
|
|
|
14
15
|
require 'bitclust/methodentry'
|
|
15
16
|
require 'bitclust/docentry'
|
|
16
17
|
require 'drb'
|
|
17
|
-
require 'webrick/server'
|
|
18
18
|
|
|
19
19
|
module BitClust
|
|
20
20
|
|
|
@@ -26,6 +26,11 @@ module BitClust
|
|
|
26
26
|
end
|
|
27
27
|
|
|
28
28
|
def listen(url, foreground = false)
|
|
29
|
+
begin
|
|
30
|
+
require 'webrick/server'
|
|
31
|
+
rescue LoadError
|
|
32
|
+
abort "webrick is not found. You may need to `gem install webrick` to install webrick."
|
|
33
|
+
end
|
|
29
34
|
WEBrick::Daemon.start unless foreground
|
|
30
35
|
DRb.start_service url, @db
|
|
31
36
|
DRb.thread.join
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
1
2
|
require 'bitclust/nameutils'
|
|
2
3
|
require 'bitclust/methodid'
|
|
3
4
|
|
|
@@ -8,12 +9,14 @@ module BitClust
|
|
|
8
9
|
include NameUtils
|
|
9
10
|
|
|
10
11
|
module_function
|
|
11
|
-
|
|
12
|
+
|
|
12
13
|
def search_pattern(db, pat)
|
|
13
14
|
pat = to_pattern(pat)
|
|
14
15
|
return [] if pat.empty? or /\A\s+\z/ =~ pat
|
|
15
16
|
cname, type, mname = parse_method_spec_pattern(pat)
|
|
16
|
-
|
|
17
|
+
# @type var cs: Array[ClassEntry]
|
|
18
|
+
# @type var ms: Array[MethodEntry]
|
|
19
|
+
cs = []; ms = []
|
|
17
20
|
if cname and not cname.empty?
|
|
18
21
|
if mname
|
|
19
22
|
ms = find_class_method(db, cname, type, mname)
|
|
@@ -22,9 +25,9 @@ module BitClust
|
|
|
22
25
|
cs = find_class(db, cname)
|
|
23
26
|
end
|
|
24
27
|
elsif type == '$'
|
|
25
|
-
ms = find_special_vars(db, mname)
|
|
28
|
+
ms = find_special_vars(db, mname || raise)
|
|
26
29
|
else
|
|
27
|
-
ms = find_methods(db, mname)
|
|
30
|
+
ms = find_methods(db, mname || raise)
|
|
28
31
|
end
|
|
29
32
|
ms = ms.sort_by{|e| [e.library.name, e.klass.name] }
|
|
30
33
|
cs = cs.sort_by{|e| [e.library.name] }
|
|
@@ -34,9 +37,9 @@ module BitClust
|
|
|
34
37
|
def find_class(db, cname)
|
|
35
38
|
db.classes.find_all{|c| /\b#{Regexp.escape(cname)}\w*\z/ =~ c.name }
|
|
36
39
|
end
|
|
37
|
-
|
|
40
|
+
|
|
38
41
|
def find_class_method(db, cname, type, mname)
|
|
39
|
-
ret = []
|
|
42
|
+
ret = [] #: Array[MethodEntry]
|
|
40
43
|
db.classes.each{|c|
|
|
41
44
|
if /\b#{Regexp.escape(cname)}/ =~ c.name
|
|
42
45
|
ret += c.methods.find_all{|m|
|
|
@@ -61,24 +64,34 @@ module BitClust
|
|
|
61
64
|
|
|
62
65
|
def to_pattern(pat)
|
|
63
66
|
pat = pat.to_str
|
|
64
|
-
pat = pat[/\A\s*(.*?)\s*\z/, 1]
|
|
67
|
+
pat = pat[/\A\s*(.*?)\s*\z/, 1] || raise
|
|
65
68
|
end
|
|
66
|
-
|
|
69
|
+
|
|
67
70
|
def parse_method_spec_pattern(pat)
|
|
68
71
|
if /\s/ =~ pat
|
|
69
72
|
return parse_method_spec_pattern0(pat)
|
|
70
73
|
end
|
|
71
74
|
return pat, nil, nil if /\A[A-Z]\w*\z/ =~ pat
|
|
72
75
|
return nil, '$', $1 if /\$(\S*)/ =~ pat
|
|
73
|
-
|
|
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
|
|
74
86
|
c = _c.reverse if _c
|
|
75
|
-
t = _t.tr('
|
|
87
|
+
t = _t.tr(',?', '##').sub(/\#\./, '.#') if _t
|
|
76
88
|
m = _m.reverse
|
|
77
89
|
return c, t, m
|
|
78
90
|
end
|
|
79
91
|
|
|
80
|
-
def parse_method_spec_pattern0(
|
|
81
|
-
|
|
92
|
+
def parse_method_spec_pattern0(pat)
|
|
93
|
+
# @type var q: Array[String]
|
|
94
|
+
q = _ = pat.scan(/\S+/)[0..1]
|
|
82
95
|
q = q.reverse unless /\A[A-Z]/ =~ q[0]
|
|
83
96
|
return q[0], nil, q[1]
|
|
84
97
|
end
|
data/lib/bitclust/subcommand.rb
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
1
2
|
require 'pathname'
|
|
2
3
|
|
|
3
4
|
require 'bitclust'
|
|
@@ -23,6 +24,12 @@ module BitClust
|
|
|
23
24
|
@parser.parse! argv
|
|
24
25
|
end
|
|
25
26
|
|
|
27
|
+
# グローバル --database(options[:prefix])を必要とするか。
|
|
28
|
+
# DB パスを自前の引数で受けるサブコマンド(searchpage)は false を返す
|
|
29
|
+
def needs_database?
|
|
30
|
+
true
|
|
31
|
+
end
|
|
32
|
+
|
|
26
33
|
def help
|
|
27
34
|
@parser.help
|
|
28
35
|
end
|
|
@@ -56,7 +63,7 @@ module BitClust
|
|
|
56
63
|
def align_progress_bar_title(title)
|
|
57
64
|
size = title.size
|
|
58
65
|
if size > 14
|
|
59
|
-
title[0..13]
|
|
66
|
+
title[0..13] || raise
|
|
60
67
|
else
|
|
61
68
|
title + ' ' * (14 - size)
|
|
62
69
|
end
|