bitclust-core 1.2.6 → 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 (160) 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 +49 -5
  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 +17 -17
  24. data/lib/bitclust/capi_converter.rb +20 -0
  25. data/lib/bitclust/classentry.rb +68 -17
  26. data/lib/bitclust/compat.rb +3 -2
  27. data/lib/bitclust/completion.rb +59 -27
  28. data/lib/bitclust/crossrubyutils.rb +9 -5
  29. data/lib/bitclust/database.rb +29 -8
  30. data/lib/bitclust/doc_converter.rb +63 -0
  31. data/lib/bitclust/docentry.rb +2 -1
  32. data/lib/bitclust/entity_splitter.rb +221 -0
  33. data/lib/bitclust/entry.rb +48 -7
  34. data/lib/bitclust/exception.rb +1 -0
  35. data/lib/bitclust/functiondatabase.rb +14 -1
  36. data/lib/bitclust/functionentry.rb +6 -1
  37. data/lib/bitclust/functionreferenceparser.rb +5 -4
  38. data/lib/bitclust/generators/epub.rb +11 -3
  39. data/lib/bitclust/htmlutils.rb +2 -1
  40. data/lib/bitclust/include_graph.rb +375 -0
  41. data/lib/bitclust/include_pruner.rb +144 -0
  42. data/lib/bitclust/interface.rb +2 -1
  43. data/lib/bitclust/libraryentry.rb +9 -7
  44. data/lib/bitclust/lineinput.rb +8 -5
  45. data/lib/bitclust/link_checker.rb +179 -0
  46. data/lib/bitclust/markdown_bridge.rb +186 -0
  47. data/lib/bitclust/markdown_orchestrator.rb +267 -0
  48. data/lib/bitclust/markdown_to_rrd.rb +824 -0
  49. data/lib/bitclust/markdown_tree.rb +217 -0
  50. data/lib/bitclust/mdcompiler.rb +883 -0
  51. data/lib/bitclust/mdparser.rb +347 -0
  52. data/lib/bitclust/messagecatalog.rb +4 -3
  53. data/lib/bitclust/method_since_calculator.rb +117 -0
  54. data/lib/bitclust/methoddatabase.rb +125 -14
  55. data/lib/bitclust/methodentry.rb +130 -7
  56. data/lib/bitclust/methodid.rb +13 -11
  57. data/lib/bitclust/methodsignature.rb +11 -4
  58. data/lib/bitclust/nameutils.rb +42 -22
  59. data/lib/bitclust/parseutils.rb +1 -0
  60. data/lib/bitclust/preprocessor.rb +70 -32
  61. data/lib/bitclust/progress_bar.rb +1 -0
  62. data/lib/bitclust/rdcompiler.rb +197 -96
  63. data/lib/bitclust/refsdatabase.rb +24 -8
  64. data/lib/bitclust/reloadable_request_handler.rb +72 -0
  65. data/lib/bitclust/requesthandler.rb +22 -16
  66. data/lib/bitclust/ridatabase.rb +11 -4
  67. data/lib/bitclust/rrd_to_markdown.rb +925 -0
  68. data/lib/bitclust/rrdparser.rb +197 -30
  69. data/lib/bitclust/runner.rb +18 -2
  70. data/lib/bitclust/screen.rb +81 -11
  71. data/lib/bitclust/search_index_generator.rb +288 -0
  72. data/lib/bitclust/searcher.rb +72 -22
  73. data/lib/bitclust/server.rb +6 -1
  74. data/lib/bitclust/silent_progress_bar.rb +1 -0
  75. data/lib/bitclust/simplesearcher.rb +25 -12
  76. data/lib/bitclust/subcommand.rb +8 -1
  77. data/lib/bitclust/subcommands/ancestors_command.rb +10 -2
  78. data/lib/bitclust/subcommands/checklink_command.rb +54 -0
  79. data/lib/bitclust/subcommands/chm_command.rb +16 -7
  80. data/lib/bitclust/subcommands/classes_command.rb +6 -0
  81. data/lib/bitclust/subcommands/epub_command.rb +1 -0
  82. data/lib/bitclust/subcommands/extract_command.rb +6 -0
  83. data/lib/bitclust/subcommands/htmlfile_command.rb +22 -7
  84. data/lib/bitclust/subcommands/init_command.rb +5 -1
  85. data/lib/bitclust/subcommands/list_command.rb +13 -4
  86. data/lib/bitclust/subcommands/lookup_command.rb +29 -9
  87. data/lib/bitclust/subcommands/methods_command.rb +11 -2
  88. data/lib/bitclust/subcommands/methodsince_command.rb +80 -0
  89. data/lib/bitclust/subcommands/preproc_command.rb +6 -0
  90. data/lib/bitclust/subcommands/property_command.rb +3 -1
  91. data/lib/bitclust/subcommands/query_command.rb +1 -0
  92. data/lib/bitclust/subcommands/searchpage_command.rb +102 -0
  93. data/lib/bitclust/subcommands/server_command.rb +16 -10
  94. data/lib/bitclust/subcommands/setup_command.rb +25 -10
  95. data/lib/bitclust/subcommands/statichtml_command.rb +132 -6
  96. data/lib/bitclust/subcommands/update_command.rb +111 -2
  97. data/lib/bitclust/syntax_highlighter.rb +52 -5
  98. data/lib/bitclust/textutils.rb +4 -3
  99. data/lib/bitclust/version.rb +2 -1
  100. data/lib/bitclust/version_badges.rb +90 -0
  101. data/lib/bitclust/whole_file_gate.rb +137 -0
  102. data/lib/bitclust.rb +1 -0
  103. data/test/test_app.rb +269 -0
  104. data/test/test_bitclust.rb +2 -0
  105. data/test/test_capi_converter.rb +91 -0
  106. data/test/test_class_screen.rb +413 -0
  107. data/test/test_copy_doc_md.rb +53 -0
  108. data/test/test_doc_converter.rb +78 -0
  109. data/test/test_entity_splitter.rb +231 -0
  110. data/test/test_entry.rb +207 -2
  111. data/test/test_eol_warning.rb +43 -0
  112. data/test/test_functionreferenceparser.rb +11 -0
  113. data/test/test_include_graph.rb +640 -0
  114. data/test/test_include_pruner.rb +133 -0
  115. data/test/test_link_checker.rb +114 -0
  116. data/test/test_lookup_command.rb +135 -0
  117. data/test/test_markdown_bridge.rb +217 -0
  118. data/test/test_markdown_orchestrator.rb +478 -0
  119. data/test/test_markdown_to_rrd.rb +757 -0
  120. data/test/test_markdown_tree.rb +369 -0
  121. data/test/test_mdcompiler.rb +1276 -0
  122. data/test/test_mdparser.rb +538 -0
  123. data/test/test_method_screen.rb +166 -0
  124. data/test/test_method_since_calculator.rb +290 -0
  125. data/test/test_methoddatabase.rb +16 -0
  126. data/test/test_methodentry.rb +328 -0
  127. data/test/test_methodsignature.rb +3 -1
  128. data/test/test_methodsince_command.rb +168 -0
  129. data/test/test_nameutils.rb +22 -0
  130. data/test/test_preprocessor.rb +215 -0
  131. data/test/test_rdcompiler.rb +429 -27
  132. data/test/test_refsdatabase.rb +32 -0
  133. data/test/test_rrd_to_markdown.rb +968 -0
  134. data/test/test_rrdparser.rb +295 -2
  135. data/test/test_run_ruby_wasm.rb +54 -0
  136. data/test/test_run_worker_prelude.rb +83 -0
  137. data/test/test_runner.rb +51 -0
  138. data/test/test_search_index_generator.rb +513 -0
  139. data/test/test_search_screen.rb +48 -0
  140. data/test/test_searcher.rb +153 -0
  141. data/test/test_searchpage_command.rb +167 -0
  142. data/test/test_simplesearcher.rb +28 -0
  143. data/test/test_statichtml_command.rb +204 -0
  144. data/test/test_syntax_highlighter.rb +60 -0
  145. data/test/test_whole_file_gate.rb +175 -0
  146. data/theme/default/js/NOTICE +42 -0
  147. data/theme/default/js/run-worker.js +111 -0
  148. data/theme/default/js/run.js +350 -0
  149. data/theme/default/js/search_controller.js +134 -0
  150. data/theme/default/js/search_init.js +176 -0
  151. data/theme/default/js/search_navigation.js +110 -0
  152. data/theme/default/js/search_page.js +202 -0
  153. data/theme/default/js/search_ranker.js +244 -0
  154. data/theme/default/script.js +91 -18
  155. data/theme/default/search.css +149 -0
  156. data/theme/default/style.css +193 -21
  157. data/theme/default/syntax-highlight.css +8 -0
  158. data/theme/lillia/style.css +45 -1
  159. metadata +178 -21
  160. data/README +0 -32
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+ #
3
+ # bitclust/subcommands/searchpage_command.rb
4
+ #
5
+ # Generates a standalone, cross-version, client-side search page from
6
+ # multiple version databases:
7
+ #
8
+ # bitclust searchpage --outputdir=DIR db-3.0 db-3.1 ... db-4.1
9
+ #
10
+ # The output (index.html + js/search_data.js + assets) is fully static and
11
+ # replaces the server-backed rurema-search app at docs.ruby-lang.org/ja/search/.
12
+ # Each database's version is read from its own properties, and the merged
13
+ # index tags every entry with the versions it appears in.
14
+ #
15
+
16
+ require 'pathname'
17
+ require 'fileutils'
18
+ require 'json'
19
+ require 'optparse'
20
+
21
+ require 'bitclust'
22
+ require 'bitclust/subcommand'
23
+ require 'bitclust/search_index_generator'
24
+
25
+ module BitClust
26
+ module Subcommands
27
+ class SearchpageCommand < Subcommand
28
+ # search_init.js is the in-page dropdown wiring used by statichtml
29
+ # layouts; this page ships its own wiring (search_page.js) instead.
30
+ VENDORED_JS_FILES = %w[
31
+ search_navigation.js search_ranker.js search_controller.js
32
+ ].freeze
33
+
34
+ def initialize
35
+ super
36
+ @outputdir = nil
37
+ @themedir = srcdir_root + "theme/default"
38
+ @templatedir = srcdir_root + "data/bitclust/searchpage"
39
+ @suffix = ".html"
40
+ @fs_casesensitive = false
41
+ @verbose = false
42
+ @parser.banner = "Usage: #{File.basename($0, '.*')} searchpage [options] <dbpath>..."
43
+ @parser.on('-o', '--outputdir=PATH', 'Output directory.') {|path|
44
+ @outputdir = Pathname.new(path)
45
+ }
46
+ @parser.on('--themedir=PATH', 'Theme directory.') {|path|
47
+ @themedir = Pathname.new(path)
48
+ }
49
+ @parser.on('--fs-casesensitive', 'Filesystem is case-sensitive.') {
50
+ @fs_casesensitive = true
51
+ }
52
+ @parser.on('--verbose', 'Show progress.') {
53
+ @verbose = true
54
+ }
55
+ end
56
+
57
+ # DB paths are taken as positional arguments (one per version), so the
58
+ # global --database option is not required.
59
+ def needs_database?
60
+ false
61
+ end
62
+
63
+ def exec(argv, options)
64
+ outputdir = @outputdir or error("no --outputdir given")
65
+ error("no database given (pass one path per version)") if argv.empty?
66
+
67
+ generator = SearchIndexGenerator.new(suffix: @suffix,
68
+ fs_casesensitive: @fs_casesensitive)
69
+ version_indexes = argv.map {|path|
70
+ db = MethodDatabase.new(path)
71
+ fdb = FunctionDatabase.new(path) if File.directory?(File.join(path, 'function'))
72
+ version = db.properties['version'] or
73
+ error("#{path}: no version property (not a bitclust database?)")
74
+ $stderr.puts "indexing #{path} (#{version})" if @verbose
75
+ [version, generator.build_index(db, fdb)] #: [String, Array[SearchIndexGenerator::entry]]
76
+ }
77
+ versions = version_indexes.map {|version, _| version }
78
+ .sort_by {|v| Gem::Version.new(v) }
79
+
80
+ jsdir = outputdir + "js"
81
+ FileUtils.mkdir_p(jsdir)
82
+ (jsdir + "search_data.js").write(SearchIndexGenerator.merged_js(version_indexes))
83
+ VENDORED_JS_FILES.each do |js|
84
+ FileUtils.cp(@themedir + "js" + js, jsdir.to_s, :preserve => true)
85
+ end
86
+ # Ship the MIT notice for the vendored Aliki files alongside them.
87
+ FileUtils.cp(@themedir + "js" + "NOTICE", jsdir.to_s, :preserve => true)
88
+ FileUtils.cp(@themedir + "js" + "search_page.js", jsdir.to_s, :preserve => true)
89
+ FileUtils.cp(@themedir + "search.css", outputdir.to_s, :preserve => true)
90
+ (outputdir + "index.html").write(render_page(versions))
91
+ $stderr.puts "generated search page for #{versions.join(', ')}" if @verbose
92
+ end
93
+
94
+ private
95
+
96
+ def render_page(versions)
97
+ template = (@templatedir + "index.html").read
98
+ template.sub('%%SEARCH_VERSIONS%%', JSON.generate(versions))
99
+ end
100
+ end
101
+ end
102
+ end
@@ -1,10 +1,10 @@
1
+ # frozen_string_literal: true
1
2
  require 'pathname'
2
3
  require 'erb'
3
4
  require 'find'
4
5
  require 'pp'
5
6
  require 'optparse'
6
7
  require 'yaml'
7
- require 'webrick'
8
8
  require 'uri'
9
9
 
10
10
  require 'bitclust'
@@ -15,7 +15,6 @@ module BitClust
15
15
  class ServerCommand < Subcommand
16
16
  def initialize
17
17
  super
18
- require 'webrick'
19
18
  require 'uri'
20
19
 
21
20
  @params = {
@@ -108,15 +107,20 @@ module BitClust
108
107
  exit 1
109
108
  end
110
109
  if @pid_file
111
- if File.exist?(@pid_file)
110
+ if File.exist?(@pid_file || raise)
112
111
  $stderr.puts "There is still #{@pid_file}. Is another process running?"
113
112
  exit 1
114
113
  end
115
- @pid_file = File.expand_path(@pid_file)
114
+ @pid_file = File.expand_path(@pid_file || raise)
116
115
  end
117
116
  end
118
117
 
119
118
  def exec(argv, options)
119
+ begin
120
+ require 'webrick'
121
+ rescue LoadError
122
+ abort "webrick is not found. You may need to `gem install webrick` to install webrick."
123
+ end
120
124
  require 'bitclust/app'
121
125
  if @debugp
122
126
  @params[:Logger] = WEBrick::Log.new($stderr, WEBrick::Log::DEBUG)
@@ -129,7 +133,7 @@ module BitClust
129
133
  @params[:Logger] = WEBrick::Log.new($stderr, WEBrick::Log::INFO)
130
134
  @params[:AccessLog] = []
131
135
  end
132
- basepath = URI.parse(@baseurl).path
136
+ basepath = URI.parse(@baseurl || raise).path or raise
133
137
  server = WEBrick::HTTPServer.new(@params)
134
138
 
135
139
  if @autop
@@ -168,17 +172,19 @@ module BitClust
168
172
  server.mount File.join(basepath, 'theme/'), WEBrick::HTTPServlet::FileHandler, @themedir
169
173
 
170
174
  if @debugp
171
- trap(:INT) { server.shutdown }
175
+ Signal.trap(:INT) { server.shutdown }
172
176
  else
173
177
  WEBrick::Daemon.start do
174
- trap(:TERM) {
178
+ Signal.trap(:TERM) {
175
179
  server.shutdown
176
180
  begin
177
- File.unlink @pid_file if @pid_file
181
+ pid_file = @pid_file
182
+ File.unlink pid_file if pid_file
178
183
  rescue Errno::ENOENT
179
184
  end
180
185
  }
181
- File.open(@pid_file, 'w') {|f| f.write Process.pid } if @pid_file
186
+ pid_file = @pid_file
187
+ File.open(pid_file, 'w') {|f| f.write Process.pid } if pid_file
182
188
  end
183
189
  end
184
190
  exit if $".include?("exerb/mkexy.rb")
@@ -205,7 +211,7 @@ module BitClust
205
211
  end
206
212
 
207
213
  def load_config_file
208
- home_directory = Pathname(ENV['HOME'])
214
+ home_directory = Pathname(ENV.fetch('HOME'))
209
215
  config_path = home_directory + ".bitclust/config"
210
216
  if config_path.exist?
211
217
  config = YAML.load_file(config_path)
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require 'pathname'
2
3
  require 'erb'
3
4
  require 'find'
@@ -19,7 +20,7 @@ module BitClust
19
20
  @prepare = nil
20
21
  @cleanup = nil
21
22
  @purge = nil
22
- @versions = ["2.4.0", "2.5.0", "2.6.0"]
23
+ @versions = ["3.3.0", "3.4.0", "4.0.0"]
23
24
  @update = true
24
25
  @parser.banner = "Usage: #{File.basename($0, '.*')} setup [options]"
25
26
  @parser.on('--prepare', 'Prepare config file and checkout repository. Do not create database.') {
@@ -39,6 +40,11 @@ module BitClust
39
40
  }
40
41
  end
41
42
 
43
+ # DB は ~/.bitclust 配下に自前で作るのでグローバル --database は不要
44
+ def needs_database?
45
+ false
46
+ end
47
+
42
48
  def exec(argv, options)
43
49
  purge if @purge
44
50
  prepare
@@ -48,18 +54,18 @@ module BitClust
48
54
  prefix = "#{@config[:database_prefix]}-#{version}"
49
55
  FileUtils.rm_rf(prefix) if @cleanup
50
56
  init_argv = ["version=#{version}", "encoding=#{@config[:encoding]}"]
51
- init_options = { :prefix => prefix }
57
+ # @type var init_options: Subcommand::options
58
+ init_options = { :prefix => prefix, :capi => false }
52
59
  InitCommand.new.exec(init_argv, init_options)
53
- update_method_database(prefix, ["--stdlibtree=#{@config[:stdlibtree]}"])
54
- update_argv = Pathname(@config[:capi_src]).children.select(&:file?).map{|v| v.realpath.to_s }
55
- update_function_database(prefix, update_argv)
60
+ update_method_database(prefix, ["--markdowntree=#{@config[:mdtree]}"])
61
+ update_function_database(prefix, ["--markdowntree=#{@config[:capi_mdtree]}"])
56
62
  end
57
63
  end
58
64
 
59
65
  private
60
66
 
61
67
  def purge
62
- home_directory = Pathname(ENV["HOME"])
68
+ home_directory = Pathname(ENV.fetch("HOME"))
63
69
  config_dir = home_directory + ".bitclust"
64
70
  print "Remove all generated files..."
65
71
  FileUtils.rm_rf(config_dir.to_s)
@@ -68,7 +74,7 @@ module BitClust
68
74
  end
69
75
 
70
76
  def prepare
71
- home_directory = Pathname(ENV["HOME"]).expand_path
77
+ home_directory = Pathname(ENV.fetch("HOME")).expand_path
72
78
  config_dir = home_directory + ".bitclust"
73
79
  config_dir.mkpath
74
80
  config_path = config_dir + "config"
@@ -78,17 +84,24 @@ module BitClust
78
84
  :encoding => "utf-8",
79
85
  :versions => @versions,
80
86
  :default_version => @versions.max,
81
- :stdlibtree => (rubydoc_dir + "refm/api/src").to_s,
82
- :capi_src => (rubydoc_dir + "refm/capi/src/").to_s,
87
+ :mdtree => (rubydoc_dir + "manual/api").to_s,
88
+ :capi_mdtree => (rubydoc_dir + "manual/capi").to_s,
83
89
  :baseurl => "http://localhost:10080",
84
90
  :port => "10080",
85
91
  :pid_file => "/tmp/bitclust.pid",
86
92
  }
87
93
  if config_path.exist?
88
94
  @config = YAML.load_file(config_path)
95
+ unless @config[:mdtree]
96
+ # Markdown 移行(2026-07)前の設定ファイル。ソースの取得形態が
97
+ # 変わっているため、作り直しを案内する
98
+ warn "Old config file (before the Markdown migration) found: #{config_path}"
99
+ warn "Run `#{File.basename($0, '.*')} setup --purge` and setup again."
100
+ exit 1
101
+ end
89
102
  unless @config[:versions].sort == @versions.sort
90
103
  print("overwrite config file? > [y/N]")
91
- if /\Ay\z/i =~ $stdin.gets.chomp
104
+ if /\Ay\z/i =~ $stdin.gets&.chomp
92
105
  @config[:versions] = @versions
93
106
  @config[:default_version] = @versions.max
94
107
  generate_config(config_path, @config)
@@ -131,6 +144,7 @@ module BitClust
131
144
  end
132
145
 
133
146
  def update_method_database(prefix, argv)
147
+ # @type var options: Subcommand::options
134
148
  options = {
135
149
  :prefix => prefix,
136
150
  :capi => false,
@@ -141,6 +155,7 @@ module BitClust
141
155
  end
142
156
 
143
157
  def update_function_database(prefix, argv)
158
+ # @type var options: Subcommand::options
144
159
  options = {
145
160
  :prefix => prefix,
146
161
  :capi => true,
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  # Copyright (c) 2006-2007 Minero Aoki
2
3
  #
3
4
  # This program is free software.
@@ -11,6 +12,7 @@ require 'bitclust/nameutils'
11
12
  require 'bitclust/subcommand'
12
13
  require 'bitclust/progress_bar'
13
14
  require 'bitclust/silent_progress_bar'
15
+ require 'bitclust/search_index_generator'
14
16
 
15
17
  module BitClust
16
18
  module Subcommands
@@ -44,7 +46,7 @@ module BitClust
44
46
  end
45
47
 
46
48
  def method_url(spec)
47
- cname, tmark, mname = *split_method_spec(spec)
49
+ cname, tmark, mname = split_method_spec(spec)
48
50
  filename = html_filename(encodename_package(mname), @suffix)
49
51
  @bitclust_html_base +
50
52
  "/method/#{encodename_package(cname)}/#{typemark2char(tmark)}/#{filename}"
@@ -89,7 +91,7 @@ module BitClust
89
91
  end
90
92
 
91
93
  def edit_url(location)
92
- "#{@edit_base_url}/#{location.file}#L#{location.line}".sub(/([^:])\/\/+/, "\\1/")
94
+ "#{@edit_base_url}/#{location.file}".sub(/([^:])\/\/+/, "\\1/")
93
95
  end
94
96
 
95
97
  def encodename_package(str)
@@ -119,6 +121,10 @@ module BitClust
119
121
  @gtm_tracking_id = nil
120
122
  @meta_robots_content = ["noindex"]
121
123
  @stop_on_syntax_error = true
124
+ @eol_warning = false
125
+ @run_ruby_wasm = nil
126
+ @sitemap_baseurl = nil
127
+ @sitemap_paths = []
122
128
  @parser.banner = "Usage: #{File.basename($0, '.*')} statichtml [options]"
123
129
  @parser.on('-o', '--outputdir=PATH', 'Output directory') do |path|
124
130
  begin
@@ -155,6 +161,15 @@ module BitClust
155
161
  @parser.on('--meta-robots-content=VALUE1,VALUE2,...', Array, 'HTML <meta> element: <meta name="robots" content="VALUE1,VALUE2..."') do |values|
156
162
  @meta_robots_content = values
157
163
  end
164
+ @parser.on('--[no-]eol-warning', 'Show a warning banner that this Ruby version is no longer maintained') do |boolean|
165
+ @eol_warning = boolean
166
+ end
167
+ @parser.on('--run-ruby-wasm=WASM_URL', 'Add a RUN button to Ruby sample code, executing it in-browser with the given ruby.wasm') do |url|
168
+ @run_ruby_wasm = url
169
+ end
170
+ @parser.on('--sitemap-baseurl=URL', 'Generate sitemap.xml under the output directory, with <loc> built from this base URL (e.g. https://docs.ruby-lang.org/ja/3.4/). Omit to skip sitemap.xml generation (default)') do |url|
171
+ @sitemap_baseurl = url
172
+ end
158
173
  @parser.on('--no-stop-on-syntax-error', 'Do not stop on syntax error') do |boolean|
159
174
  @stop_on_syntax_error = boolean
160
175
  end
@@ -172,8 +187,10 @@ module BitClust
172
187
  manager = ScreenManager.new(@manager_config)
173
188
 
174
189
  db.transaction do
175
- methods = {}
190
+ methods = {} #: Hash[String, Array[MethodEntry]]
176
191
  db.methods.each_with_index do |entry, i|
192
+ next if entry.undefined?
193
+
177
194
  entry.names.each do |name|
178
195
  method_name = entry.klass.name + entry.typemark + name
179
196
  (methods[method_name] ||= []) << entry
@@ -190,22 +207,33 @@ module BitClust
190
207
  end
191
208
 
192
209
  @urlmapper.bitclust_html_base = '..'
193
- create_file(@outputdir + "library/#{html_filename("index", @suffix)}",
210
+ library_index_path = @outputdir + "library/#{html_filename("index", @suffix)}"
211
+ create_file(library_index_path,
194
212
  manager.library_index_screen(db.libraries.sort, {:database => db}).body,
195
213
  :verbose => @verbose)
196
- create_file(@outputdir + "class/#{html_filename("index", @suffix)}",
214
+ record_sitemap_path(library_index_path)
215
+ class_index_path = @outputdir + "class/#{html_filename("index", @suffix)}"
216
+ create_file(class_index_path,
197
217
  manager.class_index_screen(db.classes.sort, {:database => db}).body,
198
218
  :verbose => @verbose)
199
- create_file(@outputdir + "function/#{html_filename("index", @suffix)}",
219
+ record_sitemap_path(class_index_path)
220
+ function_index_path = @outputdir + "function/#{html_filename("index", @suffix)}"
221
+ create_file(function_index_path,
200
222
  manager.function_index_screen(fdb.functions.sort, { :database => fdb }).body,
201
223
  :verbose => @verbose)
224
+ record_sitemap_path(function_index_path)
202
225
  create_index_html(@outputdir)
226
+ create_search_index(@outputdir, db, fdb)
227
+ if baseurl = @sitemap_baseurl
228
+ create_sitemap(@outputdir, baseurl)
229
+ end
203
230
  FileUtils.cp(@manager_config[:themedir] + @manager_config[:css_url],
204
231
  @outputdir.to_s, :verbose => @verbose, :preserve => true)
205
232
  FileUtils.cp(@manager_config[:themedir] + "syntax-highlight.css",
206
233
  @outputdir.to_s, :verbose => @verbose, :preserve => true)
207
234
  FileUtils.cp(@manager_config[:themedir] + "script.js",
208
235
  @outputdir.to_s, :verbose => @verbose, :preserve => true)
236
+ copy_run_ruby_wasm_script if @run_ruby_wasm
209
237
  FileUtils.cp(@manager_config[:themedir] + @manager_config[:favicon_url],
210
238
  @outputdir.to_s, :verbose => @verbose, :preserve => true)
211
239
  Dir.mktmpdir do |tmpdir|
@@ -237,6 +265,8 @@ module BitClust
237
265
  :gtm_tracking_id => @gtm_tracking_id,
238
266
  :meta_robots_content => @meta_robots_content,
239
267
  :stop_on_syntax_error => @stop_on_syntax_error,
268
+ :eol_warning => @eol_warning,
269
+ :run_ruby_wasm => @run_ruby_wasm,
240
270
  }
241
271
  @manager_config[:urlmapper] = URLMapperEx.new(@manager_config)
242
272
  @urlmapper = @manager_config[:urlmapper]
@@ -287,6 +317,101 @@ HERE
287
317
  }
288
318
  end
289
319
 
320
+ SEARCH_JS_FILES = %w[
321
+ search_navigation.js search_ranker.js search_controller.js search_init.js
322
+ ].freeze
323
+
324
+ def create_search_index(outputdir, db, fdb)
325
+ generator = SearchIndexGenerator.new(suffix: @suffix,
326
+ fs_casesensitive: @fs_casesensitive)
327
+ jsdir = outputdir + "js"
328
+ FileUtils.mkdir_p(jsdir) unless jsdir.directory?
329
+ create_file(jsdir + "search_data.js",
330
+ generator.to_js(db, fdb),
331
+ :verbose => @verbose)
332
+ themedir = @manager_config[:themedir]
333
+ SEARCH_JS_FILES.each do |js|
334
+ FileUtils.cp(themedir + "js" + js, jsdir.to_s,
335
+ :verbose => @verbose, :preserve => true)
336
+ end
337
+ # Ship the MIT notice for the vendored Aliki files alongside them.
338
+ FileUtils.cp(themedir + "js" + "NOTICE", jsdir.to_s,
339
+ :verbose => @verbose, :preserve => true)
340
+ FileUtils.cp(themedir + "search.css", outputdir.to_s,
341
+ :verbose => @verbose, :preserve => true)
342
+ end
343
+
344
+ # A single sitemap.xml file supports at most 50,000 URLs
345
+ # (https://www.sitemaps.org/protocol.html#index). Splitting into a
346
+ # sitemap index is out of scope for now (small start); if the site
347
+ # grows past this, only the first MAX_SITEMAP_URLS pages are listed
348
+ # and a warning is printed.
349
+ MAX_SITEMAP_URLS = 50_000
350
+
351
+ # The 5 characters that need escaping to embed arbitrary text as XML
352
+ # character data (used for the <loc> contents below).
353
+ XML_ESCAPES = {
354
+ '&' => '&amp;',
355
+ '<' => '&lt;',
356
+ '>' => '&gt;',
357
+ '"' => '&quot;',
358
+ "'" => '&apos;',
359
+ }.freeze
360
+
361
+ def escape_xml(str)
362
+ str.gsub(/[&<>"']/) {|c| XML_ESCAPES.fetch(c) }
363
+ end
364
+
365
+ # Remember +path+ (an HTML page actually written under @outputdir) so
366
+ # that create_sitemap can list it later. A no-op unless
367
+ # --sitemap-baseurl was given, so sitemap generation has no effect on
368
+ # the rest of the run when the option is not used.
369
+ def record_sitemap_path(path)
370
+ return unless @sitemap_baseurl
371
+ @sitemap_paths << path.relative_path_from(@outputdir).to_s
372
+ end
373
+
374
+ # Generate outputdir/sitemap.xml: a plain XML sitemap
375
+ # (https://www.sitemaps.org/protocol.html) listing every page
376
+ # recorded via record_sitemap_path, as baseurl + relative path.
377
+ def create_sitemap(outputdir, baseurl)
378
+ paths = @sitemap_paths
379
+ if paths.size > MAX_SITEMAP_URLS
380
+ $stderr.puts "warning: #{paths.size} pages found, but a single sitemap.xml supports at most #{MAX_SITEMAP_URLS} URLs; only the first #{MAX_SITEMAP_URLS} are included. Consider a sitemap index if you need the rest."
381
+ paths = paths.first(MAX_SITEMAP_URLS)
382
+ end
383
+ base = baseurl.end_with?('/') ? baseurl : "#{baseurl}/"
384
+ xml = +%(<?xml version="1.0" encoding="UTF-8"?>\n)
385
+ xml << %(<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n)
386
+ paths.each do |path|
387
+ xml << "<url><loc>#{escape_xml(base + path)}</loc></url>\n"
388
+ end
389
+ xml << "</urlset>\n"
390
+ create_file(outputdir + "sitemap.xml", xml, :verbose => @verbose)
391
+ end
392
+
393
+ # Scripts for the RUN-button feature (statichtml --run-ruby-wasm):
394
+ # run.js sets up the button and compiles the wasm module on the main
395
+ # thread; run-worker.js is the module Worker it spawns per execution
396
+ # (see theme/default/js/run.js for why: STOP/timeout both just
397
+ # Worker#terminate() it). Both are required for the feature to work.
398
+ RUN_RUBY_WASM_JS_FILES = %w[run.js run-worker.js].freeze
399
+
400
+ # Copy the RUN-button scripts. A themedir missing one of them is
401
+ # tolerated: warn and skip instead of aborting the whole build.
402
+ def copy_run_ruby_wasm_script
403
+ jsdir = @outputdir + "js"
404
+ RUN_RUBY_WASM_JS_FILES.each do |name|
405
+ src = @manager_config[:themedir] + "js" + name
406
+ unless src.file?
407
+ $stderr.puts "warning: #{src} not found; RUN button script not copied"
408
+ next
409
+ end
410
+ FileUtils.mkdir_p(jsdir) unless jsdir.directory?
411
+ FileUtils.cp(src.to_s, jsdir.to_s, :verbose => @verbose, :preserve => true)
412
+ end
413
+ end
414
+
290
415
  def create_html_file(entry, manager, outputdir, db)
291
416
  e = entry.is_a?(Array) ? entry.sort.first : entry
292
417
  case e.type_id
@@ -325,6 +450,7 @@ HERE
325
450
  path.open('w') do |f|
326
451
  f.write(html)
327
452
  end
453
+ record_sitemap_path(path)
328
454
  end
329
455
 
330
456
  def create_file(path, str, options = {})
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require 'pathname'
2
3
  require 'erb'
3
4
  require 'find'
@@ -15,11 +16,15 @@ module BitClust
15
16
  def initialize
16
17
  super
17
18
  @root = nil
19
+ @markdowntree = nil
18
20
  @library = nil
19
21
  @parser.banner = "Usage: #{File.basename($0, '.*')} update [<file>...]"
20
22
  @parser.on('--stdlibtree=ROOT', 'Process stdlib source directory tree.') {|path|
21
23
  @root = path
22
24
  }
25
+ @parser.on('--markdowntree=ROOT', 'Process Markdown source directory tree.') {|path|
26
+ @markdowntree = path
27
+ }
23
28
  @parser.on('--library-name=NAME', 'Use NAME for library name in file mode.') {|name|
24
29
  @library = name
25
30
  }
@@ -27,25 +32,129 @@ module BitClust
27
32
 
28
33
  def parse(argv)
29
34
  super
30
- if not @root and argv.empty?
35
+ if not @root and not @markdowntree and argv.empty?
31
36
  error "no input file given"
32
37
  end
33
38
  end
34
39
 
35
40
  def exec(argv, options)
36
41
  super
42
+ if @markdowntree
43
+ # ネイティブ md パース(M3)。source には md が入り、描画は
44
+ # MDCompiler(GFM)が選択される。ブリッジは検証用に温存
45
+ # (BITCLUST_MD_BRIDGE=1 で旧経路)
46
+ if ENV['BITCLUST_MD_BRIDGE'] == '1'
47
+ argv = @db.is_a?(FunctionDatabase) ? prepare_capi_markdowntree
48
+ : (prepare_markdowntree; argv)
49
+ else
50
+ @db.transaction {
51
+ @db.update_by_markdowntree(@markdowntree || raise)
52
+ }
53
+ return
54
+ end
55
+ end
37
56
  @db.transaction {
38
57
  if @root
39
- @db.update_by_stdlibtree @root
58
+ db = @db
59
+ db.is_a?(MethodDatabase) or raise
60
+ db.update_by_stdlibtree(@root || raise)
40
61
  end
41
62
  argv.each do |path|
42
63
  @db.update_by_file path, @library || guess_library_name(path)
43
64
  end
44
65
  }
66
+ remap_source_locations if @markdowntree
67
+ ensure
68
+ FileUtils.remove_entry(@bridge_dir) if @bridge_dir
45
69
  end
46
70
 
47
71
  private
48
72
 
73
+ # Markdown ツリーを旧形式の rd ツリーへブリッジし、既存の
74
+ # stdlibtree 更新機構にそのまま渡す。
75
+ # doc(散文ページ)は stdlibtree/../../doc から読まれるため、
76
+ # md ツリー側に doc/ が同居していればレイアウトを合わせる
77
+ # (manual/api を渡すと manual/doc を拾う。md ならブリッジ変換、
78
+ # .rd のまま(移行中)なら symlink)。
79
+ def prepare_markdowntree
80
+ require 'tmpdir'
81
+ require 'bitclust/markdown_bridge'
82
+ @bridge_dir = Dir.mktmpdir('bitclust-md-bridge')
83
+ root = File.join(@bridge_dir, 'api/src')
84
+ bridge = MarkdownBridge.build(@markdowntree || raise, root)
85
+ @location_map = { root => [@markdowntree, bridge.source_map] }
86
+ # markdowntree と同じ渡され方(相対/絶対)を保った隣接 doc パス。
87
+ # copy_doc は stdlibtree 相対(../../doc/...)の location を記録する
88
+ doc = File.join(File.dirname(@markdowntree || raise), 'doc')
89
+ if File.directory?(doc)
90
+ if Dir.glob(File.join(doc, '**/*.md')).any?
91
+ doc_map = MarkdownBridge.build_doc(doc, File.join(@bridge_dir, 'doc'))
92
+ else
93
+ FileUtils.ln_s(File.expand_path(doc), File.join(@bridge_dir, 'doc'))
94
+ doc_map = nil
95
+ end
96
+ @location_map[File.join(@bridge_dir, 'doc')] = [doc, doc_map]
97
+ @location_map['../../doc'] = [doc, doc_map]
98
+ end
99
+ @root ||= root
100
+ end
101
+
102
+ # C API の Markdown ツリー(manual/capi)を旧形式の .rd 群へ変換し、
103
+ # ファイル引数として返す(--capi update <files> の経路に載せる)
104
+ def prepare_capi_markdowntree
105
+ require 'tmpdir'
106
+ require 'bitclust/markdown_to_rrd'
107
+ @bridge_dir = Dir.mktmpdir('bitclust-capi-bridge')
108
+ # src/ 配下に置く: guess_library_name が src/ 以降を id とするため
109
+ # (関数の filename が旧経路と同じ「eval.c」等になる)
110
+ src = File.join(@bridge_dir, 'src')
111
+ FileUtils.mkdir_p(src)
112
+ capi_map = {} #: Hash[String, String]
113
+ files = Dir.glob(File.join(@markdowntree || raise, '*.md')).sort.map do |f|
114
+ name = "#{File.basename(f, '.md')}.rd"
115
+ rd = File.join(src, name)
116
+ File.write(rd, MarkdownToRRD.convert(File.read(f), capi: true))
117
+ capi_map[name] = "#{File.basename(f, '.md')}.md"
118
+ rd
119
+ end
120
+ @location_map = { src => [@markdowntree, capi_map] }
121
+ files
122
+ end
123
+
124
+ # source_location のブリッジ一時パスを manual/ の md パスへ再マップする。
125
+ # 編集リンク(statichtml --edit-base-url)が凍結 refm でなく編集対象の
126
+ # md を指すようにするため。行番号はブリッジ rd 基準の近似で、
127
+ # front matter 等の分だけ md とずれることがある
128
+ def remap_source_locations
129
+ db = @db
130
+ entries =
131
+ if db.is_a?(FunctionDatabase)
132
+ db.functions
133
+ else
134
+ db.is_a?(MethodDatabase) or raise
135
+ db.libraries + db.classes + db.methods + db.docs
136
+ end
137
+ entries.each do |entry|
138
+ loc = entry.source_location or next
139
+ remapped = remap_location(loc) or next
140
+ entry.source_location = remapped
141
+ entry.save
142
+ end
143
+ end
144
+
145
+ def remap_location(loc)
146
+ file = loc.file
147
+ file.is_a?(String) or raise
148
+ @location_map.each do |bridge_root, (md_root, map)|
149
+ prefix = "#{bridge_root}/"
150
+ next unless file.start_with?(prefix)
151
+ rel = file.delete_prefix(prefix)
152
+ rel = map[rel] || rel if map
153
+ return Location.new(File.join(md_root, rel), loc.line)
154
+ end
155
+ nil
156
+ end
157
+
49
158
  def guess_library_name(path)
50
159
  if %r<(\A|/)src/> =~ path
51
160
  path.sub(%r<.*(\A|/)src/>, '').sub(/\.rd\z/, '')