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.
- checksums.yaml +4 -4
- data/Gemfile +3 -0
- data/README.md +65 -0
- data/Rakefile +32 -0
- data/data/bitclust/catalog/ja_JP.UTF-8 +19 -1
- data/data/bitclust/searchpage/index.html +90 -0
- data/data/bitclust/template/class +69 -8
- data/data/bitclust/template/function +2 -2
- data/data/bitclust/template/layout +11 -3
- data/data/bitclust/template/method +2 -2
- data/data/bitclust/template/search +1 -1
- data/data/bitclust/template.epub/class +22 -1
- data/data/bitclust/template.epub/function +2 -2
- data/data/bitclust/template.epub/layout +1 -3
- data/data/bitclust/template.epub/method +2 -2
- data/data/bitclust/template.lillia/class +26 -5
- data/data/bitclust/template.lillia/layout +1 -3
- data/data/bitclust/template.lillia/method +2 -2
- data/data/bitclust/template.offline/class +48 -4
- data/data/bitclust/template.offline/function +2 -2
- data/data/bitclust/template.offline/layout +27 -3
- data/data/bitclust/template.offline/method +2 -2
- data/lib/bitclust/app.rb +16 -17
- data/lib/bitclust/capi_converter.rb +20 -0
- data/lib/bitclust/classentry.rb +67 -17
- data/lib/bitclust/compat.rb +2 -2
- data/lib/bitclust/completion.rb +58 -27
- data/lib/bitclust/crossrubyutils.rb +8 -5
- data/lib/bitclust/database.rb +28 -8
- data/lib/bitclust/doc_converter.rb +63 -0
- data/lib/bitclust/docentry.rb +1 -1
- data/lib/bitclust/entity_splitter.rb +221 -0
- data/lib/bitclust/entry.rb +47 -7
- data/lib/bitclust/functiondatabase.rb +13 -1
- data/lib/bitclust/functionentry.rb +5 -1
- data/lib/bitclust/functionreferenceparser.rb +4 -4
- data/lib/bitclust/generators/epub.rb +10 -3
- data/lib/bitclust/htmlutils.rb +1 -1
- data/lib/bitclust/include_graph.rb +375 -0
- data/lib/bitclust/include_pruner.rb +144 -0
- data/lib/bitclust/interface.rb +1 -1
- data/lib/bitclust/libraryentry.rb +8 -7
- data/lib/bitclust/lineinput.rb +7 -5
- data/lib/bitclust/link_checker.rb +179 -0
- data/lib/bitclust/markdown_bridge.rb +186 -0
- data/lib/bitclust/markdown_orchestrator.rb +267 -0
- data/lib/bitclust/markdown_to_rrd.rb +824 -0
- data/lib/bitclust/markdown_tree.rb +217 -0
- data/lib/bitclust/mdcompiler.rb +883 -0
- data/lib/bitclust/mdparser.rb +347 -0
- data/lib/bitclust/messagecatalog.rb +3 -3
- data/lib/bitclust/method_since_calculator.rb +117 -0
- data/lib/bitclust/methoddatabase.rb +124 -14
- data/lib/bitclust/methodentry.rb +125 -7
- data/lib/bitclust/methodid.rb +12 -11
- data/lib/bitclust/methodsignature.rb +10 -4
- data/lib/bitclust/nameutils.rb +41 -22
- data/lib/bitclust/preprocessor.rb +66 -29
- data/lib/bitclust/rdcompiler.rb +195 -95
- data/lib/bitclust/refsdatabase.rb +23 -8
- data/lib/bitclust/reloadable_request_handler.rb +72 -0
- data/lib/bitclust/requesthandler.rb +21 -16
- data/lib/bitclust/ridatabase.rb +10 -4
- data/lib/bitclust/rrd_to_markdown.rb +925 -0
- data/lib/bitclust/rrdparser.rb +196 -30
- data/lib/bitclust/runner.rb +17 -2
- data/lib/bitclust/screen.rb +80 -11
- data/lib/bitclust/search_index_generator.rb +288 -0
- data/lib/bitclust/searcher.rb +69 -20
- data/lib/bitclust/simplesearcher.rb +24 -12
- data/lib/bitclust/subcommand.rb +7 -1
- data/lib/bitclust/subcommands/ancestors_command.rb +9 -2
- data/lib/bitclust/subcommands/checklink_command.rb +54 -0
- data/lib/bitclust/subcommands/chm_command.rb +15 -7
- data/lib/bitclust/subcommands/classes_command.rb +5 -0
- data/lib/bitclust/subcommands/extract_command.rb +5 -0
- data/lib/bitclust/subcommands/htmlfile_command.rb +21 -7
- data/lib/bitclust/subcommands/init_command.rb +4 -1
- data/lib/bitclust/subcommands/list_command.rb +12 -4
- data/lib/bitclust/subcommands/lookup_command.rb +28 -9
- data/lib/bitclust/subcommands/methods_command.rb +10 -2
- data/lib/bitclust/subcommands/methodsince_command.rb +80 -0
- data/lib/bitclust/subcommands/preproc_command.rb +5 -0
- data/lib/bitclust/subcommands/property_command.rb +2 -1
- data/lib/bitclust/subcommands/searchpage_command.rb +102 -0
- data/lib/bitclust/subcommands/server_command.rb +10 -8
- data/lib/bitclust/subcommands/setup_command.rb +24 -10
- data/lib/bitclust/subcommands/statichtml_command.rb +129 -6
- data/lib/bitclust/subcommands/update_command.rb +110 -2
- data/lib/bitclust/syntax_highlighter.rb +48 -4
- data/lib/bitclust/textutils.rb +3 -3
- data/lib/bitclust/version.rb +1 -1
- data/lib/bitclust/version_badges.rb +90 -0
- data/lib/bitclust/whole_file_gate.rb +137 -0
- data/test/test_app.rb +269 -0
- data/test/test_bitclust.rb +2 -0
- data/test/test_capi_converter.rb +91 -0
- data/test/test_class_screen.rb +413 -0
- data/test/test_copy_doc_md.rb +53 -0
- data/test/test_doc_converter.rb +78 -0
- data/test/test_entity_splitter.rb +231 -0
- data/test/test_entry.rb +207 -2
- data/test/test_eol_warning.rb +43 -0
- data/test/test_functionreferenceparser.rb +11 -0
- data/test/test_include_graph.rb +640 -0
- data/test/test_include_pruner.rb +133 -0
- data/test/test_link_checker.rb +114 -0
- data/test/test_lookup_command.rb +135 -0
- data/test/test_markdown_bridge.rb +217 -0
- data/test/test_markdown_orchestrator.rb +478 -0
- data/test/test_markdown_to_rrd.rb +757 -0
- data/test/test_markdown_tree.rb +369 -0
- data/test/test_mdcompiler.rb +1276 -0
- data/test/test_mdparser.rb +538 -0
- data/test/test_method_screen.rb +166 -0
- data/test/test_method_since_calculator.rb +290 -0
- data/test/test_methoddatabase.rb +16 -0
- data/test/test_methodentry.rb +328 -0
- data/test/test_methodsignature.rb +3 -1
- data/test/test_methodsince_command.rb +168 -0
- data/test/test_nameutils.rb +22 -0
- data/test/test_preprocessor.rb +215 -0
- data/test/test_rdcompiler.rb +401 -30
- data/test/test_refsdatabase.rb +32 -0
- data/test/test_rrd_to_markdown.rb +968 -0
- data/test/test_rrdparser.rb +295 -2
- data/test/test_run_ruby_wasm.rb +54 -0
- data/test/test_run_worker_prelude.rb +83 -0
- data/test/test_runner.rb +51 -0
- data/test/test_search_index_generator.rb +513 -0
- data/test/test_search_screen.rb +48 -0
- data/test/test_searcher.rb +132 -0
- data/test/test_searchpage_command.rb +167 -0
- data/test/test_simplesearcher.rb +28 -0
- data/test/test_statichtml_command.rb +204 -0
- data/test/test_syntax_highlighter.rb +60 -0
- data/test/test_whole_file_gate.rb +175 -0
- data/theme/default/js/NOTICE +42 -0
- data/theme/default/js/run-worker.js +111 -0
- data/theme/default/js/run.js +350 -0
- data/theme/default/js/search_controller.js +134 -0
- data/theme/default/js/search_init.js +176 -0
- data/theme/default/js/search_navigation.js +110 -0
- data/theme/default/js/search_page.js +202 -0
- data/theme/default/js/search_ranker.js +244 -0
- data/theme/default/script.js +91 -18
- data/theme/default/search.css +149 -0
- data/theme/default/style.css +193 -21
- data/theme/default/syntax-highlight.css +8 -0
- data/theme/lillia/style.css +45 -1
- metadata +161 -20
- 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
|
+
|