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,883 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
#
|
|
3
|
+
# bitclust/mdcompiler.rb
|
|
4
|
+
#
|
|
5
|
+
# Markdown ソース → HTML のネイティブコンパイラ(Markdown 移行フェーズ3)。
|
|
6
|
+
#
|
|
7
|
+
# RDCompiler のサブクラスとして、行レベルのディスパッチだけを Markdown 記法
|
|
8
|
+
# (MARKUP_SPEC)に差し替える。HTML の出力部品(見出し・リスト・dl・
|
|
9
|
+
# シンタックスハイライト・リンク解決)はすべて RDCompiler から継承する。
|
|
10
|
+
#
|
|
11
|
+
# M1(等価モード、既定): 変換器が生成する md に対して、対応する rd を
|
|
12
|
+
# RDCompiler にかけた場合と同一の HTML を出力する(tools/md-compile-check.rb
|
|
13
|
+
# が全実データで検証する)。
|
|
14
|
+
#
|
|
15
|
+
# M2(GFM モード、option :gfm => true): GFM の表現を描画する —
|
|
16
|
+
# インラインコードスパン `x` → <code>、行頭 **N.** → <strong>、
|
|
17
|
+
# GFM テーブル(ヘッダ + 区切り行必須)→ <table>。
|
|
18
|
+
# M1 との差は <code>/<strong>/<table> 系マークアップのみ
|
|
19
|
+
# (tools/md-compile-check.rb --gfm が全実データで検証する)。
|
|
20
|
+
|
|
21
|
+
require 'bitclust/rdcompiler'
|
|
22
|
+
require 'bitclust/markdown_to_rrd'
|
|
23
|
+
|
|
24
|
+
module BitClust
|
|
25
|
+
|
|
26
|
+
class MDCompiler < RDCompiler
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
# RDCompiler のテキストコンパイル(エスケープ・参照解決)を
|
|
31
|
+
# GFM モードのセグメント処理から呼ぶための別名
|
|
32
|
+
alias rd_compile_text compile_text
|
|
33
|
+
|
|
34
|
+
def gfm?
|
|
35
|
+
@option[:gfm]
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# メソッド系シグネチャ(キーワード付き h3)。capi(@type == :function)は
|
|
39
|
+
# キーワード無しの ### 全部がシグネチャ(capi に本文見出しは無い)
|
|
40
|
+
METHOD_SIGNATURE_RE = /\A### (?:module_function def |def |const |gvar )/
|
|
41
|
+
|
|
42
|
+
def signature_re
|
|
43
|
+
@type == :function ? /\A### / : METHOD_SIGNATURE_RE
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# $1: 行頭の空白(ネストレベル判定用)、$2: マーカー文字列そのもの
|
|
47
|
+
# ("- " や "12. " 等。CommonMark の項目内容カラム幅の算出に使う。
|
|
48
|
+
# spec 0.31.2 #list-items)
|
|
49
|
+
MD_ITEM_RE = /\A(\s*)(- |\d+\. )/
|
|
50
|
+
FENCE_RE = /\A`{3,}/
|
|
51
|
+
# インデントされたフェンス(リスト項目・dlist 説明内のコードブロック)
|
|
52
|
+
INDENTED_FENCE_RE = /\A([ \t]+)(`{3,})/
|
|
53
|
+
# dd 段落の継続行(インデント行。ただしインデントフェンスの手前で止まる)
|
|
54
|
+
DD_TEXT_RE = /\A[ \t](?![ \t]*`{3})/
|
|
55
|
+
# 用語の後ろに {#id} があればアンカー id として扱う(用語集の各用語への
|
|
56
|
+
# リンク用。rurema/doctree#2634)。id は $2 に入る。
|
|
57
|
+
DLIST_RE = /\A- \*\*(.+?)\*\*:(?:[ \t]*\{#([\w-]+)\})?(?:\s|$)/
|
|
58
|
+
# dlist の dt は常に「- 」マーカー(幅2)。CommonMark の項目内容カラム
|
|
59
|
+
# (spec 0.31.2 Example 255-258): 空行を挟んだ継続はこの幅以上の
|
|
60
|
+
# インデントが無いと dd に属さない(#3232, znz レビュー)
|
|
61
|
+
DLIST_CONTENT_WIDTH = 2
|
|
62
|
+
# CommonMark のインデントコードブロック相当の桁数(言語指定が付けられる
|
|
63
|
+
# フェンスを推奨するため、この実装では検知して警告するのみで非対応)
|
|
64
|
+
INDENTED_CODE_BLOCK_WIDTH = 4
|
|
65
|
+
INFO_RE = /\A- \*\*(?:param|arg|return|raise)\*\*/
|
|
66
|
+
SEE_RE = /\A- \*\*SEE\*\*/
|
|
67
|
+
# @undef など変換器が生のまま渡す未知メタデータ
|
|
68
|
+
# (RDCompiler の entry_info ループ条件 /\A@(?!see)\w+/ と同じ)
|
|
69
|
+
RAW_META_RE = /\A@(?!see)\w+/
|
|
70
|
+
# 生のまま残った #@samplecode が前処理で //emlist になったもの
|
|
71
|
+
EMLIST_LEFTOVER_RE = %r<\A//emlist(?:\[(?:[^\[\]]+?)?\]\[\w+?\])?\{>
|
|
72
|
+
|
|
73
|
+
def library_file
|
|
74
|
+
while @f.next?
|
|
75
|
+
case @f.peek
|
|
76
|
+
when signature_re
|
|
77
|
+
entry_chunk
|
|
78
|
+
when /\A\\?#/
|
|
79
|
+
if /\A\\#/ =~ @f.peek
|
|
80
|
+
paragraph # 行頭 # のリテラル本文(エスケープ済み)
|
|
81
|
+
else
|
|
82
|
+
headline @f.gets || raise
|
|
83
|
+
end
|
|
84
|
+
when SEE_RE
|
|
85
|
+
# findings#1: doc/lib ページの @see も SEE_ALSO(rd 側と同期)
|
|
86
|
+
see
|
|
87
|
+
when DLIST_RE
|
|
88
|
+
dlist
|
|
89
|
+
when MD_ITEM_RE
|
|
90
|
+
@item_stack = []
|
|
91
|
+
item_list(($1 || raise).size)
|
|
92
|
+
raise "@item_stack should be empty. #{@item_stack.inspect}" unless @item_stack.empty?
|
|
93
|
+
when FENCE_RE
|
|
94
|
+
code_fence
|
|
95
|
+
when INDENTED_FENCE_RE
|
|
96
|
+
indented_code_fence
|
|
97
|
+
when EMLIST_LEFTOVER_RE
|
|
98
|
+
# リスト脈絡などで生のまま残った #@samplecode は前処理で
|
|
99
|
+
# //emlist になる。RDCompiler と同じ独立ブロックとして描画する
|
|
100
|
+
emlist
|
|
101
|
+
when /\A\s*\|/
|
|
102
|
+
paragraph unless try_table
|
|
103
|
+
else
|
|
104
|
+
if @f.peek&.strip&.empty?
|
|
105
|
+
@f.gets
|
|
106
|
+
else
|
|
107
|
+
warn_if_indented_code_block(@f.peek || raise)
|
|
108
|
+
paragraph
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def entry_chunk
|
|
115
|
+
@out.puts '<dl>' if @option[:force]
|
|
116
|
+
first = true
|
|
117
|
+
attrs = [] #: Array[String]
|
|
118
|
+
while @f.next?
|
|
119
|
+
if signature_re =~ @f.peek
|
|
120
|
+
method_signature(@f.gets || raise, first)
|
|
121
|
+
first = false
|
|
122
|
+
elsif !first && METHOD_ATTRIBUTE_LINE_RE =~ @f.peek
|
|
123
|
+
# メソッド属性行は本文には描画しない(undef のみ後でメッセージ)
|
|
124
|
+
attrs.concat attribute_tokens(@f.gets)
|
|
125
|
+
else
|
|
126
|
+
break
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
@out.puts %Q(<dd class="#{@type.to_s}-description">)
|
|
130
|
+
undef_message if attrs.include?('undef')
|
|
131
|
+
while @f.next?
|
|
132
|
+
case @f.peek
|
|
133
|
+
when signature_re
|
|
134
|
+
break
|
|
135
|
+
when /\A\#{3,}\s/
|
|
136
|
+
headline @f.gets || raise
|
|
137
|
+
when /\A\\#/
|
|
138
|
+
entry_paragraph # エスケープされた行頭 # リテラル
|
|
139
|
+
when /\A\#{1,2}\s/
|
|
140
|
+
if @option[:force]
|
|
141
|
+
break
|
|
142
|
+
else
|
|
143
|
+
raise "#{@type.to_s} entry includes headline: #{@f.peek.inspect}"
|
|
144
|
+
end
|
|
145
|
+
when SEE_RE
|
|
146
|
+
see
|
|
147
|
+
when INFO_RE
|
|
148
|
+
entry_info
|
|
149
|
+
when DLIST_RE
|
|
150
|
+
dlist
|
|
151
|
+
when MD_ITEM_RE
|
|
152
|
+
@item_stack = []
|
|
153
|
+
item_list(($1 || raise).size)
|
|
154
|
+
raise "@item_stack should be empty. #{@item_stack.inspect}" unless @item_stack.empty?
|
|
155
|
+
when FENCE_RE
|
|
156
|
+
code_fence
|
|
157
|
+
when INDENTED_FENCE_RE
|
|
158
|
+
indented_code_fence
|
|
159
|
+
when EMLIST_LEFTOVER_RE
|
|
160
|
+
emlist
|
|
161
|
+
when /\A@todo\b/
|
|
162
|
+
# findings#3: rd 側と同じく行頭アンカー付きで
|
|
163
|
+
todo
|
|
164
|
+
when RAW_META_RE
|
|
165
|
+
entry_info
|
|
166
|
+
when /\A\s*\|/
|
|
167
|
+
entry_paragraph unless try_table
|
|
168
|
+
else
|
|
169
|
+
if @f.peek&.strip&.empty?
|
|
170
|
+
@f.gets
|
|
171
|
+
else
|
|
172
|
+
warn_if_indented_code_block(@f.peek || raise)
|
|
173
|
+
entry_paragraph
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
@out.puts '</dd>'
|
|
178
|
+
@out.puts '</dl>' if @option[:force]
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# md シグネチャ行を rd 形式(--- ...)へ落とし、既存のシグネチャ処理
|
|
182
|
+
# (MethodSignature.parse・permalink・edit link)を継承する
|
|
183
|
+
def method_signature(sig_line, first)
|
|
184
|
+
super(sig_line.sub(signature_re, '--- '), first)
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def headline(line)
|
|
188
|
+
hashes = line[/\A#+/] || raise
|
|
189
|
+
label = line.sub(/\A#+\s*/, '').strip
|
|
190
|
+
frag = nil
|
|
191
|
+
if label =~ /\{#([^}]+)\}\z/
|
|
192
|
+
frag = $1
|
|
193
|
+
label = label.sub(/\s*\{#[^}]+\}\z/, '')
|
|
194
|
+
end
|
|
195
|
+
level = @hlevel + (hashes.size - 3)
|
|
196
|
+
line h(level, escape_html(restore_rd_text(label)), frag)
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def read_paragraph(f)
|
|
200
|
+
f.span(%r{\A(?!\#|`{3}|- |\d+\. |//emlist(?:\[(?:[^\[\]]+?)?\]\[\w+?\])?\{)\S|\A\\#})
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def read_entry_paragraph(f)
|
|
204
|
+
f.span(%r{\A(?!\#|`{3}|- |\d+\. |@[a-z]|//emlist(?:\[(?:[^\[\]]+?)?\]\[\w+?\])?\{)\S|\A\\#})
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def paragraph
|
|
208
|
+
line '<p>'
|
|
209
|
+
line compile_text(text_node_from_lines(consume_paragraph_lines { read_paragraph(@f) }))
|
|
210
|
+
line '</p>'
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def entry_paragraph
|
|
214
|
+
line '<p>'
|
|
215
|
+
line compile_text(text_node_from_lines(consume_paragraph_lines { read_entry_paragraph(@f) }))
|
|
216
|
+
line '</p>'
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
# 段落行の読み取り。リストと空行で切り離された残余のインデント行は
|
|
220
|
+
# どのブロック処理にも該当しないため、ここで段落として消費する
|
|
221
|
+
# (読み取りが空のままだとディスパッチが進まなくなる)
|
|
222
|
+
def consume_paragraph_lines
|
|
223
|
+
lines = yield.map { |l| unescape_hash(l) }
|
|
224
|
+
lines = @f.span(DD_TEXT_RE) if lines.empty?
|
|
225
|
+
lines = [@f.gets || raise] if lines.empty? # 最低1行は必ず進める
|
|
226
|
+
lines
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def unescape_hash(line)
|
|
230
|
+
line.sub(/\A\\#/, '#')
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
# - **param** `name` -- desc / - **return** -- desc / - **raise** `Ex` -- desc
|
|
234
|
+
# および生の @xxx(未知メタデータ、RDCompiler と同じく UNKNOWN_META_INFO)
|
|
235
|
+
def entry_info
|
|
236
|
+
line '<dl>'
|
|
237
|
+
while @f.next? and INFO_RE =~ @f.peek || RAW_META_RE =~ @f.peek || /\A$/ =~ @f.peek
|
|
238
|
+
header = @f.gets or raise
|
|
239
|
+
next if /\A$/ =~ header
|
|
240
|
+
if RAW_META_RE =~ header
|
|
241
|
+
cmd = header.slice!(/\A@\w+/) or raise
|
|
242
|
+
@f.ungets(header)
|
|
243
|
+
line "<dt>[UNKNOWN_META_INFO] #{escape_html(cmd)}:</dt>"
|
|
244
|
+
dd_without_p
|
|
245
|
+
next
|
|
246
|
+
end
|
|
247
|
+
case header
|
|
248
|
+
when /\A- \*\*(param|arg)\*\*\s+`([^`]+)`( --(?:.*))?$/m
|
|
249
|
+
line "<dt class='#{@type.to_s}-param'>[PARAM] #{name_html($2 || raise)}:</dt>"
|
|
250
|
+
rest = $3 ? ($3 || raise).sub(/\A --/, '') : +"\n"
|
|
251
|
+
when /\A- \*\*raise\*\*\s+`([^`]+)`( --(?:.*))?$/m
|
|
252
|
+
line "<dt>[EXCEPTION] #{name_html($1 || raise)}:</dt>"
|
|
253
|
+
rest = $2 ? ($2 || raise).sub(/\A --/, '') : +"\n"
|
|
254
|
+
when /\A- \*\*return\*\*( --(?:.*))?$/m
|
|
255
|
+
line "<dt>[RETURN]</dt>"
|
|
256
|
+
rest = $1 ? ($1 || raise).sub(/\A --/, '') : +"\n"
|
|
257
|
+
else
|
|
258
|
+
raise "must not happen: #{header.inspect}"
|
|
259
|
+
end
|
|
260
|
+
# 「@raise Ex 」(説明なし・末尾スペース)は rd では dd 内の空白テキスト行に
|
|
261
|
+
# なるため、rest の空白は潰さずそのまま戻す(+"\n" は凍結回避)
|
|
262
|
+
@f.ungets(rest)
|
|
263
|
+
dd_without_p
|
|
264
|
+
end
|
|
265
|
+
line '</dl>'
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
# - **SEE** [m:X], [m:Y](継続行あり)
|
|
269
|
+
def see
|
|
270
|
+
header = @f.gets or raise
|
|
271
|
+
header = header.sub(SEE_RE, '')
|
|
272
|
+
body = [header] + @f.span(/\A\s+\S/)
|
|
273
|
+
line '<p>'
|
|
274
|
+
line '[SEE_ALSO] ' + compile_text(text_node_from_lines(body))
|
|
275
|
+
line '</p>'
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
# - **`term`**: / - **term**: の定義リスト(説明はインデント行)
|
|
279
|
+
def dlist
|
|
280
|
+
line '<dl>'
|
|
281
|
+
while @f.next? and DLIST_RE =~ @f.peek
|
|
282
|
+
@f.while_match(DLIST_RE) do |l|
|
|
283
|
+
l =~ DLIST_RE or raise
|
|
284
|
+
# rd の dt は term を strip する(term 末尾スペースは dt に含めない)。
|
|
285
|
+
# GFM モード: `term` のコードスパンは <code> として描画し、
|
|
286
|
+
# 中身の参照はその中で解決する(<code><a>...</a></code>。spec/eval)
|
|
287
|
+
term = ($1 || raise)
|
|
288
|
+
id = $2 # {#id} があれば dt にアンカーを付ける(term の再マッチ前に退避)
|
|
289
|
+
if gfm? && term =~ /\A`(.+)`\z/
|
|
290
|
+
inner = ($1 || raise).strip
|
|
291
|
+
line dt("<code>#{rd_compile_text(MarkdownToRRD.restore_inline(inner))}</code>", id)
|
|
292
|
+
else
|
|
293
|
+
term = strip_code_span(term).strip
|
|
294
|
+
line dt(compile_text(term), id)
|
|
295
|
+
end
|
|
296
|
+
inline = l.sub(DLIST_RE, '').strip
|
|
297
|
+
@f.ungets(" #{inline}\n") unless inline.empty?
|
|
298
|
+
end
|
|
299
|
+
dd_with_p
|
|
300
|
+
end
|
|
301
|
+
line '</dl>'
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
# 行頭の空白の桁数(タブも1文字として数える。MD_ITEM_RE のネスト
|
|
305
|
+
# レベル判定と同じ単純な文字数カウント。CommonMark 本来のタブ展開は
|
|
306
|
+
# 行わない — このコンパイラの他の桁数判定と揃えるため)
|
|
307
|
+
def indent_width(line)
|
|
308
|
+
(line[/\A[ \t]*/] || '').size
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
# 空行(1行以上)の直後を先読みし、リスト項目・dd の内容読み取りを
|
|
312
|
+
# どう終えるか判定する。CommonMark のリスト項目/dd は、空行を挟んだ
|
|
313
|
+
# 継続ブロックがマーカーの内容カラム幅以上インデントされていなければ
|
|
314
|
+
# 項目に属さない(spec 0.31.2 Example 255-258・262)。
|
|
315
|
+
#
|
|
316
|
+
# 戻り値:
|
|
317
|
+
# :continue … 空行の後も width 桁以上インデントされた内容が続く。
|
|
318
|
+
# このブロックの一部として続く(空行は消費する)
|
|
319
|
+
# :boundary … 空行の後は boundary_pred が真になる行(次の dt・次の
|
|
320
|
+
# 項目マーカーなど、外側のループがそのまま続けられる
|
|
321
|
+
# 区切り)。このブロックの内容としては終わるが、空行は
|
|
322
|
+
# 消費する(そうしないと外側のループが空行で止まってしまい、
|
|
323
|
+
# 本来1つの <dl>/<ul> であるべきものが分裂する)
|
|
324
|
+
# :stop … どちらでもない。空行は消費しない(トップレベルの段落へ
|
|
325
|
+
# 「逃がす」。spec 0.31.2 Example 255)
|
|
326
|
+
def blank_run_lookahead(width)
|
|
327
|
+
blanks = [] #: Array[String]
|
|
328
|
+
blanks << (@f.gets || raise) while @f.peek&.strip&.empty?
|
|
329
|
+
next_line = @f.peek
|
|
330
|
+
if next_line && indent_width(next_line) >= width
|
|
331
|
+
:continue
|
|
332
|
+
elsif next_line && yield(next_line)
|
|
333
|
+
:boundary
|
|
334
|
+
else
|
|
335
|
+
blanks.reverse_each { |b| @f.ungets(b) }
|
|
336
|
+
:stop
|
|
337
|
+
end
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
# item_list の空行境界判定用: 次の項目マーカー行か(dlist の dt や
|
|
341
|
+
# SEE/param 等、別ディスパッチ経路の行は除く。real_item_marker? と
|
|
342
|
+
# 同じ除外基準)
|
|
343
|
+
def item_marker_line?(line)
|
|
344
|
+
!!(MD_ITEM_RE =~ line && !(DLIST_RE =~ line) && !(SEE_RE =~ line) && !(INFO_RE =~ line))
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
# リスト外・フェンス外で 4 桁以上インデントされたブロックは CommonMark
|
|
348
|
+
# ではインデントコードブロックになるが、このコンパイラは非対応
|
|
349
|
+
# (言語指定できるフェンスへの書き換えを推奨するため。spec 0.31.2
|
|
350
|
+
# Example 264)。互換性を壊さないよう描画は変更せず、検知したら
|
|
351
|
+
# stderr に警告のみ出す(#3232)。将来エラー化する場合は
|
|
352
|
+
# stop_on_syntax_error? に倣ったオプションを追加する
|
|
353
|
+
def warn_if_indented_code_block(line)
|
|
354
|
+
return if indent_width(line) < INDENTED_CODE_BLOCK_WIDTH
|
|
355
|
+
loc = line.location || "#{@f.name}:#{@f.lineno}"
|
|
356
|
+
$stderr.puts "#{loc}: warning: 4+ spaces indented block is not " \
|
|
357
|
+
"rendered as a code block (CommonMark would treat this as an " \
|
|
358
|
+
"indented code block; use a fenced ```lang block instead): " \
|
|
359
|
+
"#{line.strip.inspect}"
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
def strip_code_span(text)
|
|
363
|
+
text.sub(/\A`(.+)`\z/, '\1')
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
# param 名・raise 例外名: GFM モードでは <code> で包む
|
|
367
|
+
def name_html(name)
|
|
368
|
+
gfm? ? "<code>#{escape_html(name)}</code>" : escape_html(name)
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
# GFM テーブルの区切り行(|---|:--:|... 。少なくとも1つの - が必要)
|
|
372
|
+
TABLE_DELIM_RE = /\A\s*\|?\s*:?-+:?\s*(?:\|\s*:?-+:?\s*)*\|?\s*$/
|
|
373
|
+
|
|
374
|
+
# GFM テーブル。厳格判定: ヘッダ行の直後に区切り行がある場合のみ
|
|
375
|
+
# テーブルとして描画する(本文が | で始まる散文と誤認しない。FalseClass)。
|
|
376
|
+
# テーブルでなければ何も消費せず false を返す
|
|
377
|
+
def try_table
|
|
378
|
+
return false unless gfm?
|
|
379
|
+
header = @f.gets or return false
|
|
380
|
+
unless @f.peek && TABLE_DELIM_RE =~ @f.peek
|
|
381
|
+
@f.ungets(header)
|
|
382
|
+
return false
|
|
383
|
+
end
|
|
384
|
+
aligns = parse_table_aligns(@f.gets || raise)
|
|
385
|
+
line '<table>'
|
|
386
|
+
line '<thead>'
|
|
387
|
+
table_row(header, 'th', aligns)
|
|
388
|
+
line '</thead>'
|
|
389
|
+
line '<tbody>'
|
|
390
|
+
while @f.peek =~ /\A\s*\|/
|
|
391
|
+
table_row(@f.gets || raise, 'td', aligns)
|
|
392
|
+
end
|
|
393
|
+
line '</tbody>'
|
|
394
|
+
line '</table>'
|
|
395
|
+
true
|
|
396
|
+
end
|
|
397
|
+
|
|
398
|
+
def parse_table_aligns(delim)
|
|
399
|
+
split_table_row(delim).map { |cell|
|
|
400
|
+
case cell
|
|
401
|
+
when /\A:-+:\z/ then 'center'
|
|
402
|
+
when /\A:-+\z/ then 'left'
|
|
403
|
+
when /\A-+:\z/ then 'right'
|
|
404
|
+
end
|
|
405
|
+
}
|
|
406
|
+
end
|
|
407
|
+
|
|
408
|
+
def table_row(row, tag, aligns)
|
|
409
|
+
cells = split_table_row(row)
|
|
410
|
+
string '<tr>'
|
|
411
|
+
cells.each_with_index do |cell, i|
|
|
412
|
+
align = aligns[i] ? %Q( align="#{aligns[i]}") : ''
|
|
413
|
+
string "<#{tag}#{align}>#{compile_text(cell)}</#{tag}>"
|
|
414
|
+
end
|
|
415
|
+
line '</tr>'
|
|
416
|
+
end
|
|
417
|
+
|
|
418
|
+
# 行をセルに分割(先頭/末尾の | を除去、\| はリテラルの |)
|
|
419
|
+
def split_table_row(row)
|
|
420
|
+
row.strip.sub(/\A\|/, '').sub(/\|\z/, '')
|
|
421
|
+
.gsub('\\|', "\x03").split('|', -1)
|
|
422
|
+
.map { |c| c.gsub("\x03", '|').strip }
|
|
423
|
+
end
|
|
424
|
+
|
|
425
|
+
# フェンスドコードブロック:
|
|
426
|
+
# - ```lang title="cap"(emlist/samplecode 由来)→ ハイライト付き <pre>
|
|
427
|
+
# - ```(3個・lang なし。//emlist{ 由来)→ 素の <pre>(rstrip escape)
|
|
428
|
+
# - ````+(4個以上。インデントテキスト由来)→ 素の <pre>(rd の list 相当)
|
|
429
|
+
def code_fence
|
|
430
|
+
open_line = @f.gets or raise
|
|
431
|
+
fence = open_line[/\A`+/] or raise
|
|
432
|
+
lang, caption, invalid = parse_fence_info(open_line[fence.size..].to_s.strip)
|
|
433
|
+
terminator = /\A`{#{fence.size}}\s*$/
|
|
434
|
+
if lang
|
|
435
|
+
highlighted_fence_body(lang, caption, terminator, invalid: invalid)
|
|
436
|
+
elsif fence.size == 3
|
|
437
|
+
line '<pre>'
|
|
438
|
+
@f.until_terminator(terminator) do |code_line|
|
|
439
|
+
line escape_html(code_line.rstrip)
|
|
440
|
+
end
|
|
441
|
+
line '</pre>'
|
|
442
|
+
else
|
|
443
|
+
# 変換器はベースインデント(= フェンス長 - 3)を除去して格納している。
|
|
444
|
+
# RDCompiler の list は detab(タブをカラム位置で空白展開)を元の
|
|
445
|
+
# カラムで行うため、ベースインデントを戻してから同じ処理を通す。
|
|
446
|
+
# #@since/#@else ゲートで分断されたインデントブロックは前処理後に
|
|
447
|
+
# 隣接フェンスとして現れるため、続けてマージする(rd では連続
|
|
448
|
+
# インデント行として1つの <pre> になる。ArgumentError 等)
|
|
449
|
+
segments = [[fence.size - 3, collect_fence_lines(terminator)]] #: Array[[Integer, Array[String]]]
|
|
450
|
+
loop do
|
|
451
|
+
# rd の pre は空白行を跨ぐ(list は /\A\S/ まで継続)ため、
|
|
452
|
+
# フェンス間の空白のみ行も次が 4+ フェンスならブロックの一部
|
|
453
|
+
blanks = [] #: Array[String]
|
|
454
|
+
blanks << (@f.gets || raise) while @f.peek =~ /\A\s*$/
|
|
455
|
+
unless @f.peek =~ /\A(`{4,})\s*$/
|
|
456
|
+
blanks.reverse_each { |b| @f.ungets(b) }
|
|
457
|
+
break
|
|
458
|
+
end
|
|
459
|
+
next_fence = ($1 || raise)
|
|
460
|
+
@f.gets
|
|
461
|
+
segments.last[1].concat(blanks)
|
|
462
|
+
segments << [next_fence.size - 3,
|
|
463
|
+
collect_fence_lines(/\A`{#{next_fence.size}}\s*$/)]
|
|
464
|
+
end
|
|
465
|
+
lines = segments.flat_map { |base, ls|
|
|
466
|
+
ls.map { |l| l =~ /\A\s*\z/ ? l : (' ' * base) + l }
|
|
467
|
+
}
|
|
468
|
+
lines = unindent_block(canonicalize(lines))
|
|
469
|
+
while lines.last&.empty?
|
|
470
|
+
lines.pop
|
|
471
|
+
end
|
|
472
|
+
line '<pre>'
|
|
473
|
+
lines.each do |code_line|
|
|
474
|
+
line escape_html(code_line)
|
|
475
|
+
end
|
|
476
|
+
line '</pre>'
|
|
477
|
+
end
|
|
478
|
+
end
|
|
479
|
+
|
|
480
|
+
def collect_fence_lines(terminator)
|
|
481
|
+
lines = [] #: Array[String]
|
|
482
|
+
@f.until_terminator(terminator) do |code_line|
|
|
483
|
+
lines << code_line
|
|
484
|
+
end
|
|
485
|
+
lines
|
|
486
|
+
end
|
|
487
|
+
|
|
488
|
+
# info string のパース。文法: lang [invalid] [title="cap"]
|
|
489
|
+
# invalid は「構文として完全でないコード」の印で、ruby では Ripper の
|
|
490
|
+
# 構文チェックをせず Rouge の lexer で色付けする(issue #251)
|
|
491
|
+
def parse_fence_info(rest)
|
|
492
|
+
if rest =~ /\A(\w+)?(\s+invalid)?(?:\s+title="((?:[^"\\]|\\.)*)")?\z/
|
|
493
|
+
# title の gsub(別の正規表現マッチ)が $~ を上書きするため、
|
|
494
|
+
# $1〜$3 は gsub より前に読み切る
|
|
495
|
+
lang = $1
|
|
496
|
+
invalid = !$2.nil?
|
|
497
|
+
title = $3
|
|
498
|
+
[lang, title&.gsub(/\\(["\\])/, '\1'), invalid]
|
|
499
|
+
else
|
|
500
|
+
[nil, nil, false]
|
|
501
|
+
end
|
|
502
|
+
end
|
|
503
|
+
|
|
504
|
+
# ハイライト付き <pre> の本体。caption はタブとして pre の前に置く
|
|
505
|
+
# (rd 側と同期)。strip_re はインデントフェンスのデデント用
|
|
506
|
+
def highlighted_fence_body(lang, caption, terminator, strip_re = nil, invalid: false)
|
|
507
|
+
line "<span class=\"caption\">#{escape_html(caption)}</span>" if caption
|
|
508
|
+
# <code> の直後に改行を入れると pre の内容の先頭に余計な空行として
|
|
509
|
+
# 表示されるため、コード本体は <code> に直接続ける(#254、rd 側と同期)
|
|
510
|
+
string "<pre class=\"highlight #{lang}\"><code>"
|
|
511
|
+
src = +""
|
|
512
|
+
@f.until_terminator(terminator) do |code_line|
|
|
513
|
+
src << (strip_re ? code_line.sub(strip_re, '') : code_line)
|
|
514
|
+
end
|
|
515
|
+
string highlight_source(src, lang, caption, invalid: invalid)
|
|
516
|
+
line '</code></pre>'
|
|
517
|
+
end
|
|
518
|
+
|
|
519
|
+
# リスト項目・dlist 説明の中のインデントされたフェンス(GFM のリスト内
|
|
520
|
+
# コードブロック)。GFM と同じく、内容と閉じフェンスからフェンス行の
|
|
521
|
+
# インデント幅までを除去する
|
|
522
|
+
def indented_code_fence
|
|
523
|
+
open_line = @f.gets or raise
|
|
524
|
+
INDENTED_FENCE_RE =~ open_line or raise
|
|
525
|
+
indent = ($1 || raise)
|
|
526
|
+
fence = ($2 || raise)
|
|
527
|
+
lang, caption, invalid = parse_fence_info(open_line[(indent.size + fence.size)..].to_s.strip)
|
|
528
|
+
terminator = /\A[ \t]{0,#{indent.size}}`{#{fence.size}}\s*$/
|
|
529
|
+
strip_re = /\A[ \t]{1,#{indent.size}}/
|
|
530
|
+
if lang
|
|
531
|
+
highlighted_fence_body(lang, caption, terminator, strip_re, invalid: invalid)
|
|
532
|
+
else
|
|
533
|
+
line '<pre>'
|
|
534
|
+
@f.until_terminator(terminator) do |code_line|
|
|
535
|
+
line escape_html(code_line.sub(strip_re, '').rstrip)
|
|
536
|
+
end
|
|
537
|
+
line '</pre>'
|
|
538
|
+
end
|
|
539
|
+
end
|
|
540
|
+
|
|
541
|
+
# 箇条書き(- item / N. item、ネスト・継続行あり)
|
|
542
|
+
def item_list(level = 0)
|
|
543
|
+
open_tag = nil
|
|
544
|
+
close_tag = nil
|
|
545
|
+
case @f.peek
|
|
546
|
+
when /\A\s*- /
|
|
547
|
+
open_tag = "<ul>"
|
|
548
|
+
close_tag = "</ul>"
|
|
549
|
+
when /\A\s*\d+\. /
|
|
550
|
+
open_tag = "<ol>"
|
|
551
|
+
close_tag = "</ol>"
|
|
552
|
+
end
|
|
553
|
+
line open_tag
|
|
554
|
+
@item_stack.push(close_tag)
|
|
555
|
+
@f.while_match(MD_ITEM_RE) do |item_line|
|
|
556
|
+
string "<li>"
|
|
557
|
+
@item_stack.push("</li>")
|
|
558
|
+
string compile_text(item_line.sub(/\A\s*(?:-|\d+\.)\s?/, '').strip)
|
|
559
|
+
item_line =~ MD_ITEM_RE or raise
|
|
560
|
+
# この項目の内容カラム幅(マーカー前の空白 + マーカー自身。
|
|
561
|
+
# "- " なら2、"12. " なら4)。空行を挟んだ継続がこの項目に
|
|
562
|
+
# 属するかどうかの判定に使う
|
|
563
|
+
content_width = ($1 || raise).size + ($2 || raise).size
|
|
564
|
+
# 継続行(折り返しテキスト)とインデントフェンス(項目内コードブロック)。
|
|
565
|
+
# CommonMark に合わせ、空行を挟まない直接継続(lazy continuation)は
|
|
566
|
+
# インデント幅を問わず許容するが、フェンスや空行を挟んだ継続は
|
|
567
|
+
# content_width 以上のインデントが無ければ項目に属さない
|
|
568
|
+
# (spec 0.31.2 Example 255-258・262-263、doctree#3232 znz レビュー。
|
|
569
|
+
# 従来はここで空行に当たると即座に項目の外へ「脱走」していた)
|
|
570
|
+
while @f.peek
|
|
571
|
+
if @f.peek&.strip&.empty?
|
|
572
|
+
if blank_run_lookahead(content_width) { |l| item_marker_line?(l) } == :continue
|
|
573
|
+
next
|
|
574
|
+
else
|
|
575
|
+
break
|
|
576
|
+
end
|
|
577
|
+
elsif INDENTED_FENCE_RE =~ @f.peek
|
|
578
|
+
break if indent_width(@f.peek || raise) < content_width
|
|
579
|
+
nl
|
|
580
|
+
indented_code_fence
|
|
581
|
+
elsif /\A\s+(?!- |\d+\. )\S/ =~ @f.peek
|
|
582
|
+
nl
|
|
583
|
+
string compile_text((@f.gets || raise).strip)
|
|
584
|
+
else
|
|
585
|
+
break
|
|
586
|
+
end
|
|
587
|
+
end
|
|
588
|
+
if (m = MD_ITEM_RE.match(@f.peek)) && level < (m[1] || raise).size
|
|
589
|
+
item_list((m[1] || raise).size)
|
|
590
|
+
line @item_stack.pop # current level li
|
|
591
|
+
break if MD_ITEM_RE =~ @f.peek and level > ($1 || raise).size
|
|
592
|
+
elsif m && level > (m[1] || raise).size
|
|
593
|
+
line @item_stack.pop # current level li
|
|
594
|
+
break
|
|
595
|
+
else
|
|
596
|
+
line @item_stack.pop # current level li
|
|
597
|
+
end
|
|
598
|
+
end
|
|
599
|
+
line @item_stack.pop unless @item_stack.empty?
|
|
600
|
+
end
|
|
601
|
+
|
|
602
|
+
# RDCompiler の dd_with_p / dd_without_p の md 版:
|
|
603
|
+
# コードブロック(rd では //emlist)の判定をインデントフェンスに差し替える。
|
|
604
|
+
# RD の //emlist は桁0で書いても dd に取り込まれたが、Markdown では
|
|
605
|
+
# CommonMark に合わせて「項目の内容カラムにインデントされたフェンス」だけを
|
|
606
|
+
# 説明(dd)の一部として取り込む。桁0のフェンスは dd の外(トップレベル)。
|
|
607
|
+
#
|
|
608
|
+
# CommonMark に合わせ、空行を挟まない直接継続(lazy continuation)は
|
|
609
|
+
# インデント幅を問わず許容するが、フェンスや空行を挟んだ継続は
|
|
610
|
+
# DLIST_CONTENT_WIDTH(2) 以上のインデントが無ければ dd に属さない
|
|
611
|
+
# (spec 0.31.2 Example 255-258、doctree#3232 znz レビュー。symref.md
|
|
612
|
+
# の「半角スペース1個だけの継続」はこのため dd の外の段落になる)
|
|
613
|
+
def dd_with_p
|
|
614
|
+
line '<dd>'
|
|
615
|
+
while @f.peek
|
|
616
|
+
if @f.peek&.strip&.empty?
|
|
617
|
+
if blank_run_lookahead(DLIST_CONTENT_WIDTH) { |l| !!(DLIST_RE =~ l) } == :continue
|
|
618
|
+
next
|
|
619
|
+
else
|
|
620
|
+
break
|
|
621
|
+
end
|
|
622
|
+
elsif INDENTED_FENCE_RE =~ @f.peek
|
|
623
|
+
break if indent_width(@f.peek || raise) < DLIST_CONTENT_WIDTH
|
|
624
|
+
indented_code_fence
|
|
625
|
+
elsif /\A[ \t]/ =~ @f.peek
|
|
626
|
+
line '<p>'
|
|
627
|
+
line compile_text(text_node_from_lines(@f.span(DD_TEXT_RE)))
|
|
628
|
+
line '</p>'
|
|
629
|
+
else
|
|
630
|
+
break
|
|
631
|
+
end
|
|
632
|
+
end
|
|
633
|
+
line '</dd>'
|
|
634
|
+
end
|
|
635
|
+
|
|
636
|
+
def dd_without_p
|
|
637
|
+
line '<dd>'
|
|
638
|
+
while /\A[ \t]/ =~ @f.peek
|
|
639
|
+
case @f.peek
|
|
640
|
+
when INDENTED_FENCE_RE
|
|
641
|
+
indented_code_fence
|
|
642
|
+
when /\A[ \t]/
|
|
643
|
+
line compile_text(text_node_from_lines(@f.span(DD_TEXT_RE)))
|
|
644
|
+
end
|
|
645
|
+
end
|
|
646
|
+
line '</dd>'
|
|
647
|
+
end
|
|
648
|
+
|
|
649
|
+
# テキストノード: インライン記法を rd 形式へ復元してから既存の
|
|
650
|
+
# コンパイル(エスケープ・リンク解決)を継承する。
|
|
651
|
+
# M1 等価モード: 変換器が付けた __X__ の自動コードスパンは剥がす
|
|
652
|
+
def compile_text(str)
|
|
653
|
+
return compile_gfm_text(str) if gfm?
|
|
654
|
+
super(restore_rd_text(str))
|
|
655
|
+
end
|
|
656
|
+
|
|
657
|
+
# GFM モードの地の文 `**strong**`。保守的サブセット: 同一テキスト
|
|
658
|
+
# ノード内で閉じる(段落は行結合済みなので GFM と同じく soft break は
|
|
659
|
+
# またげる)・
|
|
660
|
+
# 開閉の内側が空白でない・内容に `*` を含まない(入れ子なし)・
|
|
661
|
+
# 単語内には適用しない(`2**32` のような冪乗演算子を巻き込まないため。
|
|
662
|
+
# 開きの直前と閉じの直後が英数字・`_`・`*` なら対象外)。
|
|
663
|
+
# `*em*`/`_em_` は多義性が高いため対象外(bitclust#301)
|
|
664
|
+
INLINE_STRONG_RE = /(?<![0-9A-Za-z_*])\*\*(?=\S)([^*\n]+?)(?<=\S)\*\*(?![0-9A-Za-z_*])/
|
|
665
|
+
|
|
666
|
+
# GFM モードのテキストコンパイル:
|
|
667
|
+
# コードスパン(CommonMark 6.1、N 連バッククォート)→ <code>(中身は
|
|
668
|
+
# 参照解決しない・HTML エスケープのみ)、行頭 **N.** → <strong>、
|
|
669
|
+
# Markdown リンク(<url> 自動リンク・[テキスト](URL)・
|
|
670
|
+
# [テキスト](#アンカー))→ <a>。
|
|
671
|
+
# コードスパンの抽出はリンク抽出より先に行う(CommonMark のインライン
|
|
672
|
+
# 優先順位どおり。コードスパン内は他の記法を解決しない・
|
|
673
|
+
# extract_md_links 側で改めて分割する必要がない)。
|
|
674
|
+
# エスケープ済み \` はコードスパンの外だけリテラルのバッククォートに
|
|
675
|
+
# 復元される(restore_inline の convert_bare_refs が \[ \] \` の復元を
|
|
676
|
+
# 担う。コードスパン内ではバックスラッシュエスケープが無効という
|
|
677
|
+
# CommonMark のルールどおり、中身は素通しでバックスラッシュごと残す)
|
|
678
|
+
def compile_gfm_text(str)
|
|
679
|
+
code_spans = [] #: Array[String]
|
|
680
|
+
str = extract_code_spans(str, code_spans)
|
|
681
|
+
links = [] #: Array[String]
|
|
682
|
+
str = extract_md_links(str, links)
|
|
683
|
+
str = str.gsub(/^\*\*(\d+\.)\*\* /, "\x01\\1\x02 ")
|
|
684
|
+
str = str.gsub(INLINE_STRONG_RE, "\x01\\1\x02")
|
|
685
|
+
rd_compile_text(MarkdownToRRD.restore_inline(str))
|
|
686
|
+
.gsub("\x01", '<strong>').gsub("\x02", '</strong>')
|
|
687
|
+
.gsub(/\x03(\d+)\x03/) { links[($1 || raise).to_i] || raise }
|
|
688
|
+
.gsub(/\x04(\d+)\x04/) { code_spans[($1 || raise).to_i] || raise }
|
|
689
|
+
end
|
|
690
|
+
|
|
691
|
+
# CommonMark 6.1 のインラインコードスパンを描画済み <code> へ退避し
|
|
692
|
+
# \x04idx\x04 プレースホルダに置き換える(extract_md_links と同じ流儀)。
|
|
693
|
+
# 開始と同じ長さのバッククォート列で閉じる(最長一致ではなく同長
|
|
694
|
+
# ペアリング)。閉じる相手が見つからない開始列はコードスパンにせず、
|
|
695
|
+
# そのまま次の候補から探索を続ける(非対称の開始列=リテラル)。
|
|
696
|
+
# 改行はまたがない(テキストノードの行区切りを保持する既存の制約を
|
|
697
|
+
# 維持する)。
|
|
698
|
+
# \` は開始候補にしない(CommonMark: バックスラッシュエスケープされた
|
|
699
|
+
# バッククォートはコードスパンを開始しない。2.4 の \`not code\` 例)。
|
|
700
|
+
# 閉じ側の探索はエスケープを見ない(コードスパン内ではバックスラッシュ
|
|
701
|
+
# エスケープが無効なため、開いた後は同じ長さのバッククォート列が
|
|
702
|
+
# 来れば必ず閉じる。`foo\`bar` → <code>foo\</code>bar` という
|
|
703
|
+
# GitHub の実描画と同じ)
|
|
704
|
+
def extract_code_spans(str, saved)
|
|
705
|
+
result = +''
|
|
706
|
+
i = 0
|
|
707
|
+
len = str.length
|
|
708
|
+
while i < len
|
|
709
|
+
c = str[i] or raise
|
|
710
|
+
if c == '`' && (i.zero? || str[i - 1] != '\\')
|
|
711
|
+
run_len = 1
|
|
712
|
+
run_len += 1 while str[i + run_len] == '`'
|
|
713
|
+
close = find_code_span_close(str, i + run_len, run_len)
|
|
714
|
+
if close
|
|
715
|
+
content = str[(i + run_len)...close] || raise
|
|
716
|
+
saved << "<code>#{escape_html(normalize_code_span(content))}</code>"
|
|
717
|
+
result << "\x04#{saved.size - 1}\x04"
|
|
718
|
+
i = close + run_len
|
|
719
|
+
next
|
|
720
|
+
else
|
|
721
|
+
result << ('`' * run_len)
|
|
722
|
+
i += run_len
|
|
723
|
+
next
|
|
724
|
+
end
|
|
725
|
+
end
|
|
726
|
+
result << c
|
|
727
|
+
i += 1
|
|
728
|
+
end
|
|
729
|
+
result
|
|
730
|
+
end
|
|
731
|
+
|
|
732
|
+
# from 位置以降で、run_len と同じ長さのバッククォート列(開始位置)を
|
|
733
|
+
# 探す。改行をまたいだら諦める(見つからないのと同じ扱い)
|
|
734
|
+
def find_code_span_close(str, from, run_len)
|
|
735
|
+
i = from
|
|
736
|
+
len = str.length
|
|
737
|
+
while i < len
|
|
738
|
+
c = str[i] or raise
|
|
739
|
+
case c
|
|
740
|
+
when "\n"
|
|
741
|
+
return nil
|
|
742
|
+
when '`'
|
|
743
|
+
j = i
|
|
744
|
+
j += 1 while str[j] == '`'
|
|
745
|
+
return i if j - i == run_len
|
|
746
|
+
i = j
|
|
747
|
+
else
|
|
748
|
+
i += 1
|
|
749
|
+
end
|
|
750
|
+
end
|
|
751
|
+
nil
|
|
752
|
+
end
|
|
753
|
+
|
|
754
|
+
# CommonMark 6.1: 前後が両方スペースで、かつ内容が全部スペースでは
|
|
755
|
+
# ない場合に前後を1個ずつ剥ぐ(`` `a` `` の内容を `a` として書ける
|
|
756
|
+
# ようにするための規則)
|
|
757
|
+
def normalize_code_span(content)
|
|
758
|
+
if content.start_with?(' ') && content.end_with?(' ') && content.match?(/[^ ]/)
|
|
759
|
+
content[1..-2] || ''
|
|
760
|
+
else
|
|
761
|
+
content
|
|
762
|
+
end
|
|
763
|
+
end
|
|
764
|
+
|
|
765
|
+
AUTOLINK_RE = %r{<(https?://[^<>\s]+)>}
|
|
766
|
+
# インラインリンクの宛先として受ける形。散文の「[c:String](を参照)」の
|
|
767
|
+
# ような括弧書きをリンクと誤認しないため URL と #フラグメントに限る
|
|
768
|
+
# (MARKUP_SPEC §7.4/§7.5)
|
|
769
|
+
# リンク宛先として受け付けるもの: 外部 URL・ページ内アンカー・
|
|
770
|
+
# rurema 参照 scheme(c:/m:/lib:/d:/f:。ラベル付きで参照したいとき用)
|
|
771
|
+
MD_LINK_DEST_RE = %r{\A(?:https?://|\#|(?:c|m|lib|d|f|ref|man):)}
|
|
772
|
+
|
|
773
|
+
# Markdown のリンクを描画済み <a> へ退避し \x03idx\x03 プレースホルダに
|
|
774
|
+
# 置き換える。後段の rd_compile_text(HTML エスケープ・参照解決)を
|
|
775
|
+
# 素通しし、compile_gfm_text の最後で戻す
|
|
776
|
+
def extract_md_links(str, saved)
|
|
777
|
+
str = str.gsub(AUTOLINK_RE) {
|
|
778
|
+
saved << direct_url($1 || raise)
|
|
779
|
+
"\x03#{saved.size - 1}\x03"
|
|
780
|
+
}
|
|
781
|
+
return str unless str.include?('[')
|
|
782
|
+
result = +''
|
|
783
|
+
i = 0
|
|
784
|
+
while i < str.length
|
|
785
|
+
if str[i] == '\\' && i + 1 < str.length
|
|
786
|
+
result << (str[i, 2] || raise)
|
|
787
|
+
i += 2
|
|
788
|
+
next
|
|
789
|
+
end
|
|
790
|
+
if str[i] == '['
|
|
791
|
+
close = matching_delimiter(str, i, '[', ']')
|
|
792
|
+
if close && str[close + 1] == '(' &&
|
|
793
|
+
(dest_end = matching_delimiter(str, close + 1, '(', ')', space_ends: true)) &&
|
|
794
|
+
(dest = str[(close + 2)...dest_end]) && MD_LINK_DEST_RE =~ dest
|
|
795
|
+
saved << md_link(str[(i + 1)...close] || raise, dest)
|
|
796
|
+
result << "\x03#{saved.size - 1}\x03"
|
|
797
|
+
i = dest_end + 1
|
|
798
|
+
next
|
|
799
|
+
end
|
|
800
|
+
end
|
|
801
|
+
result << (str[i] || raise)
|
|
802
|
+
i += 1
|
|
803
|
+
end
|
|
804
|
+
result
|
|
805
|
+
end
|
|
806
|
+
|
|
807
|
+
# open 位置の括弧に対応する閉じ括弧の位置(\ エスケープ対応・ネスト可)。
|
|
808
|
+
# space_ends: リンク宛先用。空白が現れたらリンクではない(nil)
|
|
809
|
+
def matching_delimiter(str, open, open_char, close_char, space_ends: false)
|
|
810
|
+
depth = 0
|
|
811
|
+
i = open
|
|
812
|
+
while i < str.length
|
|
813
|
+
c = str[i]
|
|
814
|
+
if c == '\\'
|
|
815
|
+
i += 1
|
|
816
|
+
elsif space_ends && c =~ /\s/
|
|
817
|
+
return nil
|
|
818
|
+
elsif c == open_char
|
|
819
|
+
depth += 1
|
|
820
|
+
elsif c == close_char
|
|
821
|
+
depth -= 1
|
|
822
|
+
return i if depth == 0
|
|
823
|
+
end
|
|
824
|
+
i += 1
|
|
825
|
+
end
|
|
826
|
+
nil
|
|
827
|
+
end
|
|
828
|
+
|
|
829
|
+
# 表示テキスト付きリンクの描画。テキストは参照解決しないプレーン表示
|
|
830
|
+
# (リンク内リンクは HTML として成立しないため)。外部 URL は rd の
|
|
831
|
+
# [[url:]] と同じ external クラス、#アンカーはページ内リンク。
|
|
832
|
+
# 宛先が rurema 参照 scheme(c:/m:/lib:/d:/f:)のときは内部リンクとして
|
|
833
|
+
# 解決する([`egd_bytes(filename, 255)`](m:OpenSSL::Random?.egd_bytes)
|
|
834
|
+
# のように、呼び出し形をラベルにしてメソッドへリンクできる)
|
|
835
|
+
def md_link(text, dest)
|
|
836
|
+
if (m = /\A(c|m|lib|d|f|ref|man):(.+)\z/m.match(dest))
|
|
837
|
+
return ref_md_link(m[1] || raise, m[2] || raise, text)
|
|
838
|
+
end
|
|
839
|
+
label = escape_html(unescape_md_brackets(text))
|
|
840
|
+
href = escape_html(unescape_md_brackets(dest))
|
|
841
|
+
if dest.start_with?('#')
|
|
842
|
+
%Q(<a href="#{href}">#{label}</a>)
|
|
843
|
+
else
|
|
844
|
+
%Q(<a class="external" href="#{href}">#{label}</a>)
|
|
845
|
+
end
|
|
846
|
+
end
|
|
847
|
+
|
|
848
|
+
# 参照 scheme 宛先のリンク。解決・compileerror 表示・link_checker への
|
|
849
|
+
# 通知は bracket_link にそのまま乗せ、ラベルだけ後から差し替える
|
|
850
|
+
# (a_href がラベルを HTML エスケープするため、コードスパンを含む
|
|
851
|
+
# ラベル HTML はプレースホルダ経由で入れる)
|
|
852
|
+
MD_REF_LABEL_PLACEHOLDER = "\x05L\x05"
|
|
853
|
+
def ref_md_link(type, arg, text)
|
|
854
|
+
arg = unescape_md_brackets(arg)
|
|
855
|
+
# md ソースの module function 表記 ?. を内部表記 .# へ正規化する
|
|
856
|
+
# (単一ブラケット参照の restore_inline と同じ規則。bitclust#282)
|
|
857
|
+
arg = arg.sub('?.', '.#') if type == 'm'
|
|
858
|
+
html = bracket_link("#{type}:#{arg}", MD_REF_LABEL_PLACEHOLDER)
|
|
859
|
+
html.gsub(MD_REF_LABEL_PLACEHOLDER, md_ref_link_label(text))
|
|
860
|
+
end
|
|
861
|
+
|
|
862
|
+
# ラベルの描画: 全体がコードスパン(`...`)ならば <code>、それ以外は
|
|
863
|
+
# プレーン表示。ラベル内のその他のインライン記法は解釈しない
|
|
864
|
+
def md_ref_link_label(text)
|
|
865
|
+
text = unescape_md_brackets(text)
|
|
866
|
+
if (m = /\A`(.+)`\z/m.match(text))
|
|
867
|
+
"<code>#{escape_html(m[1] || raise)}</code>"
|
|
868
|
+
else
|
|
869
|
+
escape_html(text)
|
|
870
|
+
end
|
|
871
|
+
end
|
|
872
|
+
|
|
873
|
+
def unescape_md_brackets(str)
|
|
874
|
+
str.gsub(/\\([\[\]\\])/, '\1')
|
|
875
|
+
end
|
|
876
|
+
|
|
877
|
+
def restore_rd_text(str)
|
|
878
|
+
# M1 等価モード: md のコードスパンを rd の元表記へ戻して描画する
|
|
879
|
+
MarkdownToRRD.restore_text(str)
|
|
880
|
+
end
|
|
881
|
+
end
|
|
882
|
+
|
|
883
|
+
end
|