bitclust-core 1.6.1 → 1.8.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.
Files changed (90) hide show
  1. checksums.yaml +4 -4
  2. data/Rakefile +1 -0
  3. data/data/bitclust/catalog/ja_JP.UTF-8 +4 -2
  4. data/data/bitclust/template/class-index +0 -1
  5. data/data/bitclust/template/function-index +1 -0
  6. data/data/bitclust/template/layout +1 -0
  7. data/data/bitclust/template.epub/class +5 -5
  8. data/data/bitclust/template.epub/class-index +0 -1
  9. data/data/bitclust/template.epub/contents +7 -4
  10. data/data/bitclust/template.epub/layout +4 -4
  11. data/data/bitclust/template.offline/class-index +0 -1
  12. data/data/bitclust/template.offline/function-index +1 -0
  13. data/data/bitclust/template.offline/layout +3 -0
  14. data/lib/bitclust/classentry.rb +1 -1
  15. data/lib/bitclust/docentry.rb +1 -1
  16. data/lib/bitclust/functionentry.rb +1 -1
  17. data/lib/bitclust/generators/epub.rb +69 -5
  18. data/lib/bitclust/irb.rb +74 -0
  19. data/lib/bitclust/markdown_bridge.rb +11 -6
  20. data/lib/bitclust/markdown_exporter.rb +311 -0
  21. data/lib/bitclust/markdown_tree.rb +118 -11
  22. data/lib/bitclust/mdcompiler.rb +15 -1
  23. data/lib/bitclust/mdparser.rb +42 -1
  24. data/lib/bitclust/methoddatabase.rb +17 -4
  25. data/lib/bitclust/methodentry.rb +24 -0
  26. data/lib/bitclust/methodsignature.rb +1 -1
  27. data/lib/bitclust/nameutils.rb +13 -0
  28. data/lib/bitclust/preprocessor.rb +4 -37
  29. data/lib/bitclust/rbs_overload_matcher.rb +179 -0
  30. data/lib/bitclust/rbs_sig_importer.rb +276 -0
  31. data/lib/bitclust/rbs_signatures.rb +125 -0
  32. data/lib/bitclust/rdcompiler.rb +12 -1
  33. data/lib/bitclust/remote.rb +405 -0
  34. data/lib/bitclust/runner.rb +7 -10
  35. data/lib/bitclust/screen.rb +6 -1
  36. data/lib/bitclust/search_index_generator.rb +28 -10
  37. data/lib/bitclust/searcher.rb +54 -25
  38. data/lib/bitclust/subcommands/chm_command.rb +1 -1
  39. data/lib/bitclust/subcommands/epub_command.rb +2 -2
  40. data/lib/bitclust/subcommands/extract_command.rb +7 -0
  41. data/lib/bitclust/subcommands/htmlfile_command.rb +7 -0
  42. data/lib/bitclust/subcommands/methods_command.rb +5 -28
  43. data/lib/bitclust/subcommands/query_command.rb +1 -0
  44. data/lib/bitclust/subcommands/rbssig_command.rb +99 -0
  45. data/lib/bitclust/subcommands/server_command.rb +21 -10
  46. data/lib/bitclust/subcommands/setup_command.rb +32 -11
  47. data/lib/bitclust/subcommands/statichtml_command.rb +59 -0
  48. data/lib/bitclust/syntax_highlighter.rb +65 -4
  49. data/lib/bitclust/user_dirs.rb +62 -0
  50. data/lib/bitclust/version.rb +1 -1
  51. data/lib/bitclust-irb.rb +2 -0
  52. data/test/test_chm_command.rb +22 -0
  53. data/test/test_epub_command.rb +60 -0
  54. data/test/test_epub_generator.rb +330 -0
  55. data/test/test_extract_command.rb +45 -0
  56. data/test/test_irb_plugin.rb +212 -0
  57. data/test/test_link_checker.rb +1 -1
  58. data/test/test_markdown_bridge.rb +35 -0
  59. data/test/test_markdown_exporter.rb +260 -0
  60. data/test/test_markdown_tree.rb +144 -0
  61. data/test/test_mdcompiler.rb +47 -5
  62. data/test/test_mdparser.rb +223 -4
  63. data/test/test_methodentry.rb +111 -0
  64. data/test/test_methods_command.rb +32 -0
  65. data/test/test_methodsignature.rb +3 -1
  66. data/test/test_opensearchdescription_screen.rb +39 -0
  67. data/test/test_preprocessor.rb +26 -0
  68. data/test/test_query_command.rb +30 -0
  69. data/test/test_rbs_overload_matcher.rb +170 -0
  70. data/test/test_rbs_sig_importer.rb +237 -0
  71. data/test/test_rbssig_command.rb +186 -0
  72. data/test/test_rdcompiler.rb +99 -1
  73. data/test/test_remote.rb +374 -0
  74. data/test/test_runner.rb +61 -1
  75. data/test/test_search_index_generator.rb +81 -0
  76. data/test/test_searcher.rb +63 -0
  77. data/test/test_server_command.rb +41 -0
  78. data/test/test_setup_command.rb +164 -0
  79. data/test/test_statichtml_command.rb +137 -0
  80. data/test/test_syntax_highlighter.rb +124 -0
  81. data/test/test_user_dirs.rb +62 -0
  82. data/theme/default/js/run.js +14 -0
  83. data/theme/default/js/search_init.js +3 -1
  84. data/theme/default/js/search_page.js +7 -1
  85. data/theme/default/js/version_switcher.js +147 -0
  86. data/theme/default/search.css +21 -0
  87. data/theme/default/style.css +83 -7
  88. data/theme/lillia/style.css +26 -0
  89. metadata +71 -2
  90. data/lib/bitclust/ridatabase.rb +0 -359
@@ -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
@@ -1,8 +1,10 @@
1
1
  require "test/unit"
2
2
  require "bitclust"
3
3
  require "bitclust/searcher"
4
+ require "bitclust/user_dirs"
4
5
  require "fileutils"
5
6
  require "tmpdir"
7
+ require "yaml"
6
8
 
7
9
  class TestTerminalView < Test::Unit::TestCase
8
10
  include BitClust
@@ -151,3 +153,64 @@ class TestSearcherModuleFunctionQuery < Test::Unit::TestCase
151
153
  assert_equal ["Foo", ".", "b?"], parse("Foo.b?")
152
154
  end
153
155
  end
156
+
157
+ # refe/bitclust search が、環境変数指定が無いとき設定ファイル(UserDirs)
158
+ # 経由で DB を見つけること。XDG の場所→従来の ~/.bitclust の順
159
+ class TestSearcherDefaultDbpath < Test::Unit::TestCase
160
+ include BitClust
161
+
162
+ ENV_KEYS = %w[HOME XDG_CONFIG_HOME XDG_DATA_HOME XDG_CACHE_HOME
163
+ REFE2_SERVER BITCLUST_SERVER REFE2_DATADIR BITCLUST_DATADIR].freeze
164
+
165
+ def setup
166
+ @saved = ENV_KEYS.to_h {|k| [k, ENV[k]] }
167
+ @home = Dir.mktmpdir
168
+ ENV['HOME'] = @home
169
+ (ENV_KEYS - %w[HOME]).each {|k| ENV.delete(k) }
170
+ end
171
+
172
+ def teardown
173
+ @saved.each {|k, v| v ? ENV[k] = v : ENV.delete(k) }
174
+ FileUtils.rm_rf(@home)
175
+ end
176
+
177
+ def write_config(path, prefix)
178
+ FileUtils.mkdir_p(File.dirname(path))
179
+ File.write(path, { :default_version => '9.9', :database_prefix => prefix }.to_yaml)
180
+ end
181
+
182
+ def test_default_dbpath_via_xdg_config
183
+ prefix = File.join(@home, 'data', 'bitclust', 'db')
184
+ MethodDatabase.new("#{prefix}-9.9").init
185
+ write_config(File.join(@home, '.config', 'bitclust', 'config'), prefix)
186
+ searcher = Searcher.new
187
+ assert_equal("#{prefix}-9.9", searcher.send(:default_dbpath))
188
+ assert_true(searcher.local_database?)
189
+ end
190
+
191
+ def test_default_dbpath_via_legacy_config
192
+ prefix = File.join(@home, '.bitclust', 'db')
193
+ MethodDatabase.new("#{prefix}-9.9").init
194
+ write_config(File.join(@home, '.bitclust', 'config'), prefix)
195
+ searcher = Searcher.new
196
+ assert_equal("#{prefix}-9.9", searcher.send(:default_dbpath))
197
+ assert_true(searcher.local_database?)
198
+ end
199
+
200
+ def test_default_dbpath_prefers_xdg_over_legacy
201
+ legacy_prefix = File.join(@home, '.bitclust', 'db')
202
+ MethodDatabase.new("#{legacy_prefix}-9.9").init
203
+ write_config(File.join(@home, '.bitclust', 'config'), legacy_prefix)
204
+ xdg_prefix = File.join(@home, 'data', 'bitclust', 'db')
205
+ MethodDatabase.new("#{xdg_prefix}-9.9").init
206
+ write_config(File.join(@home, '.config', 'bitclust', 'config'), xdg_prefix)
207
+ searcher = Searcher.new
208
+ assert_equal("#{xdg_prefix}-9.9", searcher.send(:default_dbpath))
209
+ end
210
+
211
+ def test_default_dbpath_nil_without_config
212
+ searcher = Searcher.new
213
+ assert_nil(searcher.send(:default_dbpath))
214
+ assert_false(searcher.local_database?)
215
+ end
216
+ end
@@ -0,0 +1,41 @@
1
+ require 'test/unit'
2
+ require 'stringio'
3
+ require 'bitclust'
4
+ require 'bitclust/subcommands/server_command'
5
+
6
+ class TestServerCommand < Test::Unit::TestCase
7
+ def build(argv)
8
+ cmd = BitClust::Subcommands::ServerCommand.new
9
+ cmd.parse(argv)
10
+ cmd
11
+ end
12
+
13
+ def test_does_not_require_global_database
14
+ assert_false BitClust::Subcommands::ServerCommand.new.needs_database?
15
+ end
16
+
17
+ def test_parse_accepts_short_option
18
+ cmd = build(['--baseurl=', '-d', '/tmp/db-x'])
19
+ assert_equal '/tmp/db-x', cmd.__send__(:resolve_dbpath, {:prefix => nil, :capi => false})
20
+ end
21
+
22
+ def test_parse_accepts_long_option
23
+ cmd = build(['--baseurl=', '--database=/tmp/db-y'])
24
+ assert_equal '/tmp/db-y', cmd.__send__(:resolve_dbpath, {:prefix => '/tmp/global', :capi => false})
25
+ end
26
+
27
+ def test_global_database_is_used_as_fallback
28
+ cmd = build(['--baseurl='])
29
+ assert_equal '/tmp/global', cmd.__send__(:resolve_dbpath, {:prefix => '/tmp/global', :capi => false})
30
+ end
31
+
32
+ def test_missing_database_aborts_with_guidance
33
+ cmd = build(['--baseurl='])
34
+ err = StringIO.new
35
+ orig, $stderr = $stderr, err
36
+ assert_raise(SystemExit) { cmd.__send__(:resolve_dbpath, {:prefix => nil, :capi => false}) }
37
+ assert_match(/--database/, err.string)
38
+ ensure
39
+ $stderr = orig
40
+ end
41
+ end
@@ -0,0 +1,164 @@
1
+ # frozen_string_literal: true
2
+ require 'test/unit'
3
+ require 'bitclust'
4
+ require 'bitclust/subcommands/setup_command'
5
+ require 'bitclust/user_dirs'
6
+ require 'tmpdir'
7
+ require 'fileutils'
8
+ require 'yaml'
9
+
10
+ # setup サブコマンドの既定バージョン。
11
+ #
12
+ # テストリスト:
13
+ # [x] 既定の対象バージョンに teeny が付いていない
14
+ # (#%if (version == "V") や #%version V の等値ゲートは文字列一致なので、
15
+ # teeny 付きの版名で DB を作ると doctree CI・生成済みドキュメントと
16
+ # ゲートの判定がずれる)
17
+ class TestSetupCommand < Test::Unit::TestCase
18
+ def test_default_versions_have_no_teeny
19
+ versions = BitClust::Subcommands::SetupCommand.new.instance_variable_get(:@versions)
20
+ assert_false(versions.empty?)
21
+ versions.each do |version|
22
+ assert_match(/\A\d+\.\d+\z/, version)
23
+ end
24
+ end
25
+ end
26
+
27
+ # setup サブコマンドが選ぶ、設定・DB・doctree checkout の置き場所。
28
+ #
29
+ # テストリスト:
30
+ # [x] 何も無ければ XDG の場所(~/.config/bitclust, ~/.local/share/bitclust)を使う
31
+ # [x] 従来の ~/.bitclust/config だけあれば、そのレイアウトを維持する
32
+ # [x] 両方あれば XDG を使う
33
+ class TestSetupCommandLayout < Test::Unit::TestCase
34
+ ENV_KEYS = %w[HOME XDG_CONFIG_HOME XDG_DATA_HOME XDG_CACHE_HOME].freeze
35
+
36
+ def setup
37
+ @saved = ENV_KEYS.to_h {|k| [k, ENV[k]] }
38
+ @home = Dir.mktmpdir
39
+ ENV['HOME'] = @home
40
+ (ENV_KEYS - %w[HOME]).each {|k| ENV.delete(k) }
41
+ @cmd = BitClust::Subcommands::SetupCommand.new
42
+ end
43
+
44
+ def teardown
45
+ @saved.each {|k, v| v ? ENV[k] = v : ENV.delete(k) }
46
+ FileUtils.rm_rf(@home)
47
+ end
48
+
49
+ def touch_config(path)
50
+ FileUtils.mkdir_p(File.dirname(path))
51
+ FileUtils.touch(path)
52
+ end
53
+
54
+ def test_layout_uses_xdg_when_nothing_exists
55
+ config_path, database_prefix, rubydoc_dir = @cmd.send(:layout)
56
+ assert_equal(File.join(@home, '.config', 'bitclust', 'config'), config_path.to_s)
57
+ assert_equal(File.join(@home, '.local', 'share', 'bitclust', 'db'), database_prefix)
58
+ assert_equal(File.join(@home, '.local', 'share', 'bitclust', 'rubydoc'), rubydoc_dir.to_s)
59
+ end
60
+
61
+ def test_layout_keeps_legacy_when_only_legacy_config_exists
62
+ touch_config(File.join(@home, '.bitclust', 'config'))
63
+ config_path, database_prefix, rubydoc_dir = @cmd.send(:layout)
64
+ assert_equal(File.join(@home, '.bitclust', 'config'), config_path.to_s)
65
+ assert_equal(File.join(@home, '.bitclust', 'db'), database_prefix)
66
+ assert_equal(File.join(@home, '.bitclust', 'rubydoc'), rubydoc_dir.to_s)
67
+ end
68
+
69
+ def test_layout_uses_xdg_when_both_exist
70
+ touch_config(File.join(@home, '.bitclust', 'config'))
71
+ touch_config(File.join(@home, '.config', 'bitclust', 'config'))
72
+ config_path, database_prefix, _rubydoc_dir = @cmd.send(:layout)
73
+ assert_equal(File.join(@home, '.config', 'bitclust', 'config'), config_path.to_s)
74
+ assert_equal(File.join(@home, '.local', 'share', 'bitclust', 'db'), database_prefix)
75
+ end
76
+ end
77
+
78
+ # setup --prepare が layout の選択どおりに設定ファイルを書くこと
79
+ # (checkout はネットワークに出るのでスタブする)
80
+ class TestSetupCommandPrepare < Test::Unit::TestCase
81
+ ENV_KEYS = %w[HOME XDG_CONFIG_HOME XDG_DATA_HOME XDG_CACHE_HOME].freeze
82
+
83
+ def setup
84
+ @saved = ENV_KEYS.to_h {|k| [k, ENV[k]] }
85
+ @home = Dir.mktmpdir
86
+ ENV['HOME'] = @home
87
+ (ENV_KEYS - %w[HOME]).each {|k| ENV.delete(k) }
88
+ @cmd = BitClust::Subcommands::SetupCommand.new
89
+ stub(@cmd).checkout {|_dir| nil }
90
+ end
91
+
92
+ def teardown
93
+ @saved.each {|k, v| v ? ENV[k] = v : ENV.delete(k) }
94
+ FileUtils.rm_rf(@home)
95
+ end
96
+
97
+ def test_prepare_writes_config_under_xdg_config_home
98
+ @cmd.send(:prepare)
99
+ config_path = File.join(@home, '.config', 'bitclust', 'config')
100
+ assert_true(File.exist?(config_path))
101
+ config = YAML.load_file(config_path)
102
+ assert_equal(File.join(@home, '.local', 'share', 'bitclust', 'db'), config[:database_prefix])
103
+ end
104
+
105
+ def test_prepare_keeps_legacy_layout_when_legacy_config_exists
106
+ legacy_config = File.join(@home, '.bitclust', 'config')
107
+ FileUtils.mkdir_p(File.dirname(legacy_config))
108
+ versions = @cmd.instance_variable_get(:@versions)
109
+ File.write(legacy_config, {
110
+ :database_prefix => File.join(@home, '.bitclust', 'db'),
111
+ :encoding => 'utf-8',
112
+ :versions => versions,
113
+ :default_version => versions.max,
114
+ :mdtree => File.join(@home, '.bitclust', 'rubydoc', 'manual', 'api'),
115
+ :capi_mdtree => File.join(@home, '.bitclust', 'rubydoc', 'manual', 'capi'),
116
+ :baseurl => 'http://localhost:10080',
117
+ :port => '10080',
118
+ :pid_file => '/tmp/bitclust.pid',
119
+ }.to_yaml)
120
+ @cmd.send(:prepare)
121
+ assert_false(File.exist?(File.join(@home, '.config', 'bitclust', 'config')))
122
+ config = YAML.load_file(legacy_config)
123
+ assert_equal(File.join(@home, '.bitclust', 'db'), config[:database_prefix])
124
+ end
125
+ end
126
+
127
+ # setup --purge が新旧すべての置き場所を消すこと
128
+ class TestSetupCommandPurge < Test::Unit::TestCase
129
+ ENV_KEYS = %w[HOME XDG_CONFIG_HOME XDG_DATA_HOME XDG_CACHE_HOME].freeze
130
+
131
+ def setup
132
+ @saved = ENV_KEYS.to_h {|k| [k, ENV[k]] }
133
+ @home = Dir.mktmpdir
134
+ ENV['HOME'] = @home
135
+ (ENV_KEYS - %w[HOME]).each {|k| ENV.delete(k) }
136
+ @cmd = BitClust::Subcommands::SetupCommand.new
137
+ end
138
+
139
+ def teardown
140
+ @saved.each {|k, v| v ? ENV[k] = v : ENV.delete(k) }
141
+ FileUtils.rm_rf(@home)
142
+ end
143
+
144
+ def test_purge_removes_legacy_xdg_config_data_and_cache_dirs
145
+ dirs = [
146
+ File.join(@home, '.bitclust'),
147
+ File.join(@home, '.config', 'bitclust'),
148
+ File.join(@home, '.local', 'share', 'bitclust'),
149
+ File.join(@home, '.cache', 'bitclust'),
150
+ ]
151
+ dirs.each {|dir| FileUtils.mkdir_p(dir) }
152
+ out, _err = capture_output do
153
+ assert_raise(SystemExit) { @cmd.send(:purge) }
154
+ end
155
+ dirs.each do |dir|
156
+ assert_false(File.exist?(dir), dir)
157
+ assert_include(out, dir)
158
+ end
159
+ end
160
+
161
+ def test_purge_is_fine_when_nothing_exists
162
+ assert_raise(SystemExit) { capture_output { @cmd.send(:purge) } }
163
+ end
164
+ 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
@@ -123,6 +123,98 @@ class TestSyntaxHighlighter < Test::Unit::TestCase
123
123
  assert_equal(expected, highlight(source))
124
124
  end
125
125
 
126
+ sub_test_case "symbol span" do
127
+ test 'ivar symbol' do
128
+ source = "x = :@records\n"
129
+ expected = "x <span class=\"o\">=</span> <span class=\"ss\">:@records</span>\n"
130
+ assert_equal(expected, highlight(source))
131
+ end
132
+
133
+ test 'cvar symbol' do
134
+ source = "x = :@@cv\n"
135
+ expected = "x <span class=\"o\">=</span> <span class=\"ss\">:@@cv</span>\n"
136
+ assert_equal(expected, highlight(source))
137
+ end
138
+
139
+ test 'gvar symbol' do
140
+ source = "x = :$stdout\n"
141
+ expected = "x <span class=\"o\">=</span> <span class=\"ss\">:$stdout</span>\n"
142
+ assert_equal(expected, highlight(source))
143
+ end
144
+
145
+ test 'operator symbols' do
146
+ source = <<~END
147
+ a = :+
148
+ b = :<<
149
+ c = :[]
150
+ d = :[]=
151
+ e = :<=>
152
+ f = :!
153
+ END
154
+ expected = <<~END
155
+ a <span class="o">=</span> <span class="ss">:+</span>
156
+ b <span class="o">=</span> <span class="ss">:&lt;&lt;</span>
157
+ c <span class="o">=</span> <span class="ss">:[]</span>
158
+ d <span class="o">=</span> <span class="ss">:[]=</span>
159
+ e <span class="o">=</span> <span class="ss">:&lt;=&gt;</span>
160
+ f <span class="o">=</span> <span class="ss">:!</span>
161
+ END
162
+ assert_equal(expected, highlight(source))
163
+ end
164
+
165
+ test '%s dynamic symbol literal' do
166
+ source = "x = %s!sym!\n"
167
+ expected = "x <span class=\"o\">=</span> <span class=\"ss\">%s!sym!</span>\n"
168
+ assert_equal(expected, highlight(source))
169
+ end
170
+
171
+ test 'double-quoted dynamic symbol' do
172
+ source = "x = :\"dyn\"\n"
173
+ expected = "x <span class=\"o\">=</span> <span class=\"ss\">:&quot;dyn&quot;</span>\n"
174
+ assert_equal(expected, highlight(source))
175
+ end
176
+
177
+ test 'well-formedness of mixed symbol spans' do
178
+ source = <<~END
179
+ x = :@records
180
+ y = :@@cv
181
+ z = :$stdout
182
+ a = :+
183
+ b = :<<
184
+ c = :[]
185
+ s = %s!sym!
186
+ d = :"dyn"
187
+ END
188
+ result = highlight(source)
189
+ assert_equal(result.scan('<span').size, result.scan('</span>').size)
190
+ end
191
+
192
+ test 'symbol does not leak the stack into later tokens' do
193
+ source = <<~END
194
+ x = :@records
195
+ def foo
196
+ end
197
+ END
198
+ baseline = highlight("def foo\nend\n")
199
+ result = highlight(source)
200
+ assert(
201
+ result.end_with?(baseline),
202
+ "expected result to end with unaffected `def foo/end` rendering:\n" \
203
+ " baseline: #{baseline.inspect}\n" \
204
+ " result: #{result.inspect}"
205
+ )
206
+ end
207
+
208
+ test 'label is not treated as a symbol' do
209
+ source = "h = { a: 1 }\nfoo(:x, k: 2)\n"
210
+ result = highlight(source)
211
+ assert_equal(result.scan('<span').size, result.scan('</span>').size)
212
+ assert_match(%r{<span class="ss">a:</span>}, result)
213
+ assert_match(%r{<span class="ss">k:</span>}, result)
214
+ assert_match(%r{<span class="ss">:x</span>}, result)
215
+ end
216
+ end
217
+
126
218
  test 'one liner module' do
127
219
  source = <<~END
128
220
  module Foo; end
@@ -142,4 +234,36 @@ class TestSyntaxHighlighter < Test::Unit::TestCase
142
234
  END
143
235
  assert_equal(expected, highlight(source))
144
236
  end
237
+
238
+ sub_test_case "heredoc argument and quoted label" do
239
+ def assert_balanced(html)
240
+ assert_equal html.scan(/<span\b/).size, html.scan(%r{</span>}).size, html
241
+ end
242
+
243
+ test 'identifier after a heredoc argument is rendered normally' do
244
+ html = highlight("method(arg1, <<LABEL, arg2)\n x\nLABEL\n")
245
+ assert_include(html, ', arg2<span class="p">)</span>')
246
+ assert_balanced(html)
247
+ end
248
+
249
+ test 'block argument after a heredoc argument' do
250
+ html = highlight("ERB.new(<<~'END_PRODUCT', eoutvar: \"@product\").result b\n x\nEND_PRODUCT\n")
251
+ assert_balanced(html)
252
+ assert_not_include(html, '<span class="no">b')
253
+ end
254
+
255
+ test 'quoted symbol keys close the string span' do
256
+ html = highlight(%q({ "a":"A", 'b':"B" }) + "\n")
257
+ assert_balanced(html)
258
+ assert_include(html, %q(<span class="s1">'b':</span>))
259
+ assert_include(html, %q(<span class="s2">a</span><span class="s2">&quot;:</span>))
260
+ end
261
+
262
+ test 'quoted symbol key does not leak the stack' do
263
+ with = highlight(%q({ "a":"A" }) + "\ndef foo\nend\n")
264
+ without = highlight("def foo\nend\n")
265
+ assert_include(with, without.lines[0].chomp)
266
+ assert_balanced(with)
267
+ end
268
+ end
145
269
  end