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
@@ -161,9 +161,6 @@ module BitClust
161
161
  data << "<span class=\"nf\">#{token}</span>"
162
162
  when @stack.last == :embexpr
163
163
  data << "<span class=\"n\">#{token}</span>"
164
- when @stack.last == :heredoc
165
- style = COLORS[:heredoc_beg]
166
- data << "<span class=\"#{style}\">#{token}"
167
164
  when @stack.last == :method_call
168
165
  data << "<span class=\"nf\">#{token}</span>"
169
166
  @stack.pop
@@ -192,6 +189,48 @@ module BitClust
192
189
  data
193
190
  end
194
191
 
192
+ # on_symbeg が開いた <span class="ss"> を、シンボルの末尾トークン
193
+ # (HTML エスケープ済み)を添えて閉じ、@stack の :symbol を外す。
194
+ # on_ident/on_const/on_kw は自前で閉じるので、それ以外の
195
+ # 末尾になりうるトークン(ivar/cvar/gvar/backtick/op/文字列終端)から呼ぶ
196
+ def close_symbol_span(token, data)
197
+ data << "#{escape_html(token)}</span>"
198
+ @stack.pop
199
+ data
200
+ end
201
+
202
+ def on_ivar(token, data)
203
+ if @stack.last == :symbol
204
+ close_symbol_span(token, data)
205
+ else
206
+ on_default(:on_ivar, token, data)
207
+ end
208
+ end
209
+
210
+ def on_cvar(token, data)
211
+ if @stack.last == :symbol
212
+ close_symbol_span(token, data)
213
+ else
214
+ on_default(:on_cvar, token, data)
215
+ end
216
+ end
217
+
218
+ def on_gvar(token, data)
219
+ if @stack.last == :symbol
220
+ close_symbol_span(token, data)
221
+ else
222
+ on_default(:on_gvar, token, data)
223
+ end
224
+ end
225
+
226
+ def on_backtick(token, data)
227
+ if @stack.last == :symbol
228
+ close_symbol_span(token, data)
229
+ else
230
+ on_default(:on_backtick, token, data)
231
+ end
232
+ end
233
+
195
234
  def on_kw(token, data)
196
235
  case
197
236
  when @stack.last == :symbol
@@ -221,6 +260,8 @@ module BitClust
221
260
 
222
261
  def on_op(token, data)
223
262
  case
263
+ when @stack.last == :symbol
264
+ close_symbol_span(token, data)
224
265
  when token == "::" && [:class, :module].include?(_ = @stack.last)
225
266
  @name_buffer << token
226
267
  when token == "<<" && @stack.last == :class
@@ -312,7 +353,7 @@ module BitClust
312
353
 
313
354
  def on_symbeg(token, data)
314
355
  style = COLORS[:symbeg]
315
- data << "<span class=\"#{style}\">#{token}"
356
+ data << "<span class=\"#{style}\">#{escape_html(token)}"
316
357
  @stack << :symbol
317
358
  data
318
359
  end
@@ -336,14 +377,34 @@ module BitClust
336
377
  data << escape_html(token)
337
378
  when @stack.last == :string2
338
379
  data << "<span class=\"s2\">#{escape_html(token)}</span>"
380
+ when @stack.last == :symbol
381
+ data << escape_html(token)
339
382
  else
340
383
  on_default(:on_tstring_content, token, data)
341
384
  end
342
385
  data
343
386
  end
344
387
 
388
+ # "a": / 'b': 形式のシンボルキー。開いている文字列の span を
389
+ # 文字列終端と同じ流儀で閉じる
390
+ def on_label_end(token, data)
391
+ case @stack.last
392
+ when :string1
393
+ data << "#{escape_html(token)}</span>"
394
+ @stack.pop
395
+ when :string2
396
+ data << "<span class=\"s2\">#{escape_html(token)}</span>"
397
+ @stack.pop
398
+ else
399
+ on_default(:on_label_end, token, data)
400
+ end
401
+ data
402
+ end
403
+
345
404
  def on_tstring_end(token, data)
346
405
  case
406
+ when @stack.last == :symbol
407
+ data << "#{escape_html(token)}</span>"
347
408
  when token == "'"
348
409
  data << "#{token}</span>"
349
410
  when %i[qwords words qsymbols symbols].include?(_ = @stack.last)
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+ #
3
+ # ユーザーごとの設定ファイル・DB・キャッシュの置き場所。
4
+ # XDG Base Directory(XDG_CONFIG_HOME / XDG_DATA_HOME / XDG_CACHE_HOME、
5
+ # 未設定なら ~/.config ~/.local/share ~/.cache)配下の bitclust/ を使い、
6
+ # 従来の ~/.bitclust は互換のため読み取りの fallback として残す。
7
+
8
+ require 'pathname'
9
+ require 'yaml'
10
+
11
+ module BitClust
12
+ module UserDirs
13
+ module_function
14
+
15
+ def home
16
+ Pathname(ENV.fetch('HOME')).expand_path
17
+ end
18
+
19
+ # 従来の置き場所(bitclust setup が 1.7 まで作っていた場所)
20
+ def legacy_dir
21
+ home + '.bitclust'
22
+ end
23
+
24
+ def config_home
25
+ xdg_dir('XDG_CONFIG_HOME', '.config')
26
+ end
27
+
28
+ def data_home
29
+ xdg_dir('XDG_DATA_HOME', File.join('.local', 'share'))
30
+ end
31
+
32
+ def cache_home
33
+ xdg_dir('XDG_CACHE_HOME', '.cache')
34
+ end
35
+
36
+ # 設定ファイルの候補(優先順)
37
+ def config_candidates
38
+ [config_home + 'config', legacy_dir + 'config']
39
+ end
40
+
41
+ # 存在する設定ファイル。XDG の場所を先に見て、無ければ従来の
42
+ # ~/.bitclust/config。どちらも無ければ nil
43
+ def config_file
44
+ config_candidates.find(&:exist?)
45
+ end
46
+
47
+ # 設定ファイルを読む。無ければ nil
48
+ def load_config
49
+ path = config_file
50
+ path ? YAML.load_file(path) : nil
51
+ end
52
+
53
+ # 環境変数が絶対パスならそれを、無ければ HOME 直下の既定を使う
54
+ # (XDG の仕様では相対パスの指定は無視する)
55
+ def xdg_dir(env_key, default_subdir)
56
+ value = ENV[env_key]
57
+ base = (value && Pathname(value).absolute?) ? Pathname(value) : home + default_subdir
58
+ base + 'bitclust'
59
+ end
60
+ private_class_method :xdg_dir
61
+ end
62
+ end
@@ -1,4 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
  module BitClust
3
- VERSION = "1.6.1"
3
+ VERSION = "1.8.0"
4
4
  end
@@ -0,0 +1,2 @@
1
+ # frozen_string_literal: true
2
+ require 'bitclust/irb'
@@ -0,0 +1,22 @@
1
+ require 'test/unit'
2
+ require 'bitclust'
3
+ require 'bitclust/subcommands/chm_command'
4
+
5
+ class TestChmSitemap < Test::Unit::TestCase
6
+ def test_to_html_builds_nested_sitemap
7
+ root = BitClust::Subcommands::ChmCommand::Sitemap.new('Ruby', 'doc/index.html')
8
+ child = BitClust::Subcommands::ChmCommand::Sitemap::Content.new('Array <=> & "q"', 'class/-array.html')
9
+ root << child
10
+ html = root.to_html
11
+ assert_match(%r{<param name="Name" value="Ruby">}, html)
12
+ assert_match(%r{<param name="Local" value="doc/index.html">}, html)
13
+ assert_match(%r{<UL>.*Array &lt;=&gt; &amp; &quot;q&quot;.*</UL>}m, html)
14
+ end
15
+
16
+ def test_to_html_without_children_has_no_ul
17
+ leaf = BitClust::Subcommands::ChmCommand::Sitemap::Content.new('Leaf')
18
+ html = leaf.to_html
19
+ assert_not_match(/<UL>/, html)
20
+ assert_not_match(/Local/, html)
21
+ end
22
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+ require 'test/unit'
3
+ require 'test/unit/rr'
4
+ require 'tmpdir'
5
+ require 'bitclust'
6
+ require 'bitclust/subcommands/epub_command'
7
+ require 'bitclust/generators/epub'
8
+
9
+ # `bitclust epub` は常に失敗していた:
10
+ #
11
+ # 1) --catalog=PATH は @catalogdir に保存されるが、exec は(定義されていない
12
+ # ため常に nil の)@catalog をジェネレータへ渡していた。ジェネレータは
13
+ # それを statichtml へ --catalog=(空文字列)として渡し、
14
+ # Pathname.new("").realpath が Errno::ENOENT で失敗する。
15
+ # 2) @catalogdir の既定値が nil だったので、--catalog を指定しない限り
16
+ # `bitclust epub -o DIR` は必ず失敗していた
17
+ # (chm_command / statichtml_command と同じく、既定は同梱の
18
+ # data/bitclust/catalog にすべき)。
19
+ class TestEPUBCommandCatalog < Test::Unit::TestCase
20
+ def test_default_catalogdir_is_the_bundled_catalog
21
+ cmd = BitClust::Subcommands::EPUBCommand.new
22
+ cmd.parse([])
23
+ expected = cmd.srcdir_root + "data/bitclust/catalog"
24
+ assert_equal(expected, cmd.instance_variable_get(:@catalogdir))
25
+ assert_true(expected.directory?, "#{expected} must exist so --catalog can be omitted")
26
+ end
27
+
28
+ def test_catalog_option_overrides_the_default
29
+ Dir.mktmpdir do |dir|
30
+ cmd = BitClust::Subcommands::EPUBCommand.new
31
+ cmd.parse(["--catalog=#{dir}"])
32
+ assert_equal(Pathname.new(dir).realpath, cmd.instance_variable_get(:@catalogdir))
33
+ end
34
+ end
35
+
36
+ def capture_generator_options(cmd)
37
+ captured = nil
38
+ fake_generator = Object.new
39
+ stub(fake_generator).generate { nil }
40
+ stub(BitClust::Generators::EPUB).new {|opts| captured = opts; fake_generator }
41
+ cmd.exec([], { :prefix => nil, :capi => false })
42
+ captured
43
+ end
44
+
45
+ def test_exec_passes_the_default_catalogdir_to_the_generator
46
+ cmd = BitClust::Subcommands::EPUBCommand.new
47
+ cmd.parse([])
48
+ opts = capture_generator_options(cmd)
49
+ assert_equal(cmd.instance_variable_get(:@catalogdir), opts[:catalog])
50
+ end
51
+
52
+ def test_exec_passes_the_overridden_catalogdir_to_the_generator
53
+ Dir.mktmpdir do |dir|
54
+ cmd = BitClust::Subcommands::EPUBCommand.new
55
+ cmd.parse(["--catalog=#{dir}"])
56
+ opts = capture_generator_options(cmd)
57
+ assert_equal(Pathname.new(dir).realpath, opts[:catalog])
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,330 @@
1
+ # frozen_string_literal: true
2
+ require 'test/unit'
3
+ require 'tmpdir'
4
+ require 'fileutils'
5
+ require 'rexml/document'
6
+ require 'bitclust'
7
+ require 'bitclust/generators/epub'
8
+ require 'bitclust/screen'
9
+ require 'bitclust/subcommands/statichtml_command'
10
+
11
+ # contents.opf のマニフェストは、statichtml が OEBPS 以下に生成する
12
+ # class/method/library/doc/function ページと css/image リソースを
13
+ # すべて(ユニークな id で)列挙しなければならない(EPUB の仕様上必須)。
14
+ class TestEpubGeneratorContentsOpf < Test::Unit::TestCase
15
+ def build_epub_generator
16
+ datadir = File.expand_path('../data/bitclust', __dir__)
17
+ BitClust::Generators::EPUB.new(:templatedir => Pathname.new("#{datadir}/template.epub"))
18
+ end
19
+
20
+ def setup
21
+ @dir = Dir.mktmpdir
22
+ @epub_directory = Pathname.new(@dir)
23
+ @oebps = @epub_directory + "OEBPS"
24
+ FileUtils.mkdir_p(@oebps + "class")
25
+ FileUtils.mkdir_p(@oebps + "method/-array/i")
26
+ FileUtils.mkdir_p(@oebps + "doc")
27
+ FileUtils.mkdir_p(@oebps + "library")
28
+ FileUtils.mkdir_p(@oebps + "function")
29
+ FileUtils.mkdir_p(@oebps + "images")
30
+
31
+ File.write(@oebps + "class/-array.xhtml", "<html/>")
32
+ File.write(@oebps + "class/index.xhtml", "<html/>") # クラス一覧ページ
33
+ File.write(@oebps + "method/-array/i/each.xhtml", "<html/>")
34
+ File.write(@oebps + "doc/index.xhtml", "<html/>") # マニュアルのトップページ
35
+ File.write(@oebps + "doc/help.xhtml", "<html/>")
36
+ File.write(@oebps + "library/index.xhtml", "<html/>")
37
+ File.write(@oebps + "library/json.xhtml", "<html/>")
38
+ File.write(@oebps + "function/index.xhtml", "<html/>")
39
+ File.write(@oebps + "function/rb_ary_new.xhtml", "<html/>")
40
+ File.write(@oebps + "style.css", "body{}")
41
+ File.write(@oebps + "images/rurema.png", "\x89PNG\r\n")
42
+ File.write(@epub_directory + "nav.xhtml", "<html/>")
43
+
44
+ @generator = build_epub_generator
45
+ @generator.send(:generate_contents_opf, @epub_directory)
46
+ @opf_path = @epub_directory + "contents.opf"
47
+ @xml = File.read(@opf_path)
48
+ @doc = REXML::Document.new(@xml)
49
+ end
50
+
51
+ def teardown
52
+ FileUtils.rm_rf(@dir)
53
+ end
54
+
55
+ def items
56
+ REXML::XPath.match(@doc, "//manifest/item")
57
+ end
58
+
59
+ def test_contents_opf_is_well_formed_xml
60
+ assert_nothing_raised { REXML::Document.new(@xml) }
61
+ end
62
+
63
+ def test_manifest_lists_every_xhtml_page_under_oebps
64
+ hrefs = items.map {|item| item.attributes['href'] }
65
+ %w[
66
+ OEBPS/class/-array.xhtml
67
+ OEBPS/class/index.xhtml
68
+ OEBPS/method/-array/i/each.xhtml
69
+ OEBPS/doc/index.xhtml
70
+ OEBPS/doc/help.xhtml
71
+ OEBPS/library/index.xhtml
72
+ OEBPS/library/json.xhtml
73
+ OEBPS/function/index.xhtml
74
+ OEBPS/function/rb_ary_new.xhtml
75
+ nav.xhtml
76
+ ].each do |href|
77
+ assert_include(hrefs, href)
78
+ end
79
+ end
80
+
81
+ def test_manifest_lists_non_xhtml_resources_that_exist
82
+ hrefs = items.map {|item| item.attributes['href'] }
83
+ assert_include(hrefs, "OEBPS/style.css")
84
+ assert_include(hrefs, "OEBPS/images/rurema.png")
85
+ style_item = items.find {|item| item.attributes['href'] == "OEBPS/style.css" }
86
+ assert_equal("text/css", style_item.attributes['media-type'])
87
+ image_item = items.find {|item| item.attributes['href'] == "OEBPS/images/rurema.png" }
88
+ assert_equal("image/png", image_item.attributes['media-type'])
89
+ end
90
+
91
+ def test_manifest_item_ids_are_unique
92
+ ids = items.map {|item| item.attributes['id'] }
93
+ assert_equal(ids.size, ids.uniq.size, "duplicate manifest item ids: #{ids}")
94
+ end
95
+
96
+ def test_manifest_item_ids_are_valid_xml_ncnames
97
+ ids = items.map {|item| item.attributes['id'] }
98
+ assert_false(ids.empty?)
99
+ ids.each do |id|
100
+ assert_match(/\A[A-Za-z_][A-Za-z0-9_.-]*\z/, id, "not a valid NCName: #{id.inspect}")
101
+ end
102
+ end
103
+
104
+ # doc/index.xhtml はマニフェスト中に決め打ちの id="index" として
105
+ # 一度だけ登場し、doc 以下を走査する側から重複登録されない。
106
+ def test_doc_index_is_listed_exactly_once_with_the_index_id
107
+ index_items = items.select {|item| item.attributes['href'] == "OEBPS/doc/index.xhtml" }
108
+ assert_equal(1, index_items.size)
109
+ assert_equal("index", index_items.first.attributes['id'])
110
+ end
111
+
112
+ def test_manifest_references_xhtml_not_html_for_the_index
113
+ hrefs = items.map {|item| item.attributes['href'] }
114
+ assert_not_include(hrefs, "OEBPS/doc/index.html")
115
+ end
116
+
117
+ # 目次ページ (nav) ・マニュアルのトップページ・クラスページだけが
118
+ # spine(読み進める順序)に載る。約 11,700 あるメソッドページなどを
119
+ # spine に加えると読書順序が壊れるので、マニフェストには載るが spine
120
+ # には載らない。
121
+ def test_spine_still_excludes_method_library_doc_and_function_pages
122
+ spine_idrefs = REXML::XPath.match(@doc, "//spine/itemref").map {|el| el.attributes['idref'] }
123
+ assert_equal(%w[toc index], spine_idrefs.first(2))
124
+
125
+ class_item_ids = items.select {|item| item.attributes['href'].to_s.start_with?('OEBPS/class/') }
126
+ .map {|item| item.attributes['id'] }
127
+ assert_equal(%w[toc index] + class_item_ids, spine_idrefs)
128
+
129
+ other_ids = items.map {|item| item.attributes['id'] } - %w[toc index] - class_item_ids
130
+ assert_false(other_ids.empty?)
131
+ other_ids.each do |id|
132
+ assert_not_include(spine_idrefs, id)
133
+ end
134
+ end
135
+ end
136
+
137
+ # EPUB のパッケージ(contents.opf)に載る前に、statichtml が出力ルートへ
138
+ # 書く 2 行のリダイレクトスタブ(<meta http-equiv=refresh> + <a>、
139
+ # ルート要素を持たず XML として整形式でない)を消す。
140
+ class TestEpubGeneratorIndexStub < Test::Unit::TestCase
141
+ def build_epub_generator
142
+ datadir = File.expand_path('../data/bitclust', __dir__)
143
+ BitClust::Generators::EPUB.new(:templatedir => Pathname.new("#{datadir}/template.epub"))
144
+ end
145
+
146
+ def setup
147
+ @dir = Dir.mktmpdir
148
+ @contents_directory = Pathname.new(@dir) + "OEBPS"
149
+ FileUtils.mkdir_p(@contents_directory)
150
+ @generator = build_epub_generator
151
+ end
152
+
153
+ def teardown
154
+ FileUtils.rm_rf(@dir)
155
+ end
156
+
157
+ def test_removes_the_redirect_stub_statichtml_writes_at_the_root
158
+ stub_path = @contents_directory + "index.xhtml"
159
+ File.write(stub_path, "<meta http-equiv=\"refresh\" content=\"0; URL=doc/index.xhtml\">\n<a href=\"doc/index.xhtml\">Go</a>\n")
160
+ assert_raise(REXML::ParseException) { REXML::Document.new(File.read(stub_path)) }
161
+
162
+ @generator.send(:remove_index_stub, @contents_directory)
163
+
164
+ assert_false(stub_path.exist?)
165
+ end
166
+
167
+ def test_is_a_noop_when_the_stub_is_absent
168
+ stub_path = @contents_directory + "index.xhtml"
169
+ assert_nothing_raised { @generator.send(:remove_index_stub, @contents_directory) }
170
+ assert_false(stub_path.exist?)
171
+ end
172
+
173
+ # doc/index.xhtml (マニュアルのトップページ) と紛れないことを確認する
174
+ def test_does_not_touch_the_doc_index_page
175
+ doc_dir = @contents_directory + "doc"
176
+ FileUtils.mkdir_p(doc_dir)
177
+ doc_index = doc_dir + "index.xhtml"
178
+ File.write(doc_index, "<html/>")
179
+ @generator.send(:remove_index_stub, @contents_directory)
180
+ assert_true(doc_index.exist?)
181
+ end
182
+ end
183
+
184
+ # data/bitclust/template.epub が整形式 XHTML を出力すること(layout の
185
+ # <meta>/<link> の自己終了、class のインデックス一覧での "<=>" や "&" を
186
+ # 含むメソッド名のエスケープ)を検証する。
187
+ #
188
+ # Screen#run_template は ERB 化したテンプレート本体をレシーバの
189
+ # self.class(ClassScreen / MethodScreen そのもの)に「respond_to? なら
190
+ # コンパイルしない」というガード付きでキャッシュする。同一プロセス内で
191
+ # template.offline 用の別テスト(例: test_class_screen.rb)が先に走ると、
192
+ # そちらのコンパイル結果を再利用してしまい、このテストが実際には
193
+ # template.epub を検証できなくなる(test_method_screen.rb に既知の注記
194
+ # あり)。
195
+ #
196
+ # 専用のサブクラスに分けるだけでは防げない: respond_to? は継承した
197
+ # メソッドにも true を返すので、スーパークラス(本物の ClassScreen /
198
+ # MethodScreen)側で他のテストが先に class_template 等をコンパイル済みだと、
199
+ # このサブクラスの run_template はガードに阻まれて自分の
200
+ # @template_repository(= template.epub)で再コンパイルしない。
201
+ # そこで、最初の render より前に明示的に def_method して
202
+ # class_template / method_template / layout をこのサブクラス自身に
203
+ # 直接定義しておく。直接定義されたメソッドは継承したものより優先されるので、
204
+ # 他のテストの実行順序に関係なく template.epub の内容を使わせられる。
205
+ class EpubTestClassScreen < BitClust::ClassScreen; end
206
+ class EpubTestMethodScreen < BitClust::MethodScreen; end
207
+ class EpubTestClassIndexScreen < BitClust::ClassIndexScreen; end
208
+
209
+ epub_template_repository = BitClust::TemplateRepository.new(
210
+ "#{File.expand_path('../data/bitclust', __dir__)}/template.epub"
211
+ )
212
+ [
213
+ [EpubTestClassScreen, 'class', 'class_template'],
214
+ [EpubTestMethodScreen, 'method', 'method_template'],
215
+ [EpubTestClassIndexScreen, 'class-index', 'class_index_template'],
216
+ ].each do |klass, id, method_name|
217
+ ERB.new(epub_template_repository.load(id)).def_method(klass, method_name, "#{id}.erb")
218
+ end
219
+ [EpubTestClassScreen, EpubTestMethodScreen, EpubTestClassIndexScreen].each do |klass|
220
+ ERB.new(epub_template_repository.load('layout')).def_method(klass, 'layout', 'layout.erb')
221
+ end
222
+
223
+ class TestEpubTemplateWellFormedness < Test::Unit::TestCase
224
+ SRC = <<'HERE'
225
+ = module Helper
226
+ 比較演算子を提供するヘルパーモジュール
227
+ = class Spaceship < Object
228
+ extend Helper
229
+ 比較演算子とビット演算子を持つサンプルクラス
230
+ == Instance Methods
231
+ --- <=>(other) -> Integer
232
+
233
+ 比較する。
234
+
235
+ --- &(other) -> Spaceship
236
+
237
+ 論理積を返す。
238
+ HERE
239
+
240
+ def setup
241
+ datadir = File.expand_path('../data/bitclust', __dir__)
242
+ @lib, @db = BitClust::RRDParser.parse(SRC, 'testlib', { 'version' => '3.4' })
243
+ urlmapper = BitClust::Subcommands::StatichtmlCommand::URLMapperEx.new(
244
+ :suffix => '.xhtml',
245
+ :css_url => 'style.css',
246
+ :favicon_url => 'rurema.png'
247
+ )
248
+ urlmapper.bitclust_html_base = '..'
249
+ @manager = BitClust::ScreenManager.new(
250
+ :templatedir => "#{datadir}/template.epub",
251
+ :catalogdir => "#{datadir}/catalog",
252
+ :encoding => 'utf-8',
253
+ :default_encoding => 'utf-8',
254
+ :urlmapper => urlmapper,
255
+ :target_version => '3.4'
256
+ )
257
+ end
258
+
259
+ def class_entry
260
+ @lib.fetch_class('Spaceship')
261
+ end
262
+
263
+ def method_entry(name)
264
+ class_entry.fetch_method(BitClust::MethodSpec.parse("Spaceship##{name}"))
265
+ end
266
+
267
+ def render_class_page
268
+ @manager.send(:new_screen, EpubTestClassScreen, class_entry, :database => @db).body
269
+ end
270
+
271
+ def render_method_page(name)
272
+ @manager.send(:new_screen, EpubTestMethodScreen, [method_entry(name)], :database => @db).body
273
+ end
274
+
275
+ def render_class_index_page
276
+ @manager.send(:new_screen, EpubTestClassIndexScreen, [class_entry], :database => @db).body
277
+ end
278
+
279
+ def test_class_page_escapes_spaceship_operator_in_the_index_list
280
+ html = render_class_page
281
+ assert_include(html, '&lt;=&gt;')
282
+ end
283
+
284
+ def test_class_page_escapes_ampersand_operator_in_the_index_list
285
+ html = render_class_page
286
+ # "&" 単体がエスケープされて "&amp;" として出る(リンクテキストの終端)
287
+ assert_match(/&amp;<\/a>/, html)
288
+ end
289
+
290
+ def test_class_page_is_well_formed_xml
291
+ html = render_class_page
292
+ assert_nothing_raised { REXML::Document.new(html) }
293
+ end
294
+
295
+ def test_method_page_for_spaceship_operator_is_well_formed_xml
296
+ html = render_method_page('<=>')
297
+ assert_nothing_raised { REXML::Document.new(html) }
298
+ end
299
+
300
+ def test_method_page_for_ampersand_operator_is_well_formed_xml
301
+ html = render_method_page('&')
302
+ assert_nothing_raised { REXML::Document.new(html) }
303
+ end
304
+
305
+ def test_layout_void_elements_are_self_closing
306
+ html = render_class_page
307
+ assert_not_match(%r{<meta[^>]*[^/]>}, html)
308
+ assert_not_match(%r{<link[^>]*[^/]>}, html)
309
+ end
310
+
311
+ # template.epub/class の "extend:" 行も <br>(非自己終了)を出しており、
312
+ # そのクラスページ全体が整形式 XML にならなかった(実データベースの
313
+ # Array・CSV クラスで再現した。どちらも extend/aliases 行を持つ)。
314
+ def test_class_page_extend_line_uses_a_self_closing_br
315
+ html = render_class_page
316
+ assert_include(html, 'extend: ')
317
+ assert_not_match(%r{<br[^>]*[^/]>}, html)
318
+ assert_nothing_raised { REXML::Document.new(html) }
319
+ end
320
+
321
+ # template.epub/class-index には対応する <ul> の無い孤立した </ul> が
322
+ # あり、実データベースで OEBPS/class/index.xhtml が整形式にならなかった
323
+ # (template.offline/class-index にも同じ孤立タグがあるが、HTML5 として
324
+ # 読まれる分には実害がないため未修正のまま。EPUB の XHTML では致命的)。
325
+ def test_class_index_page_has_no_orphaned_closing_tag
326
+ html = render_class_index_page
327
+ assert_not_include(html, '</ul>')
328
+ assert_nothing_raised { REXML::Document.new(html) }
329
+ end
330
+ end
@@ -0,0 +1,45 @@
1
+ require 'test/unit'
2
+ require 'stringio'
3
+ require 'tmpdir'
4
+ require 'bitclust'
5
+ require 'bitclust/subcommands/extract_command'
6
+ require 'bitclust/subcommands/htmlfile_command'
7
+
8
+ class TestRdOnlyCommandsRejectMarkdown < Test::Unit::TestCase
9
+ def with_md_file
10
+ Dir.mktmpdir do |dir|
11
+ path = File.join(dir, 'Array.md')
12
+ File.write(path, "---\nlibrary: _builtin\n---\n# class Array < Object\n")
13
+ yield path
14
+ end
15
+ end
16
+
17
+ def capture_stderr
18
+ err = StringIO.new
19
+ orig, $stderr = $stderr, err
20
+ yield
21
+ err.string
22
+ ensure
23
+ $stderr = orig
24
+ end
25
+
26
+ def test_extract_explains_markdown_is_unsupported
27
+ with_md_file do |path|
28
+ cmd = BitClust::Subcommands::ExtractCommand.new
29
+ cmd.parse([path])
30
+ msg = capture_stderr { assert_raise(SystemExit) { cmd.exec([path], {:prefix => nil, :capi => false}) } }
31
+ assert_match(/Markdown/, msg)
32
+ assert_match(/refm/, msg)
33
+ end
34
+ end
35
+
36
+ def test_htmlfile_explains_markdown_is_unsupported
37
+ with_md_file do |path|
38
+ cmd = BitClust::Subcommands::HtmlfileCommand.new
39
+ cmd.parse([path])
40
+ msg = capture_stderr { assert_raise(SystemExit) { cmd.exec([path], {:prefix => nil, :capi => false}) } }
41
+ assert_match(/Markdown/, msg)
42
+ assert_match(/rake generate|bitclust server/, msg)
43
+ end
44
+ end
45
+ end