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,925 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BitClust
|
|
4
|
+
class RRDToMarkdown
|
|
5
|
+
def self.convert(rrd, extra_front_matter: {}, capi: false)
|
|
6
|
+
rrd = normalize_dlist_colon_spacing(rrd)
|
|
7
|
+
new(rrd, extra_front_matter: extra_front_matter, capi: capi).convert
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
# RDCompiler の dlist 継続は /\A:/(スペース不要)で dt になる
|
|
11
|
+
# (spec/operator の「:再定義できない演算子」)。dlist 文脈にある
|
|
12
|
+
# 「:term」だけを正規形「: term」に直す。段落継続の「:SYMBOL」行
|
|
13
|
+
# (openssl/ASN1 等)は RDCompiler では段落テキストなので触らない。
|
|
14
|
+
# 文脈判定は RDCompiler のディスパッチを行単位で再現する
|
|
15
|
+
def self.normalize_dlist_colon_spacing(rrd)
|
|
16
|
+
state = :none # :none | :para | :dlist
|
|
17
|
+
in_code = nil # コードブロックの終端パターン
|
|
18
|
+
rrd.lines.map { |l|
|
|
19
|
+
if in_code
|
|
20
|
+
in_code = nil if l =~ in_code
|
|
21
|
+
next l
|
|
22
|
+
end
|
|
23
|
+
case l
|
|
24
|
+
when /\A\#@samplecode/
|
|
25
|
+
in_code = /\A\#@end/
|
|
26
|
+
l
|
|
27
|
+
when /\A\/\/emlist.*\{/
|
|
28
|
+
in_code = %r<\A//\}>
|
|
29
|
+
state = :none unless state == :dlist # dd 内の emlist は dlist 継続
|
|
30
|
+
l
|
|
31
|
+
when /\A\#@/
|
|
32
|
+
l # ディレクティブは文脈に透明
|
|
33
|
+
when /\A:\s/
|
|
34
|
+
state = :dlist unless state == :para # 段落継続中の「: 」行は段落
|
|
35
|
+
l
|
|
36
|
+
when /\A:(?=\S)/
|
|
37
|
+
if state == :dlist
|
|
38
|
+
": #{l[1..]}"
|
|
39
|
+
else
|
|
40
|
+
state = :para # 段落(開始または継続)
|
|
41
|
+
l
|
|
42
|
+
end
|
|
43
|
+
when /\A[ \t]*\n?\z/
|
|
44
|
+
state = :none unless state == :dlist # 空行: dd は空行を跨ぐ
|
|
45
|
+
l
|
|
46
|
+
when /\A[ \t]/
|
|
47
|
+
state = :none unless state == :dlist # dd 説明は継続、段落は終了
|
|
48
|
+
l
|
|
49
|
+
when /\A(?:---|=)/
|
|
50
|
+
state = :none
|
|
51
|
+
l
|
|
52
|
+
else
|
|
53
|
+
state = :para
|
|
54
|
+
l
|
|
55
|
+
end
|
|
56
|
+
}.join
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# ファイル単体からは決められない front matter(library 所属・構造 since/until)を
|
|
60
|
+
# 注入するための口。include グラフを解析したオーケストレータが値を計算して渡す。
|
|
61
|
+
# name はファイル名の大文字小文字衝突回避で改名されたライブラリファイルの名前保持用
|
|
62
|
+
EXTRA_FRONT_MATTER_KEYS = %w[type name library since until].freeze
|
|
63
|
+
|
|
64
|
+
# capi: C API リファレンス(refm/capi)モード。シグネチャは C の
|
|
65
|
+
# 「--- <型付きシグネチャ>」で、def 等のキーワードを付けずに ### へ変換する
|
|
66
|
+
def initialize(rrd, extra_front_matter: {}, capi: false)
|
|
67
|
+
@src = rrd
|
|
68
|
+
@capi = capi
|
|
69
|
+
@extra_front_matter = extra_front_matter.transform_keys(&:to_s)
|
|
70
|
+
unknown = @extra_front_matter.keys - EXTRA_FRONT_MATTER_KEYS
|
|
71
|
+
unless unknown.empty?
|
|
72
|
+
raise ArgumentError, "unknown extra front matter keys: #{unknown.join(', ')}"
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def convert
|
|
77
|
+
@lines = @src.lines
|
|
78
|
+
@out = []
|
|
79
|
+
@index = 0
|
|
80
|
+
@front_matter = {}
|
|
81
|
+
@current_section = nil
|
|
82
|
+
# front matter はファイル単位。複数エンティティを含むファイルでは
|
|
83
|
+
# include/extend/alias の帰属が曖昧になるため、単一エンティティ時のみ front matter 化する
|
|
84
|
+
# (分割前の安全策。分割後は全ファイルが単一エンティティになる)。
|
|
85
|
+
@single_entity = @lines.count { |l| l =~ /\A= / } == 1
|
|
86
|
+
|
|
87
|
+
collect_library_metadata
|
|
88
|
+
process_body
|
|
89
|
+
@front_matter.merge!(@extra_front_matter)
|
|
90
|
+
emit_front_matter + @out.join
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
private
|
|
94
|
+
|
|
95
|
+
SAMPLECODE_RE = /\A\#@samplecode/
|
|
96
|
+
|
|
97
|
+
def collect_library_metadata
|
|
98
|
+
# capi には require/category 等の library メタデータは存在しない。
|
|
99
|
+
# 本文が「require の C 版です。」のように require で始まる場合の誤認を防ぐ
|
|
100
|
+
return if @capi
|
|
101
|
+
scan = @index
|
|
102
|
+
# [:cat, val] | [:req, val] | [:sub, val] | [:dir, line] | [:cmt, line] | [:blank]
|
|
103
|
+
# (tuple の長さが混在するため untyped の列で扱う)
|
|
104
|
+
tokens = [] #: Array[untyped]
|
|
105
|
+
nest = 0
|
|
106
|
+
# 直近の「nest==0 の空行直後」= [scan, tokens.size]。
|
|
107
|
+
# メタ確定をここまでで打ち切れる安全な切れ目
|
|
108
|
+
# (md→rd の再生成がメタ群の後に空行を出すため、空行が必須)
|
|
109
|
+
checkpoint = nil #: [Integer, Integer]?
|
|
110
|
+
while scan < @lines.length
|
|
111
|
+
l = @lines[scan]
|
|
112
|
+
case l
|
|
113
|
+
when /\Acategory\s+(.*)$/
|
|
114
|
+
# 版条件つき category は「単一値の存在ゲート」のみ対応(値の差し替えは
|
|
115
|
+
# 据え置き)。判定は build_metadata_front_matter のグループ検査で行う
|
|
116
|
+
tokens << [:cat, ($1 || raise).strip]; scan += 1
|
|
117
|
+
when /\Arequire\s+(.*)$/
|
|
118
|
+
tokens << [:req, ($1 || raise).strip]; scan += 1
|
|
119
|
+
when /\Asublibrary\s+(.*)$/
|
|
120
|
+
tokens << [:sub, ($1 || raise).strip]; scan += 1
|
|
121
|
+
when /\A\#@(?:since|until|if)\b/
|
|
122
|
+
tokens << [:dir, l]; nest += 1; scan += 1
|
|
123
|
+
when /\A\#@else\b/
|
|
124
|
+
tokens << [:dir, l]; scan += 1
|
|
125
|
+
when /\A\#@end\b/
|
|
126
|
+
break if nest == 0
|
|
127
|
+
tokens << [:dir, l]; nest -= 1; scan += 1
|
|
128
|
+
when /\A\s*$/
|
|
129
|
+
tokens << [:blank]; scan += 1
|
|
130
|
+
checkpoint = [scan, tokens.size] if nest == 0
|
|
131
|
+
when /\A\#@\#/
|
|
132
|
+
# #@# コメント(irb.rd の Author 行・require 群中の注記)→
|
|
133
|
+
# メタトークンとして保持。本文の前置き(rss.rd 型)だった場合は
|
|
134
|
+
# 確定時に末尾トリムで body へ戻す
|
|
135
|
+
tokens << [:cmt, l]; scan += 1
|
|
136
|
+
when /\A\#@/
|
|
137
|
+
# #@todo 等 → チェックポイントまでで確定
|
|
138
|
+
return unless checkpoint
|
|
139
|
+
scan, tokens = checkpoint[0], tokens[0, checkpoint[1]] || raise
|
|
140
|
+
nest = 0
|
|
141
|
+
break
|
|
142
|
+
else
|
|
143
|
+
break if nest == 0 # body
|
|
144
|
+
# 版分岐の途中で body に到達(set.rd/thread.rd の版分岐つき散文)
|
|
145
|
+
# → チェックポイントまでで確定し、版分岐ごと body に渡す
|
|
146
|
+
return unless checkpoint
|
|
147
|
+
scan, tokens = checkpoint[0], tokens[0, checkpoint[1]] || raise
|
|
148
|
+
nest = 0
|
|
149
|
+
break
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
# 末尾の cmt/blank 連鎖に #@# が含まれる場合、それはメタでなく本文の
|
|
153
|
+
# 前置きコメント(rss.rd の「#@# = rss」)→ 最初の cmt 以降を body へ戻す
|
|
154
|
+
# (連鎖先頭の空行はメタ領域の終端空行として残す)
|
|
155
|
+
chain_start = (tokens.rindex { |t| !%i[cmt blank].include?(t[0]) } || -1) + 1
|
|
156
|
+
if (first_cmt = (tokens[chain_start..] || raise).index { |t| t[0] == :cmt })
|
|
157
|
+
drop = tokens.length - (chain_start + first_cmt)
|
|
158
|
+
scan -= drop
|
|
159
|
+
tokens = tokens[0...-drop] || raise
|
|
160
|
+
end
|
|
161
|
+
return unless tokens.any? { |t| %i[cat req sub].include?(t[0]) }
|
|
162
|
+
return if nest != 0 # ファイル全体の版ゲート内で EOF → 据え置き
|
|
163
|
+
return unless build_metadata_front_matter(tokens)
|
|
164
|
+
@index = scan # メタ領域(末尾空行含む)を消費
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# 先頭メタデータ領域の tokens を front matter へ組み立てる。
|
|
168
|
+
# category は scalar(単一値の存在ゲートならゲート行ごとブロック化)、
|
|
169
|
+
# require/sublibrary は組み立て済みブロック行
|
|
170
|
+
# (#@ ディレクティブや #@# コメントを挟める)。
|
|
171
|
+
# #@ ブロックは「単一種の完結ブロック」のみ対応。種の混在ブロックや
|
|
172
|
+
# category の値差し替えは据え置き(false = 収集全体を bail)。
|
|
173
|
+
# 最初のメタ行より前の #@#(irb.rd の Author 行)は leading スロットへ。
|
|
174
|
+
def build_metadata_front_matter(tokens)
|
|
175
|
+
# 先頭の #@# コメント群: 直後に空行が続くもの(irb.rd の Author 行)は
|
|
176
|
+
# leading スロットへ。メタ行が直続するもの(irb/workspace.rd の
|
|
177
|
+
# require 注記)はそのメタブロックの先頭行として list 側に流す
|
|
178
|
+
first_non_cmt = tokens.index { |t| t[0] != :cmt } || tokens.length
|
|
179
|
+
if first_non_cmt > 0 && tokens[first_non_cmt]&.first == :blank
|
|
180
|
+
@front_matter['leading'] = tokens[0...first_non_cmt].map { |t| t[1] }
|
|
181
|
+
tokens = tokens[first_non_cmt..]
|
|
182
|
+
end
|
|
183
|
+
toks = tokens.reject { |t| t[0] == :blank }
|
|
184
|
+
|
|
185
|
+
# [:scalar, val] | [:block, md行列]
|
|
186
|
+
category = nil #: [Symbol, untyped]?
|
|
187
|
+
# 'require' / 'sublibrary' => 組み立て済み md 行
|
|
188
|
+
blocks = {} #: Hash[String, Array[String]]
|
|
189
|
+
i = 0
|
|
190
|
+
while i < toks.length
|
|
191
|
+
kind, val = toks[i]
|
|
192
|
+
case kind
|
|
193
|
+
when :cat
|
|
194
|
+
return false if category
|
|
195
|
+
category = [:scalar, val]
|
|
196
|
+
i += 1
|
|
197
|
+
when :req, :sub
|
|
198
|
+
(blocks[kind == :req ? 'require' : 'sublibrary'] ||= []) << " - #{val}\n"
|
|
199
|
+
i += 1
|
|
200
|
+
when :cmt
|
|
201
|
+
# コメントは同じブロック内の位置を保って流す: 直後のメタトークンの
|
|
202
|
+
# 種のブロックへ(irb/workspace.rd の require 注記)
|
|
203
|
+
nxt = toks[i + 1]
|
|
204
|
+
return false unless nxt && %i[req sub].include?(nxt[0])
|
|
205
|
+
(blocks[nxt[0] == :req ? 'require' : 'sublibrary'] ||= []) << val
|
|
206
|
+
i += 1
|
|
207
|
+
when :dir
|
|
208
|
+
# 完結した単一種のゲートブロックを読む
|
|
209
|
+
depth = 0
|
|
210
|
+
group = [] #: Array[[Symbol, String]]
|
|
211
|
+
gkinds = [] #: Array[Symbol]
|
|
212
|
+
j = i
|
|
213
|
+
loop do
|
|
214
|
+
return false unless toks[j]
|
|
215
|
+
k2, v2 = toks[j]
|
|
216
|
+
group << toks[j]
|
|
217
|
+
if k2 == :dir
|
|
218
|
+
if v2 =~ /\A\#@end/
|
|
219
|
+
depth -= 1
|
|
220
|
+
elsif v2 !~ /\A\#@else/
|
|
221
|
+
depth += 1
|
|
222
|
+
end
|
|
223
|
+
elsif k2 != :cmt
|
|
224
|
+
gkinds << k2
|
|
225
|
+
end
|
|
226
|
+
j += 1
|
|
227
|
+
break if depth == 0
|
|
228
|
+
end
|
|
229
|
+
gkinds.uniq!
|
|
230
|
+
return false if gkinds.size != 1
|
|
231
|
+
case gkinds.first
|
|
232
|
+
when :cat
|
|
233
|
+
return false if category || group.count { |t| t[0] == :cat } > 1
|
|
234
|
+
category = [:block, group.map { |t| t[0] == :cat ? "category: #{t[1]}\n" : t[1] }]
|
|
235
|
+
else
|
|
236
|
+
key = gkinds.first == :req ? 'require' : 'sublibrary'
|
|
237
|
+
(blocks[key] ||= []).concat(
|
|
238
|
+
group.map { |t| %i[dir cmt].include?(t[0]) ? t[1] : " - #{t[1]}\n" })
|
|
239
|
+
end
|
|
240
|
+
i = j
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
return false if category.nil? && blocks.empty?
|
|
244
|
+
@front_matter['category'] = category[0] == :scalar ? category[1] : category[1] if category
|
|
245
|
+
blocks.each { |k, v| @front_matter[k] = v }
|
|
246
|
+
true
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
def emit_front_matter
|
|
250
|
+
return '' if @front_matter.empty?
|
|
251
|
+
lines = ["---\n"]
|
|
252
|
+
# 順序: type, name, library, include, extend, alias, since, until, category, require, sublibrary(MARKUP_SPEC §1.7)
|
|
253
|
+
%w[type name library].each do |key|
|
|
254
|
+
next unless v = @front_matter[key]
|
|
255
|
+
if v.is_a?(Array)
|
|
256
|
+
# 多重所属のゲート付きリスト(MARKUP_SPEC §1.2)。
|
|
257
|
+
# #@ 行は生 YAML ではコメント = 和集合リストとして無害に表示される
|
|
258
|
+
lines << "#{key}:\n"
|
|
259
|
+
v.each do |m|
|
|
260
|
+
lines << "\#@since #{m['since']}\n" if m['since']
|
|
261
|
+
lines << "\#@until #{m['until']}\n" if m['until']
|
|
262
|
+
lines << " - #{m['name']}\n"
|
|
263
|
+
lines << "\#@end\n" if m['until']
|
|
264
|
+
lines << "\#@end\n" if m['since']
|
|
265
|
+
end
|
|
266
|
+
else
|
|
267
|
+
lines << "#{key}: #{v}\n"
|
|
268
|
+
end
|
|
269
|
+
end
|
|
270
|
+
%w[include extend alias].each do |key|
|
|
271
|
+
if block = @front_matter[key]
|
|
272
|
+
lines << "#{key}:\n"
|
|
273
|
+
block.each { |bl| lines << bl } # block は組み立て済みの行(` - item\n` / `#@...\n`)
|
|
274
|
+
end
|
|
275
|
+
end
|
|
276
|
+
%w[since until].each do |key|
|
|
277
|
+
if v = @front_matter[key]
|
|
278
|
+
lines << "#{key}: \"#{v}\"\n" # "3.10" が float 3.1 に化けないよう常にクォート(§1.2)
|
|
279
|
+
end
|
|
280
|
+
end
|
|
281
|
+
if block = @front_matter['leading']
|
|
282
|
+
block.each { |bl| lines << bl } # メタ領域先頭の #@# コメント行
|
|
283
|
+
end
|
|
284
|
+
if v = @front_matter['category']
|
|
285
|
+
if v.is_a?(Array)
|
|
286
|
+
lines.concat(v) # ゲート付き category(#@ 行 + `category: 値` の組み立て済み行)
|
|
287
|
+
else
|
|
288
|
+
lines << "category: #{v}\n"
|
|
289
|
+
end
|
|
290
|
+
end
|
|
291
|
+
%w[require sublibrary].each do |key|
|
|
292
|
+
if block = @front_matter[key]
|
|
293
|
+
lines << "#{key}:\n"
|
|
294
|
+
block.each { |bl| lines << bl } # 組み立て済み行(` - item\n` / `#@...\n`)
|
|
295
|
+
end
|
|
296
|
+
end
|
|
297
|
+
lines << "---\n"
|
|
298
|
+
lines.join
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
def process_body
|
|
302
|
+
while @index < @lines.length
|
|
303
|
+
line = current_line
|
|
304
|
+
case line
|
|
305
|
+
when SAMPLECODE_RE
|
|
306
|
+
convert_samplecode(line)
|
|
307
|
+
when /\A\/\/emlist/
|
|
308
|
+
convert_emlist(line)
|
|
309
|
+
when /\A---(?=[^\s-])/, /\A--- /
|
|
310
|
+
# RDCompiler は /\A---/ で受理するため「---name」(スペース無し)も
|
|
311
|
+
# シグネチャ(正規形「--- name」に直して変換する)
|
|
312
|
+
convert_signature(line)
|
|
313
|
+
when /\A={1,4}\[a:([^\]]+)\]\s+(.*)/
|
|
314
|
+
convert_anchored_heading(line, $1, $2)
|
|
315
|
+
when /\A===== /
|
|
316
|
+
convert_h5(line)
|
|
317
|
+
when /\A====\s+(.*)/
|
|
318
|
+
convert_h4(line, $1)
|
|
319
|
+
when /\A===\s+(.*)/
|
|
320
|
+
convert_h3(line, $1)
|
|
321
|
+
when /\A@param\s/
|
|
322
|
+
convert_param(line)
|
|
323
|
+
when /\A@raise\s/
|
|
324
|
+
convert_raise(line)
|
|
325
|
+
when /\A@return\s/
|
|
326
|
+
convert_return(line)
|
|
327
|
+
when /\A@see\s/
|
|
328
|
+
convert_see(line)
|
|
329
|
+
when /\A(\s+)\*\s/
|
|
330
|
+
convert_ulist_item(line)
|
|
331
|
+
when /\A(\s+)\(\d+\)\s/
|
|
332
|
+
convert_olist_item(line)
|
|
333
|
+
when /\A:\s+(.*)/
|
|
334
|
+
term = $1
|
|
335
|
+
# RDCompiler は段落中(テキスト行直後)の「: 」行を段落の継続とする。
|
|
336
|
+
# 直前の出力が段落テキストなら dlist 化せずそのまま流す(news/1.8.5 型)
|
|
337
|
+
if @out.last =~ /\A(?!- |#|`|@)\S/
|
|
338
|
+
@out << convert_inline_refs(line)
|
|
339
|
+
advance
|
|
340
|
+
else
|
|
341
|
+
convert_dlist_item(line, term)
|
|
342
|
+
end
|
|
343
|
+
when /\A\#@/
|
|
344
|
+
raw_passthrough(line)
|
|
345
|
+
when /\A== /
|
|
346
|
+
convert_h2(line)
|
|
347
|
+
when /\A= /
|
|
348
|
+
convert_h1(line)
|
|
349
|
+
when /\A(\s+)\S/
|
|
350
|
+
convert_indented_code(line)
|
|
351
|
+
when /\A(\d+)\.\s/
|
|
352
|
+
convert_text_number(line)
|
|
353
|
+
else
|
|
354
|
+
passthrough(line)
|
|
355
|
+
end
|
|
356
|
+
end
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
# dd(記述リストの説明)/ @param 等の説明の中でフェンスを出すときの
|
|
360
|
+
# 内容カラムのインデント幅。項目マーカー「- 」の幅に合わせる
|
|
361
|
+
DD_CONTENT_INDENT = ' '
|
|
362
|
+
|
|
363
|
+
# フェンス内容行にインデントを前置する。ただし:
|
|
364
|
+
# - 空行には前置しない(末尾空白を増やさないため)
|
|
365
|
+
# - 前処理ディレクティブ行(#@since/#@until/#@if/#@else/#@end 等)には
|
|
366
|
+
# 前置しない。Preprocessor はこれらを行頭カラム0でしか認識しない
|
|
367
|
+
# (bitclust/lib/bitclust/preprocessor.rb の /\A\#@since\b/ 等)ため、
|
|
368
|
+
# インデントすると版解決が効かなくなる
|
|
369
|
+
def indent_content_line(line, indent)
|
|
370
|
+
return line if indent.empty?
|
|
371
|
+
return line if line =~ /\A[ \t]*\n?\z/
|
|
372
|
+
return line if line =~ /\A\#@/
|
|
373
|
+
indent + line
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
# indent: 呼び出し文脈の内容カラム(トップレベルは既定の ''、
|
|
377
|
+
# dd/@param 系の説明内から呼ぶ場合は DD_CONTENT_INDENT)。
|
|
378
|
+
# CommonMark でリスト項目/dd の説明に確実にネストさせるための字下げで、
|
|
379
|
+
# 新しい MDCompiler の INDENTED_FENCE_RE がこれを見て dd/li に取り込む
|
|
380
|
+
def convert_emlist(line, indent = '')
|
|
381
|
+
# //emlist[caption][lang]{...//}
|
|
382
|
+
caption = nil
|
|
383
|
+
lang = nil
|
|
384
|
+
if line =~ /\A\/\/emlist\[([^\]]*)\]\[(\w+)\]\{/
|
|
385
|
+
caption = $1
|
|
386
|
+
lang = $2
|
|
387
|
+
elsif line =~ /\A\/\/emlist\[([^\]]*)\]\{/
|
|
388
|
+
caption = $1
|
|
389
|
+
elsif line =~ /\A\/\/emlist\{/
|
|
390
|
+
# no caption, no lang
|
|
391
|
+
end
|
|
392
|
+
parts = [] #: Array[String]
|
|
393
|
+
parts << "```"
|
|
394
|
+
parts << (lang || "")
|
|
395
|
+
if caption && !caption.empty?
|
|
396
|
+
escaped_caption = caption.gsub('\\', '\\\\\\\\').gsub('"', '\\"')
|
|
397
|
+
parts << " title=\"#{escaped_caption}\""
|
|
398
|
+
end
|
|
399
|
+
@out << indent + parts.join + "\n"
|
|
400
|
+
advance
|
|
401
|
+
while @index < @lines.length
|
|
402
|
+
line = current_line
|
|
403
|
+
if line =~ /\A\/\/\}\s*$/
|
|
404
|
+
@out << indent + "```\n"
|
|
405
|
+
advance
|
|
406
|
+
return
|
|
407
|
+
end
|
|
408
|
+
@out << indent_content_line(line, indent)
|
|
409
|
+
advance
|
|
410
|
+
end
|
|
411
|
+
end
|
|
412
|
+
|
|
413
|
+
DIRECTIVE_NEST_RE = /\A\#@(?:since|until|if)\b/
|
|
414
|
+
DIRECTIVE_END_RE = /\A\#@end\s*$/
|
|
415
|
+
|
|
416
|
+
def convert_samplecode(line, indent = '')
|
|
417
|
+
label = line.sub(SAMPLECODE_RE, '').strip
|
|
418
|
+
if label.empty?
|
|
419
|
+
@out << indent + "```ruby\n"
|
|
420
|
+
else
|
|
421
|
+
escaped_label = label.gsub('\\', '\\\\\\\\').gsub('"', '\\"')
|
|
422
|
+
@out << indent + "```ruby title=\"#{escaped_label}\"\n"
|
|
423
|
+
end
|
|
424
|
+
advance
|
|
425
|
+
nest = 0
|
|
426
|
+
while @index < @lines.length
|
|
427
|
+
line = current_line
|
|
428
|
+
if line =~ DIRECTIVE_NEST_RE
|
|
429
|
+
nest += 1
|
|
430
|
+
@out << line # ディレクティブは常にカラム0(Preprocessor 要件)
|
|
431
|
+
advance
|
|
432
|
+
elsif line =~ DIRECTIVE_END_RE
|
|
433
|
+
if nest > 0
|
|
434
|
+
nest -= 1
|
|
435
|
+
@out << line # 同上
|
|
436
|
+
advance
|
|
437
|
+
else
|
|
438
|
+
@out << indent + "```\n"
|
|
439
|
+
advance
|
|
440
|
+
return
|
|
441
|
+
end
|
|
442
|
+
else
|
|
443
|
+
@out << indent_content_line(line, indent)
|
|
444
|
+
advance
|
|
445
|
+
end
|
|
446
|
+
end
|
|
447
|
+
end
|
|
448
|
+
|
|
449
|
+
def convert_signature(line)
|
|
450
|
+
# 「--- 」または「---name」(スペース無し形)のプレフィックスだけ除去。
|
|
451
|
+
# 2つ目以降の空白はシグネチャの一部として保持する(B1)
|
|
452
|
+
sig = line.sub(/\A---(?: |(?=[^\s-]))/, '').chomp
|
|
453
|
+
# B1: 空白を保持(正規化しない)
|
|
454
|
+
|
|
455
|
+
if @capi
|
|
456
|
+
# C API: シグネチャは型から始まり自己記述的。キーワードは付けない
|
|
457
|
+
@out << "### #{sig}\n"
|
|
458
|
+
advance
|
|
459
|
+
return
|
|
460
|
+
end
|
|
461
|
+
|
|
462
|
+
if @current_section == :module_function
|
|
463
|
+
@out << "### module_function def #{sig}\n"
|
|
464
|
+
elsif sig =~ /\A\$/ && sig !~ /[({]/
|
|
465
|
+
@out << "### gvar #{sig}\n"
|
|
466
|
+
elsif sig =~ /\A[A-Z]/ && sig !~ /[({]/
|
|
467
|
+
@out << "### const #{sig}\n"
|
|
468
|
+
else
|
|
469
|
+
@out << "### def #{sig}\n"
|
|
470
|
+
end
|
|
471
|
+
advance
|
|
472
|
+
end
|
|
473
|
+
|
|
474
|
+
def convert_param(line)
|
|
475
|
+
l = line.chomp
|
|
476
|
+
if l =~ /\A@param(\s+)(\S+)(\s+)(.*)\z/
|
|
477
|
+
@out << "- **param**#{$1}`#{$2}` --#{$3}#{convert_inline_refs($4 || raise)}\n"
|
|
478
|
+
elsif l =~ /\A@param(\s+)(\S+)\z/
|
|
479
|
+
@out << "- **param**#{$1}`#{$2}` --\n"
|
|
480
|
+
end
|
|
481
|
+
advance
|
|
482
|
+
collect_continuation_lines(greedy: true)
|
|
483
|
+
end
|
|
484
|
+
|
|
485
|
+
def convert_raise(line)
|
|
486
|
+
l = line.chomp
|
|
487
|
+
if l =~ /\A@raise(\s+)(\S+)(\s+)(.*)\z/
|
|
488
|
+
@out << "- **raise**#{$1}`#{$2}` --#{$3}#{convert_inline_refs($4 || raise)}\n"
|
|
489
|
+
elsif l =~ /\A@raise(\s+)(\S+)\z/
|
|
490
|
+
@out << "- **raise**#{$1}`#{$2}` --\n"
|
|
491
|
+
end
|
|
492
|
+
advance
|
|
493
|
+
collect_continuation_lines(greedy: true)
|
|
494
|
+
end
|
|
495
|
+
|
|
496
|
+
def convert_return(line)
|
|
497
|
+
# B2: @return 後のスペースを保持
|
|
498
|
+
rest = line.sub(/\A@return/, '').chomp
|
|
499
|
+
@out << "- **return** --#{convert_inline_refs(rest)}\n"
|
|
500
|
+
advance
|
|
501
|
+
collect_continuation_lines(greedy: true)
|
|
502
|
+
end
|
|
503
|
+
|
|
504
|
+
def convert_see(line)
|
|
505
|
+
# @see → - **SEE**、[[→[ 置換、区切りは保持
|
|
506
|
+
rest = line.sub(/\A@see(\s+)/, '').chomp
|
|
507
|
+
space = $1
|
|
508
|
+
converted = convert_inline_refs(rest)
|
|
509
|
+
@out << "- **SEE**#{space}#{converted}\n"
|
|
510
|
+
advance
|
|
511
|
+
collect_continuation_lines # RDCompiler の see は空白のみ行で停止する(greedy 不可)
|
|
512
|
+
end
|
|
513
|
+
|
|
514
|
+
# greedy: RDCompiler の dd_without_p と同じ貪欲さ(@param 等のメタデータ用)。
|
|
515
|
+
# 空白を含む行(" \n" や空白行を挟んだ例示ブロック)も継続し、
|
|
516
|
+
# 完全な空行("\n")でのみ停止する。@see は従来規則(空白のみ行で停止)
|
|
517
|
+
def collect_continuation_lines(greedy: false)
|
|
518
|
+
nest = 0
|
|
519
|
+
while @index < @lines.length
|
|
520
|
+
line = current_line
|
|
521
|
+
if line =~ /\A\#@(?:since|until|if)\b/
|
|
522
|
+
nest += 1
|
|
523
|
+
raw_passthrough(line)
|
|
524
|
+
elsif line =~ /\A\#@else/
|
|
525
|
+
raw_passthrough(line)
|
|
526
|
+
elsif line =~ /\A\#@end/
|
|
527
|
+
# nest 0 の #@end(メタデータより前に開いたゲートの閉じ)も透過する。
|
|
528
|
+
# 版解決後は消えて継続行が dd に直結するため(fileutils の @param)
|
|
529
|
+
nest -= 1 if nest > 0
|
|
530
|
+
raw_passthrough(line)
|
|
531
|
+
elsif line =~ /\A\s+\S/ && line !~ /\A@/ && line !~ /\A---/ && line !~ /\A=/ && line !~ SAMPLECODE_RE
|
|
532
|
+
@out << convert_inline_refs(line)
|
|
533
|
+
advance
|
|
534
|
+
elsif greedy && line =~ /\A[ \t]+$/
|
|
535
|
+
@out << line
|
|
536
|
+
advance
|
|
537
|
+
elsif greedy && line =~ SAMPLECODE_RE
|
|
538
|
+
# RDCompiler の dd はコードブロックも説明の一部。新 MDCompiler が
|
|
539
|
+
# dd に取り込めるよう、内容カラムにインデントしたフェンスで出す
|
|
540
|
+
convert_samplecode(line, DD_CONTENT_INDENT)
|
|
541
|
+
elsif greedy && line =~ /\A\/\/emlist/
|
|
542
|
+
convert_emlist(line, DD_CONTENT_INDENT)
|
|
543
|
+
else
|
|
544
|
+
break
|
|
545
|
+
end
|
|
546
|
+
end
|
|
547
|
+
end
|
|
548
|
+
|
|
549
|
+
def convert_anchored_heading(line, anchor, text)
|
|
550
|
+
prefix = '#' * line[/\A=+/].length
|
|
551
|
+
@out << "#{prefix} #{escape_backticks(text)} {##{anchor}}\n"
|
|
552
|
+
advance
|
|
553
|
+
end
|
|
554
|
+
|
|
555
|
+
def convert_h3(line, text)
|
|
556
|
+
@out << "### #{text}\n"
|
|
557
|
+
advance
|
|
558
|
+
end
|
|
559
|
+
|
|
560
|
+
def convert_h4(line, text)
|
|
561
|
+
@out << "#### #{text}\n"
|
|
562
|
+
advance
|
|
563
|
+
end
|
|
564
|
+
|
|
565
|
+
def convert_h5(line)
|
|
566
|
+
# プレフィックス置換のみ(余分な空白・末尾空白を保持。psych.rd)
|
|
567
|
+
@out << line.sub(/\A===== /, '##### ')
|
|
568
|
+
advance
|
|
569
|
+
end
|
|
570
|
+
|
|
571
|
+
def convert_ulist_item(line)
|
|
572
|
+
# B4: 元のインデント幅と * 後のスペースを保持
|
|
573
|
+
line =~ /\A(\s+)\*(\s+)(.*)/
|
|
574
|
+
indent = $1 || raise
|
|
575
|
+
space = $2 || raise
|
|
576
|
+
text = $3 || raise
|
|
577
|
+
content_indent = indent.length + 1 + space.length
|
|
578
|
+
@out << "#{indent}-#{space}#{convert_inline_refs(text.chomp)}\n"
|
|
579
|
+
advance
|
|
580
|
+
# 継続行を収集(空行・リスト項目で停止)。
|
|
581
|
+
# RDCompiler の項目継続はインデント深さ不問(項目より浅い折り返しも継続)
|
|
582
|
+
while @index < @lines.length
|
|
583
|
+
l = current_line
|
|
584
|
+
if l =~ SAMPLECODE_RE
|
|
585
|
+
break # カラム0 の samplecode は項目の外(トップレベルで変換。news/2_5_0)
|
|
586
|
+
elsif l =~ /\A\#@/
|
|
587
|
+
raw_passthrough(l)
|
|
588
|
+
elsif l =~ /\A\s*$/
|
|
589
|
+
break # 空行でリスト継続終了
|
|
590
|
+
elsif l =~ /\A(\s+)\S/ && l !~ /\A\s+\*\s/ && l !~ /\A\s+\(\d+\)\s/
|
|
591
|
+
@out << convert_inline_refs(l)
|
|
592
|
+
advance
|
|
593
|
+
else
|
|
594
|
+
break
|
|
595
|
+
end
|
|
596
|
+
end
|
|
597
|
+
end
|
|
598
|
+
|
|
599
|
+
def convert_olist_item(line)
|
|
600
|
+
# B4: 元のインデント幅を保持
|
|
601
|
+
line =~ /\A(\s+)\((\d+)\)\s+(.*)/
|
|
602
|
+
indent = $1 || raise
|
|
603
|
+
num = $2 || raise
|
|
604
|
+
text = $3 || raise
|
|
605
|
+
@out << "#{indent}#{num}. #{convert_inline_refs(text.chomp)}\n"
|
|
606
|
+
advance
|
|
607
|
+
# 継続行を収集(ulist と同じ。RDCompiler の項目継続はインデント深さ不問)
|
|
608
|
+
while @index < @lines.length
|
|
609
|
+
l = current_line
|
|
610
|
+
if l =~ SAMPLECODE_RE
|
|
611
|
+
break # ulist と同じ: カラム0 の samplecode は項目の外
|
|
612
|
+
elsif l =~ /\A\#@/
|
|
613
|
+
raw_passthrough(l)
|
|
614
|
+
elsif l =~ /\A\s*$/
|
|
615
|
+
break
|
|
616
|
+
elsif l =~ /\A(\s+)\S/ && l !~ /\A\s+\*\s/ && l !~ /\A\s+\(\d+\)\s/
|
|
617
|
+
@out << convert_inline_refs(l)
|
|
618
|
+
advance
|
|
619
|
+
else
|
|
620
|
+
break
|
|
621
|
+
end
|
|
622
|
+
end
|
|
623
|
+
end
|
|
624
|
+
|
|
625
|
+
def code_like_term?(term)
|
|
626
|
+
# バッククォート/バックスラッシュを含む term はスパン化しない
|
|
627
|
+
# (`term` の境界が \` エスケープと曖昧になるため。symref 等)
|
|
628
|
+
term.length < 40 && term !~ /\p{Hiragana}|\p{Katakana}|\p{Han}|[`\\]/
|
|
629
|
+
end
|
|
630
|
+
|
|
631
|
+
def format_dlist_term(term)
|
|
632
|
+
t = convert_inline_refs(term.chomp)
|
|
633
|
+
if code_like_term?(t)
|
|
634
|
+
"**`#{t}`**"
|
|
635
|
+
else
|
|
636
|
+
"**#{t}**"
|
|
637
|
+
end
|
|
638
|
+
end
|
|
639
|
+
|
|
640
|
+
def convert_dlist_item(line, term)
|
|
641
|
+
advance
|
|
642
|
+
formatted = format_dlist_term(term)
|
|
643
|
+
@out << "- #{formatted}:\n"
|
|
644
|
+
# 説明を収集。RDCompiler の dd_with_p と同じ貪欲さ:
|
|
645
|
+
# 空行(何行でも)を跨いでインデント行か emlist が続く限り説明の一部
|
|
646
|
+
# (インデント段落と emlist を交互に何個でも受ける。String#% 型)
|
|
647
|
+
while @index < @lines.length
|
|
648
|
+
l = current_line
|
|
649
|
+
if l =~ /\A(\s+)\S/
|
|
650
|
+
@out << convert_inline_refs(l)
|
|
651
|
+
advance
|
|
652
|
+
elsif l =~ SAMPLECODE_RE
|
|
653
|
+
# 項目の内容カラムにインデントしたフェンスにする(CommonMark で
|
|
654
|
+
# dd にネストさせ、新 MDCompiler の INDENTED_FENCE_RE に取り込ませる)
|
|
655
|
+
convert_samplecode(l, DD_CONTENT_INDENT)
|
|
656
|
+
elsif l =~ /\A\/\/emlist/
|
|
657
|
+
convert_emlist(l, DD_CONTENT_INDENT)
|
|
658
|
+
elsif l =~ /\A\#@/
|
|
659
|
+
# #@ 指令行は dd の文脈に透明(版解決後は消えて説明が連続する。
|
|
660
|
+
# ここで打ち切ると後続インデント行がコード扱いになる。pack-template)
|
|
661
|
+
raw_passthrough(l)
|
|
662
|
+
elsif l =~ /\A\s*$/
|
|
663
|
+
# 透明なのは版解決で消える指令のみ。#@samplecode/#@include は
|
|
664
|
+
# 中身がカラム0のため透過するとコード本体を見て dd を誤って打ち切る
|
|
665
|
+
scan = @index + 1
|
|
666
|
+
scan += 1 while scan < @lines.length &&
|
|
667
|
+
(@lines[scan] =~ /\A\s*$/ ||
|
|
668
|
+
(@lines[scan] =~ /\A\#@/ &&
|
|
669
|
+
@lines[scan] !~ SAMPLECODE_RE &&
|
|
670
|
+
@lines[scan] !~ /\A\#@include/))
|
|
671
|
+
nxt = scan < @lines.length ? @lines[scan] : nil
|
|
672
|
+
if nxt && (nxt =~ /\A\s+\S/ || nxt =~ SAMPLECODE_RE || nxt =~ /\A\/\/emlist/)
|
|
673
|
+
@out << l
|
|
674
|
+
advance
|
|
675
|
+
else
|
|
676
|
+
break
|
|
677
|
+
end
|
|
678
|
+
else
|
|
679
|
+
break
|
|
680
|
+
end
|
|
681
|
+
end
|
|
682
|
+
end
|
|
683
|
+
|
|
684
|
+
# indent: 呼び出し文脈の内容カラム(他のフェンス出力メソッドと揃えるため
|
|
685
|
+
# 用意。現状このメソッドはトップレベル(process_body)からしか呼ばれず、
|
|
686
|
+
# RDCompiler の list(インデントのみのコード相当)は dd/li には取り込まれ
|
|
687
|
+
# ないため、呼び出し側は常に既定の '' のまま。将来 dd/li 文脈から呼ぶ
|
|
688
|
+
# 必要が生じた場合のための口として引数だけ揃えておく
|
|
689
|
+
def convert_indented_code(line, indent = '')
|
|
690
|
+
# 行を収集してベースインデントを取得
|
|
691
|
+
code_lines = [] #: Array[String]
|
|
692
|
+
while @index < @lines.length
|
|
693
|
+
line = current_line
|
|
694
|
+
if line =~ /\A(\s+)\S/
|
|
695
|
+
code_lines << line
|
|
696
|
+
advance
|
|
697
|
+
elsif line =~ /\A\s*$/
|
|
698
|
+
# RDCompiler の list は /\A\S/ まで継続する(空行は何行でも跨ぐ)。
|
|
699
|
+
# 空行列の先に再びインデント行が来るなら同じブロックの一部
|
|
700
|
+
scan = @index + 1
|
|
701
|
+
scan += 1 while scan < @lines.length && @lines[scan] =~ /\A\s*$/
|
|
702
|
+
if scan < @lines.length && @lines[scan] =~ /\A\s+\S/
|
|
703
|
+
code_lines << line
|
|
704
|
+
advance
|
|
705
|
+
else
|
|
706
|
+
break
|
|
707
|
+
end
|
|
708
|
+
else
|
|
709
|
+
break
|
|
710
|
+
end
|
|
711
|
+
end
|
|
712
|
+
|
|
713
|
+
# ベースインデント(空行以外の最小インデント)を検出
|
|
714
|
+
base_indent = code_lines
|
|
715
|
+
.reject { |l| l =~ /\A\s*$/ }
|
|
716
|
+
.map { |l| l =~ /\A(\s+)/; ($1 || raise).length }
|
|
717
|
+
.min || 1
|
|
718
|
+
|
|
719
|
+
fence = '`' * (3 + base_indent)
|
|
720
|
+
@out << indent + "#{fence}\n"
|
|
721
|
+
code_lines.each do |l|
|
|
722
|
+
if l =~ /\A\s*$/
|
|
723
|
+
@out << l
|
|
724
|
+
else
|
|
725
|
+
@out << indent + (l[base_indent..] || raise) # ベースインデントを除去
|
|
726
|
+
end
|
|
727
|
+
end
|
|
728
|
+
@out << indent + "#{fence}\n"
|
|
729
|
+
end
|
|
730
|
+
|
|
731
|
+
def convert_h1(line)
|
|
732
|
+
@out << line.sub(/\A= /, '# ')
|
|
733
|
+
advance
|
|
734
|
+
collect_header_relations
|
|
735
|
+
end
|
|
736
|
+
|
|
737
|
+
# H1 直後の include/extend/alias を front matter へ集約する。
|
|
738
|
+
# #@ で囲まれた版条件つきのヘッダ関係は当面 body 据え置き(スライス2で対応)。
|
|
739
|
+
def collect_header_relations
|
|
740
|
+
return unless @single_entity
|
|
741
|
+
scan = @index
|
|
742
|
+
# [:blank] | [:rel, kind, val] | [:dir, line](tuple の長さが混在するため untyped の列で扱う)
|
|
743
|
+
tokens = [] #: Array[untyped]
|
|
744
|
+
nest = 0
|
|
745
|
+
saw_rel = false
|
|
746
|
+
region_end = @index
|
|
747
|
+
while scan < @lines.length
|
|
748
|
+
l = @lines[scan]
|
|
749
|
+
case l
|
|
750
|
+
when /\A(include|extend|alias)\s+(.+)$/
|
|
751
|
+
tokens << [:rel, $1, ($2 || raise).strip]; scan += 1; saw_rel = true; region_end = scan
|
|
752
|
+
when /\A\#@(?:since|until|if)\b/
|
|
753
|
+
tokens << [:dir, l]; nest += 1; scan += 1
|
|
754
|
+
when /\A\#@else\b/
|
|
755
|
+
tokens << [:dir, l]; scan += 1
|
|
756
|
+
when /\A\#@end\b/
|
|
757
|
+
break if nest == 0
|
|
758
|
+
tokens << [:dir, l]; nest -= 1; scan += 1; region_end = scan
|
|
759
|
+
when /\A\s*$/
|
|
760
|
+
tokens << [:blank]; scan += 1
|
|
761
|
+
when /\A\#@/
|
|
762
|
+
break if nest == 0 # level-0 の #@ コメント等は body 境界
|
|
763
|
+
tokens << [:dir, l]; scan += 1
|
|
764
|
+
else
|
|
765
|
+
return if nest > 0 # #@ ブロック内の本文 → ヘッダ関係でない → 据え置き
|
|
766
|
+
break # level-0 の本文 → ヘッダ領域終端
|
|
767
|
+
end
|
|
768
|
+
end
|
|
769
|
+
return unless saw_rel
|
|
770
|
+
return if nest != 0 # 不均衡 → 据え置き
|
|
771
|
+
return unless build_header_front_matter(tokens)
|
|
772
|
+
@index = region_end
|
|
773
|
+
end
|
|
774
|
+
|
|
775
|
+
# tokens を front matter のブロック行(` - item\n` / `#@...\n`)へ組み立てる。
|
|
776
|
+
# 素の関係行と、#@ ブロック(内部は単一種のみ)の並びを種別ごとに集約する。
|
|
777
|
+
# 1ブロック内の種別混在(データ上存在しない)は据え置き。
|
|
778
|
+
def build_header_front_matter(tokens)
|
|
779
|
+
toks = tokens.reject { |t| t[0] == :blank }
|
|
780
|
+
# [kind, 組み立て済み行の配列]
|
|
781
|
+
chunks = [] #: Array[[untyped, Array[String]]]
|
|
782
|
+
i = 0
|
|
783
|
+
while i < toks.length
|
|
784
|
+
t = toks[i]
|
|
785
|
+
if t[0] == :rel
|
|
786
|
+
chunks << [t[1], [" - #{t[2]}\n"]]
|
|
787
|
+
i += 1
|
|
788
|
+
next
|
|
789
|
+
end
|
|
790
|
+
# #@ ブロック: 対応する #@end までを1チャンクに
|
|
791
|
+
depth = 0
|
|
792
|
+
lines = [] #: Array[String]
|
|
793
|
+
kinds = [] #: Array[String]
|
|
794
|
+
while i < toks.length
|
|
795
|
+
tt = toks[i]
|
|
796
|
+
if tt[0] == :dir
|
|
797
|
+
lines << tt[1]
|
|
798
|
+
depth += 1 if tt[1] =~ /\A\#@(?:since|until|if)\b/
|
|
799
|
+
depth -= 1 if tt[1] =~ /\A\#@end\b/
|
|
800
|
+
else
|
|
801
|
+
lines << " - #{tt[2]}\n"
|
|
802
|
+
kinds << tt[1]
|
|
803
|
+
end
|
|
804
|
+
i += 1
|
|
805
|
+
break if depth.zero?
|
|
806
|
+
end
|
|
807
|
+
return false if depth != 0 || kinds.uniq.size != 1
|
|
808
|
+
chunks << [kinds.first, lines]
|
|
809
|
+
end
|
|
810
|
+
chunks.group_by(&:first).each do |kind, group|
|
|
811
|
+
@front_matter[kind] = group.flat_map(&:last)
|
|
812
|
+
end
|
|
813
|
+
true
|
|
814
|
+
end
|
|
815
|
+
|
|
816
|
+
def convert_h2(line)
|
|
817
|
+
@out << line.sub(/\A== /, '## ')
|
|
818
|
+
# セクション追跡
|
|
819
|
+
if line =~ /Module\s+Functions?/i
|
|
820
|
+
@current_section = :module_function
|
|
821
|
+
else
|
|
822
|
+
@current_section = nil
|
|
823
|
+
end
|
|
824
|
+
advance
|
|
825
|
+
end
|
|
826
|
+
|
|
827
|
+
def convert_text_number(line)
|
|
828
|
+
# RD の "N. text" → MD の "**N.** text" (太字番号テキスト)
|
|
829
|
+
converted = line.sub(/\A(\d+\.)\s/, '**\\1** ')
|
|
830
|
+
@out << convert_inline_refs(converted)
|
|
831
|
+
advance
|
|
832
|
+
end
|
|
833
|
+
|
|
834
|
+
def raw_passthrough(line)
|
|
835
|
+
@out << line
|
|
836
|
+
advance
|
|
837
|
+
end
|
|
838
|
+
|
|
839
|
+
def passthrough(line)
|
|
840
|
+
# 行頭 # のリテラル本文(旧 news 等)は MD 見出しと誤認されるためエスケープ
|
|
841
|
+
line = "\\#{line}" if line.start_with?('#')
|
|
842
|
+
@out << add_code_spans(convert_inline_refs(line))
|
|
843
|
+
advance
|
|
844
|
+
end
|
|
845
|
+
|
|
846
|
+
# テキスト行の __WORD__ パターンをコードスパンに変換。
|
|
847
|
+
# ブラケットリンク内は除外(convert_inline_refs 後に呼ばれるため、
|
|
848
|
+
# 保護対象は md の一重括弧 [type:...]。[m:Delegator#__getobj__] 等)
|
|
849
|
+
def add_code_spans(text)
|
|
850
|
+
# クロスリファレンス部分を保護して変換
|
|
851
|
+
parts = text.split(/(\[[a-zA-Z][\w-]*:[^\]]*\])/)
|
|
852
|
+
parts.map { |part|
|
|
853
|
+
if part.start_with?('[') && part.end_with?(']')
|
|
854
|
+
part
|
|
855
|
+
else
|
|
856
|
+
part.gsub(/(__\w+__)/, '`\\1`')
|
|
857
|
+
end
|
|
858
|
+
}.join
|
|
859
|
+
end
|
|
860
|
+
|
|
861
|
+
# BitClust::RDCompiler::BracketLink と同等の正規表現
|
|
862
|
+
BRACKET_LINK = /\[\[[\w-]+?:[!-~]+?(?:\[\] )?\]\]/n
|
|
863
|
+
|
|
864
|
+
def convert_inline_refs(text)
|
|
865
|
+
remain = text.b
|
|
866
|
+
result = +''.b
|
|
867
|
+
while (m = BRACKET_LINK.match(remain))
|
|
868
|
+
result << escape_ref_lookalikes(m.pre_match) << convert_one_ref(m[0])
|
|
869
|
+
remain = m.post_match
|
|
870
|
+
end
|
|
871
|
+
result << escape_ref_lookalikes(remain)
|
|
872
|
+
result.force_encoding(text.encoding)
|
|
873
|
+
end
|
|
874
|
+
|
|
875
|
+
def convert_one_ref(match)
|
|
876
|
+
inner = match[2..-3] # strip [[ and ]]
|
|
877
|
+
# RD の "[] " (末尾スペース) → メソッド名の [] として扱う
|
|
878
|
+
inner = inner.sub(/\[\] \z/, '[]')
|
|
879
|
+
# .# → ? (モジュール関数参照)
|
|
880
|
+
inner = inner.sub(/\.#/, '?.')
|
|
881
|
+
# メソッド名内の [ ] をバックスラッシュエスケープ
|
|
882
|
+
if inner =~ /\A([\w-]+:)(.*)/n
|
|
883
|
+
prefix = $1 || raise
|
|
884
|
+
target = $2 || raise
|
|
885
|
+
target = target.gsub('\\', '\\\\\\\\').gsub('[', '\\[').gsub(']', '\\]')
|
|
886
|
+
inner = prefix + target
|
|
887
|
+
end
|
|
888
|
+
"[#{inner}]"
|
|
889
|
+
end
|
|
890
|
+
|
|
891
|
+
# 参照に見えるリテラル本文 [ruby-talk:198440] 等(旧 news)をエスケープする。
|
|
892
|
+
# エスケープしないと md→rd の bare-ref 復元で [[...]] に誤変換される
|
|
893
|
+
# 本文バッククォートの GFM 忠実化(M2):
|
|
894
|
+
# - GNU 風引用 `token'(rd に code マークアップがなかった代替記法。
|
|
895
|
+
# getoptlong の `--version' 等)→ インラインコードスパン `token`
|
|
896
|
+
# - それ以外の生バッククォート → \` エスケープ(偶発スパン防止)
|
|
897
|
+
# md→rd が両方とも復元する。__WORD__ の自動スパンはこの後の
|
|
898
|
+
# add_code_spans が付けるため対象外
|
|
899
|
+
def escape_ref_lookalikes(bin)
|
|
900
|
+
escape_backticks(
|
|
901
|
+
bin.gsub(/(?<!\[)\[([a-zA-Z][a-zA-Z-]*:[^\[\]]*)\](?!\])/n) { "\\[#{$1}]" }
|
|
902
|
+
)
|
|
903
|
+
end
|
|
904
|
+
|
|
905
|
+
# GNU 風引用 `token' → コードスパン、残りの生バッククォート → \` 。
|
|
906
|
+
# 除外(エスケープで温存):
|
|
907
|
+
# - 開き ` の直前が \(rd の正規表現特殊変数 \` 等)
|
|
908
|
+
# - 中身にバックスラッシュ(DOSISH の `\' 等。閉じ ` が \` と曖昧になる)
|
|
909
|
+
# - TeX 風二重引用 ``text''(rdoc.rd。開き直前の ` と閉じ直後の ' で判定)
|
|
910
|
+
# 見出しなど参照変換を通さないパスからも使う
|
|
911
|
+
def escape_backticks(bin)
|
|
912
|
+
bin.gsub(/(?<![\\`])`([^`'\s\\]+)'(?!')/) { "\x00#{$1}\x00" }
|
|
913
|
+
.gsub('`', '\\\\`')
|
|
914
|
+
.gsub("\x00", '`')
|
|
915
|
+
end
|
|
916
|
+
|
|
917
|
+
def current_line
|
|
918
|
+
@lines[@index]
|
|
919
|
+
end
|
|
920
|
+
|
|
921
|
+
def advance
|
|
922
|
+
@index += 1
|
|
923
|
+
end
|
|
924
|
+
end
|
|
925
|
+
end
|