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,347 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
#
|
|
3
|
+
# bitclust/mdparser.rb
|
|
4
|
+
#
|
|
5
|
+
# Markdown ソースを直接パースして DB エントリを作るパーサ(フェーズ3 M3)。
|
|
6
|
+
#
|
|
7
|
+
# RRDParser のサブクラスとして、行レベルのディスパッチだけを Markdown 記法
|
|
8
|
+
# (MARKUP_SPEC)に差し替える。構造の組み立て(Context・Signature・Chunk)は
|
|
9
|
+
# すべて RRDParser から継承する。エントリの source には md 断片がそのまま入る。
|
|
10
|
+
#
|
|
11
|
+
# - `= class X < Y` → `# class X < Y`
|
|
12
|
+
# - `== Class Methods` 等 → `## Class Methods`(名称は同一)
|
|
13
|
+
# - `--- sig` → `### def sig` / `### module_function def` / `### const` / `### gvar`
|
|
14
|
+
# - category/require/sublibrary・include/extend/alias → front matter
|
|
15
|
+
# - `#@since` 等の指令は md でも同一記法のため Preprocessor は無変更で使う
|
|
16
|
+
# (front matter 内の `#@` 行も先に版解決される)
|
|
17
|
+
|
|
18
|
+
require 'bitclust/rrdparser'
|
|
19
|
+
require 'bitclust/functionreferenceparser'
|
|
20
|
+
require 'bitclust/methodsignature'
|
|
21
|
+
|
|
22
|
+
module BitClust
|
|
23
|
+
|
|
24
|
+
# C API リファレンス(manual/capi)の md を直接パースする。
|
|
25
|
+
# capi の md は本文見出しを持たず、### 全行がシグネチャ
|
|
26
|
+
# (--- <C sig> 相当、キーワード無し)。source は本文の md がそのまま入る
|
|
27
|
+
class MDFunctionParser < FunctionReferenceParser
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def file_entries(f)
|
|
31
|
+
f.skip_blank_lines
|
|
32
|
+
f.while_match(/\A### /) do |header|
|
|
33
|
+
entry header.sub(/\A###/, '').strip, f.break(/\A### /)
|
|
34
|
+
f.skip_blank_lines
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
class MDParser < RRDParser
|
|
40
|
+
|
|
41
|
+
# doc ページの md からタイトル(# 見出し、アンカー付き含む)と本文を分ける
|
|
42
|
+
# (RRDParser.split_doc の md 版)
|
|
43
|
+
def MDParser.split_doc(source)
|
|
44
|
+
if m = /^#( +(.*?))(?: \{#[^}]+\})?\r?\n/.match(source)
|
|
45
|
+
return ($2 || raise), m.post_match
|
|
46
|
+
end
|
|
47
|
+
return ["", source]
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# メソッド系シグネチャ(キーワード付き h3)
|
|
51
|
+
SIG_RE = /\A### (?:module_function def |def |const |gvar )/
|
|
52
|
+
# H1(エンティティ見出し)。エスケープされた行頭リテラル \# は除外
|
|
53
|
+
H1_RE = /\A#[^#]/
|
|
54
|
+
# H2(レベル2セクション見出し)
|
|
55
|
+
H2_RE = /\A##[^#]/
|
|
56
|
+
# klass.source / library.source の終端(H1・H2・シグネチャ)
|
|
57
|
+
BREAK_RE = /\A##?[^#]|\A### (?:module_function def |def |const |gvar )/
|
|
58
|
+
|
|
59
|
+
def parse(f, libname, params = {})
|
|
60
|
+
@context = Context.new(@db, libname)
|
|
61
|
+
f = LineInput.new(Preprocessor.wrap(f, params))
|
|
62
|
+
@front_matter = read_front_matter(f)
|
|
63
|
+
do_parse f
|
|
64
|
+
@context.library
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
private
|
|
68
|
+
|
|
69
|
+
# BREAK_RE まで行を収集する(f.break のフェンス対応版)。
|
|
70
|
+
# コードフェンス内の行頭 `# コメント` 等が H1/H2 と衝突しないよう、
|
|
71
|
+
# フェンス内は無条件に本文として消費する
|
|
72
|
+
def md_break(f)
|
|
73
|
+
lines = [] #: Array[String?]
|
|
74
|
+
fence_len = nil
|
|
75
|
+
while (line = f.peek)
|
|
76
|
+
if fence_len
|
|
77
|
+
lines.push(f.gets)
|
|
78
|
+
fence_len = nil if line =~ /\A`{#{fence_len}}\s*$/
|
|
79
|
+
elsif line =~ /\A(`{3,})/
|
|
80
|
+
fence_len = ($1 || raise).length
|
|
81
|
+
lines.push(f.gets)
|
|
82
|
+
elsif line =~ BREAK_RE
|
|
83
|
+
break
|
|
84
|
+
else
|
|
85
|
+
lines.push(f.gets)
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
lines
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# 断片の front matter を @front_matter へマージ(リストは連結、
|
|
92
|
+
# スカラーは後勝ち)
|
|
93
|
+
def merge_front_matter(fm)
|
|
94
|
+
fm.each do |k, v|
|
|
95
|
+
if v.is_a?(Array) && @front_matter[k].is_a?(Array)
|
|
96
|
+
@front_matter[k] |= v
|
|
97
|
+
else
|
|
98
|
+
@front_matter[k] = v
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# front matter(--- ... ---)を読む。YAML のサブセット:
|
|
104
|
+
# スカラー(type/library/category/since/until)とリスト
|
|
105
|
+
# (include/extend/alias/require/sublibrary)のみ。#@/#% 行は
|
|
106
|
+
# Preprocessor で版解決済みの残り(#@#/#%# コメント等)なので読み飛ばす
|
|
107
|
+
def read_front_matter(f)
|
|
108
|
+
fm = {} #: front_matter
|
|
109
|
+
return fm unless f.peek && f.peek =~ /\A---\s*$/
|
|
110
|
+
f.gets
|
|
111
|
+
key = nil
|
|
112
|
+
while (line = f.gets)
|
|
113
|
+
break if line =~ /\A---\s*$/
|
|
114
|
+
case line
|
|
115
|
+
when /\A\#[@%]/
|
|
116
|
+
next
|
|
117
|
+
when /\A(\w+):\s*$/
|
|
118
|
+
key = $1
|
|
119
|
+
fm[key] = []
|
|
120
|
+
when /\A(\w+):\s*(.+?)\s*$/
|
|
121
|
+
fm[$1] = ($2 || raise).sub(/\A"(.*)"\z/, '\1')
|
|
122
|
+
key = nil
|
|
123
|
+
when /\A\s+-\s+(\S.*?)\s*$/
|
|
124
|
+
list = fm[key]
|
|
125
|
+
list << $1 if key && list.is_a?(Array)
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
fm
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def do_parse(f)
|
|
132
|
+
f.skip_blank_lines
|
|
133
|
+
# #@include 展開で断片の front matter がストリーム先頭に現れる
|
|
134
|
+
# (rdoc/parser/simple → parsers/parse_simple の require リスト等)。
|
|
135
|
+
# 連続するブロックを読んでメタデータをマージする
|
|
136
|
+
while f.peek && f.peek =~ /\A---\s*$/
|
|
137
|
+
merge_front_matter read_front_matter(f)
|
|
138
|
+
f.skip_blank_lines
|
|
139
|
+
end
|
|
140
|
+
# RRDParser と同じく未指定でも呼ぶ(category=nil を明示的に設定)
|
|
141
|
+
@context.categorize @front_matter['category'] if @front_matter['type'] == 'library'
|
|
142
|
+
Array(@front_matter['require']).each { |r| @context.require r }
|
|
143
|
+
Array(@front_matter['sublibrary']).each { |s| @context.sublibrary s }
|
|
144
|
+
prose = md_break(f).join('').rstrip
|
|
145
|
+
# メンバーファイル(library: 参照のみ)のパースで lib 本文を
|
|
146
|
+
# 上書きしないよう、lib ファイル(type: library)のときだけ設定する
|
|
147
|
+
@context.library.source = prose if @front_matter['type'] == 'library'
|
|
148
|
+
read_classes f
|
|
149
|
+
if line = f.gets # error
|
|
150
|
+
case line
|
|
151
|
+
when H2_RE
|
|
152
|
+
parse_error "met level-2 header in library document; maybe you forgot level-1 header", line
|
|
153
|
+
when SIG_RE
|
|
154
|
+
parse_error "met bare method entry in library document; maybe you forgot reopen/redefine level-1 header", line
|
|
155
|
+
else
|
|
156
|
+
parse_error "unexpected line in library document", line
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
RELATION_KEYS = %w[include extend alias].freeze
|
|
162
|
+
|
|
163
|
+
def read_classes(f)
|
|
164
|
+
entity_count = 0
|
|
165
|
+
f.while_match(H1_RE) do |line|
|
|
166
|
+
entity_count += 1
|
|
167
|
+
if entity_count > 1 && RELATION_KEYS.any? { |k| @front_matter.key?(k) }
|
|
168
|
+
# 案B: 関係を持つファイルは単一エンティティ(front matter の
|
|
169
|
+
# include/extend/alias の帰属が曖昧になるため)
|
|
170
|
+
parse_error "multiple entities in a file with front matter relations", line
|
|
171
|
+
end
|
|
172
|
+
type, name, superclass = *parse_level1_header(line)
|
|
173
|
+
case type
|
|
174
|
+
when 'class'
|
|
175
|
+
@context.define_class name, (superclass || 'Object'), location: line.location
|
|
176
|
+
read_class_body f
|
|
177
|
+
when 'module'
|
|
178
|
+
parse_error "superclass given for module", line if superclass
|
|
179
|
+
@context.define_module name, location: line.location
|
|
180
|
+
read_class_body f
|
|
181
|
+
when 'object'
|
|
182
|
+
@context.define_object name, superclass, location: line.location
|
|
183
|
+
read_object_body f
|
|
184
|
+
when 'reopen'
|
|
185
|
+
@context.reopen_class name
|
|
186
|
+
read_reopen_body f
|
|
187
|
+
when 'redefine'
|
|
188
|
+
@context.redefine_class name
|
|
189
|
+
read_reopen_body f
|
|
190
|
+
else
|
|
191
|
+
parse_error "wrong level-1 header", line
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def parse_level1_header(line)
|
|
197
|
+
m = /\A(\S+)\s*([^\s<]+)(?:\s*<\s*(\S+))?\z/.match(line.sub(/\A#/, '').strip)
|
|
198
|
+
unless m
|
|
199
|
+
parse_error "level-1 header syntax error", line
|
|
200
|
+
end
|
|
201
|
+
return m[1], m[2], m[3]
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# 関係(include/extend/alias)は front matter が置き場(案B)。
|
|
205
|
+
# ただし #@include 断片(rdoc/parsers/parse_rb 等)は rd 形式の
|
|
206
|
+
# 関係行を本文に保持しているため、RRDParser 同様ストリームからも読む
|
|
207
|
+
def read_aliases(f)
|
|
208
|
+
Array(@front_matter['alias']).each { |name| @context.alias name }
|
|
209
|
+
f.while_match(/\Aalias\s/) do |line|
|
|
210
|
+
@context.alias line.split[1]
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def read_includes(f, reopen = false)
|
|
215
|
+
Array(@front_matter['include']).each do |name|
|
|
216
|
+
reopen ? @context.dynamic_include(name) : @context.include(name)
|
|
217
|
+
end
|
|
218
|
+
f.while_match(/\Ainclude\s/) do |line|
|
|
219
|
+
name = line.split[1]
|
|
220
|
+
reopen ? @context.dynamic_include(name) : @context.include(name)
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def read_extends(f, reopen = false)
|
|
225
|
+
Array(@front_matter['extend']).each do |name|
|
|
226
|
+
reopen ? @context.dynamic_extend(name) : @context.extend(name)
|
|
227
|
+
end
|
|
228
|
+
f.while_match(/\Aextend\s/) do |line|
|
|
229
|
+
name = line.split[1]
|
|
230
|
+
reopen ? @context.dynamic_extend(name) : @context.extend(name)
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def read_class_body(f)
|
|
235
|
+
f.skip_blank_lines
|
|
236
|
+
read_aliases f
|
|
237
|
+
read_extends f
|
|
238
|
+
read_includes f
|
|
239
|
+
f.skip_blank_lines
|
|
240
|
+
@context.klass&.source = md_break(f).join('').rstrip
|
|
241
|
+
read_level2_blocks f
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
def read_reopen_body(f)
|
|
245
|
+
f.skip_blank_lines
|
|
246
|
+
read_extends f, true
|
|
247
|
+
read_includes f, true
|
|
248
|
+
f.skip_blank_lines
|
|
249
|
+
read_level2_blocks f
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
def read_object_body(f)
|
|
253
|
+
f.skip_blank_lines
|
|
254
|
+
read_aliases f
|
|
255
|
+
read_extends f
|
|
256
|
+
f.skip_blank_lines
|
|
257
|
+
@context.klass&.source = md_break(f).join('').rstrip
|
|
258
|
+
@context.visibility = :public
|
|
259
|
+
@context.type = :singleton_method
|
|
260
|
+
read_level2_blocks f
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
def read_level2_blocks(f)
|
|
264
|
+
read_entries f
|
|
265
|
+
f.skip_blank_lines
|
|
266
|
+
f.while_match(H2_RE) do |line|
|
|
267
|
+
case line.sub(/\A##/, '').strip
|
|
268
|
+
when /\A((?:public|private|protected)\s+)?(?:(class|singleton|instance)\s+)?methods?\z/i
|
|
269
|
+
# @type var visibility: :public | :private | :protected
|
|
270
|
+
visibility = ($1 || 'public').downcase.strip.intern
|
|
271
|
+
@context.visibility = visibility
|
|
272
|
+
t = ($2 || 'instance').downcase.sub(/class/, 'singleton')
|
|
273
|
+
@context.type = _ = "#{t}_method".intern
|
|
274
|
+
when /\AModule\s+Functions?\z/i
|
|
275
|
+
@context.module_function
|
|
276
|
+
when /\AConstants?\z/i
|
|
277
|
+
@context.constant
|
|
278
|
+
when /\ASpecial\s+Variables?\z/i
|
|
279
|
+
@context.special_variable
|
|
280
|
+
else
|
|
281
|
+
parse_error "unknown level-2 header", line
|
|
282
|
+
end
|
|
283
|
+
read_entries f
|
|
284
|
+
end
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
def read_chunks(f)
|
|
288
|
+
f.skip_blank_lines
|
|
289
|
+
result = [] #: Array[Chunk]
|
|
290
|
+
f.while_match(SIG_RE) do |line|
|
|
291
|
+
f.ungets line
|
|
292
|
+
result.push read_chunk(f)
|
|
293
|
+
end
|
|
294
|
+
result
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
def read_chunk(f)
|
|
298
|
+
# シグネチャ行の直後には {: ...} 属性行を置ける(rd 側と同じ)。
|
|
299
|
+
# 属性行を挟んでもひとつのチャンク(別名グループ)として読む
|
|
300
|
+
header = [] #: Array[String]
|
|
301
|
+
sig_lines = [] #: Array[String]
|
|
302
|
+
while f.next?
|
|
303
|
+
if SIG_RE =~ f.peek
|
|
304
|
+
line = f.gets or raise
|
|
305
|
+
header.push line
|
|
306
|
+
sig_lines.push line
|
|
307
|
+
elsif !sig_lines.empty? && METHOD_ATTRIBUTE_LINE_RE =~ f.peek
|
|
308
|
+
header.push(f.gets || raise)
|
|
309
|
+
else
|
|
310
|
+
break
|
|
311
|
+
end
|
|
312
|
+
end
|
|
313
|
+
body = md_break(f)
|
|
314
|
+
src = (header + body).join('')
|
|
315
|
+
src.location = sig_lines[0].location
|
|
316
|
+
sigs = sig_lines.map {|line| method_signature(line) }
|
|
317
|
+
mainsig = check_chunk_signatures(sigs, sig_lines[0])
|
|
318
|
+
names = sigs.map {|s| s.name }.compact.uniq.sort
|
|
319
|
+
Chunk.new(mainsig, names, src)
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
# md シグネチャ行を rd 形式(--- ...)へ落とし、既存の
|
|
323
|
+
# Signature パースを継承する。エラー時は元の行を報告する
|
|
324
|
+
def method_signature(line)
|
|
325
|
+
rd_line = line.sub(SIG_RE, '--- ')
|
|
326
|
+
begin
|
|
327
|
+
# 描画(MethodSignature.parse)が受理できないシグネチャは
|
|
328
|
+
# statichtml まで進んでから落ちる(か、誤った名前で DB に入る)ので
|
|
329
|
+
# パース時に拒否する。RBS 形式(def name:)や self. プレフィクスが該当
|
|
330
|
+
MethodSignature.parse(rd_line)
|
|
331
|
+
rescue ParseError
|
|
332
|
+
parse_error "unsupported method signature", line
|
|
333
|
+
end
|
|
334
|
+
case
|
|
335
|
+
when m = SIGNATURE.match(rd_line)
|
|
336
|
+
klass, typemark_, name = m.captures
|
|
337
|
+
typemark = _ = typemark_
|
|
338
|
+
Signature.new(klass, typemark, name)
|
|
339
|
+
when m = GVAR.match(rd_line)
|
|
340
|
+
Signature.new(nil, '$', (m[1] || raise)[1..-1])
|
|
341
|
+
else
|
|
342
|
+
parse_error "wrong method signature", line
|
|
343
|
+
end
|
|
344
|
+
end
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
end
|
|
@@ -53,7 +53,7 @@ module BitClust
|
|
|
53
53
|
|
|
54
54
|
def MessageCatalog.load_with_locales(prefix, locales)
|
|
55
55
|
path, loc = find_catalog(prefix, locales)
|
|
56
|
-
path ? load_file(path, loc) : new({}, 'C')
|
|
56
|
+
path ? load_file(path, loc || raise) : new({}, 'C')
|
|
57
57
|
end
|
|
58
58
|
|
|
59
59
|
def MessageCatalog.env_locales
|
|
@@ -72,10 +72,10 @@ module BitClust
|
|
|
72
72
|
private_class_method :find_catalog
|
|
73
73
|
|
|
74
74
|
def MessageCatalog.load_file(path, locale)
|
|
75
|
-
h = {}
|
|
75
|
+
h = {} #: Hash[String, String]
|
|
76
76
|
fopen(path, 'r:UTF-8') {|f|
|
|
77
77
|
f.each do |key|
|
|
78
|
-
h[key.chomp] = f.gets.chomp
|
|
78
|
+
h[key.chomp] = (f.gets || raise).chomp
|
|
79
79
|
end
|
|
80
80
|
}
|
|
81
81
|
new(h, locale)
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
#
|
|
3
|
+
# bitclust/method_since_calculator.rb
|
|
4
|
+
#
|
|
5
|
+
|
|
6
|
+
require 'bitclust/exception'
|
|
7
|
+
|
|
8
|
+
module BitClust
|
|
9
|
+
|
|
10
|
+
# 複数バージョンの DB(バージョンラダー)からメソッド名別の初出/削除
|
|
11
|
+
# バージョンを算出し、対象 DB のメソッドエントリへ書き込む(bitclust#132 P2)。
|
|
12
|
+
#
|
|
13
|
+
# ライブラリは意図的に無視する: 同じメソッドが版によって別ライブラリの
|
|
14
|
+
# 下でドキュメント化されていても、同一メソッドとして union で扱う。
|
|
15
|
+
#
|
|
16
|
+
# 算出値は「著者が明示した値(将来 P4 で {: since ...} 等から記録される
|
|
17
|
+
# 想定)」より優先度が低い。#apply は MethodEntry#fill_since/fill_until を
|
|
18
|
+
# 使うため、既に値がある名前は上書きしない。
|
|
19
|
+
class MethodSinceCalculator
|
|
20
|
+
|
|
21
|
+
def initialize(dbs)
|
|
22
|
+
versions = dbs.map {|db| db.propget('version') }
|
|
23
|
+
versions.each do |v|
|
|
24
|
+
if v.nil? || v.empty?
|
|
25
|
+
raise UserError, "database has no version property"
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
dup = versions.tally.select {|_, n| n > 1 }.keys
|
|
29
|
+
unless dup.empty?
|
|
30
|
+
raise UserError, "duplicate version(s) in ladder: #{dup.join(', ')}"
|
|
31
|
+
end
|
|
32
|
+
sorted = dbs.sort_by {|db| Gem::Version.new(db.propget('version') || raise) }
|
|
33
|
+
@versions = sorted.map {|db| db.propget('version') || raise }
|
|
34
|
+
@ladder = sorted
|
|
35
|
+
@first = {} #: Hash[key, String]
|
|
36
|
+
@last = {} #: Hash[key, String]
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# ラダーの各 DB を1つずつ走査して [クラス名, typechar, 生名] ごとの
|
|
40
|
+
# 初出/最終出現バージョンを記録する。走査済みの DB への参照は
|
|
41
|
+
# メモリを膨らませないようその場で捨てる
|
|
42
|
+
def scan
|
|
43
|
+
ladder = @ladder or raise "scan was already called"
|
|
44
|
+
@ladder = nil
|
|
45
|
+
until ladder.empty?
|
|
46
|
+
db = ladder.shift
|
|
47
|
+
version = db.propget('version') || raise
|
|
48
|
+
db.classes.each do |c|
|
|
49
|
+
c.entries.each do |m|
|
|
50
|
+
next if m.kind == :undefined
|
|
51
|
+
m.names.each do |name|
|
|
52
|
+
key = [c.name, m.typechar, name] #: key
|
|
53
|
+
@first[key] ||= version
|
|
54
|
+
@last[key] = version
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
self
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# key = [クラス名, typechar, 生名]。ラダー最古版から存在する(フロア)
|
|
63
|
+
# 場合は「不明」を意味するので nil を返す
|
|
64
|
+
def since_for(key)
|
|
65
|
+
first = @first[key] or return nil
|
|
66
|
+
first == (@versions.first || raise) ? nil : first
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# ラダー最新版でも存在する場合は「まだ削除されていない」ので nil。
|
|
70
|
+
# そうでなければ最終出現バージョンの次のラダーバージョン(削除された版)
|
|
71
|
+
def until_for(key)
|
|
72
|
+
last = @last[key] or return nil
|
|
73
|
+
return nil if last == (@versions.last || raise)
|
|
74
|
+
idx = @versions.index(last) or raise "must not happen: #{last.inspect}"
|
|
75
|
+
@versions[idx + 1] || raise
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# 対象 DB の各メソッドエントリへ算出済みの since/until を書き込む。
|
|
79
|
+
# target_db のバージョンはラダーに含まれていなければならない。
|
|
80
|
+
# 既に値がある名前は fill_since/fill_until の仕様により上書きされない
|
|
81
|
+
# (著者による明示値が算出値より優先される)。冪等: 2回目の apply は
|
|
82
|
+
# 何も変更しない(floor_skipped を除く)
|
|
83
|
+
def apply(target_db)
|
|
84
|
+
version = target_db.propget('version')
|
|
85
|
+
unless @versions.include?(version)
|
|
86
|
+
raise UserError, "#{version.inspect} is not one of the ladder versions: #{@versions.join(', ')}"
|
|
87
|
+
end
|
|
88
|
+
stats = {entries_updated: 0, since_filled: 0, until_filled: 0, floor_skipped: 0} #: stats
|
|
89
|
+
target_db.classes.each do |c|
|
|
90
|
+
c.entries.each do |m|
|
|
91
|
+
next if m.kind == :undefined
|
|
92
|
+
changed = false
|
|
93
|
+
m.names.each do |name|
|
|
94
|
+
key = [c.name, m.typechar, name] #: key
|
|
95
|
+
if since_v = since_for(key)
|
|
96
|
+
if m.fill_since(name, since_v)
|
|
97
|
+
stats[:since_filled] += 1
|
|
98
|
+
changed = true
|
|
99
|
+
end
|
|
100
|
+
else
|
|
101
|
+
stats[:floor_skipped] += 1
|
|
102
|
+
end
|
|
103
|
+
if (until_v = until_for(key)) && m.fill_until(name, until_v)
|
|
104
|
+
stats[:until_filled] += 1
|
|
105
|
+
changed = true
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
if changed
|
|
109
|
+
m.save
|
|
110
|
+
stats[:entries_updated] += 1
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
stats
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
@@ -18,6 +18,7 @@ require 'bitclust/refsdatabase'
|
|
|
18
18
|
require 'bitclust/rrdparser'
|
|
19
19
|
require 'bitclust/exception'
|
|
20
20
|
require 'fileutils'
|
|
21
|
+
require 'pathname'
|
|
21
22
|
|
|
22
23
|
module BitClust
|
|
23
24
|
|
|
@@ -47,14 +48,15 @@ module BitClust
|
|
|
47
48
|
attr_writer :refs
|
|
48
49
|
|
|
49
50
|
def init
|
|
50
|
-
|
|
51
|
-
FileUtils.
|
|
52
|
-
|
|
53
|
-
Dir.mkdir "#{
|
|
54
|
-
Dir.mkdir "#{
|
|
55
|
-
Dir.mkdir "#{
|
|
56
|
-
|
|
57
|
-
FileUtils.touch "#{
|
|
51
|
+
prefix = @prefix || raise
|
|
52
|
+
FileUtils.rm_rf prefix
|
|
53
|
+
FileUtils.mkdir_p prefix
|
|
54
|
+
Dir.mkdir "#{prefix}/library"
|
|
55
|
+
Dir.mkdir "#{prefix}/class"
|
|
56
|
+
Dir.mkdir "#{prefix}/method"
|
|
57
|
+
Dir.mkdir "#{prefix}/doc"
|
|
58
|
+
FileUtils.touch "#{prefix}/properties"
|
|
59
|
+
FileUtils.touch "#{prefix}/refs"
|
|
58
60
|
end
|
|
59
61
|
|
|
60
62
|
#
|
|
@@ -162,6 +164,56 @@ module BitClust
|
|
|
162
164
|
RRDParser.new(self).parse_file(path, libname, properties())
|
|
163
165
|
end
|
|
164
166
|
|
|
167
|
+
# Markdown ツリー(manual/api)を直接パースして DB を更新する(M3)。
|
|
168
|
+
# front matter の library: が所属を表すため、ライブラリごとに
|
|
169
|
+
# lib ファイル → メンバーファイル(reopen/redefine のみのファイルは後置。
|
|
170
|
+
# dynamic include の対象 module が先に定義されている必要があるため)の
|
|
171
|
+
# 順でパースする。版ゲート(since/until)外のライブラリ/メンバーはスキップ。
|
|
172
|
+
# エントリの source には md 断片が入り、source_location は md の実パスを指す
|
|
173
|
+
def update_by_markdowntree(md_root)
|
|
174
|
+
require 'bitclust/markdown_tree'
|
|
175
|
+
require 'bitclust/mdparser'
|
|
176
|
+
@md_root = md_root
|
|
177
|
+
# 描画層(screen.rb)が MDCompiler を選択するためのマーカー
|
|
178
|
+
propset 'source_format', 'markdown'
|
|
179
|
+
tree = MarkdownTree.scan(md_root)
|
|
180
|
+
version = properties()["version"]
|
|
181
|
+
tree.libraries.sort.each do |libname, lib|
|
|
182
|
+
next unless md_version_covers?(version, lib[:since], lib[:until])
|
|
183
|
+
lib_entry = MDParser.new(self).parse_file(File.join(md_root, lib[:path]), libname, properties())
|
|
184
|
+
lib_location = lib_entry.source_location
|
|
185
|
+
# 多重所属(ゲート付き library リスト)は、この版でゲートが生きている
|
|
186
|
+
# membership を持つライブラリだけがメンバーとして取り込む
|
|
187
|
+
members = tree.entities.select { |_, e|
|
|
188
|
+
e[:memberships].any? { |m|
|
|
189
|
+
m[:library] == libname && md_version_covers?(version, m[:since], m[:until])
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
sorted = members.keys.sort_by { |path|
|
|
193
|
+
reopen_only = members[path][:kinds].all? { |kind, _| %w[reopen redefine].include?(kind) }
|
|
194
|
+
[reopen_only ? 1 : 0, path]
|
|
195
|
+
}
|
|
196
|
+
sorted.each do |path|
|
|
197
|
+
entity = members[path]
|
|
198
|
+
next unless md_version_covers?(version, entity[:since], entity[:until])
|
|
199
|
+
MDParser.new(self).parse_file(File.join(md_root, path), libname, properties())
|
|
200
|
+
end
|
|
201
|
+
# parse_file は毎回 library の source_location を上書きするため
|
|
202
|
+
# lib ファイルの値へ戻す
|
|
203
|
+
lib_entry.source_location = lib_location
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
# since は「その版以降」、until は「その版未満」(ブリッジの
|
|
208
|
+
# #@since/#@until ラッパーと同じ意味論)
|
|
209
|
+
def md_version_covers?(version, since_version, until_version)
|
|
210
|
+
v = Gem::Version.new(version)
|
|
211
|
+
return false if since_version && v < Gem::Version.new(since_version)
|
|
212
|
+
return false if until_version && v >= Gem::Version.new(until_version)
|
|
213
|
+
true
|
|
214
|
+
end
|
|
215
|
+
private :md_version_covers?
|
|
216
|
+
|
|
165
217
|
def refs
|
|
166
218
|
@refs ||= RefsDatabase.load(realpath('refs'))
|
|
167
219
|
end
|
|
@@ -176,10 +228,12 @@ module BitClust
|
|
|
176
228
|
end
|
|
177
229
|
|
|
178
230
|
def copy_doc
|
|
231
|
+
return copy_doc_md if @md_root
|
|
232
|
+
return unless @root
|
|
179
233
|
root_path = Pathname.new(@root).expand_path
|
|
180
234
|
Dir.glob("#{@root}/../../doc/**/*.rd").each do |f|
|
|
181
235
|
if %r!\A#{Regexp.escape(@root)}/\.\./\.\./doc/([-\./\w]+)\.rd\z! =~ f
|
|
182
|
-
id = libname2id($1)
|
|
236
|
+
id = libname2id($1 || raise)
|
|
183
237
|
se = DocEntry.new(self, id)
|
|
184
238
|
s = Preprocessor.read(f, properties)
|
|
185
239
|
title, source = RRDParser.split_doc(s)
|
|
@@ -192,6 +246,65 @@ module BitClust
|
|
|
192
246
|
end
|
|
193
247
|
end
|
|
194
248
|
|
|
249
|
+
# ネイティブ md ツリーの doc ページ登録(manual/api の隣の manual/doc)。
|
|
250
|
+
# ページ = 他の doc ファイルから #@include 参照されていない .md。
|
|
251
|
+
# source には md がそのまま入り、source_location は md の実パスを指す
|
|
252
|
+
def copy_doc_md
|
|
253
|
+
require 'bitclust/mdparser'
|
|
254
|
+
# source_location は md_root と同じ形で格納する(api 側と同じ慣例。
|
|
255
|
+
# Rakefile の相対 manual/api なら manual/doc/... になる)。絶対化すると
|
|
256
|
+
# Windows のドライブレターのコロンで file:line 分割が壊れ、
|
|
257
|
+
# 編集リンクも実行環境のパス依存になる
|
|
258
|
+
doc_root = Pathname.new(File.join(@md_root, '..', 'doc')).cleanpath.to_s
|
|
259
|
+
return unless File.directory?(doc_root)
|
|
260
|
+
files = Dir.glob("#{doc_root}/**/*.md").sort
|
|
261
|
+
referenced = files.flat_map { |f|
|
|
262
|
+
base = File.dirname(f)
|
|
263
|
+
File.read(f).scan(/^\#[@%]include\((.*?)\)/).map { |t|
|
|
264
|
+
p = File.expand_path(t[0] || raise, base)
|
|
265
|
+
[p, "#{p}.md", p.sub(/\.rd\z/, '.md')]
|
|
266
|
+
}.flatten
|
|
267
|
+
}.to_set
|
|
268
|
+
version = properties()["version"]
|
|
269
|
+
files.each do |f|
|
|
270
|
+
next if referenced.include?(File.expand_path(f))
|
|
271
|
+
# front matter の since/until でページ自体をバージョンで出し分ける
|
|
272
|
+
# (ライブラリ側 update_by_markdowntree と同じ意味論)。範囲外の版では
|
|
273
|
+
# ページを登録しない(例: doc/spec/safelevel は until: "3.2" で 3.2 以降
|
|
274
|
+
# 非生成)
|
|
275
|
+
since_version, until_version = doc_md_version_range(f)
|
|
276
|
+
next unless md_version_covers?(version, since_version, until_version)
|
|
277
|
+
id = libname2id(f.delete_prefix("#{doc_root}/").sub(/\.md\z/, ''))
|
|
278
|
+
se = DocEntry.new(self, id)
|
|
279
|
+
s = Preprocessor.read(f, properties)
|
|
280
|
+
title, source = MDParser.split_doc(s)
|
|
281
|
+
se.title = title
|
|
282
|
+
se.source = source
|
|
283
|
+
se.source_location = Location.new(f, 1)
|
|
284
|
+
se.save
|
|
285
|
+
end
|
|
286
|
+
end
|
|
287
|
+
private :copy_doc_md
|
|
288
|
+
|
|
289
|
+
# doc ページの先頭 front matter から since/until を読む
|
|
290
|
+
# (MarkdownTree.scan がライブラリ/エンティティで読むのと同じ書式)。
|
|
291
|
+
# front matter が無い、または since/until を持たなければ [nil, nil]。
|
|
292
|
+
def doc_md_version_range(path)
|
|
293
|
+
since_version = nil
|
|
294
|
+
until_version = nil
|
|
295
|
+
File.open(path, 'r:UTF-8') do |io|
|
|
296
|
+
first = io.gets
|
|
297
|
+
return [nil, nil] unless first && first =~ /\A---\s*$/
|
|
298
|
+
while (line = io.gets)
|
|
299
|
+
break if line =~ /\A---\s*$/
|
|
300
|
+
since_version = $1 if line =~ /\Asince:\s*"?([^"\s]+)"?/
|
|
301
|
+
until_version = $1 if line =~ /\Auntil:\s*"?([^"\s]+)"?/
|
|
302
|
+
end
|
|
303
|
+
end
|
|
304
|
+
[since_version, until_version]
|
|
305
|
+
end
|
|
306
|
+
private :doc_md_version_range
|
|
307
|
+
|
|
195
308
|
#
|
|
196
309
|
# Doc Entry
|
|
197
310
|
#
|
|
@@ -311,7 +424,7 @@ module BitClust
|
|
|
311
424
|
check_transaction
|
|
312
425
|
id = classname2id(name)
|
|
313
426
|
if exist?("class/#{id}")
|
|
314
|
-
c = load_class(id)
|
|
427
|
+
c = load_class(id) || raise
|
|
315
428
|
c.clear
|
|
316
429
|
else
|
|
317
430
|
c = (@classmap[id] ||= ClassEntry.new(self, id))
|
|
@@ -331,6 +444,7 @@ module BitClust
|
|
|
331
444
|
private :load_class
|
|
332
445
|
|
|
333
446
|
def load_extent(entry_class)
|
|
447
|
+
# @type var h: Hash[String, untyped]
|
|
334
448
|
h = {}
|
|
335
449
|
id_extent(entry_class).each do |id|
|
|
336
450
|
h[id] = entry_class.new(self, id)
|
|
@@ -382,10 +496,6 @@ module BitClust
|
|
|
382
496
|
fetch_class(spec.klass).fetch_method(spec)
|
|
383
497
|
end
|
|
384
498
|
|
|
385
|
-
def search_method(pattern)
|
|
386
|
-
search_methods(pattern).first
|
|
387
|
-
end
|
|
388
|
-
|
|
389
499
|
def search_methods(pattern)
|
|
390
500
|
result = _search_methods(pattern)
|
|
391
501
|
if result.fail?
|