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,42 @@
1
+ Third-party notices for theme/default/js/
2
+ =========================================
3
+
4
+ The following files are vendored verbatim from RDoc's Aliki theme
5
+ (lib/rdoc/generator/template/aliki/js/), RDoc v7.2.0:
6
+
7
+ * search_navigation.js
8
+ * search_ranker.js
9
+ * search_controller.js
10
+
11
+ Per RDoc's LEGAL.rdoc, the Aliki theme is written by Stan Lo and is included
12
+ under the MIT License:
13
+
14
+ https://github.com/ruby/rdoc/blob/v7.2.0/LEGAL.rdoc
15
+
16
+ ----------------------------------------------------------------------
17
+ MIT License
18
+
19
+ Copyright (c) 2025 Stan Lo
20
+
21
+ Permission is hereby granted, free of charge, to any person obtaining a copy
22
+ of this software and associated documentation files (the "Software"), to deal
23
+ in the Software without restriction, including without limitation the rights
24
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
25
+ copies of the Software, and to permit persons to whom the Software is
26
+ furnished to do so, subject to the following conditions:
27
+
28
+ The above copyright notice and this permission notice shall be included in all
29
+ copies or substantial portions of the Software.
30
+
31
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
32
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
33
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
34
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
35
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
36
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
37
+ SOFTWARE.
38
+ ----------------------------------------------------------------------
39
+
40
+ Note: search_init.js is original to BitClust and is not part of the vendored
41
+ Aliki files. It is dual-licensed under the Ruby License or the MIT License so
42
+ it can be used together with the MIT-licensed Aliki files above.
@@ -0,0 +1,111 @@
1
+ // Module Worker that runs one RUN-button sample. Loaded by theme/default/js/run.js
2
+ // via `new Worker(url, { type: 'module' })`; one Worker instance per execution
3
+ // (see run.js for why: a fresh VM per run needs no cross-run state, and it
4
+ // makes terminate()-to-stop trivial). Named .js rather than .mjs for the same
5
+ // MIME-serving reason as run.js (see the comment at the top of that file).
6
+ //
7
+ // Protocol:
8
+ // main -> worker: { module: WebAssembly.Module, code: string }
9
+ // worker -> main: { type: 'output', text: string } (zero or more, in order)
10
+ // worker -> main: { type: 'done' } (exactly one, on success)
11
+ // worker -> main: { type: 'error', message: string } (exactly one, on failure)
12
+ // Exactly one of 'done'/'error' is posted, always last. The main thread may
13
+ // also just call worker.terminate() (STOP button / timeout); the worker does
14
+ // not need to post anything in that case.
15
+
16
+ const VM_ESM_URL = 'https://cdn.jsdelivr.net/npm/@ruby/wasm-wasi@2.9.3-2.9.4/dist/browser/+esm'
17
+
18
+ // Same idea as run.js's old (pre-Worker) PRELUDE -- redirect $stdout/$stderr
19
+ // -- but instead of buffering into a StringIO for one bulk read at the end,
20
+ // each write is forwarded to the main thread immediately via postOutput()
21
+ // so long-running or infinite-output samples show progress instead of
22
+ // growing an in-wasm string forever. $stderr shares the same sink so
23
+ // interleaving still reads naturally, matching the previous StringIO-based
24
+ // behavior. Ruby only ever hands the JS bridge a plain string, so
25
+ // $stdout.write calls self.postOutput(), a small helper defined below
26
+ // (rather than calling self.postMessage directly from Ruby) so that
27
+ // postMessage itself stays the single place that emits the { type: ... }
28
+ // envelope; main.onmessage never has to disambiguate a bare string from a
29
+ // control message.
30
+ //
31
+ // Kernel#puts/print/p reach a non-IO $stdout through its #write, but the
32
+ // manual's samples also call IO methods on $stdout/$stderr *directly*
33
+ // ($stderr.puts, $stdout.putc, $stdout.print, $stdout.flush, $stderr.tty?,
34
+ // ...). The old StringIO provided those for free, so the common ones are
35
+ // implemented here; their semantics are covered by test/test_run_worker_prelude.rb,
36
+ // which evals this prelude in a real Ruby with the JS bridge stubbed.
37
+ export const PRELUDE = `
38
+ require "js"
39
+ class JSStreamIO
40
+ def write(*parts)
41
+ text = parts.join
42
+ JS.global.call(:postOutput, text)
43
+ text.bytesize
44
+ end
45
+ def <<(obj)
46
+ write(obj)
47
+ self
48
+ end
49
+ def puts(*args)
50
+ if args.empty?
51
+ write("\n")
52
+ else
53
+ args.flatten.each do |arg|
54
+ s = arg.to_s
55
+ s = s + "\n" unless s.end_with?("\n")
56
+ write(s)
57
+ end
58
+ end
59
+ nil
60
+ end
61
+ def print(*args)
62
+ write(args.join)
63
+ nil
64
+ end
65
+ def printf(*args)
66
+ write(sprintf(*args))
67
+ nil
68
+ end
69
+ def putc(ch)
70
+ write(ch.is_a?(String) ? ch[0] : ch.chr)
71
+ ch
72
+ end
73
+ def flush; self; end
74
+ def sync; true; end
75
+ def sync=(value); value; end
76
+ def tty?; false; end
77
+ alias isatty tty?
78
+ end
79
+ $stdout = JSStreamIO.new
80
+ $stderr = $stdout
81
+ `
82
+
83
+ export function formatRunError(error) {
84
+ const text = String((error && error.message) || error)
85
+ return text.split('\n').slice(0, 20).join('\n')
86
+ }
87
+
88
+ // Guarded the same way run.js guards its init(): this file is imported by
89
+ // test/js/test_run.mjs under QuickJS, which has neither `self` (as the
90
+ // Worker global) nor a real postMessage/onmessage.
91
+ if (typeof self !== 'undefined' && typeof WorkerGlobalScope !== 'undefined') {
92
+ self.postOutput = (text) => self.postMessage({ type: 'output', text })
93
+
94
+ self.onmessage = async (event) => {
95
+ const { module, code } = event.data
96
+ try {
97
+ const { DefaultRubyVM } = await import(VM_ESM_URL)
98
+ const { vm } = await DefaultRubyVM(module, { consolePrint: false })
99
+ vm.eval(PRELUDE)
100
+ try {
101
+ vm.eval(code)
102
+ } catch (e) {
103
+ self.postMessage({ type: 'error', message: formatRunError(e) })
104
+ return
105
+ }
106
+ self.postMessage({ type: 'done' })
107
+ } catch (e) {
108
+ self.postMessage({ type: 'error', message: formatRunError(e) })
109
+ }
110
+ }
111
+ }
@@ -0,0 +1,350 @@
1
+ // RUN button for Ruby sample code: executes the sample in-browser with
2
+ // ruby.wasm, off the main thread in a Web Worker (theme/default/js/run-worker.js)
3
+ // so an infinite loop or long sleep in the sample can't freeze the page; a
4
+ // STOP button and a timeout both just Worker.terminate() it. Named .js
5
+ // rather than .mjs: module scripts are subject to strict MIME checking, and
6
+ // servers whose MIME table lacks an "mjs" entry (e.g. nginx before 1.21.4)
7
+ // serve .mjs as application/octet-stream, which browsers refuse to execute.
8
+ // The same reasoning applies to run-worker.js. Enabled per page via
9
+ // <meta name="rurema-run-ruby-wasm" content="<ruby+stdlib.wasm URL>">
10
+ // which the layout emits when statichtml was invoked with --run-ruby-wasm.
11
+ // The wasm URL is chosen by the build side to match the documented Ruby
12
+ // version. This file only compiles that wasm (WebAssembly.compileStreaming
13
+ // needs no loader library, just the bytes); the CDN-hosted @ruby/wasm-wasi
14
+ // loader is pinned and imported inside run-worker.js, which is the only
15
+ // place that actually instantiates a VM from the compiled module.
16
+ const OUTPUT_LIMIT = 64 * 1024
17
+ const RUN_TIMEOUT_MS = 30 * 1000
18
+
19
+ // Resolved relative to this module's own URL (not the page URL), so it keeps
20
+ // working regardless of how deep the current page is under the site root or
21
+ // how custom_js_url() built run.js's own <script src>. Computed lazily
22
+ // (rather than as a module-level constant) because the `URL` global does
23
+ // not exist under QuickJS, which imports this file for its pure-function
24
+ // tests without ever calling into the browser-only code path below.
25
+ function workerUrl() {
26
+ return new URL('run-worker.js', import.meta.url)
27
+ }
28
+
29
+ // Wrap an async loader so concurrent and repeated calls share one in-flight
30
+ // promise, while a rejected attempt clears the cache so the next call
31
+ // retries instead of staying broken forever.
32
+ export function createOnceLoader(loadFn) {
33
+ let cached
34
+ return function () {
35
+ if (!cached) {
36
+ cached = (async () => {
37
+ try {
38
+ return await loadFn()
39
+ } catch (error) {
40
+ cached = undefined
41
+ throw error
42
+ }
43
+ })()
44
+ }
45
+ return cached
46
+ }
47
+ }
48
+
49
+ export function formatRunError(error) {
50
+ const text = String((error && error.message) || error)
51
+ return text.split('\n').slice(0, 20).join('\n')
52
+ }
53
+
54
+ export function truncateOutput(text, limit = OUTPUT_LIMIT) {
55
+ if (text.length <= limit) return text
56
+ return text.slice(0, limit) + '\n... (truncated)'
57
+ }
58
+
59
+ // Appends one output chunk to the accumulated text, applying the same
60
+ // display cap as truncateOutput. Once the cap is hit, further chunks are
61
+ // dropped (not just re-truncating the same prefix again and again), so a
62
+ // runaway output loop does the "... (truncated)" marker once and then does
63
+ // O(1) work per chunk instead of O(output length).
64
+ export function accumulateOutput(current, chunk, limit = OUTPUT_LIMIT) {
65
+ if (current.endsWith('\n... (truncated)')) return current
66
+ return truncateOutput(current + chunk, limit)
67
+ }
68
+
69
+ export const STOPPED_NOTE = '(停止しました)'
70
+ // 出力が空のサンプルでも実行が終わったことがわかるように、
71
+ // 正常終了時も必ずこの注記を出力欄の末尾に付ける
72
+ export const DONE_NOTE = '(正常終了しました)'
73
+ export function timeoutNote(seconds) {
74
+ return `(${seconds}秒でタイムアウトしました)`
75
+ }
76
+
77
+ // Text used when flattening the highlighted sample for editing. The
78
+ // compiler emits a newline right after <code>, which textContent keeps —
79
+ // left as-is it shows up as an empty first line once the block becomes
80
+ // editable (script.js strips the same artifact for COPY).
81
+ export function editableText(text) {
82
+ return text.replace(/^\n+/, '')
83
+ }
84
+
85
+ // Compiles the wasm module once (cached like the old single-VM version) and
86
+ // hands a fresh Worker + that same compiled Module to each run. The Module
87
+ // is a structured-cloneable postMessage payload, so terminate()-ing a run
88
+ // and starting another never needs to recompile.
89
+ function createRunner(wasmUrl) {
90
+ const loadModule = createOnceLoader(async () => {
91
+ return WebAssembly.compileStreaming(fetch(wasmUrl)).catch(async () => {
92
+ // Retry without streaming for servers that send a non-wasm MIME type
93
+ // (e.g. `ruby -run -e httpd` when testing locally).
94
+ const response = await fetch(wasmUrl)
95
+ return WebAssembly.compile(await response.arrayBuffer())
96
+ })
97
+ })
98
+ return {
99
+ loadModule,
100
+ // One Worker per run (see module comment): terminate() always yields a
101
+ // clean slate, and samples never see another run's VM state.
102
+ spawn() {
103
+ return new Worker(workerUrl(), { type: 'module' })
104
+ },
105
+ }
106
+ }
107
+
108
+ function setupBlock(pre, runner) {
109
+ const code = pre.querySelector('code')
110
+ if (!code) return
111
+
112
+ const button = document.createElement('button')
113
+ button.type = 'button'
114
+ button.className = 'highlight__run-button'
115
+ button.textContent = 'RUN'
116
+ // script.js has already inserted the toolbar row (with the COPY button
117
+ // in its button group) right before the pre — its window.onload handler
118
+ // fires before our load listener. Prepend RUN into the same group so
119
+ // COPY stays rightmost. The buttons live outside the pre: keeping them
120
+ // inside required a zero top padding, made the cursor flicker between
121
+ // pointer and text, and broke the editable-code layout.
122
+ const toolbar = pre.previousElementSibling
123
+ let group = toolbar && toolbar.classList.contains('highlight__toolbar')
124
+ ? toolbar.querySelector('.highlight__button-group')
125
+ : null
126
+ if (!group) {
127
+ // script.js が無いテーマ向けの保険: 最小構成のツールバーを自前で作る
128
+ const ownToolbar = document.createElement('div')
129
+ ownToolbar.className = 'highlight__toolbar'
130
+ group = document.createElement('span')
131
+ group.className = 'highlight__button-group'
132
+ ownToolbar.appendChild(group)
133
+ pre.before(ownToolbar)
134
+ }
135
+ group.prepend(button)
136
+
137
+ let output
138
+ let outputTextNode
139
+ const ensureOutput = () => {
140
+ if (!output) {
141
+ output = document.createElement('pre')
142
+ output.className = 'highlight__run-output'
143
+ output.setAttribute('aria-live', 'polite')
144
+ // 出力テキストは専用のテキストノードに持つ(丸ごと差し替えられる)
145
+ outputTextNode = document.createTextNode('')
146
+ output.appendChild(outputTextNode)
147
+ pre.after(output)
148
+ // 実行結果もコピーできるようにする(script.js の公開フック)。
149
+ // 出力は実行のたびに変わるので、クリック時のテキストを渡す
150
+ if (window.ruremaAddCopyButton) {
151
+ window.ruremaAddCopyButton(output, () => outputTextNode.data)
152
+ // 出力欄のヘッダー行と出力欄は、入力欄の pre に密着させて
153
+ // 1つのブロックに見せる(境の 2px の白は CSS 側)。左端には
154
+ // 出力欄だとわかるように「実行結果」のタブを付ける
155
+ const outToolbar = output.previousElementSibling
156
+ if (outToolbar && outToolbar.classList.contains('highlight__toolbar')) {
157
+ outToolbar.classList.add('highlight__toolbar--attached')
158
+ const label = document.createElement('span')
159
+ label.className = 'caption'
160
+ label.textContent = '実行結果'
161
+ outToolbar.insertBefore(label, outToolbar.firstChild)
162
+ pre.classList.add('highlight--with-output')
163
+ }
164
+ }
165
+ }
166
+ return output
167
+ }
168
+ const setOutputText = (text) => {
169
+ outputTextNode.data = text
170
+ }
171
+
172
+ // plaintext-only が使えないブラウザ向けの paste フォールバック。
173
+ // 既定のリッチペーストは改行を <br>/<div> に変えたり先頭の改行を
174
+ // 落としたりする(貼り付けた内容が前の行にくっつく)ので、text/plain
175
+ // をキャレット位置にそのまま挿入する
176
+ const pastePlainText = (event) => {
177
+ if (!event.clipboardData) return
178
+ event.preventDefault()
179
+ const text = event.clipboardData.getData('text/plain')
180
+ const selection = window.getSelection()
181
+ if (!selection || selection.rangeCount === 0) return
182
+ const range = selection.getRangeAt(0)
183
+ range.deleteContents()
184
+ const node = document.createTextNode(text)
185
+ range.insertNode(node)
186
+ range.setStartAfter(node)
187
+ range.collapse(true)
188
+ selection.removeAllRanges()
189
+ selection.addRange(range)
190
+ }
191
+
192
+ // After the first run the sample becomes editable (like a scratchpad);
193
+ // Ctrl+Enter (or Cmd+Enter) re-runs the edited code. The COPY button
194
+ // keeps copying the original text.
195
+ const enableEditing = () => {
196
+ if (code.isContentEditable) return
197
+ // 編集でハイライトの span に文字が食い込むと、貼り付けた文字が
198
+ // その場の色を引き継いで中途半端に崩れるため、編集可能にする
199
+ // 時点でプレーンテキスト化して色を消す。コンパイラが <code> の
200
+ // 直後に置く改行(編集開始時に先頭の空行として見える)もここで除く
201
+ code.textContent = editableText(code.textContent)
202
+ // 改行入力・貼り付けが <br>/<div> にならないよう plaintext-only を
203
+ // 優先する。未対応ブラウザは代入が SyntaxError になるか値が変わらない
204
+ // ので、従来どおり true にして paste だけ自前で処理する
205
+ try {
206
+ code.contentEditable = 'plaintext-only'
207
+ } catch (e) {
208
+ // fall through
209
+ }
210
+ if (code.contentEditable !== 'plaintext-only') {
211
+ code.contentEditable = 'true'
212
+ code.addEventListener('paste', pastePlainText)
213
+ }
214
+ code.spellcheck = false
215
+ pre.dataset.editing = 'true'
216
+ pre.addEventListener('keydown', (event) => {
217
+ if (event.key === 'Enter' && (event.ctrlKey || event.metaKey)) {
218
+ event.preventDefault()
219
+ execute()
220
+ }
221
+ })
222
+ // code は内容ぶんの高さしかないので、その外側(pre の padding や
223
+ // 1行サンプルの下の余白)をクリックしたときも入力欄に入れるよう、
224
+ // pre 自身へのクリックで code にフォーカスして末尾にキャレットを置く
225
+ pre.addEventListener('click', (event) => {
226
+ if (event.target !== pre) return
227
+ code.focus()
228
+ const selection = window.getSelection()
229
+ if (!selection) return
230
+ const range = document.createRange()
231
+ range.selectNodeContents(code)
232
+ range.collapse(false)
233
+ selection.removeAllRanges()
234
+ selection.addRange(range)
235
+ })
236
+ }
237
+
238
+ // Non-null exactly while a run's Worker is alive; STOP terminates it and
239
+ // the timeout/message handlers all check this to avoid acting twice on
240
+ // the same run (e.g. a timeout racing a message that just arrived).
241
+ let current = null
242
+
243
+ const finish = (out, label, isError) => {
244
+ if (current && current.timer) clearTimeout(current.timer)
245
+ current = null
246
+ if (isError) out.classList.add('highlight__run-output--error')
247
+ enableEditing()
248
+ button.textContent = label
249
+ button.disabled = false
250
+ button.removeAttribute('aria-busy')
251
+ }
252
+
253
+ // Final notes (stop / timeout / the run's error message) are appended
254
+ // directly, bypassing accumulateOutput's 64KB cap: the cap exists to stop
255
+ // unbounded *output* growth, and these one-shot notes are bounded — they
256
+ // must stay visible precisely in the runaway-output case where the cap
257
+ // has already been hit and accumulateOutput would drop them.
258
+ const appendNote = (text) => {
259
+ const separator = outputTextNode.data === '' ? '' : '\n'
260
+ setOutputText(outputTextNode.data + separator + text)
261
+ }
262
+
263
+ const stop = () => {
264
+ if (!current) return
265
+ current.worker.terminate()
266
+ const out = ensureOutput()
267
+ appendNote(STOPPED_NOTE)
268
+ finish(out, 'RUN', false)
269
+ }
270
+
271
+ const execute = async () => {
272
+ if (current) return // mid-run click on the (now STOP-labeled) button; use the dedicated STOP path
273
+ if (button.disabled) return
274
+ button.disabled = true
275
+ button.setAttribute('aria-busy', 'true')
276
+ button.textContent = 'LOADING...'
277
+ const out = ensureOutput()
278
+ out.classList.remove('highlight__run-output--error')
279
+ setOutputText('')
280
+
281
+ let module
282
+ try {
283
+ module = await runner.loadModule()
284
+ } catch (e) {
285
+ setOutputText(formatRunError(e))
286
+ finish(out, 'RETRY', true)
287
+ return
288
+ }
289
+
290
+ const worker = runner.spawn()
291
+ const timer = setTimeout(() => {
292
+ if (!current) return
293
+ worker.terminate()
294
+ appendNote(timeoutNote(RUN_TIMEOUT_MS / 1000))
295
+ finish(out, 'RUN', false)
296
+ }, RUN_TIMEOUT_MS)
297
+ current = { worker, timer }
298
+
299
+ button.textContent = 'STOP'
300
+ button.disabled = false
301
+ button.setAttribute('aria-busy', 'true')
302
+
303
+ worker.onmessage = (event) => {
304
+ const message = event.data
305
+ if (!current || current.worker !== worker) return
306
+ if (message.type === 'output') {
307
+ setOutputText(accumulateOutput(outputTextNode.data, message.text))
308
+ } else if (message.type === 'done') {
309
+ appendNote(DONE_NOTE)
310
+ finish(out, 'RUN', false)
311
+ } else if (message.type === 'error') {
312
+ appendNote(message.message)
313
+ finish(out, 'RUN', true)
314
+ }
315
+ }
316
+ worker.onerror = (event) => {
317
+ if (!current || current.worker !== worker) return
318
+ event.preventDefault()
319
+ setOutputText(formatRunError(event.message || event))
320
+ finish(out, 'RETRY', true)
321
+ }
322
+ worker.postMessage({ module, code: code.textContent })
323
+ }
324
+ button.addEventListener('click', () => {
325
+ if (current) {
326
+ stop()
327
+ } else {
328
+ execute()
329
+ }
330
+ })
331
+ }
332
+
333
+ function init() {
334
+ const meta = document.querySelector('meta[name="rurema-run-ruby-wasm"]')
335
+ if (!meta || !meta.content) return
336
+ const blocks = document.querySelectorAll('pre.highlight.ruby')
337
+ if (blocks.length === 0) return
338
+ const runner = createRunner(meta.content)
339
+ blocks.forEach((pre) => setupBlock(pre, runner))
340
+ }
341
+
342
+ // Guarded so the module stays side-effect-free outside a browser
343
+ // (test/js/test_run.mjs imports it under QuickJS).
344
+ if (typeof window !== 'undefined' && typeof document !== 'undefined') {
345
+ if (document.readyState === 'complete') {
346
+ init()
347
+ } else {
348
+ window.addEventListener('load', init)
349
+ }
350
+ }
@@ -0,0 +1,134 @@
1
+ /*
2
+ * Vendored from RDoc's Aliki theme (lib/rdoc/generator/template/aliki/js/), RDoc v7.2.0.
3
+ * Copyright (c) 2025 Stan Lo. Released under the MIT License.
4
+ * See https://github.com/ruby/rdoc/blob/v7.2.0/LEGAL.rdoc
5
+ */
6
+ SearchController = function(data, input, result) {
7
+ this.data = data;
8
+ this.input = input;
9
+ this.result = result;
10
+
11
+ this.current = null;
12
+ this.view = this.result.parentNode;
13
+ this.ranker = new SearchRanker(data.index);
14
+ this.init();
15
+ }
16
+
17
+ SearchController.prototype = Object.assign({}, SearchNavigation, new function() {
18
+ var suid = 1;
19
+
20
+ this.init = function() {
21
+ var _this = this;
22
+ var observer = function(e) {
23
+ switch(e.key) {
24
+ case 'ArrowUp':
25
+ case 'ArrowDown':
26
+ return;
27
+ }
28
+ _this.search(_this.input.value);
29
+ };
30
+ this.input.addEventListener('keyup', observer);
31
+ this.input.addEventListener('click', observer); // mac's clear field
32
+
33
+ this.ranker.ready(function(results, isLast) {
34
+ _this.addResults(results, isLast);
35
+ })
36
+
37
+ this.initNavigation();
38
+ this.setNavigationActive(false);
39
+ }
40
+
41
+ this.search = function(value, selectFirstMatch) {
42
+ this.selectFirstMatch = selectFirstMatch;
43
+
44
+ value = value.trim();
45
+ if (value) {
46
+ this.setNavigationActive(true);
47
+ } else {
48
+ this.setNavigationActive(false);
49
+ }
50
+
51
+ if (value == '') {
52
+ this.lastQuery = value;
53
+ this.result.innerHTML = '';
54
+ this.result.setAttribute('aria-expanded', 'false');
55
+ this.setNavigationActive(false);
56
+ } else if (value != this.lastQuery) {
57
+ this.lastQuery = value;
58
+ this.result.setAttribute('aria-busy', 'true');
59
+ this.result.setAttribute('aria-expanded', 'true');
60
+ this.firstRun = true;
61
+ this.ranker.find(value);
62
+ }
63
+ }
64
+
65
+ this.addResults = function(results, isLast) {
66
+ var target = this.result;
67
+ if (this.firstRun && (results.length > 0 || isLast)) {
68
+ this.current = null;
69
+ this.result.innerHTML = '';
70
+ }
71
+
72
+ for (var i=0, l = results.length; i < l; i++) {
73
+ var item = this.renderItem.call(this, results[i]);
74
+ item.setAttribute('id', 'search-result-' + target.childElementCount);
75
+ target.appendChild(item);
76
+ };
77
+
78
+ if (this.firstRun && results.length > 0) {
79
+ this.firstRun = false;
80
+ this.current = target.firstChild;
81
+ this.current.classList.add('search-selected');
82
+ }
83
+ //TODO: ECMAScript
84
+ //if (jQuery.browser.msie) this.$element[0].className += '';
85
+
86
+ if (this.selectFirstMatch && this.current) {
87
+ this.selectFirstMatch = false;
88
+ this.select(this.current);
89
+ }
90
+
91
+ if (isLast) {
92
+ this.selectFirstMatch = false;
93
+ this.result.setAttribute('aria-busy', 'false');
94
+ }
95
+ }
96
+
97
+ this.move = function(isDown) {
98
+ if (!this.current) return;
99
+ var next = isDown ? this.current.nextElementSibling : this.current.previousElementSibling;
100
+ if (next) {
101
+ this.current.classList.remove('search-selected');
102
+ next.classList.add('search-selected');
103
+ this.input.setAttribute('aria-activedescendant', next.getAttribute('id'));
104
+ this.scrollInElement(next, this.result);
105
+ this.current = next;
106
+ this.input.value = next.firstChild.firstChild.text;
107
+ this.input.select();
108
+ }
109
+ return true;
110
+ }
111
+
112
+ this.hlt = function(html) {
113
+ return this.escapeHTML(html).
114
+ replace(/\u0001/g, '<em>').
115
+ replace(/\u0002/g, '</em>');
116
+ }
117
+
118
+ this.escapeHTML = function(html) {
119
+ return html.replace(/[&<>"`']/g, function(c) {
120
+ return '&#' + c.charCodeAt(0) + ';';
121
+ });
122
+ }
123
+
124
+ this.hide = function() {
125
+ this.result.setAttribute('aria-expanded', 'false');
126
+ this.setNavigationActive(false);
127
+ }
128
+
129
+ this.show = function() {
130
+ this.result.setAttribute('aria-expanded', 'true');
131
+ this.setNavigationActive(true);
132
+ }
133
+ });
134
+