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.
Files changed (152) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +3 -0
  3. data/README.md +65 -0
  4. data/Rakefile +32 -0
  5. data/data/bitclust/catalog/ja_JP.UTF-8 +19 -1
  6. data/data/bitclust/searchpage/index.html +90 -0
  7. data/data/bitclust/template/class +69 -8
  8. data/data/bitclust/template/function +2 -2
  9. data/data/bitclust/template/layout +11 -3
  10. data/data/bitclust/template/method +2 -2
  11. data/data/bitclust/template/search +1 -1
  12. data/data/bitclust/template.epub/class +22 -1
  13. data/data/bitclust/template.epub/function +2 -2
  14. data/data/bitclust/template.epub/layout +1 -3
  15. data/data/bitclust/template.epub/method +2 -2
  16. data/data/bitclust/template.lillia/class +26 -5
  17. data/data/bitclust/template.lillia/layout +1 -3
  18. data/data/bitclust/template.lillia/method +2 -2
  19. data/data/bitclust/template.offline/class +48 -4
  20. data/data/bitclust/template.offline/function +2 -2
  21. data/data/bitclust/template.offline/layout +27 -3
  22. data/data/bitclust/template.offline/method +2 -2
  23. data/lib/bitclust/app.rb +16 -17
  24. data/lib/bitclust/capi_converter.rb +20 -0
  25. data/lib/bitclust/classentry.rb +67 -17
  26. data/lib/bitclust/compat.rb +2 -2
  27. data/lib/bitclust/completion.rb +58 -27
  28. data/lib/bitclust/crossrubyutils.rb +8 -5
  29. data/lib/bitclust/database.rb +28 -8
  30. data/lib/bitclust/doc_converter.rb +63 -0
  31. data/lib/bitclust/docentry.rb +1 -1
  32. data/lib/bitclust/entity_splitter.rb +221 -0
  33. data/lib/bitclust/entry.rb +47 -7
  34. data/lib/bitclust/functiondatabase.rb +13 -1
  35. data/lib/bitclust/functionentry.rb +5 -1
  36. data/lib/bitclust/functionreferenceparser.rb +4 -4
  37. data/lib/bitclust/generators/epub.rb +10 -3
  38. data/lib/bitclust/htmlutils.rb +1 -1
  39. data/lib/bitclust/include_graph.rb +375 -0
  40. data/lib/bitclust/include_pruner.rb +144 -0
  41. data/lib/bitclust/interface.rb +1 -1
  42. data/lib/bitclust/libraryentry.rb +8 -7
  43. data/lib/bitclust/lineinput.rb +7 -5
  44. data/lib/bitclust/link_checker.rb +179 -0
  45. data/lib/bitclust/markdown_bridge.rb +186 -0
  46. data/lib/bitclust/markdown_orchestrator.rb +267 -0
  47. data/lib/bitclust/markdown_to_rrd.rb +824 -0
  48. data/lib/bitclust/markdown_tree.rb +217 -0
  49. data/lib/bitclust/mdcompiler.rb +883 -0
  50. data/lib/bitclust/mdparser.rb +347 -0
  51. data/lib/bitclust/messagecatalog.rb +3 -3
  52. data/lib/bitclust/method_since_calculator.rb +117 -0
  53. data/lib/bitclust/methoddatabase.rb +124 -14
  54. data/lib/bitclust/methodentry.rb +125 -7
  55. data/lib/bitclust/methodid.rb +12 -11
  56. data/lib/bitclust/methodsignature.rb +10 -4
  57. data/lib/bitclust/nameutils.rb +41 -22
  58. data/lib/bitclust/preprocessor.rb +66 -29
  59. data/lib/bitclust/rdcompiler.rb +195 -95
  60. data/lib/bitclust/refsdatabase.rb +23 -8
  61. data/lib/bitclust/reloadable_request_handler.rb +72 -0
  62. data/lib/bitclust/requesthandler.rb +21 -16
  63. data/lib/bitclust/ridatabase.rb +10 -4
  64. data/lib/bitclust/rrd_to_markdown.rb +925 -0
  65. data/lib/bitclust/rrdparser.rb +196 -30
  66. data/lib/bitclust/runner.rb +17 -2
  67. data/lib/bitclust/screen.rb +80 -11
  68. data/lib/bitclust/search_index_generator.rb +288 -0
  69. data/lib/bitclust/searcher.rb +69 -20
  70. data/lib/bitclust/simplesearcher.rb +24 -12
  71. data/lib/bitclust/subcommand.rb +7 -1
  72. data/lib/bitclust/subcommands/ancestors_command.rb +9 -2
  73. data/lib/bitclust/subcommands/checklink_command.rb +54 -0
  74. data/lib/bitclust/subcommands/chm_command.rb +15 -7
  75. data/lib/bitclust/subcommands/classes_command.rb +5 -0
  76. data/lib/bitclust/subcommands/extract_command.rb +5 -0
  77. data/lib/bitclust/subcommands/htmlfile_command.rb +21 -7
  78. data/lib/bitclust/subcommands/init_command.rb +4 -1
  79. data/lib/bitclust/subcommands/list_command.rb +12 -4
  80. data/lib/bitclust/subcommands/lookup_command.rb +28 -9
  81. data/lib/bitclust/subcommands/methods_command.rb +10 -2
  82. data/lib/bitclust/subcommands/methodsince_command.rb +80 -0
  83. data/lib/bitclust/subcommands/preproc_command.rb +5 -0
  84. data/lib/bitclust/subcommands/property_command.rb +2 -1
  85. data/lib/bitclust/subcommands/searchpage_command.rb +102 -0
  86. data/lib/bitclust/subcommands/server_command.rb +10 -8
  87. data/lib/bitclust/subcommands/setup_command.rb +24 -10
  88. data/lib/bitclust/subcommands/statichtml_command.rb +129 -6
  89. data/lib/bitclust/subcommands/update_command.rb +110 -2
  90. data/lib/bitclust/syntax_highlighter.rb +48 -4
  91. data/lib/bitclust/textutils.rb +3 -3
  92. data/lib/bitclust/version.rb +1 -1
  93. data/lib/bitclust/version_badges.rb +90 -0
  94. data/lib/bitclust/whole_file_gate.rb +137 -0
  95. data/test/test_app.rb +269 -0
  96. data/test/test_bitclust.rb +2 -0
  97. data/test/test_capi_converter.rb +91 -0
  98. data/test/test_class_screen.rb +413 -0
  99. data/test/test_copy_doc_md.rb +53 -0
  100. data/test/test_doc_converter.rb +78 -0
  101. data/test/test_entity_splitter.rb +231 -0
  102. data/test/test_entry.rb +207 -2
  103. data/test/test_eol_warning.rb +43 -0
  104. data/test/test_functionreferenceparser.rb +11 -0
  105. data/test/test_include_graph.rb +640 -0
  106. data/test/test_include_pruner.rb +133 -0
  107. data/test/test_link_checker.rb +114 -0
  108. data/test/test_lookup_command.rb +135 -0
  109. data/test/test_markdown_bridge.rb +217 -0
  110. data/test/test_markdown_orchestrator.rb +478 -0
  111. data/test/test_markdown_to_rrd.rb +757 -0
  112. data/test/test_markdown_tree.rb +369 -0
  113. data/test/test_mdcompiler.rb +1276 -0
  114. data/test/test_mdparser.rb +538 -0
  115. data/test/test_method_screen.rb +166 -0
  116. data/test/test_method_since_calculator.rb +290 -0
  117. data/test/test_methoddatabase.rb +16 -0
  118. data/test/test_methodentry.rb +328 -0
  119. data/test/test_methodsignature.rb +3 -1
  120. data/test/test_methodsince_command.rb +168 -0
  121. data/test/test_nameutils.rb +22 -0
  122. data/test/test_preprocessor.rb +215 -0
  123. data/test/test_rdcompiler.rb +401 -30
  124. data/test/test_refsdatabase.rb +32 -0
  125. data/test/test_rrd_to_markdown.rb +968 -0
  126. data/test/test_rrdparser.rb +295 -2
  127. data/test/test_run_ruby_wasm.rb +54 -0
  128. data/test/test_run_worker_prelude.rb +83 -0
  129. data/test/test_runner.rb +51 -0
  130. data/test/test_search_index_generator.rb +513 -0
  131. data/test/test_search_screen.rb +48 -0
  132. data/test/test_searcher.rb +132 -0
  133. data/test/test_searchpage_command.rb +167 -0
  134. data/test/test_simplesearcher.rb +28 -0
  135. data/test/test_statichtml_command.rb +204 -0
  136. data/test/test_syntax_highlighter.rb +60 -0
  137. data/test/test_whole_file_gate.rb +175 -0
  138. data/theme/default/js/NOTICE +42 -0
  139. data/theme/default/js/run-worker.js +111 -0
  140. data/theme/default/js/run.js +350 -0
  141. data/theme/default/js/search_controller.js +134 -0
  142. data/theme/default/js/search_init.js +176 -0
  143. data/theme/default/js/search_navigation.js +110 -0
  144. data/theme/default/js/search_page.js +202 -0
  145. data/theme/default/js/search_ranker.js +244 -0
  146. data/theme/default/script.js +91 -18
  147. data/theme/default/search.css +149 -0
  148. data/theme/default/style.css +193 -21
  149. data/theme/default/syntax-highlight.css +8 -0
  150. data/theme/lillia/style.css +45 -1
  151. metadata +161 -20
  152. data/README +0 -32
@@ -0,0 +1,167 @@
1
+ # frozen_string_literal: true
2
+ require 'test/unit'
3
+ require 'json'
4
+ require 'fileutils'
5
+ require 'bitclust'
6
+ require 'bitclust/methoddatabase'
7
+ require 'bitclust/subcommands/searchpage_command'
8
+
9
+ # searchpage サブコマンド: 複数バージョンの DB から /ja/search/ 置換用の
10
+ # 静的検索ページ一式(index.html + 統合 search_data.js + JS/CSS アセット)を
11
+ # 生成する。rurema-search(サーバ常駐の全文検索)のリタイア先。
12
+ #
13
+ # テストリスト:
14
+ # [x] 統合 index: 全版共通エントリは versions に全版、片方だけのものはその版のみ
15
+ # [x] index.html に search_versions(昇順)と検索 UI の要素が埋まる
16
+ # [x] vendored JS + NOTICE + search_page.js + search.css がコピーされる
17
+ # [x] ページ専用 UI なので search_init.js(ページ内ボックス用)は含めない
18
+ # [x] DB を持たない引数なし呼び出しはエラー
19
+ # [x] グローバル --database 不要(needs_database? が false)
20
+ class TestSearchpageCommand < Test::Unit::TestCase
21
+ def setup
22
+ @tmpdir = "searchpage_test_tmp"
23
+ FileUtils.rm_rf(@tmpdir)
24
+ @db34 = build_db('3.4', <<~'RD')
25
+ description
26
+
27
+ = class Foo < Object
28
+ == Instance Methods
29
+ --- foo
30
+ RD
31
+ @db41 = build_db('4.1', <<~'RD')
32
+ description
33
+
34
+ = class Foo < Object
35
+ == Instance Methods
36
+ --- foo
37
+ --- bar
38
+ RD
39
+ @out = "#{@tmpdir}/out"
40
+ end
41
+
42
+ def teardown
43
+ FileUtils.rm_rf(@tmpdir)
44
+ end
45
+
46
+ def build_db(version, builtin_rd)
47
+ root = "#{@tmpdir}/tree-#{version}/refm/api/src"
48
+ FileUtils.mkdir_p("#{root}/_builtin")
49
+ File.write("#{root}/LIBRARIES", "_builtin\n")
50
+ File.write("#{root}/_builtin.rd", builtin_rd)
51
+ prefix = "#{@tmpdir}/db-#{version}"
52
+ db = BitClust::MethodDatabase.new(prefix)
53
+ db.init
54
+ db.transaction do
55
+ db.propset('version', version)
56
+ db.propset('encoding', 'utf-8')
57
+ end
58
+ db.transaction do
59
+ db.update_by_stdlibtree(root)
60
+ end
61
+ prefix
62
+ end
63
+
64
+ def run_command(argv)
65
+ cmd = BitClust::Subcommands::SearchpageCommand.new
66
+ cmd.parse(argv)
67
+ cmd.exec(argv, { prefix: nil, capi: false })
68
+ cmd
69
+ end
70
+
71
+ def test_needs_no_global_database_option
72
+ cmd = BitClust::Subcommands::SearchpageCommand.new
73
+ assert_false cmd.needs_database?
74
+ end
75
+
76
+ def test_merged_index_versions
77
+ run_command(["--outputdir=#{@out}", @db41, @db34])
78
+ js = File.read("#{@out}/js/search_data.js")
79
+ assert_match(/\Avar search_data = \{/, js)
80
+ data = JSON.parse(js.sub(/\Avar search_data = /, '').sub(/;\z/, ''))
81
+ foo = data['index'].find { |e| e['full_name'] == 'Foo#foo' }
82
+ bar = data['index'].find { |e| e['full_name'] == 'Foo#bar' }
83
+ klass = data['index'].find { |e| e['full_name'] == 'Foo' }
84
+ assert_equal %w[3.4 4.1], foo['versions']
85
+ assert_equal %w[4.1], bar['versions']
86
+ assert_equal %w[3.4 4.1], klass['versions']
87
+ end
88
+
89
+ def test_merged_index_shows_module_functions_as_question_dot_only
90
+ # bitclust#279 コメント対応: 版によって表示が ".#"(4.0 より前)と
91
+ # "?."(4.0 以降)に割れる module function は、統合ページでは "?."
92
+ # 表記の1エントリに合流させる(両表記が併記されるとわかりにくい)
93
+ db33 = build_db('3.3', <<~'RD')
94
+ description
95
+
96
+ = module Kernel
97
+ == Module Functions
98
+ --- at_exit{ ... } -> Proc
99
+ aaa
100
+ RD
101
+ db40 = build_db('4.0', <<~'RD')
102
+ description
103
+
104
+ = module Kernel
105
+ == Module Functions
106
+ --- at_exit{ ... } -> Proc
107
+ aaa
108
+ RD
109
+ run_command(["--outputdir=#{@out}", db40, db33])
110
+ js = File.read("#{@out}/js/search_data.js")
111
+ data = JSON.parse(js.sub(/\Avar search_data = /, '').sub(/;\z/, ''))
112
+ entries = data['index'].select { |e| e['name'] == 'at_exit' }
113
+ assert_equal ['Kernel?.at_exit'], entries.map { |e| e['full_name'] }
114
+ assert_equal %w[3.3 4.0], entries[0]['versions']
115
+ assert_not_match(/Kernel\.#/, js)
116
+ end
117
+
118
+ def test_index_html_embeds_versions_and_ui
119
+ run_command(["--outputdir=#{@out}", @db41, @db34])
120
+ html = File.read("#{@out}/index.html")
121
+ assert_match(/var search_versions = \["3\.4","4\.1"\];/, html)
122
+ assert_match(/var search_version_base = "\.\.\/";/, html)
123
+ assert_match(/id="search-field"/, html)
124
+ assert_match(/id="search-results"/, html)
125
+ assert_match(%r{js/search_data\.js}, html)
126
+ assert_match(%r{js/search_page\.js}, html)
127
+ end
128
+
129
+ def test_assets_are_copied
130
+ run_command(["--outputdir=#{@out}", @db41, @db34])
131
+ %w[search_navigation.js search_ranker.js search_controller.js
132
+ search_page.js NOTICE].each do |f|
133
+ assert File.exist?("#{@out}/js/#{f}"), "js/#{f} not copied"
134
+ end
135
+ assert File.exist?("#{@out}/search.css")
136
+ # ページ内ドロップダウン用の search_init.js はこのページでは使わない
137
+ assert_false File.exist?("#{@out}/js/search_init.js")
138
+ end
139
+
140
+ def test_no_database_arguments_is_an_error
141
+ cmd = BitClust::Subcommands::SearchpageCommand.new
142
+ argv = ["--outputdir=#{@out}"]
143
+ cmd.parse(argv)
144
+ assert_raise(SystemExit) do
145
+ capture_stderr { cmd.exec(argv, { prefix: nil, capi: false }) }
146
+ end
147
+ end
148
+
149
+ def test_missing_outputdir_is_an_error
150
+ cmd = BitClust::Subcommands::SearchpageCommand.new
151
+ argv = [@db34]
152
+ cmd.parse(argv)
153
+ assert_raise(SystemExit) do
154
+ capture_stderr { cmd.exec(argv, { prefix: nil, capi: false }) }
155
+ end
156
+ end
157
+
158
+ private
159
+
160
+ def capture_stderr
161
+ orig = $stderr
162
+ $stderr = StringIO.new
163
+ yield
164
+ ensure
165
+ $stderr = orig
166
+ end
167
+ end
@@ -15,6 +15,12 @@ class TestSearcher < Test::Unit::TestCase
15
15
  == Class Methods
16
16
  --- bar
17
17
  = reopen Kernel
18
+ == Module Functions
19
+ --- open
20
+
21
+ == Instance Methods
22
+ --- puts
23
+
18
24
  == Special Variables
19
25
  --- $spespe
20
26
 
@@ -45,4 +51,26 @@ HERE
45
51
  assert_equal([], search_pattern(@db, " "), 'space')
46
52
  assert_equal([], search_pattern(@db, ""), 'blank')
47
53
  end
54
+
55
+ # bitclust#250 follow-up: the dynamic server's /search accepts free-text
56
+ # queries, and a module function may be typed in either notation --
57
+ # "Kernel.#open" (bitclust's own internal spec-string form, still what
58
+ # docs for Ruby < 4.0 display) or "Kernel?.open" (what docs for Ruby >=
59
+ # 4.0 display since #277). Both must resolve to the same method,
60
+ # regardless of which notation the *query* uses -- the query parser has
61
+ # no idea (and shouldn't need to know) which doc version the user is
62
+ # thinking of.
63
+ def test_module_function_query_notation
64
+ [['Kernel?.open', 'open'], # new (>=4.0 display) notation
65
+ ['Kernel.#open', 'open'], # existing (bitclust-internal) notation: regression check
66
+ ['?.open', 'open'], # bare module-function marker, no class name
67
+ ['Kernel#puts', 'puts'], # instance method: unaffected by the "?." change
68
+ ].each{|q, expected|
69
+ ret = search_pattern(@db, q)
70
+ assert_not_equal([], ret, q)
71
+ assert_equal(expected, ret[0].name, q)
72
+ }
73
+ # Singleton-method dot notation ('Hoge.h' => 'hoge' in test_simple_search
74
+ # above) is also unaffected; kept there rather than duplicated here.
75
+ end
48
76
  end
@@ -0,0 +1,204 @@
1
+ require 'test/unit'
2
+ require 'tmpdir'
3
+ require 'stringio'
4
+ require 'bitclust'
5
+ require 'bitclust/subcommands/statichtml_command'
6
+
7
+ class TestStatichtmlURLMapperEx < Test::Unit::TestCase
8
+ def setup
9
+ @urlmapper = BitClust::Subcommands::StatichtmlCommand::URLMapperEx.new(
10
+ :suffix => 'html',
11
+ :edit_base_url => 'https://github.com/rurema/doctree/edit/master',
12
+ )
13
+ end
14
+
15
+ # The edit URL must not contain a line number, so that shifting source lines
16
+ # does not churn the generated HTML diff.
17
+ def test_edit_url_has_no_line_number
18
+ location = BitClust::Location.new('refm/api/src/_builtin/Array', 42)
19
+ assert_equal('https://github.com/rurema/doctree/edit/master/refm/api/src/_builtin/Array',
20
+ @urlmapper.edit_url(location))
21
+ end
22
+
23
+ # A location restored from the database has no line number (nil); the edit URL
24
+ # must still be a clean file link without a trailing "#L".
25
+ def test_edit_url_without_line_in_location
26
+ location = BitClust::Location.new('refm/api/src/_builtin/Array', nil)
27
+ assert_equal('https://github.com/rurema/doctree/edit/master/refm/api/src/_builtin/Array',
28
+ @urlmapper.edit_url(location))
29
+ end
30
+ end
31
+
32
+ class TestStatichtmlRunRubyWasm < Test::Unit::TestCase
33
+ def build_command(themedir, outputdir)
34
+ cmd = BitClust::Subcommands::StatichtmlCommand.new
35
+ cmd.instance_variable_set(:@manager_config, { :themedir => Pathname.new(themedir) })
36
+ cmd.instance_variable_set(:@outputdir, Pathname.new(outputdir))
37
+ cmd.instance_variable_set(:@verbose, false)
38
+ cmd
39
+ end
40
+
41
+ def test_option_defaults_to_nil
42
+ cmd = BitClust::Subcommands::StatichtmlCommand.new
43
+ assert_nil(cmd.instance_variable_get(:@run_ruby_wasm))
44
+ end
45
+
46
+ def test_option_is_parsed
47
+ url = 'https://cdn.jsdelivr.net/npm/@ruby/3.4-wasm-wasi@2.9.3-2.9.4/dist/ruby+stdlib.wasm'
48
+ cmd = BitClust::Subcommands::StatichtmlCommand.new
49
+ cmd.parse(["--run-ruby-wasm=#{url}"])
50
+ assert_equal(url, cmd.instance_variable_get(:@run_ruby_wasm))
51
+ end
52
+
53
+ def test_run_js_is_copied
54
+ Dir.mktmpdir do |dir|
55
+ themedir = File.join(dir, 'theme')
56
+ outputdir = File.join(dir, 'out')
57
+ FileUtils.mkdir_p(File.join(themedir, 'js'))
58
+ FileUtils.mkdir_p(outputdir)
59
+ File.write(File.join(themedir, 'js', 'run.js'), "// run\n")
60
+ File.write(File.join(themedir, 'js', 'run-worker.js'), "// worker\n")
61
+ cmd = build_command(themedir, outputdir)
62
+ cmd.send(:copy_run_ruby_wasm_script)
63
+ assert_true(File.file?(File.join(outputdir, 'js', 'run.js')))
64
+ assert_true(File.file?(File.join(outputdir, 'js', 'run-worker.js')))
65
+ end
66
+ end
67
+
68
+ def test_theme_without_run_js_is_tolerated
69
+ Dir.mktmpdir do |dir|
70
+ themedir = File.join(dir, 'theme')
71
+ outputdir = File.join(dir, 'out')
72
+ FileUtils.mkdir_p(themedir)
73
+ FileUtils.mkdir_p(outputdir)
74
+ cmd = build_command(themedir, outputdir)
75
+ orig_stderr, $stderr = $stderr, StringIO.new
76
+ begin
77
+ assert_nothing_raised { cmd.send(:copy_run_ruby_wasm_script) }
78
+ assert_match(/run\.js not found/, $stderr.string)
79
+ assert_match(/run-worker\.js not found/, $stderr.string)
80
+ ensure
81
+ $stderr = orig_stderr
82
+ end
83
+ assert_false(File.exist?(File.join(outputdir, 'js', 'run.js')))
84
+ assert_false(File.exist?(File.join(outputdir, 'js', 'run-worker.js')))
85
+ end
86
+ end
87
+
88
+ # A themedir with run.js but not yet upgraded with run-worker.js (or vice
89
+ # versa) should still copy whichever file it does have, rather than
90
+ # aborting or silently skipping both.
91
+ def test_partial_theme_copies_what_it_has
92
+ Dir.mktmpdir do |dir|
93
+ themedir = File.join(dir, 'theme')
94
+ outputdir = File.join(dir, 'out')
95
+ FileUtils.mkdir_p(File.join(themedir, 'js'))
96
+ FileUtils.mkdir_p(outputdir)
97
+ File.write(File.join(themedir, 'js', 'run.js'), "// run\n")
98
+ cmd = build_command(themedir, outputdir)
99
+ orig_stderr, $stderr = $stderr, StringIO.new
100
+ begin
101
+ cmd.send(:copy_run_ruby_wasm_script)
102
+ assert_match(/run-worker\.js not found/, $stderr.string)
103
+ ensure
104
+ $stderr = orig_stderr
105
+ end
106
+ assert_true(File.file?(File.join(outputdir, 'js', 'run.js')))
107
+ assert_false(File.exist?(File.join(outputdir, 'js', 'run-worker.js')))
108
+ end
109
+ end
110
+ end
111
+
112
+ class TestStatichtmlSitemap < Test::Unit::TestCase
113
+ def build_command(outputdir)
114
+ cmd = BitClust::Subcommands::StatichtmlCommand.new
115
+ cmd.instance_variable_set(:@outputdir, Pathname.new(outputdir))
116
+ cmd.instance_variable_set(:@verbose, false)
117
+ cmd
118
+ end
119
+
120
+ def test_option_defaults_to_nil
121
+ cmd = BitClust::Subcommands::StatichtmlCommand.new
122
+ assert_nil(cmd.instance_variable_get(:@sitemap_baseurl))
123
+ end
124
+
125
+ def test_option_is_parsed
126
+ cmd = BitClust::Subcommands::StatichtmlCommand.new
127
+ cmd.parse(['--sitemap-baseurl=https://docs.ruby-lang.org/ja/3.4/'])
128
+ assert_equal('https://docs.ruby-lang.org/ja/3.4/',
129
+ cmd.instance_variable_get(:@sitemap_baseurl))
130
+ end
131
+
132
+ def test_record_sitemap_path_is_noop_without_baseurl
133
+ Dir.mktmpdir do |dir|
134
+ cmd = build_command(dir)
135
+ cmd.send(:record_sitemap_path, Pathname.new(dir) + 'class' + 'String.html')
136
+ assert_empty(cmd.instance_variable_get(:@sitemap_paths))
137
+ end
138
+ end
139
+
140
+ def test_record_sitemap_path_keeps_relative_path
141
+ Dir.mktmpdir do |dir|
142
+ cmd = build_command(dir)
143
+ cmd.instance_variable_set(:@sitemap_baseurl, 'https://docs.ruby-lang.org/ja/3.4/')
144
+ cmd.send(:record_sitemap_path, Pathname.new(dir) + 'class' + 'String.html')
145
+ assert_equal(%w[class/String.html], cmd.instance_variable_get(:@sitemap_paths))
146
+ end
147
+ end
148
+
149
+ def test_create_sitemap_writes_loc_urls
150
+ Dir.mktmpdir do |dir|
151
+ cmd = build_command(dir)
152
+ cmd.instance_variable_set(:@sitemap_paths,
153
+ ['class/String.html', 'method/String/i/upcase.html'])
154
+ cmd.send(:create_sitemap, Pathname.new(dir), 'https://docs.ruby-lang.org/ja/3.4/')
155
+ xml = File.read(File.join(dir, 'sitemap.xml'))
156
+ assert_match(%r{\A<\?xml version="1\.0" encoding="UTF-8"\?>\n}, xml)
157
+ assert_match(%r{<urlset xmlns="http://www\.sitemaps\.org/schemas/sitemap/0\.9">}, xml)
158
+ assert_match(%r{<url><loc>https://docs\.ruby-lang\.org/ja/3\.4/class/String\.html</loc></url>}, xml)
159
+ assert_match(%r{<url><loc>https://docs\.ruby-lang\.org/ja/3\.4/method/String/i/upcase\.html</loc></url>}, xml)
160
+ assert_match(%r{</urlset>\n\z}, xml)
161
+ end
162
+ end
163
+
164
+ def test_create_sitemap_adds_trailing_slash_to_baseurl
165
+ Dir.mktmpdir do |dir|
166
+ cmd = build_command(dir)
167
+ cmd.instance_variable_set(:@sitemap_paths, ['class/String.html'])
168
+ cmd.send(:create_sitemap, Pathname.new(dir), 'https://docs.ruby-lang.org/ja/3.4')
169
+ xml = File.read(File.join(dir, 'sitemap.xml'))
170
+ assert_match(%r{<loc>https://docs\.ruby-lang\.org/ja/3\.4/class/String\.html</loc>}, xml)
171
+ end
172
+ end
173
+
174
+ def test_create_sitemap_escapes_special_characters
175
+ Dir.mktmpdir do |dir|
176
+ cmd = build_command(dir)
177
+ cmd.instance_variable_set(:@sitemap_paths, ['method/String/i/=3d=3d.html'])
178
+ cmd.send(:create_sitemap, Pathname.new(dir), 'https://example.com/a&b/')
179
+ xml = File.read(File.join(dir, 'sitemap.xml'))
180
+ assert_match(%r{<loc>https://example\.com/a&amp;b/method/String/i/=3d=3d\.html</loc>}, xml)
181
+ assert_not_match(/a&b/, xml)
182
+ end
183
+ end
184
+
185
+ def test_create_sitemap_warns_and_truncates_beyond_limit
186
+ Dir.mktmpdir do |dir|
187
+ cmd = build_command(dir)
188
+ limit = BitClust::Subcommands::StatichtmlCommand::MAX_SITEMAP_URLS
189
+ paths = (1..(limit + 1)).map {|i| "class/C#{i}.html" }
190
+ cmd.instance_variable_set(:@sitemap_paths, paths)
191
+ orig_stderr, $stderr = $stderr, StringIO.new
192
+ begin
193
+ cmd.send(:create_sitemap, Pathname.new(dir), 'https://example.com/')
194
+ assert_match(/warning:.*#{limit}/, $stderr.string)
195
+ ensure
196
+ $stderr = orig_stderr
197
+ end
198
+ xml = File.read(File.join(dir, 'sitemap.xml'))
199
+ assert_equal(limit, xml.scan('<url>').size)
200
+ assert_match(%r{<loc>https://example\.com/class/C1\.html</loc>}, xml)
201
+ assert_not_match(/C#{limit + 1}\.html/, xml)
202
+ end
203
+ end
204
+ end
@@ -82,4 +82,64 @@ class TestSyntaxHighlighter < Test::Unit::TestCase
82
82
  END
83
83
  assert_equal(expected, highlight(source))
84
84
  end
85
+
86
+ test 'symbol const' do
87
+ source = <<~END
88
+ class Foo
89
+ def to_hash
90
+ {:Bar => 'a',
91
+ :Baz => 'b'
92
+ }
93
+ end
94
+ end
95
+ END
96
+ expected = <<~END
97
+ <span class="k">class</span> <span class="nn"></span><span class="o"></span><span class="nc">Foo</span>
98
+ <span class="k">def</span> <span class="nf">to_hash</span>
99
+ <span class="p">{</span><span class="ss">:Bar</span> <span class="o">=&gt;</span> <span class="s1">'a'</span>,
100
+ <span class="ss">:Baz</span> <span class="o">=&gt;</span> <span class="s1">'b'</span>
101
+ <span class="p">}</span>
102
+ <span class="k">end</span>
103
+ <span class="k">end</span>
104
+ END
105
+ assert_equal(expected, highlight(source))
106
+ end
107
+
108
+ test 'symbol list' do
109
+ source = <<~END
110
+ %i[hellow world].each {|s| p s }
111
+ END
112
+ expected = <<~END
113
+ <span class="ss">%i[hellow world]</span><span class="p">.</span><span class="nf">each</span> <span class="p">{</span><span class="o">|</span>s<span class="o">|</span> <span class="nb">p</span> s <span class="p">}</span>
114
+ END
115
+ assert_equal(expected, highlight(source))
116
+
117
+ source = <<~END
118
+ %I[hellow world].each {|s| p s }
119
+ END
120
+ expected = <<~END
121
+ <span class="ss">%I[hellow world]</span><span class="p">.</span><span class="nf">each</span> <span class="p">{</span><span class="o">|</span>s<span class="o">|</span> <span class="nb">p</span> s <span class="p">}</span>
122
+ END
123
+ assert_equal(expected, highlight(source))
124
+ end
125
+
126
+ test 'one liner module' do
127
+ source = <<~END
128
+ module Foo; end
129
+ END
130
+ expected = <<~END
131
+ <span class="k">module</span> <span class="nn">Foo</span>; <span class="k">end</span>
132
+ END
133
+ assert_equal(expected, highlight(source))
134
+ end
135
+
136
+ test 'one liner class' do
137
+ source = <<~END
138
+ class Foo; end
139
+ END
140
+ expected = <<~END
141
+ <span class="k">class</span> <span class="nn"></span><span class="o"></span><span class="nc">Foo</span>; <span class="k">end</span>
142
+ END
143
+ assert_equal(expected, highlight(source))
144
+ end
85
145
  end
@@ -0,0 +1,175 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'test/unit'
4
+
5
+ require 'bitclust/whole_file_gate'
6
+ require 'bitclust/include_graph'
7
+
8
+ # WholeFileGate: ファイル全体を包む単一の版ゲート(#@else 無し)を検出し、
9
+ # スコープの下で不要(常に真)または front matter の since/until で表現できる場合に
10
+ # ゲート行を外す(O4)。表現できないもの(#@else 付き、スコープ外)は据え置き。
11
+ #
12
+ # テストリスト:
13
+ # [x] ゲートで始まらないファイル → detect nil
14
+ # [x] #@end の後に内容が続く(部分ゲート)→ nil
15
+ # [x] ファイル全体を包む #@since/#@until/#@if → Condition
16
+ # [x] トップレベルに #@else がある(fiddle.rd)→ nil
17
+ # [x] 内部にネストしたゲート・#@samplecode があっても正しく対応を取る(rss.rd)
18
+ # [x] 先頭の空行はスキップして判定、#@end の後の空行は許容
19
+ # [x] unwrap: 開き/閉じ行を除去し、先頭に残る空行は削る(doctree 正規化と整合)
20
+ # [x] unwrap_for_scope: スコープ内で常に真の since → [unwrapped, {}]
21
+ # [x] unwrap_for_scope: in-scope の until(fiber/set)→ [unwrapped, {until: v}]
22
+ # [x] unwrap_for_scope: in-scope の since → [unwrapped, {since: v}]
23
+ # [x] unwrap_for_scope: スコープ外(until 2.7.0 等)→ nil(据え置き)
24
+ # [x] unwrap_for_scope: 常に真と証明できる #@if(version >= "X", X <= 下限)→ [unwrapped, {}]
25
+ # [x] unwrap_for_scope: その他の #@if 条件 → nil(据え置き)
26
+ class TestWholeFileGate < Test::Unit::TestCase
27
+ def scope
28
+ BitClust::IncludeGraph::Scope.new("3.0", "4.2")
29
+ end
30
+
31
+ def detect(src)
32
+ BitClust::WholeFileGate.detect(src)
33
+ end
34
+
35
+ def unwrap_for_scope(src)
36
+ BitClust::WholeFileGate.unwrap_for_scope(src, scope)
37
+ end
38
+
39
+ def test_detect_returns_nil_without_gate
40
+ assert_nil detect("category Math\n\n本文。\n")
41
+ end
42
+
43
+ def test_detect_returns_nil_for_partial_gate
44
+ # cgi/util.rd: 先頭ゲートがファイル途中で閉じる
45
+ assert_nil detect("\#@since 1.9.1\n本文。\n\#@end\n続き。\n")
46
+ end
47
+
48
+ def test_detect_whole_file_since
49
+ cond = detect("\#@since 1.9.1\n本文。\n\#@end\n")
50
+ assert_equal :since, cond.kind
51
+ assert_equal "1.9.1", cond.version
52
+ end
53
+
54
+ def test_detect_whole_file_if
55
+ cond = detect("\#@if (version >= \"1.8.2\")\n本文。\n\#@end\n")
56
+ assert_equal :if, cond.kind
57
+ assert_equal '(version >= "1.8.2")', cond.version
58
+ end
59
+
60
+ def test_detect_returns_nil_with_top_level_else
61
+ # fiddle.rd: #@until 2.0.0 ... #@else ... #@end
62
+ assert_nil detect("\#@until 2.0.0\n古い。\n\#@else\n新しい。\n\#@end\n")
63
+ end
64
+
65
+ def test_detect_handles_nested_gates_and_samplecode
66
+ src = "\#@since 1.9.1\n\#@until 3.0\n本文。\n\#@end\n\#@samplecode\ncode\n\#@end\n\#@end\n"
67
+ cond = detect(src)
68
+ assert_equal :since, cond.kind
69
+ end
70
+
71
+ def test_detect_skips_leading_and_trailing_blanks
72
+ cond = detect("\n\#@since 1.9.1\n本文。\n\#@end\n\n")
73
+ assert_equal :since, cond.kind
74
+ end
75
+
76
+ def test_unwrap_for_scope_vacuous_since
77
+ # cmath.rd / _builtin/Encoding: #@since 1.9.1(+直後の空行)で全体が包まれる
78
+ src = "\#@since 1.9.1\n\ncategory Math\n\n本文。\n\#@end\n"
79
+ unwrapped, gate = unwrap_for_scope(src)
80
+ assert_equal "category Math\n\n本文。\n", unwrapped
81
+ assert_equal({}, gate)
82
+ end
83
+
84
+ def test_unwrap_for_scope_in_scope_until
85
+ # fiber.rd: #@until 3.1
86
+ src = "\#@until 3.1\n本文。\n\n\#@end\n"
87
+ unwrapped, gate = unwrap_for_scope(src)
88
+ assert_equal "本文。\n\n", unwrapped
89
+ assert_equal({ until: "3.1" }, gate)
90
+ end
91
+
92
+ def test_unwrap_for_scope_in_scope_since
93
+ src = "\#@since 3.2\n本文。\n\#@end\n"
94
+ unwrapped, gate = unwrap_for_scope(src)
95
+ assert_equal "本文。\n", unwrapped
96
+ assert_equal({ since: "3.2" }, gate)
97
+ end
98
+
99
+ def test_unwrap_for_scope_leaves_out_of_scope_file
100
+ # profile.rd / irb/slex.rd: until 2.7.0 は [3.0,4.2) の対象外
101
+ assert_nil unwrap_for_scope("\#@until 2.7.0\n本文。\n\#@end\n")
102
+ end
103
+
104
+ def test_unwrap_for_scope_provably_true_if
105
+ # rss.rd: (version >= "1.8.2") はスコープ下限 3.0 以下なので常に真
106
+ src = "\#@if (version >= \"1.8.2\")\n本文。\n\#@end\n"
107
+ unwrapped, gate = unwrap_for_scope(src)
108
+ assert_equal "本文。\n", unwrapped
109
+ assert_equal({}, gate)
110
+ end
111
+
112
+ def test_unwrap_for_scope_leaves_unprovable_if
113
+ assert_nil unwrap_for_scope("\#@if (version == \"3.1\")\n本文。\n\#@end\n")
114
+ assert_nil unwrap_for_scope("\#@if (version >= \"3.1\")\n本文。\n\#@end\n")
115
+ end
116
+
117
+ def test_unwrap_for_scope_returns_nil_without_whole_file_gate
118
+ assert_nil unwrap_for_scope("category Math\n\n本文。\n")
119
+ end
120
+
121
+ # ---- regate_metadata: 据え置きゲートのメタデータ領域を独立ゲートに分離 ----
122
+ # 旧版サルベージで据え置かれた library ルートの全体ゲート(rubygems.rd の
123
+ # #@since 1.9.1 等)は、メタデータが本文に埋もれると native パースが
124
+ # front matter から読めない。意味等価な正規形(メタデータブロック + 本文ブロック、
125
+ # それぞれ同じゲートで包む)に書き換えることで、既存のゲート対応
126
+ # メタデータ収集器がそのまま front matter 化できる
127
+
128
+ def regate(src)
129
+ BitClust::WholeFileGate.regate_metadata(src)
130
+ end
131
+
132
+ def test_regate_metadata_splits_metadata_and_body
133
+ # 種別(category/require/sublibrary)ごとに独立のゲートブロックへ分ける
134
+ # (メタデータ収集器の「#@ ブロックは単一種のみ」の制約に合わせる)
135
+ src = "\#@since 1.9.1\n" \
136
+ "require rubygems/defaults\n" \
137
+ "require rubygems/version\n" \
138
+ "\n" \
139
+ "sublibrary rubygems/gem_runner\n" \
140
+ "\n" \
141
+ "本文。\n" \
142
+ "\#@end\n"
143
+ expected = "\#@since 1.9.1\n" \
144
+ "require rubygems/defaults\n" \
145
+ "require rubygems/version\n" \
146
+ "\#@end\n" \
147
+ "\n" \
148
+ "\#@since 1.9.1\n" \
149
+ "sublibrary rubygems/gem_runner\n" \
150
+ "\#@end\n" \
151
+ "\n" \
152
+ "\#@since 1.9.1\n" \
153
+ "本文。\n" \
154
+ "\#@end\n"
155
+ assert_equal expected, regate(src)
156
+ end
157
+
158
+ def test_regate_metadata_skips_leading_blank_in_gate
159
+ src = "\#@since 1.9.1\n\ncategory Math\n\n本文。\n\#@end\n"
160
+ expected = "\#@since 1.9.1\ncategory Math\n\#@end\n\n\#@since 1.9.1\n本文。\n\#@end\n"
161
+ assert_equal expected, regate(src)
162
+ end
163
+
164
+ def test_regate_metadata_returns_nil_without_metadata
165
+ assert_nil regate("\#@since 1.9.1\n本文。\n\#@end\n")
166
+ end
167
+
168
+ def test_regate_metadata_returns_nil_without_body
169
+ assert_nil regate("\#@since 1.9.1\nrequire foo\n\#@end\n")
170
+ end
171
+
172
+ def test_regate_metadata_returns_nil_without_whole_file_gate
173
+ assert_nil regate("require foo\n\n本文。\n")
174
+ end
175
+ end