bitclust-core 1.6.1 → 1.7.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/Rakefile +1 -0
- data/data/bitclust/catalog/ja_JP.UTF-8 +4 -2
- data/data/bitclust/template/function-index +1 -0
- data/data/bitclust/template/layout +1 -0
- data/data/bitclust/template.offline/function-index +1 -0
- data/data/bitclust/template.offline/layout +3 -0
- data/lib/bitclust/classentry.rb +1 -1
- data/lib/bitclust/docentry.rb +1 -1
- data/lib/bitclust/functionentry.rb +1 -1
- data/lib/bitclust/irb.rb +68 -0
- data/lib/bitclust/markdown_exporter.rb +311 -0
- data/lib/bitclust/mdcompiler.rb +15 -1
- data/lib/bitclust/mdparser.rb +42 -1
- data/lib/bitclust/methodentry.rb +24 -0
- data/lib/bitclust/methodsignature.rb +1 -1
- data/lib/bitclust/preprocessor.rb +4 -1
- data/lib/bitclust/rbs_overload_matcher.rb +179 -0
- data/lib/bitclust/rbs_sig_importer.rb +276 -0
- data/lib/bitclust/rbs_signatures.rb +125 -0
- data/lib/bitclust/rdcompiler.rb +12 -1
- data/lib/bitclust/runner.rb +2 -0
- data/lib/bitclust/screen.rb +4 -0
- data/lib/bitclust/search_index_generator.rb +28 -10
- data/lib/bitclust/searcher.rb +12 -2
- data/lib/bitclust/subcommands/rbssig_command.rb +99 -0
- data/lib/bitclust/subcommands/setup_command.rb +4 -1
- data/lib/bitclust/subcommands/statichtml_command.rb +59 -0
- data/lib/bitclust/version.rb +1 -1
- data/lib/bitclust-irb.rb +2 -0
- data/test/test_irb_plugin.rb +118 -0
- data/test/test_link_checker.rb +1 -1
- data/test/test_markdown_exporter.rb +260 -0
- data/test/test_mdcompiler.rb +47 -5
- data/test/test_mdparser.rb +184 -4
- data/test/test_methodentry.rb +111 -0
- data/test/test_methodsignature.rb +3 -1
- data/test/test_preprocessor.rb +26 -0
- data/test/test_rbs_overload_matcher.rb +170 -0
- data/test/test_rbs_sig_importer.rb +237 -0
- data/test/test_rbssig_command.rb +186 -0
- data/test/test_rdcompiler.rb +99 -1
- data/test/test_search_index_generator.rb +81 -0
- data/test/test_setup_command.rb +21 -0
- data/test/test_statichtml_command.rb +137 -0
- data/theme/default/js/run.js +14 -0
- data/theme/default/js/search_init.js +3 -1
- data/theme/default/js/search_page.js +7 -1
- data/theme/default/js/version_switcher.js +147 -0
- data/theme/default/search.css +21 -0
- data/theme/default/style.css +83 -7
- data/theme/lillia/style.css +26 -0
- metadata +35 -1
data/test/test_rdcompiler.rb
CHANGED
|
@@ -28,9 +28,102 @@ class TestRDCompiler < Test::Unit::TestCase
|
|
|
28
28
|
mock(method_entry).names.any_times{ names }
|
|
29
29
|
mock(method_entry).since_map.any_times{ since_map }
|
|
30
30
|
mock(method_entry).until_map.any_times{ until_map }
|
|
31
|
+
mock(method_entry).rbs_signature_overloads.any_times { nil }
|
|
31
32
|
assert_equal(expected, @c.compile_method(method_entry))
|
|
32
33
|
end
|
|
33
34
|
|
|
35
|
+
# RBS シグネチャ表示: rbs_sig property を持つエントリだけが、メソッド
|
|
36
|
+
# 見出しの <dt> 群の直後・説明 <dd> の直前に 1 オーバーロード 1 行の
|
|
37
|
+
# <dt class="rbs-signature"> を出す(dd だと説明の一部に見える。pre を
|
|
38
|
+
# 使うと script.js が COPY ボタンを付けてしまうので code のみ)。
|
|
39
|
+
# 型名セグメントは DB に存在するクラスだけリンク化し、テキストは
|
|
40
|
+
# エスケープした上で -> を → にする。
|
|
41
|
+
def test_method_with_rbs_signatures_renders_dt_lines
|
|
42
|
+
stub(@db).fetch_class('String') { :exists }
|
|
43
|
+
stub(@db).fetch_class('Integer') { raise BitClust::ClassNotFound, 'Integer' }
|
|
44
|
+
overloads = [
|
|
45
|
+
{'segments' => [['t', '('], ['c', 'Integer'], ['t', ') -> '], ['c', 'String']]},
|
|
46
|
+
{'segments' => [['t', '(String & Integer) -> void']]},
|
|
47
|
+
]
|
|
48
|
+
method_entry = rbs_method_entry("--- index(val) -> Integer\n\n説明\n", overloads)
|
|
49
|
+
|
|
50
|
+
html = @c.compile_method(method_entry)
|
|
51
|
+
assert_include html,
|
|
52
|
+
%Q[(Integer) → <a href="dummy/class/String">String</a>]
|
|
53
|
+
assert_include html, '(String & Integer) → void'
|
|
54
|
+
assert_match(
|
|
55
|
+
%r!</dt>\n(<dt class="rbs-signature"><code>.+</code></dt>\n){2}<dd class="method-description">!,
|
|
56
|
+
html)
|
|
57
|
+
assert_not_match(/<pre|<dd class="rbs-/, html)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# 引数パターンでチャンクが分かれるメソッド(Array.new 型)では、各
|
|
61
|
+
# オーバーロードが引数名・アリティ・ブロック有無で最も近いチャンクの
|
|
62
|
+
# 見出し直下にだけ出る(全チャンクへの重複表示をしない)
|
|
63
|
+
def test_method_with_rbs_signatures_assigns_overloads_to_chunks
|
|
64
|
+
src = <<~RD
|
|
65
|
+
--- index(sub) -> Integer | nil
|
|
66
|
+
|
|
67
|
+
文字列で探す。
|
|
68
|
+
|
|
69
|
+
--- index(pattern) {|s| ... } -> Integer | nil
|
|
70
|
+
|
|
71
|
+
ブロックで探す。
|
|
72
|
+
RD
|
|
73
|
+
overloads = [
|
|
74
|
+
{'segments' => [['t', '(String sub) -> Integer?']],
|
|
75
|
+
'params' => ['sub'], 'arity' => [1, 1]},
|
|
76
|
+
{'segments' => [['t', '(Regexp pattern) { (String) -> bool } -> Integer?']],
|
|
77
|
+
'params' => ['pattern'], 'arity' => [1, 1], 'block' => 'req'},
|
|
78
|
+
]
|
|
79
|
+
method_entry = rbs_method_entry(src, overloads)
|
|
80
|
+
|
|
81
|
+
html = @c.compile_method(method_entry)
|
|
82
|
+
assert_match(
|
|
83
|
+
%r!<code>index\(sub\).*?</dt>\n<dt class="rbs-signature"><code>\(String sub\) → Integer\?</code></dt>\n<dd!m,
|
|
84
|
+
html)
|
|
85
|
+
assert_match(
|
|
86
|
+
%r!<code>index\(pattern\).*?</dt>\n<dt class="rbs-signature"><code>\(Regexp pattern\) \{ \(String\) → bool \} → Integer\?</code></dt>\n<dd!m,
|
|
87
|
+
html)
|
|
88
|
+
assert_equal 2, html.scan('rbs-signature').size
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# 旧形式の DB(メタ情報なし)ではチャンク振り分けができないので、
|
|
92
|
+
# 先頭チャンクにまとめて出す(重複はしない)
|
|
93
|
+
def test_method_with_rbs_signatures_without_meta_falls_back_to_first_chunk
|
|
94
|
+
src = <<~RD
|
|
95
|
+
--- index(sub) -> Integer | nil
|
|
96
|
+
|
|
97
|
+
文字列で探す。
|
|
98
|
+
|
|
99
|
+
--- index(pattern) {|s| ... } -> Integer | nil
|
|
100
|
+
|
|
101
|
+
ブロックで探す。
|
|
102
|
+
RD
|
|
103
|
+
overloads = [
|
|
104
|
+
{'segments' => [['t', '(String sub) -> Integer?']]},
|
|
105
|
+
{'segments' => [['t', '(Regexp pattern) { (String) -> bool } -> Integer?']]},
|
|
106
|
+
]
|
|
107
|
+
html = @c.compile_method(rbs_method_entry(src, overloads))
|
|
108
|
+
assert_equal 2, html.scan('rbs-signature').size
|
|
109
|
+
assert_match(
|
|
110
|
+
%r!<code>index\(sub\).*?</dt>\n(<dt class="rbs-signature">.*\n){2}<dd!,
|
|
111
|
+
html)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def rbs_method_entry(src, overloads)
|
|
115
|
+
method_entry = Object.new
|
|
116
|
+
mock(method_entry).source { src }
|
|
117
|
+
mock(method_entry).index_id.any_times { 'dummy' }
|
|
118
|
+
mock(method_entry).defined?.any_times { true }
|
|
119
|
+
mock(method_entry).id.any_times { 'String/i.index._builtin' }
|
|
120
|
+
mock(method_entry).names.any_times { [] }
|
|
121
|
+
mock(method_entry).since_map.any_times { {} }
|
|
122
|
+
mock(method_entry).until_map.any_times { {} }
|
|
123
|
+
mock(method_entry).rbs_signature_overloads.any_times { overloads }
|
|
124
|
+
method_entry
|
|
125
|
+
end
|
|
126
|
+
|
|
34
127
|
# badge のカタログ訳(「Ruby %s から」等)を検証するテストで使う、
|
|
35
128
|
# 日本語カタログを読み込んだ compiler を作る(setup の @c はカタログ無しの
|
|
36
129
|
# C ロケールなので、_() はキーそのまま(英語)を返してしまう)
|
|
@@ -230,6 +323,7 @@ HERE
|
|
|
230
323
|
mock(method_entry).names.any_times { ['mf'] }
|
|
231
324
|
mock(method_entry).since_map.any_times { {} }
|
|
232
325
|
mock(method_entry).until_map.any_times { {} }
|
|
326
|
+
mock(method_entry).rbs_signature_overloads.any_times { nil }
|
|
233
327
|
mock(method_entry).klass.any_times { klass }
|
|
234
328
|
mock(method_entry).display_typemark.any_times { '?.' }
|
|
235
329
|
|
|
@@ -250,6 +344,7 @@ HERE
|
|
|
250
344
|
mock(method_entry).names.any_times { ['mf'] }
|
|
251
345
|
mock(method_entry).since_map.any_times { {} }
|
|
252
346
|
mock(method_entry).until_map.any_times { {} }
|
|
347
|
+
mock(method_entry).rbs_signature_overloads.any_times { nil }
|
|
253
348
|
|
|
254
349
|
html = @c.compile_method(method_entry)
|
|
255
350
|
assert_include(html, '<code>mf</code>')
|
|
@@ -979,7 +1074,7 @@ HERE
|
|
|
979
1074
|
|
|
980
1075
|
data("library root" => ['[[lib:/]]', '<a href="dummy/library/">ライブラリ一覧</a>'],
|
|
981
1076
|
"builtin library" => ['[[lib:_builtin]]', '<a href="dummy/library/_builtin">組み込みライブラリ</a>'],
|
|
982
|
-
"C API root" => ['[[f:/]]', '<a href="dummy/function/"
|
|
1077
|
+
"C API root" => ['[[f:/]]', '<a href="dummy/function/">C API 関数一覧</a>'])
|
|
983
1078
|
def test_bracket_link_with_catalog(data)
|
|
984
1079
|
target, expected = data
|
|
985
1080
|
prefix = File.expand_path('../data/bitclust/catalog', __dir__)
|
|
@@ -1029,6 +1124,7 @@ HERE
|
|
|
1029
1124
|
mock(method_entry).names.any_times { [] }
|
|
1030
1125
|
mock(method_entry).since_map.any_times { {} }
|
|
1031
1126
|
mock(method_entry).until_map.any_times { {} }
|
|
1127
|
+
mock(method_entry).rbs_signature_overloads.any_times { nil }
|
|
1032
1128
|
html = @c.compile_method(method_entry)
|
|
1033
1129
|
assert_not_include(html, 'UNKNOWN_META_INFO')
|
|
1034
1130
|
assert_not_include(html, '{:')
|
|
@@ -1052,6 +1148,7 @@ HERE
|
|
|
1052
1148
|
mock(method_entry).names.any_times { [] }
|
|
1053
1149
|
mock(method_entry).since_map.any_times { {} }
|
|
1054
1150
|
mock(method_entry).until_map.any_times { {} }
|
|
1151
|
+
mock(method_entry).rbs_signature_overloads.any_times { nil }
|
|
1055
1152
|
html = @c.compile_method(method_entry)
|
|
1056
1153
|
assert_not_include(html, '{:')
|
|
1057
1154
|
assert_include(html, 'to_a2')
|
|
@@ -1074,6 +1171,7 @@ HERE
|
|
|
1074
1171
|
mock(method_entry).names.any_times { [] }
|
|
1075
1172
|
mock(method_entry).since_map.any_times { {} }
|
|
1076
1173
|
mock(method_entry).until_map.any_times { {} }
|
|
1174
|
+
mock(method_entry).rbs_signature_overloads.any_times { nil }
|
|
1077
1175
|
html = @c.compile_method(method_entry)
|
|
1078
1176
|
assert_not_include(html, '{:')
|
|
1079
1177
|
assert_include(html, 'このメソッドは定義されていません。')
|
|
@@ -511,3 +511,84 @@ HERE
|
|
|
511
511
|
end
|
|
512
512
|
end
|
|
513
513
|
end
|
|
514
|
+
|
|
515
|
+
class TestSearchIndexSnippet < Test::Unit::TestCase
|
|
516
|
+
# 検索結果の説明文(snippet)。meta description と同じ entry.description
|
|
517
|
+
# (最初の説明段落のプレーンテキスト)を、空でないときだけ持たせる。
|
|
518
|
+
#
|
|
519
|
+
# テストリスト:
|
|
520
|
+
# [x] class/method/special variable の各エントリが snippet を持つ
|
|
521
|
+
# [x] 説明が空のエントリは snippet キー自体を持たない
|
|
522
|
+
# [x] merge は複数版で同一エントリのとき最新版の snippet を採用する
|
|
523
|
+
# [x] 最新版に無いページは収録されている最終版の snippet を保つ
|
|
524
|
+
def setup
|
|
525
|
+
@prefix = 'db_snip'
|
|
526
|
+
@base = 'tree_snip'
|
|
527
|
+
@root = "#{@base}/refm/api/src"
|
|
528
|
+
FileUtils.mkdir_p("#{@root}")
|
|
529
|
+
File.open("#{@root}/LIBRARIES", 'w+') { |f| f.puts '_builtin' }
|
|
530
|
+
File.open("#{@root}/_builtin.rd", 'w+') do |f|
|
|
531
|
+
f.puts <<'HERE'
|
|
532
|
+
description
|
|
533
|
+
|
|
534
|
+
= class Foo < Object
|
|
535
|
+
クラスの説明。
|
|
536
|
+
== Instance Methods
|
|
537
|
+
--- foo
|
|
538
|
+
|
|
539
|
+
メソッドの説明。
|
|
540
|
+
--- undocumented
|
|
541
|
+
= module Kernel
|
|
542
|
+
== Special Variables
|
|
543
|
+
--- $; -> String | nil
|
|
544
|
+
|
|
545
|
+
区切り文字。
|
|
546
|
+
HERE
|
|
547
|
+
end
|
|
548
|
+
@db = BitClust::MethodDatabase.new(@prefix)
|
|
549
|
+
@db.init
|
|
550
|
+
@db.transaction do
|
|
551
|
+
[%w[version 3.4], %w[encoding utf-8]].each { |k, v| @db.propset(k, v) }
|
|
552
|
+
end
|
|
553
|
+
@db.transaction { @db.update_by_stdlibtree(@root) }
|
|
554
|
+
@gen = BitClust::SearchIndexGenerator.new
|
|
555
|
+
end
|
|
556
|
+
|
|
557
|
+
def teardown
|
|
558
|
+
FileUtils.rm_r([@prefix, @base], :force => true)
|
|
559
|
+
end
|
|
560
|
+
|
|
561
|
+
def find_entry(index, full_name)
|
|
562
|
+
index.find { |e| e[:full_name] == full_name }
|
|
563
|
+
end
|
|
564
|
+
|
|
565
|
+
def test_snippet_carries_entry_description
|
|
566
|
+
index = @gen.build_index(@db)
|
|
567
|
+
assert_equal 'クラスの説明。', find_entry(index, 'Foo')[:snippet]
|
|
568
|
+
assert_equal 'メソッドの説明。', find_entry(index, 'Foo#foo')[:snippet]
|
|
569
|
+
assert_equal '区切り文字。', find_entry(index, '$;')[:snippet]
|
|
570
|
+
end
|
|
571
|
+
|
|
572
|
+
def test_snippet_is_omitted_when_description_is_empty
|
|
573
|
+
index = @gen.build_index(@db)
|
|
574
|
+
e = find_entry(index, 'Foo#undocumented')
|
|
575
|
+
assert_not_nil e
|
|
576
|
+
assert_false e.key?(:snippet)
|
|
577
|
+
end
|
|
578
|
+
|
|
579
|
+
def test_merge_takes_snippet_from_newest_version
|
|
580
|
+
old_e = { name: 'X', full_name: 'X', type: 'class', path: 'class/-x.html', snippet: '古い説明' }
|
|
581
|
+
new_e = { name: 'X', full_name: 'X', type: 'class', path: 'class/-x.html', snippet: '新しい説明' }
|
|
582
|
+
merged = BitClust::SearchIndexGenerator.merge([['3.4', [new_e]], ['3.3', [old_e]]])
|
|
583
|
+
assert_equal 1, merged.size
|
|
584
|
+
assert_equal '新しい説明', merged.first[:snippet]
|
|
585
|
+
assert_equal %w[3.3 3.4], merged.first[:versions]
|
|
586
|
+
end
|
|
587
|
+
|
|
588
|
+
def test_merge_keeps_snippet_of_last_version_having_the_page
|
|
589
|
+
only_old = { name: 'Y', full_name: 'Y', type: 'class', path: 'class/-y.html', snippet: '最終収録版の説明' }
|
|
590
|
+
merged = BitClust::SearchIndexGenerator.merge([['3.4', []], ['3.3', [only_old]]])
|
|
591
|
+
assert_equal '最終収録版の説明', merged.first[:snippet]
|
|
592
|
+
assert_equal %w[3.3], merged.first[:versions]
|
|
593
|
+
end
|
|
594
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
require 'test/unit'
|
|
3
|
+
require 'bitclust'
|
|
4
|
+
require 'bitclust/subcommands/setup_command'
|
|
5
|
+
|
|
6
|
+
# setup サブコマンドの既定バージョン。
|
|
7
|
+
#
|
|
8
|
+
# テストリスト:
|
|
9
|
+
# [x] 既定の対象バージョンに teeny が付いていない
|
|
10
|
+
# (#%if (version == "V") や #%version V の等値ゲートは文字列一致なので、
|
|
11
|
+
# teeny 付きの版名で DB を作ると doctree CI・生成済みドキュメントと
|
|
12
|
+
# ゲートの判定がずれる)
|
|
13
|
+
class TestSetupCommand < Test::Unit::TestCase
|
|
14
|
+
def test_default_versions_have_no_teeny
|
|
15
|
+
versions = BitClust::Subcommands::SetupCommand.new.instance_variable_get(:@versions)
|
|
16
|
+
assert_false(versions.empty?)
|
|
17
|
+
versions.each do |version|
|
|
18
|
+
assert_match(/\A\d+\.\d+\z/, version)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -202,3 +202,140 @@ class TestStatichtmlSitemap < Test::Unit::TestCase
|
|
|
202
202
|
end
|
|
203
203
|
end
|
|
204
204
|
end
|
|
205
|
+
|
|
206
|
+
class TestStatichtmlMarkdownOutput < Test::Unit::TestCase
|
|
207
|
+
def build_command(markdown_output: true)
|
|
208
|
+
cmd = BitClust::Subcommands::StatichtmlCommand.new
|
|
209
|
+
argv = ["--fs-casesensitive"]
|
|
210
|
+
argv.unshift("--markdown-output") if markdown_output
|
|
211
|
+
cmd.parse(argv)
|
|
212
|
+
cmd.instance_variable_set(:@verbose, false)
|
|
213
|
+
cmd.send(:create_manager_config)
|
|
214
|
+
urlmapper = cmd.instance_variable_get(:@urlmapper)
|
|
215
|
+
urlmapper.bitclust_html_base = '..'
|
|
216
|
+
cmd.instance_variable_set(:@markdown_exporter,
|
|
217
|
+
BitClust::MarkdownExporter.new(urlmapper, '.html'))
|
|
218
|
+
cmd
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def markdown_db
|
|
222
|
+
db = BitClust::MethodDatabase.dummy({ "version" => "3.4" })
|
|
223
|
+
def db.properties = { 'source_format' => 'markdown' }
|
|
224
|
+
db
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
def test_option_defaults_to_false
|
|
228
|
+
cmd = BitClust::Subcommands::StatichtmlCommand.new
|
|
229
|
+
assert_false(cmd.instance_variable_get(:@markdown_output))
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
def test_option_is_parsed
|
|
233
|
+
cmd = BitClust::Subcommands::StatichtmlCommand.new
|
|
234
|
+
cmd.parse(["--markdown-output"])
|
|
235
|
+
assert_true(cmd.instance_variable_get(:@markdown_output))
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def test_markdown_path_replaces_html_suffix
|
|
239
|
+
cmd = BitClust::Subcommands::StatichtmlCommand.new
|
|
240
|
+
assert_equal("out/class/ARGF.class.md",
|
|
241
|
+
cmd.send(:markdown_path, Pathname.new("out/class/ARGF.class.html")).to_s)
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
def test_writes_md_next_to_html_for_markdown_db
|
|
245
|
+
Dir.mktmpdir do |dir|
|
|
246
|
+
cmd = build_command
|
|
247
|
+
db = markdown_db
|
|
248
|
+
entry = BitClust::DocEntry.new(db, "help")
|
|
249
|
+
entry.title = "ヘルプ"
|
|
250
|
+
entry.source = "\n[c:String] を参照。\n"
|
|
251
|
+
path = Pathname.new(dir) + "doc/help.html"
|
|
252
|
+
FileUtils.mkdir_p(path.dirname)
|
|
253
|
+
cmd.send(:create_markdown_file_p, entry, path, db)
|
|
254
|
+
md = File.read(File.join(dir, "doc", "help.md"))
|
|
255
|
+
assert_equal("# ヘルプ\n\n[String](../class/String.md) を参照。\n", md)
|
|
256
|
+
end
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
def test_skips_md_for_non_markdown_db
|
|
260
|
+
Dir.mktmpdir do |dir|
|
|
261
|
+
cmd = build_command
|
|
262
|
+
db = BitClust::MethodDatabase.dummy({ "version" => "3.4" })
|
|
263
|
+
def db.properties = {}
|
|
264
|
+
entry = BitClust::DocEntry.new(db, "help")
|
|
265
|
+
entry.title = "ヘルプ"
|
|
266
|
+
entry.source = "\n本文\n"
|
|
267
|
+
path = Pathname.new(dir) + "doc/help.html"
|
|
268
|
+
FileUtils.mkdir_p(path.dirname)
|
|
269
|
+
cmd.send(:create_markdown_file_p, entry, path, db)
|
|
270
|
+
assert_false(File.exist?(File.join(dir, "doc", "help.md")))
|
|
271
|
+
end
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
def test_warns_when_db_is_not_markdown
|
|
275
|
+
cmd = build_command
|
|
276
|
+
db = BitClust::MethodDatabase.dummy({ "version" => "3.4" })
|
|
277
|
+
def db.properties = {}
|
|
278
|
+
orig_stderr, $stderr = $stderr, StringIO.new
|
|
279
|
+
begin
|
|
280
|
+
cmd.send(:warn_markdown_output_skipped, db, "the method database")
|
|
281
|
+
assert_match(/--markdown-output is ignored/, $stderr.string)
|
|
282
|
+
ensure
|
|
283
|
+
$stderr = orig_stderr
|
|
284
|
+
end
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
def test_no_warning_without_option
|
|
288
|
+
cmd = build_command(markdown_output: false)
|
|
289
|
+
db = BitClust::MethodDatabase.dummy({ "version" => "3.4" })
|
|
290
|
+
def db.properties = {}
|
|
291
|
+
orig_stderr, $stderr = $stderr, StringIO.new
|
|
292
|
+
begin
|
|
293
|
+
cmd.send(:warn_markdown_output_skipped, db, "the method database")
|
|
294
|
+
assert_equal("", $stderr.string)
|
|
295
|
+
ensure
|
|
296
|
+
$stderr = orig_stderr
|
|
297
|
+
end
|
|
298
|
+
end
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
class TestStatichtmlVersionSwitcher < Test::Unit::TestCase
|
|
302
|
+
def build_command(themedir, outputdir)
|
|
303
|
+
cmd = BitClust::Subcommands::StatichtmlCommand.new
|
|
304
|
+
cmd.instance_variable_set(:@manager_config, { :themedir => Pathname.new(themedir) })
|
|
305
|
+
cmd.instance_variable_set(:@outputdir, Pathname.new(outputdir))
|
|
306
|
+
cmd.instance_variable_set(:@verbose, false)
|
|
307
|
+
cmd
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
def test_version_switcher_js_is_copied
|
|
311
|
+
Dir.mktmpdir do |dir|
|
|
312
|
+
themedir = File.join(dir, 'theme')
|
|
313
|
+
outputdir = File.join(dir, 'out')
|
|
314
|
+
FileUtils.mkdir_p(File.join(themedir, 'js'))
|
|
315
|
+
FileUtils.mkdir_p(outputdir)
|
|
316
|
+
File.write(File.join(themedir, 'js', 'version_switcher.js'), "// vs\n")
|
|
317
|
+
cmd = build_command(themedir, outputdir)
|
|
318
|
+
cmd.send(:copy_version_switcher_script)
|
|
319
|
+
assert_true(File.file?(File.join(outputdir, 'js', 'version_switcher.js')))
|
|
320
|
+
end
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
# An older themedir without the file must not abort the whole build.
|
|
324
|
+
def test_theme_without_version_switcher_js_is_tolerated
|
|
325
|
+
Dir.mktmpdir do |dir|
|
|
326
|
+
themedir = File.join(dir, 'theme')
|
|
327
|
+
outputdir = File.join(dir, 'out')
|
|
328
|
+
FileUtils.mkdir_p(themedir)
|
|
329
|
+
FileUtils.mkdir_p(outputdir)
|
|
330
|
+
cmd = build_command(themedir, outputdir)
|
|
331
|
+
orig_stderr, $stderr = $stderr, StringIO.new
|
|
332
|
+
begin
|
|
333
|
+
assert_nothing_raised { cmd.send(:copy_version_switcher_script) }
|
|
334
|
+
assert_match(/version_switcher\.js not found/, $stderr.string)
|
|
335
|
+
ensure
|
|
336
|
+
$stderr = orig_stderr
|
|
337
|
+
end
|
|
338
|
+
assert_false(File.exist?(File.join(outputdir, 'js', 'version_switcher.js')))
|
|
339
|
+
end
|
|
340
|
+
end
|
|
341
|
+
end
|
data/theme/default/js/run.js
CHANGED
|
@@ -134,6 +134,20 @@ function setupBlock(pre, runner) {
|
|
|
134
134
|
}
|
|
135
135
|
group.prepend(button)
|
|
136
136
|
|
|
137
|
+
// RUN の使い方と制限(ruby.wasm で動かないサンプルがあること等)を
|
|
138
|
+
// 説明するヘルプページの RUN 節への動線。リンク先はページごとに相対
|
|
139
|
+
// URL が変わるため、サーバー側が layout の meta で埋め込んだ値を使う
|
|
140
|
+
const helpUrl = document.querySelector('meta[name="rurema-run-help"]')?.content
|
|
141
|
+
if (helpUrl) {
|
|
142
|
+
const help = document.createElement('a')
|
|
143
|
+
help.className = 'highlight__help-link'
|
|
144
|
+
help.href = helpUrl
|
|
145
|
+
help.textContent = '?'
|
|
146
|
+
help.title = 'RUN ボタンのヘルプ'
|
|
147
|
+
help.setAttribute('aria-label', 'RUN ボタンのヘルプ')
|
|
148
|
+
group.append(help)
|
|
149
|
+
}
|
|
150
|
+
|
|
137
151
|
let output
|
|
138
152
|
let outputTextNode
|
|
139
153
|
const ensureOutput = () => {
|
|
@@ -112,7 +112,9 @@
|
|
|
112
112
|
this.escapeHTML(typeClass) + '">' + this.formatType(r.type) + '</span>';
|
|
113
113
|
}
|
|
114
114
|
html += '</p>';
|
|
115
|
-
|
|
115
|
+
// snippet はインデックス生成側が入れたプレーンテキスト
|
|
116
|
+
// (エントリの説明の最初の段落)。HTML として解釈させない
|
|
117
|
+
if (r.snippet) html += '<div class="search-snippet">' + this.escapeHTML(r.snippet) + '</div>';
|
|
116
118
|
li.innerHTML = html;
|
|
117
119
|
return li;
|
|
118
120
|
};
|
|
@@ -158,7 +158,13 @@
|
|
|
158
158
|
html += '<span class="search-type search-type-' +
|
|
159
159
|
this.escapeHTML(typeClass) + '">' + this.formatType(r.type) + '</span>';
|
|
160
160
|
}
|
|
161
|
-
html += '</p
|
|
161
|
+
html += '</p>';
|
|
162
|
+
// snippet はインデックス生成側が入れたプレーンテキスト
|
|
163
|
+
// (エントリの説明の最初の段落)。HTML として解釈させない
|
|
164
|
+
if (r.snippet) {
|
|
165
|
+
html += '<div class="search-snippet">' + this.escapeHTML(r.snippet) + '</div>';
|
|
166
|
+
}
|
|
167
|
+
html += '<p class="search-versions">';
|
|
162
168
|
for (var i = 0; i < versions.length; i++) {
|
|
163
169
|
html += '<a href="' + this.escapeHTML(versionHref(versions[i], r.path)) +
|
|
164
170
|
'">' + this.escapeHTML(versions[i]) + '</a>';
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
// Version switcher for statichtml pages served under a /ja/<version>/ tree.
|
|
2
|
+
//
|
|
3
|
+
// Renders a <select> into the #version-switcher slot in the top bar. Picking
|
|
4
|
+
// a version navigates to the same page under that version; if the page does
|
|
5
|
+
// not exist there, it falls back to the newest version that has it, and as a
|
|
6
|
+
// last resort to that version's top page. On the first interaction every
|
|
7
|
+
// other version is probed with a HEAD request and missing ones are disabled.
|
|
8
|
+
//
|
|
9
|
+
// Pages not served under a versioned path (local previews, /ja/search/) get
|
|
10
|
+
// no switcher: attach() is a no-op there.
|
|
11
|
+
(function () {
|
|
12
|
+
'use strict';
|
|
13
|
+
|
|
14
|
+
// 公開済みの版(新しい順)。doctree の Rakefile の
|
|
15
|
+
// SUPPORTED_VERSIONS / OLD_VERSIONS と揃える。
|
|
16
|
+
var RELEASED_VERSIONS = [
|
|
17
|
+
'4.0', '3.4', '3.3', '3.2', '3.1', '3.0',
|
|
18
|
+
'2.7.0', '2.6.0', '2.5.0', '2.4.0', '2.3.0', '2.2.0', '2.1.0', '2.0.0',
|
|
19
|
+
'1.9.3', '1.8.7'
|
|
20
|
+
];
|
|
21
|
+
var EDGE_VERSION = 'master';
|
|
22
|
+
var ALIASES = ['latest', EDGE_VERSION];
|
|
23
|
+
|
|
24
|
+
function knownVersion(version) {
|
|
25
|
+
return RELEASED_VERSIONS.indexOf(version) !== -1 ||
|
|
26
|
+
ALIASES.indexOf(version) !== -1;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function parsePath(pathname) {
|
|
30
|
+
var m = /^(.*\/ja\/)([^\/]+)(\/.+)$/.exec(pathname);
|
|
31
|
+
if (!m || !knownVersion(m[2])) return null;
|
|
32
|
+
return { base: m[1], version: m[2], rest: m[3] };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function targetUrl(parsed, version) {
|
|
36
|
+
return parsed.base + version + parsed.rest;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function optionVersions(currentVersion) {
|
|
40
|
+
var versions = [EDGE_VERSION].concat(RELEASED_VERSIONS);
|
|
41
|
+
if (versions.indexOf(currentVersion) === -1) {
|
|
42
|
+
versions.unshift(currentVersion);
|
|
43
|
+
}
|
|
44
|
+
return versions;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// true: exists / false: does not exist / null: unknown (network error)
|
|
48
|
+
function checkExists(url) {
|
|
49
|
+
return fetch(url, { method: 'HEAD' })
|
|
50
|
+
.then(function (res) { return res.ok; })
|
|
51
|
+
.catch(function () { return null; });
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Resolve where picking `version` should go. Unknown existence keeps the
|
|
55
|
+
// target so an offline or CORS-restricted page still just navigates.
|
|
56
|
+
function resolveTarget(parsed, version, exists) {
|
|
57
|
+
var target = targetUrl(parsed, version);
|
|
58
|
+
return exists(target).then(function (ok) {
|
|
59
|
+
if (ok !== false) return target;
|
|
60
|
+
var candidates = RELEASED_VERSIONS.filter(function (v) {
|
|
61
|
+
return v !== version;
|
|
62
|
+
});
|
|
63
|
+
var tryNext = function (i) {
|
|
64
|
+
if (i >= candidates.length) return parsed.base + version + '/';
|
|
65
|
+
var url = targetUrl(parsed, candidates[i]);
|
|
66
|
+
return exists(url).then(function (found) {
|
|
67
|
+
return found === true ? url : tryNext(i + 1);
|
|
68
|
+
});
|
|
69
|
+
};
|
|
70
|
+
return tryNext(0);
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function attach(deps) {
|
|
75
|
+
var doc = deps.document;
|
|
76
|
+
var slot = doc.getElementById('version-switcher');
|
|
77
|
+
var parsed = parsePath(deps.location.pathname);
|
|
78
|
+
if (!slot || !parsed) return false;
|
|
79
|
+
var exists = deps.checkExists || checkExists;
|
|
80
|
+
|
|
81
|
+
var select = doc.createElement('select');
|
|
82
|
+
select.setAttribute('aria-label', '他のバージョンの同じページを表示');
|
|
83
|
+
optionVersions(parsed.version).forEach(function (version) {
|
|
84
|
+
var option = doc.createElement('option');
|
|
85
|
+
option.value = version;
|
|
86
|
+
option.textContent = version;
|
|
87
|
+
option.selected = version === parsed.version;
|
|
88
|
+
select.appendChild(option);
|
|
89
|
+
});
|
|
90
|
+
select.value = parsed.version;
|
|
91
|
+
|
|
92
|
+
var probed = false;
|
|
93
|
+
var probe = function () {
|
|
94
|
+
if (probed) return;
|
|
95
|
+
probed = true;
|
|
96
|
+
Array.prototype.forEach.call(select.children, function (option) {
|
|
97
|
+
if (option.value === parsed.version) return;
|
|
98
|
+
exists(targetUrl(parsed, option.value)).then(function (ok) {
|
|
99
|
+
if (ok === false) option.disabled = true;
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
};
|
|
103
|
+
select.addEventListener('focus', probe);
|
|
104
|
+
select.addEventListener('mousedown', probe);
|
|
105
|
+
select.addEventListener('touchstart', probe);
|
|
106
|
+
|
|
107
|
+
select.addEventListener('change', function () {
|
|
108
|
+
var version = select.value;
|
|
109
|
+
if (version === parsed.version) return;
|
|
110
|
+
resolveTarget(parsed, version, exists).then(function (url) {
|
|
111
|
+
deps.setHref(url);
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
slot.appendChild(select);
|
|
116
|
+
return true;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
var api = {
|
|
120
|
+
RELEASED_VERSIONS: RELEASED_VERSIONS,
|
|
121
|
+
EDGE_VERSION: EDGE_VERSION,
|
|
122
|
+
parsePath: parsePath,
|
|
123
|
+
targetUrl: targetUrl,
|
|
124
|
+
optionVersions: optionVersions,
|
|
125
|
+
checkExists: checkExists,
|
|
126
|
+
resolveTarget: resolveTarget,
|
|
127
|
+
attach: attach
|
|
128
|
+
};
|
|
129
|
+
if (typeof globalThis !== 'undefined') {
|
|
130
|
+
globalThis.RuremaVersionSwitcher = api;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (typeof document !== 'undefined' && typeof location !== 'undefined') {
|
|
134
|
+
var init = function () {
|
|
135
|
+
attach({
|
|
136
|
+
document: document,
|
|
137
|
+
location: location,
|
|
138
|
+
setHref: function (url) { location.href = url; }
|
|
139
|
+
});
|
|
140
|
+
};
|
|
141
|
+
if (document.readyState === 'loading') {
|
|
142
|
+
document.addEventListener('DOMContentLoaded', init);
|
|
143
|
+
} else {
|
|
144
|
+
init();
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
})();
|
data/theme/default/search.css
CHANGED
|
@@ -26,6 +26,27 @@
|
|
|
26
26
|
text-overflow: ellipsis;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
/* Version switcher: sits next to the brand; the auto margin keeps the
|
|
30
|
+
search box pinned to the right. The slot is empty until
|
|
31
|
+
version_switcher.js fills it (only on /ja/<version>/ pages). */
|
|
32
|
+
#version-switcher {
|
|
33
|
+
flex: 0 0 auto;
|
|
34
|
+
margin-right: auto;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
#version-switcher:empty {
|
|
38
|
+
display: none;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
#version-switcher select {
|
|
42
|
+
font-size: 0.9em;
|
|
43
|
+
padding: 0.2em 0.3em;
|
|
44
|
+
border: 1px solid #99a;
|
|
45
|
+
border-radius: 4px;
|
|
46
|
+
background: #fff;
|
|
47
|
+
color: inherit;
|
|
48
|
+
}
|
|
49
|
+
|
|
29
50
|
#search-section {
|
|
30
51
|
position: relative;
|
|
31
52
|
flex: 0 0 auto;
|