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,179 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
#
|
|
3
|
+
# bitclust/link_checker.rb
|
|
4
|
+
#
|
|
5
|
+
# DB 全体を走査して [[c:]]/[[m:]]/[[lib:]]/[[d:]]/[[f:]] 参照の
|
|
6
|
+
# リンク切れを検出する。bitclust の参照リンクは描画時に存在検証されない
|
|
7
|
+
# (URL を文字列変換で組むだけ)ため、壊れた参照は静かにリンク切れになる。
|
|
8
|
+
#
|
|
9
|
+
# 参照の抽出は正規表現の自前スキャンではなく、実際のコンパイル経路
|
|
10
|
+
# (RDCompiler#bracket_link に仕込んだフック @option[:link_checker])で
|
|
11
|
+
# 行う。これにより「描画時に本当にリンクになるものだけ」が対象になり、
|
|
12
|
+
# コードスパン・コードフェンス内の参照風テキストは自然に除外される。
|
|
13
|
+
|
|
14
|
+
require 'bitclust/compat'
|
|
15
|
+
require 'bitclust/nameutils'
|
|
16
|
+
require 'bitclust/methodid'
|
|
17
|
+
require 'bitclust/exception'
|
|
18
|
+
|
|
19
|
+
module BitClust
|
|
20
|
+
|
|
21
|
+
class LinkChecker
|
|
22
|
+
|
|
23
|
+
Finding = Struct.new(:location, :ref, :message)
|
|
24
|
+
|
|
25
|
+
# bracket_link が扱う型のうち、DB 外を指すので検証対象外のもの
|
|
26
|
+
EXTERNAL_TYPES = %w[url man rfc RFC ruby-list ruby-dev ruby-ext
|
|
27
|
+
ruby-talk ruby-core feature bug misc].freeze
|
|
28
|
+
|
|
29
|
+
def initialize(db, function_database: nil)
|
|
30
|
+
@db = db
|
|
31
|
+
@fdb = function_database
|
|
32
|
+
@findings = [] #: Array[Finding]
|
|
33
|
+
@current_location = nil #: String?
|
|
34
|
+
@skipped_function_refs = 0
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
attr_reader :findings
|
|
38
|
+
attr_reader :skipped_function_refs
|
|
39
|
+
attr_accessor :current_location
|
|
40
|
+
|
|
41
|
+
def broken_count
|
|
42
|
+
@findings.size
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# DB 中の全ソース(ライブラリ・クラス・メソッド・doc ページ)を
|
|
46
|
+
# コンパイルして findings を収集する
|
|
47
|
+
def check_all
|
|
48
|
+
@db.libraries.sort_by(&:id).each do |lib|
|
|
49
|
+
check_source(lib.source_location || "library #{lib.name}") {|c| c.compile(lib.source.to_s) }
|
|
50
|
+
end
|
|
51
|
+
@db.classes.sort_by(&:id).each do |klass|
|
|
52
|
+
check_source(klass.source_location || "class #{klass.name}") {|c| c.compile(klass.source.to_s) }
|
|
53
|
+
klass.entries.sort_by(&:id).each do |m|
|
|
54
|
+
check_source(m.source_location || "#{klass.name} #{m.name}") {|c| c.compile_method(m) }
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
@db.docs.sort_by(&:id).each do |doc|
|
|
58
|
+
check_source(doc.source_location || "doc #{doc.name}") {|c| c.compile(doc.source.to_s) }
|
|
59
|
+
end
|
|
60
|
+
@findings
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# RDCompiler#bracket_link から呼ばれるフック。描画には影響しない
|
|
64
|
+
def note_ref(type, arg)
|
|
65
|
+
case type
|
|
66
|
+
when 'c' then check_class(arg)
|
|
67
|
+
when 'm' then check_method(arg)
|
|
68
|
+
when 'lib' then check_library(arg)
|
|
69
|
+
when 'd' then check_doc(arg)
|
|
70
|
+
when 'f' then check_function(arg)
|
|
71
|
+
when 'ref' then check_ref(arg)
|
|
72
|
+
else
|
|
73
|
+
unless EXTERNAL_TYPES.include?(type)
|
|
74
|
+
# 未知の型は bracket_link がリテラル [[...]] のまま出力する。
|
|
75
|
+
# ほぼ確実に型名の書き間違いなので報告する
|
|
76
|
+
record(type, arg, 'unknown link type')
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
nil
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
private
|
|
83
|
+
|
|
84
|
+
def check_source(location)
|
|
85
|
+
# Location#to_s は行番号不明のとき "path:" になるので末尾の : を落とす
|
|
86
|
+
@current_location = location.to_s.sub(/:\z/, '')
|
|
87
|
+
compiler = new_compiler
|
|
88
|
+
begin
|
|
89
|
+
yield compiler
|
|
90
|
+
rescue => err
|
|
91
|
+
# コンパイル自体の失敗はリンク切れとは別問題(構文エラー等)。
|
|
92
|
+
# チェックを止めず、報告だけして続行する
|
|
93
|
+
record('compile', location.to_s, "compile failed: #{err.class}: #{err.message}")
|
|
94
|
+
end
|
|
95
|
+
ensure
|
|
96
|
+
@current_location = nil
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def new_compiler
|
|
100
|
+
mapper_conf = Hash.new {|_h, _k| '' } #: untyped
|
|
101
|
+
urlmapper = URLMapper.new(mapper_conf)
|
|
102
|
+
opt = { :database => @db, :link_checker => self } #: RDCompiler::option
|
|
103
|
+
if @db.properties['source_format'] == 'markdown'
|
|
104
|
+
require 'bitclust/mdcompiler'
|
|
105
|
+
gfm_opt = opt.merge(:gfm => true) #: RDCompiler::option
|
|
106
|
+
MDCompiler.new(urlmapper, 1, gfm_opt)
|
|
107
|
+
else
|
|
108
|
+
require 'bitclust/rdcompiler'
|
|
109
|
+
RDCompiler.new(urlmapper, 1, opt)
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def check_class(name)
|
|
114
|
+
@db.fetch_class(name)
|
|
115
|
+
rescue NotFoundError, InvalidKey
|
|
116
|
+
record('c', name, 'class not found')
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def check_method(spec_str)
|
|
120
|
+
# rdcompiler の complete_spec と同じ補完($~ 等は Kernel の特殊変数)
|
|
121
|
+
str = spec_str.start_with?('$') ? "Kernel#{spec_str}" : spec_str
|
|
122
|
+
begin
|
|
123
|
+
spec = MethodSpec.parse(str)
|
|
124
|
+
rescue => _err
|
|
125
|
+
record('m', spec_str, 'malformed method spec')
|
|
126
|
+
return
|
|
127
|
+
end
|
|
128
|
+
# fetch_methods は不在時に空配列(truthy)を返してしまい raise しない
|
|
129
|
+
# ため、単数形の fetch_method(不在で MethodNotFound)を使う
|
|
130
|
+
@db.fetch_method(spec)
|
|
131
|
+
rescue NotFoundError, InvalidKey
|
|
132
|
+
record('m', spec_str, 'method not found')
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def check_library(name)
|
|
136
|
+
return if name == '/' || name == '_index'
|
|
137
|
+
@db.fetch_library(name)
|
|
138
|
+
rescue NotFoundError, InvalidKey
|
|
139
|
+
record('lib', name, 'library not found')
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def check_doc(name)
|
|
143
|
+
@db.fetch_doc(name)
|
|
144
|
+
rescue NotFoundError, InvalidKey
|
|
145
|
+
record('d', name, 'doc not found')
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def check_function(name)
|
|
149
|
+
return if name == '/' || name == '_index' || name.empty?
|
|
150
|
+
fdb = @fdb
|
|
151
|
+
unless fdb
|
|
152
|
+
@skipped_function_refs += 1
|
|
153
|
+
return
|
|
154
|
+
end
|
|
155
|
+
fdb.fetch_function(name)
|
|
156
|
+
rescue NotFoundError, InvalidKey
|
|
157
|
+
record('f', name, 'function not found')
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# [[ref:type:name#frag]] 形式のみ検証する(参照先本体の存在チェック)。
|
|
161
|
+
# エントリローカルの [[ref:frag]] 形式は対象外(entry 文脈が必要で、
|
|
162
|
+
# アンカー未定義でも描画は劣化表示に留まるため)
|
|
163
|
+
def check_ref(arg)
|
|
164
|
+
if /\A(\w+):(.*)\#[-\w]+\z/ =~ arg
|
|
165
|
+
type, name = $1, $2
|
|
166
|
+
case type
|
|
167
|
+
when 'c' then check_class(name || raise)
|
|
168
|
+
when 'm' then check_method(name || raise)
|
|
169
|
+
when 'lib' then check_library(name || raise)
|
|
170
|
+
when 'd' then check_doc(name || raise)
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def record(type, arg, message)
|
|
176
|
+
@findings.push Finding.new(@current_location, "#{type}:#{arg}", message)
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
end
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'fileutils'
|
|
4
|
+
require 'pathname'
|
|
5
|
+
|
|
6
|
+
require 'bitclust/markdown_tree'
|
|
7
|
+
require 'bitclust/markdown_to_rrd'
|
|
8
|
+
|
|
9
|
+
module BitClust
|
|
10
|
+
# md ツリー → 旧形式の rd ツリー(LIBRARIES + .rd)を生成するブリッジ。
|
|
11
|
+
#
|
|
12
|
+
# 既存の update 機構(LIBRARIES → RRDParser/Preprocessor)を一切変更せずに
|
|
13
|
+
# md ツリーから DB を組み立てるための変換層。MarkdownTree の発見結果から:
|
|
14
|
+
# - LIBRARIES を再生成(ライブラリの since/until は #@ ゲートとして再具現化)
|
|
15
|
+
# - ライブラリ .rd = 変換済み本文 + メンバーへの #@include を再生成
|
|
16
|
+
# - メンバー = 変換済み本文を front matter の since/until で #@ ラップ
|
|
17
|
+
# (版指定ビルドで構造ゲートが効くように)
|
|
18
|
+
# - 断片 = そのまま変換
|
|
19
|
+
# 出力ファイル名は「md 名から .md を剥いだもの」(ライブラリのみ .rd 付き)に
|
|
20
|
+
# 統一し、本文中の #@include ターゲットも emit 名へ書き換える
|
|
21
|
+
# (Preprocessor はリテラルパスで解決するため)。
|
|
22
|
+
class MarkdownBridge
|
|
23
|
+
INCLUDE_RE = /^(\#@include\s*\()(.*?)(\))/
|
|
24
|
+
|
|
25
|
+
def self.build(md_root, out_root)
|
|
26
|
+
new(md_root, out_root).build
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# doc(散文ページ)の md ツリー → 旧形式の doc/*.rd。
|
|
30
|
+
# - クロスツリー include(api 断片の transclude)は旧レイアウト
|
|
31
|
+
# (../api/src/)へ戻す(ブリッジの api ツリーは api/src に置かれるため)
|
|
32
|
+
# - doc 内ローカル include から参照される断片(spec/regexp19 等)は
|
|
33
|
+
# 拡張子なしで emit する(copy_doc は **/*.rd だけをページとして読む)
|
|
34
|
+
# 戻り値は source_map(出力相対パス => 入力 md 相対パス)
|
|
35
|
+
def self.build_doc(md_doc_root, out_doc_root)
|
|
36
|
+
require 'bitclust/markdown_to_rrd'
|
|
37
|
+
files = Dir.glob('**/*.md', base: md_doc_root).sort
|
|
38
|
+
referenced = files.flat_map { |f|
|
|
39
|
+
base = File.dirname(f)
|
|
40
|
+
File.read(File.join(md_doc_root, f)).scan(/^\#@include\((?!(?:\.\.\/)+api\/)(.*?)\)/)
|
|
41
|
+
.map { |t| File.expand_path(base == '.' ? (t[0] || raise) : File.join(base, t[0] || raise), '/')
|
|
42
|
+
.delete_prefix('/') }
|
|
43
|
+
}.to_h { |t| [t, true] }
|
|
44
|
+
|
|
45
|
+
files.to_h do |f|
|
|
46
|
+
rrd = MarkdownToRRD.convert(File.read(File.join(md_doc_root, f)))
|
|
47
|
+
rrd = rrd.gsub(%r{^(\#@include\((?:\.\./)+api/)(?!src/)}, '\1src/')
|
|
48
|
+
name = f.sub(/\.md\z/, '')
|
|
49
|
+
rel = referenced[name] ? name : "#{name}.rd"
|
|
50
|
+
full = File.join(out_doc_root, rel)
|
|
51
|
+
FileUtils.mkdir_p(File.dirname(full))
|
|
52
|
+
File.write(full, rrd)
|
|
53
|
+
[rel, f]
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
attr_reader :warnings
|
|
58
|
+
|
|
59
|
+
# 出力相対パス(emit 名) => 入力 md 相対パス。
|
|
60
|
+
# source_location を manual/ の md へ再マップするために使う
|
|
61
|
+
attr_reader :source_map
|
|
62
|
+
|
|
63
|
+
def initialize(md_root, out_root)
|
|
64
|
+
@md_root = md_root
|
|
65
|
+
@out_root = out_root
|
|
66
|
+
@tree = MarkdownTree.scan(md_root)
|
|
67
|
+
@warnings = @tree.warnings.dup
|
|
68
|
+
@source_map = {}
|
|
69
|
+
# 名前はパス由来とは限らない(ファイル名衝突回避で改名された
|
|
70
|
+
# rdoc/rdoc.lib.md は front matter の name: が正)ので、パスから引く
|
|
71
|
+
@lib_by_path = @tree.libraries.to_h { |name, lib| [lib[:path], name] }
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def build
|
|
75
|
+
files = Dir.glob('**/*.md', base: @md_root).sort
|
|
76
|
+
@dirs = files.flat_map { |f|
|
|
77
|
+
parts = File.dirname(f).split('/')
|
|
78
|
+
(1..parts.size).map { |n| parts.first(n).join('/') }
|
|
79
|
+
}.uniq - ['.']
|
|
80
|
+
emitted = files.to_h { |f| [f, emit_name(f)] }
|
|
81
|
+
@source_map = emitted.invert
|
|
82
|
+
|
|
83
|
+
files.each do |f|
|
|
84
|
+
rrd = MarkdownToRRD.convert(File.read(File.join(@md_root, f)))
|
|
85
|
+
rrd = rewrite_includes(rrd, f, emitted)
|
|
86
|
+
if (libname = @lib_by_path[f])
|
|
87
|
+
rrd << member_includes(libname, emitted)
|
|
88
|
+
elsif (entity = @tree.entities[f])
|
|
89
|
+
rrd = wrap_gate(rrd, entity)
|
|
90
|
+
end
|
|
91
|
+
write(emitted[f], rrd)
|
|
92
|
+
end
|
|
93
|
+
write('LIBRARIES', libraries_manifest)
|
|
94
|
+
self
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
private
|
|
98
|
+
|
|
99
|
+
# ライブラリは <name>.rd(update_by_stdlibtree の解決規約)。
|
|
100
|
+
# 非ライブラリは拡張子なし(doc ツリーが `../api/src/_builtin/pack-template` の
|
|
101
|
+
# ように元式の拡張子なし名で断片を transclude しているため)。
|
|
102
|
+
# ただし同名ディレクトリと衝突する場合は .rd を付ける(scanf.md と scanf/)
|
|
103
|
+
def emit_name(f)
|
|
104
|
+
return "#{@lib_by_path[f]}.rd" if @lib_by_path[f]
|
|
105
|
+
name = f.sub(/\.md\z/, '')
|
|
106
|
+
return "#{name}.rd" if @dirs.include?(name)
|
|
107
|
+
name
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# LIBRARIES: 名前順。ライブラリの版ゲートは #@ で再具現化する
|
|
111
|
+
def libraries_manifest
|
|
112
|
+
@tree.libraries.sort.map { |name, lib|
|
|
113
|
+
entry = "#{name}\n"
|
|
114
|
+
entry = "\#@since #{lib[:since]}\n#{entry}\#@end\n" if lib[:since]
|
|
115
|
+
entry = "\#@until #{lib[:until]}\n#{entry}\#@end\n" if lib[:until]
|
|
116
|
+
entry
|
|
117
|
+
}.join
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# ライブラリのメンバー(membership がこのライブラリを指すエンティティ)への
|
|
121
|
+
# #@include をライブラリ .rd のディレクトリ相対で再生成する。
|
|
122
|
+
# reopen/redefine だけのファイルは後ろに並べる(reopen が dynamic include する
|
|
123
|
+
# module は同ライブラリ内で先に定義されている必要がある: json/rake)。
|
|
124
|
+
# 多重所属(ゲート付き library リスト)は membership のゲートで
|
|
125
|
+
# include サイトをラップする(旧 LIBRARIES 世界のゲート付き include と同義)
|
|
126
|
+
def member_includes(libname, emitted)
|
|
127
|
+
base = File.dirname("#{libname}.rd")
|
|
128
|
+
members = @tree.entities.select { |path, e|
|
|
129
|
+
e[:memberships].any? { |m| m[:library] == libname }
|
|
130
|
+
}
|
|
131
|
+
return '' if members.empty?
|
|
132
|
+
|
|
133
|
+
sorted = members.keys.sort_by do |path|
|
|
134
|
+
reopen_only = members[path][:kinds].all? { |kind, _| %w[reopen redefine].include?(kind) }
|
|
135
|
+
[reopen_only ? 1 : 0, path]
|
|
136
|
+
end
|
|
137
|
+
"\n" + sorted.map { |path|
|
|
138
|
+
m = members[path][:memberships].find { |mm| mm[:library] == libname } || raise
|
|
139
|
+
inc = "\#@include(#{relative(emitted[path], base)})\n"
|
|
140
|
+
inc = "\#@since #{m[:since]}\n#{inc}\#@end\n" if m[:since]
|
|
141
|
+
inc = "\#@until #{m[:until]}\n#{inc}\#@end\n" if m[:until]
|
|
142
|
+
inc
|
|
143
|
+
}.join
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# front matter の構造ゲート(since/until)を #@ ラッパーとして再具現化する
|
|
147
|
+
def wrap_gate(rrd, entity)
|
|
148
|
+
rrd = "\#@since #{entity[:since]}\n#{rrd}\#@end\n" if entity[:since]
|
|
149
|
+
rrd = "\#@until #{entity[:until]}\n#{rrd}\#@end\n" if entity[:until]
|
|
150
|
+
rrd
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# 本文中の #@include ターゲットを emit 名(相対)へ書き換える。
|
|
154
|
+
# 解決できないターゲットはそのまま(発見段階で警告済み)
|
|
155
|
+
def rewrite_includes(rrd, md_file, emitted)
|
|
156
|
+
return rrd unless rrd.include?('#@include')
|
|
157
|
+
base = File.dirname(md_file)
|
|
158
|
+
rrd.gsub(INCLUDE_RE) do
|
|
159
|
+
pre, target, post = $1, ($2 || raise), $3
|
|
160
|
+
resolved = resolve(base, target, emitted)
|
|
161
|
+
"#{pre}#{resolved ? relative(emitted[resolved], File.dirname(emitted[md_file])) : target}#{post}"
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def resolve(base, target, emitted)
|
|
166
|
+
[target, "#{target}.md", "#{target.sub(/\.rd\z/, '')}.md"].uniq.each do |cand|
|
|
167
|
+
rel = base == '.' ? cand : File.join(base, cand)
|
|
168
|
+
rel = File.expand_path(rel, '/').delete_prefix('/')
|
|
169
|
+
return rel if emitted.key?(rel)
|
|
170
|
+
end
|
|
171
|
+
nil
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def relative(path, from_dir)
|
|
175
|
+
return path if from_dir == '.'
|
|
176
|
+
Pathname.new(File.expand_path(path, '/'))
|
|
177
|
+
.relative_path_from(File.expand_path(from_dir, '/')).to_s
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def write(rel, content)
|
|
181
|
+
full = File.join(@out_root, rel)
|
|
182
|
+
FileUtils.mkdir_p(File.dirname(full))
|
|
183
|
+
File.write(full, content)
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
end
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'pathname'
|
|
4
|
+
|
|
5
|
+
require 'bitclust/include_graph'
|
|
6
|
+
require 'bitclust/rrd_to_markdown'
|
|
7
|
+
require 'bitclust/include_pruner'
|
|
8
|
+
require 'bitclust/whole_file_gate'
|
|
9
|
+
require 'bitclust/entity_splitter'
|
|
10
|
+
require 'bitclust/rrd_to_markdown'
|
|
11
|
+
|
|
12
|
+
module BitClust
|
|
13
|
+
# RD ツリー → Markdown ツリー変換のクロスファイル方針を束ねるオーケストレータ。
|
|
14
|
+
#
|
|
15
|
+
# 単一ファイル記法変換(RRDToMarkdown)はそのままに、include グラフの解析結果から
|
|
16
|
+
# 各ファイルへ次を適用する:
|
|
17
|
+
# - grouping include の prune(エンティティ取り込みは front matter 発見へ移行)
|
|
18
|
+
# - ファイル全体を包む版ゲートの解除(front matter の since/until へ移行)
|
|
19
|
+
# - front matter 注入(member: library/構造ゲート、library ルート: type/版ゲート)
|
|
20
|
+
#
|
|
21
|
+
# スコープ(対象版範囲)はパラメータ。旧版サルベージ時は別スコープで再実行する。
|
|
22
|
+
class MarkdownOrchestrator
|
|
23
|
+
attr_reader :scope, :graph
|
|
24
|
+
|
|
25
|
+
def initialize(src_root, scope: IncludeGraph::Scope.new('3.0', '4.2'))
|
|
26
|
+
@graph = IncludeGraph.analyze(src_root)
|
|
27
|
+
@scope = scope
|
|
28
|
+
@extra = @graph.front_matter_map(scope)
|
|
29
|
+
@graph.library_front_matter_map(scope).each do |path, fm|
|
|
30
|
+
(@extra[path] ||= {}).merge!(fm)
|
|
31
|
+
end
|
|
32
|
+
@prune_sites = @graph.grouping_include_sites
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def warnings
|
|
36
|
+
@graph.warnings
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# 変換対象か。LIBRARIES は front matter による発見に置き換わるため対象外
|
|
40
|
+
def convert?(relpath)
|
|
41
|
+
relpath != 'LIBRARIES'
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# 1つの出力 .md ファイルに対応する単位。
|
|
45
|
+
# path = 出力相対パス、rrd = rd 側到達点(md→rd 検証の期待値)、
|
|
46
|
+
# front_matter = 注入する front matter
|
|
47
|
+
Unit = Struct.new(:path, :rrd, :front_matter)
|
|
48
|
+
|
|
49
|
+
# relpath の RD を出力単位の列に還元する。
|
|
50
|
+
# ヘッダ関係(include/extend/alias)を持つマルチエンティティファイルは
|
|
51
|
+
# エンティティ単位に分割する(関係の front matter 一元化のため)。
|
|
52
|
+
# 関係を持たない束ね(Errno 族等)と lib+単一エンティティ兼用ファイルは
|
|
53
|
+
# 1 単位のまま。ライブラリファイルの分割ではエンティティを <libname>/ 配下に
|
|
54
|
+
# 置き、library と版ゲートを注入する
|
|
55
|
+
def units(relpath, rrd)
|
|
56
|
+
reduced, front_matter = reduce(relpath, rrd)
|
|
57
|
+
# スコープ外ファイル(front matter 注入なし)は分割しない。
|
|
58
|
+
# サルベージは別スコープの再実行で扱う
|
|
59
|
+
segments = front_matter.empty? ? nil : split_segments(reduced)
|
|
60
|
+
return [Unit.new(output_path(relpath, front_matter), reduced, front_matter)] unless segments
|
|
61
|
+
|
|
62
|
+
library = front_matter['type'] == 'library'
|
|
63
|
+
dir = library ? relpath.sub(/\.rd\z/, '') : File.dirname(relpath)
|
|
64
|
+
entity_fm =
|
|
65
|
+
if library
|
|
66
|
+
fm = { 'library' => relpath.sub(/\.rd\z/, '') } #: IncludeGraph::front_matter
|
|
67
|
+
%w[since until].each { |k| fm[k] = front_matter[k] if front_matter[k] }
|
|
68
|
+
fm
|
|
69
|
+
else
|
|
70
|
+
front_matter
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
source_dir = File.dirname(relpath)
|
|
74
|
+
units = segments.map do |name, text|
|
|
75
|
+
next Unit.new(output_path(relpath, front_matter), text, front_matter) if name.nil? # 概要部
|
|
76
|
+
|
|
77
|
+
seg_fm = entity_fm.dup
|
|
78
|
+
if (unwrapped = WholeFileGate.unwrap_for_scope(text, @scope))
|
|
79
|
+
# エンティティセグメントのゲートは自身の存在ゲート → 常に front matter へ
|
|
80
|
+
text = unwrapped[0]
|
|
81
|
+
merge_gate(seg_fm, unwrapped[1])
|
|
82
|
+
end
|
|
83
|
+
text = rewrite_includes(text, source_dir, dir)
|
|
84
|
+
filename = "#{EntitySplitter.entity_filename(name)}.md"
|
|
85
|
+
Unit.new(dir == '.' ? filename : File.join(dir, filename), text, seg_fm)
|
|
86
|
+
end
|
|
87
|
+
if library && segments.none? { |name, _| name.nil? }
|
|
88
|
+
# 概要部が無くてもライブラリ自体が発見から消えないよう front matter のみ合成
|
|
89
|
+
units.unshift(Unit.new(output_path(relpath, front_matter), '', front_matter))
|
|
90
|
+
end
|
|
91
|
+
units
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def convert_unit(unit)
|
|
95
|
+
RRDToMarkdown.convert(unit.rrd, extra_front_matter: unit.front_matter)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# relpath の RD を新パイプライン形の Markdown へ変換する(分割なしファイル用)
|
|
99
|
+
def convert(relpath, rrd)
|
|
100
|
+
us = units(relpath, rrd)
|
|
101
|
+
raise ArgumentError, "#{relpath} splits into #{us.size} files, use units" if us.size > 1
|
|
102
|
+
convert_unit(us.first)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# 変換の rd 側到達点(prune + 全体ゲート解除 + 定数 H1 ゲート解決後の RD)と
|
|
106
|
+
# front matter。MarkdownToRRD.convert(convert(...)) はこの RD と一致する
|
|
107
|
+
def reduce(relpath, rrd)
|
|
108
|
+
front_matter = (@extra[relpath] || {}).dup
|
|
109
|
+
rrd = IncludePruner.prune(rrd, @prune_sites[relpath] || [])
|
|
110
|
+
if (unwrapped = WholeFileGate.unwrap_for_scope(rrd, @scope))
|
|
111
|
+
if unwrap_allowed?(front_matter, unwrapped[1])
|
|
112
|
+
rrd = unwrapped[0]
|
|
113
|
+
merge_gate(front_matter, unwrapped[1])
|
|
114
|
+
elsif front_matter['type'] == 'library' &&
|
|
115
|
+
(regated = WholeFileGate.regate_metadata(rrd))
|
|
116
|
+
# 据え置きゲートの library ルート: メタデータ領域を独立ゲートに分離して
|
|
117
|
+
# front matter 化できるようにする(本文はゲートのまま)
|
|
118
|
+
rrd = regated
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
rrd = EntitySplitter.resolve_header_gates(rrd, @scope)
|
|
122
|
+
rrd = normalize_entity_h1(rrd)
|
|
123
|
+
rrd = normalize_signature_spacing(rrd)
|
|
124
|
+
rrd = RRDToMarkdown.normalize_dlist_colon_spacing(rrd)
|
|
125
|
+
rrd = rrd.sub(/\A(?:[ \t]*\n)+/, '') # 先頭空行(ゲート解決の残り)を除去
|
|
126
|
+
[normalize_header_regions(rrd), front_matter]
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
private
|
|
130
|
+
|
|
131
|
+
# 分割すべきならセグメント列を、そうでなければ nil を返す。
|
|
132
|
+
# 条件: エンティティ(名前付きセグメント)が2つ以上 + いずれかがヘッダ関係を持つ。
|
|
133
|
+
# lib+単一エンティティ兼用(pathname 型、仕様が認める形)は分割しない
|
|
134
|
+
def split_segments(reduced)
|
|
135
|
+
return nil unless EntitySplitter.header_relations?(reduced)
|
|
136
|
+
segments = EntitySplitter.segments(reduced)
|
|
137
|
+
return nil unless segments
|
|
138
|
+
segments.count { |name, _| name } >= 2 ? segments : nil
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# セグメントの置き場所が元ファイルと異なるディレクトリになる場合
|
|
142
|
+
# (lib 分割 → <libname>/ 配下)、相対 #@include を新ディレクトリから
|
|
143
|
+
# 解決できるよう書き換える
|
|
144
|
+
def rewrite_includes(text, source_dir, new_dir)
|
|
145
|
+
return text if source_dir == new_dir || text !~ /\#@include/
|
|
146
|
+
base = File.expand_path(new_dir, '/')
|
|
147
|
+
text.gsub(/^(\#@include\s*\()(.*?)(\))/) do
|
|
148
|
+
pre, target, post = $1, ($2 || raise), $3
|
|
149
|
+
abs = File.expand_path(source_dir == '.' ? target : File.join(source_dir, target), '/')
|
|
150
|
+
"#{pre}#{Pathname.new(abs).relative_path_from(base)}#{post}"
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# 出力 .md の相対パス(.rd は差し替え、その他は付加)
|
|
155
|
+
def md_path(relpath)
|
|
156
|
+
relpath.end_with?('.rd') ? relpath.sub(/\.rd\z/, '.md') : "#{relpath}.md"
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# ライブラリファイルの出力パス。他ファイルの出力と大文字小文字のみで
|
|
160
|
+
# 衝突する場合(rdoc/rdoc.md と rdoc/RDoc.md)は basename に .lib を挟んで
|
|
161
|
+
# 回避する(macOS/Windows の case-insensitive FS でチェックアウト不能に
|
|
162
|
+
# なるため)。名前がパスから導出できなくなるので front matter の name: で保持する
|
|
163
|
+
def output_path(relpath, front_matter)
|
|
164
|
+
path = md_path(relpath)
|
|
165
|
+
return path unless front_matter['type'] == 'library'
|
|
166
|
+
collides = @extra.keys.any? { |other|
|
|
167
|
+
next false if other == relpath
|
|
168
|
+
op = md_path(other)
|
|
169
|
+
op != path && op.casecmp?(path)
|
|
170
|
+
}
|
|
171
|
+
return path unless collides
|
|
172
|
+
front_matter['name'] = relpath.sub(/\.rd\z/, '')
|
|
173
|
+
path.sub(/\.md\z/, '.lib.md')
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
RELATION_RE = /\A(?:include|extend|alias)\s+\S/
|
|
177
|
+
HEADER_DIR_RE = /\A\#@(?:since|until|if|else|end)\b/
|
|
178
|
+
BLANK_LINE_RE = /\A\s*\z/
|
|
179
|
+
|
|
180
|
+
# H1 直後のヘッダ関係領域を md→rd の再生成形に正規化する:
|
|
181
|
+
# 最後の関係行までの空行を除き、関係行の末尾空白を落とす。
|
|
182
|
+
# 関係を持たない H1 の直後や本文の空行・散文ゲートは触らない
|
|
183
|
+
def normalize_header_regions(rrd)
|
|
184
|
+
lines = rrd.lines
|
|
185
|
+
out = [] #: Array[String]
|
|
186
|
+
i = 0
|
|
187
|
+
while i < lines.length
|
|
188
|
+
line = lines[i]
|
|
189
|
+
out << line
|
|
190
|
+
i += 1
|
|
191
|
+
next unless line =~ EntitySplitter::H1_RE
|
|
192
|
+
region = [] #: Array[String]
|
|
193
|
+
while i < lines.length &&
|
|
194
|
+
(lines[i] =~ RELATION_RE || lines[i] =~ HEADER_DIR_RE || lines[i] =~ BLANK_LINE_RE)
|
|
195
|
+
region << lines[i]
|
|
196
|
+
i += 1
|
|
197
|
+
end
|
|
198
|
+
last_rel = region.rindex { |l| l =~ RELATION_RE }
|
|
199
|
+
if last_rel
|
|
200
|
+
head = (region[0..last_rel] || raise).reject { |l| l =~ BLANK_LINE_RE }
|
|
201
|
+
.map { |l| l =~ RELATION_RE ? "#{l.rstrip}\n" : l }
|
|
202
|
+
out.concat(head)
|
|
203
|
+
out.concat(region[(last_rel + 1)..] || raise)
|
|
204
|
+
else
|
|
205
|
+
out.concat(region)
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
out.join
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
# 「=class Encoding」のようなスペース無し H1(RRDParser は受理)を
|
|
212
|
+
# 正規形「= class」に直す。単一ファイル変換器は正規形のみ扱う
|
|
213
|
+
def normalize_entity_h1(rrd)
|
|
214
|
+
return rrd unless rrd =~ /^=(?:class|module|object|reopen|redefine)\b/
|
|
215
|
+
rrd.gsub(/^=((?:class|module|object|reopen|redefine)\b)/, '= \1')
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
# 「---critical=(bool)」のようなスペース無しシグネチャ(RDCompiler は
|
|
219
|
+
# /\A---/ で受理)を正規形「--- name」に直す。コードブロック内に
|
|
220
|
+
# 行頭 --- は現れない前提(roundtrip 検証が破れを検出する)
|
|
221
|
+
def normalize_signature_spacing(rrd)
|
|
222
|
+
rrd.gsub(/^---(?=[^\s-])/, '--- ')
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
# ファイル全体ゲートを front matter へ解除してよいか。
|
|
226
|
+
# - 常真ゲート(gate == {}): 常に解除(従来どおり)
|
|
227
|
+
# - 断片・スコープ外(front matter なし): 据え置き。front matter を付けると
|
|
228
|
+
# #@include 展開の途中に `---` が現れてパースが壊れる(_builtin/Fiber.current)
|
|
229
|
+
# - type: library ルート: LIBRARIES 由来境界に包含される場合のみ(下記)
|
|
230
|
+
# - メンバー: エンティティ自身の存在ゲートなので常に解除
|
|
231
|
+
def unwrap_allowed?(front_matter, gate)
|
|
232
|
+
return true if gate.empty?
|
|
233
|
+
return false if front_matter.empty?
|
|
234
|
+
return true unless front_matter['type'] == 'library'
|
|
235
|
+
gate_subsumed?(front_matter, gate)
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
# ライブラリルートのファイル全体ゲートが LIBRARIES 由来の既存境界に
|
|
239
|
+
# 包含されるか。包含されないゲート(cmath: LIBRARIES は until のみ +
|
|
240
|
+
# ファイルは #@since 1.9.1)をマージすると「ライブラリの存在」まで
|
|
241
|
+
# 狭めてしまう(旧世界ではスコープ下限の版にも存在して内容が空)
|
|
242
|
+
def gate_subsumed?(front_matter, gate)
|
|
243
|
+
gate.all? { |kind, v|
|
|
244
|
+
cur = front_matter[kind.to_s]
|
|
245
|
+
next false unless cur
|
|
246
|
+
if kind == :since
|
|
247
|
+
Gem::Version.new(cur) >= Gem::Version.new(v)
|
|
248
|
+
else
|
|
249
|
+
Gem::Version.new(cur) <= Gem::Version.new(v)
|
|
250
|
+
end
|
|
251
|
+
}
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
# ファイル全体ゲートと注入済みゲート(include サイト / LIBRARIES 由来)の交差を取る
|
|
255
|
+
# (since は max、until は min。両者は同じ制約の別表現なので通常は一致する)
|
|
256
|
+
def merge_gate(front_matter, gate)
|
|
257
|
+
if (v = gate[:since])
|
|
258
|
+
front_matter['since'] = [front_matter['since'], v].compact
|
|
259
|
+
.max_by { |x| Gem::Version.new(x) }
|
|
260
|
+
end
|
|
261
|
+
if (v = gate[:until])
|
|
262
|
+
front_matter['until'] = [front_matter['until'], v].compact
|
|
263
|
+
.min_by { |x| Gem::Version.new(x) }
|
|
264
|
+
end
|
|
265
|
+
end
|
|
266
|
+
end
|
|
267
|
+
end
|