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,176 @@
1
+ 'use strict';
2
+ /*
3
+ * Part of BitClust. Distributed under the Ruby License or the MIT License.
4
+ *
5
+ * Wires up the client-side search box for statichtml output.
6
+ *
7
+ * The sibling files search_navigation.js, search_ranker.js and
8
+ * search_controller.js are vendored verbatim from RDoc's Aliki theme.
9
+ * They expect two globals to be defined before this script runs:
10
+ *
11
+ * search_data - defined by js/search_data.js (the index)
12
+ * index_rel_prefix - relative path from the current page to the site root,
13
+ * set inline in the page <head> by the layout template.
14
+ */
15
+ (function() {
16
+ // BitClust extension (the vendored Aliki files above are kept verbatim).
17
+ //
18
+ // RDoc's parseQuery() rewrites "." to "::" (its class-method separator) and
19
+ // matches "::"/"."/"#" queries against full_name instead of name. Ruby's
20
+ // special variables are indexed with their "$" sigil in full_name, and one of
21
+ // them ($.) contains a literal "." — so a "$." query would become "$::" and
22
+ // never match. Wrap parseQuery so that any "$"-prefixed (special variable)
23
+ // query keeps its literal text and is matched against full_name.
24
+ // See https://github.com/rurema/bitclust/issues/194
25
+ //
26
+ // Separately (bitclust#279): BitClust keeps a literal "." in full_name for
27
+ // singleton methods ("File.open") and ".#"/"?." for module functions
28
+ // ("Kernel.#open" pre-4.0, "Kernel?.open" 4.0+ — see #250), instead of
29
+ // RDoc's "::" convention. The same "." -> "::" rewrite above then makes
30
+ // every one of those dot-qualified queries miss its own entry. Fold "?."
31
+ // to ".#" here so both module-function spellings collapse to the same
32
+ // query text before the "." -> "::" rewrite runs; the other half of the
33
+ // fix (matching against SearchIndexGenerator's match_name instead of
34
+ // full_name) is the computeScore wrap below.
35
+ if (typeof parseQuery === 'function') {
36
+ var alikiParseQuery = parseQuery;
37
+ parseQuery = function(query) {
38
+ var folded = query.indexOf('?.') === -1 ? query : query.replace(/\?\./g, '.#');
39
+ var q = alikiParseQuery(folded);
40
+ if (query.charAt(0) === '$') {
41
+ q.normalized = query.toLowerCase(); // undo the "." -> "::" rewrite
42
+ q.matchesFullName = true; // the "$" sigil lives in full_name
43
+ }
44
+ q.original = query; // keep the real original text, not the folded one
45
+ return q;
46
+ };
47
+ }
48
+
49
+ // bitclust#279 (continued): computeScore() scores a query against
50
+ // entry.full_name, which — per the comment above — doesn't use "::" the
51
+ // way parseQuery's rewrite expects. SearchIndexGenerator adds a
52
+ // match_name to affected entries (full_name with "?." folded to ".#" and
53
+ // then every "." turned into "::", mirroring parseQuery's own rewrite —
54
+ // see search_index_generator.rb's header comment). When an entry carries
55
+ // one, score a shallow clone with full_name swapped to match_name instead
56
+ // — the original entry object (and its displayed full_name) is never
57
+ // touched, so this only affects which entries match, not what is shown.
58
+ // Entries without a match_name (instance methods, constants, classes,
59
+ // special variables, ...) are scored exactly as before.
60
+ if (typeof computeScore === 'function') {
61
+ var alikiComputeScore = computeScore;
62
+ computeScore = function(entry, q) {
63
+ if (entry && entry.match_name) {
64
+ return alikiComputeScore({ name: entry.name, full_name: entry.match_name, type: entry.type }, q);
65
+ }
66
+ return alikiComputeScore(entry, q);
67
+ };
68
+ }
69
+
70
+ // bitclust#279 (continued): highlightMatch() compares q.normalized (with
71
+ // "." already rewritten to "::" and "?." folded to ".#" by the wrap
72
+ // above) against the displayed full_name, which keeps its literal
73
+ // "."/".#"/"?." — so the dot-qualified queries fixed above matched their
74
+ // entry but never earned an <em> highlight (at best the vendored fuzzy
75
+ // fallback gave up at the first ":"). Whenever the vendored code found
76
+ // nothing to mark, retry a contiguous match with the user's *original*
77
+ // query text (q.original, restored by the parseQuery wrap), treating
78
+ // ".#" and "?." as the same module-function spelling. Both marks are two
79
+ // characters long, so indexes into the folded strings map 1:1 onto the
80
+ // displayed text and the markers can be spliced straight into it.
81
+ if (typeof highlightMatch === 'function') {
82
+ var alikiHighlightMatch = highlightMatch;
83
+ highlightMatch = function(text, q) {
84
+ var marked = alikiHighlightMatch(text, q);
85
+ if (marked !== text) return marked; // the vendored code managed
86
+ if (!text || !q || !q.original) return marked;
87
+ var canonText = text.toLowerCase().replace(/\.#/g, '?.');
88
+ var canonQuery = q.original.toLowerCase().replace(/\.#/g, '?.');
89
+ if (!canonQuery) return marked;
90
+ var start = canonText.indexOf(canonQuery);
91
+ if (start === -1) return marked;
92
+ var end = start + canonQuery.length;
93
+ return text.substring(0, start) +
94
+ '\u0001' + text.substring(start, end) + '\u0002' +
95
+ text.substring(end);
96
+ };
97
+ }
98
+
99
+ function createSearchInstance(input, result) {
100
+ if (!input || !result) return null;
101
+
102
+ var search = new SearchController(search_data, input, result);
103
+
104
+ search.renderItem = function(r) {
105
+ var li = document.createElement('li');
106
+ var html = '<p class="search-match"><a href="' +
107
+ index_rel_prefix + this.escapeHTML(r.path) + '">' +
108
+ this.hlt(r.title) + '</a>';
109
+ if (r.type) {
110
+ var typeClass = r.type.replace(/_/g, '-');
111
+ html += '<span class="search-type search-type-' +
112
+ this.escapeHTML(typeClass) + '">' + this.formatType(r.type) + '</span>';
113
+ }
114
+ html += '</p>';
115
+ if (r.snippet) html += '<div class="search-snippet">' + r.snippet + '</div>';
116
+ li.innerHTML = html;
117
+ return li;
118
+ };
119
+
120
+ search.formatType = function(type) {
121
+ var labels = {
122
+ 'class': 'class', 'module': 'module', 'object': 'object',
123
+ 'constant': 'const', 'instance_method': 'method', 'class_method': 'method',
124
+ 'variable': 'var', 'library': 'lib', 'document': 'doc', 'function': 'func',
125
+ 'heading': 'section'
126
+ };
127
+ return labels[type] || type;
128
+ };
129
+
130
+ search.select = function(selected) {
131
+ if (selected) window.location.href = selected.firstChild.firstChild.href;
132
+ };
133
+
134
+ return search;
135
+ }
136
+
137
+ function hookSearch() {
138
+ var input = document.querySelector('#search-field');
139
+ var result = document.querySelector('#search-results');
140
+ if (!input || !result) return;
141
+
142
+ var search = createSearchInstance(input, result);
143
+ if (!search) return;
144
+
145
+ document.addEventListener('click', function(e) {
146
+ if (!e.target.closest('#search-section')) search.hide();
147
+ });
148
+ document.addEventListener('keydown', function(e) {
149
+ if (e.key === 'Escape' && input.matches(':focus')) { search.hide(); input.blur(); }
150
+ });
151
+ input.addEventListener('focus', function() {
152
+ if (input.value.trim()) search.show();
153
+ });
154
+
155
+ if (typeof URLSearchParams !== 'undefined') {
156
+ var q = new URLSearchParams(window.location.search).get('q');
157
+ if (q) { input.value = q; search.search(q, false); }
158
+ }
159
+ }
160
+
161
+ // Press "/" anywhere to focus the search box.
162
+ function hookFocus() {
163
+ document.addEventListener('keydown', function(e) {
164
+ if (document.activeElement && document.activeElement.tagName === 'INPUT') return;
165
+ if (e.key === '/') {
166
+ var f = document.querySelector('#search-field');
167
+ if (f) { e.preventDefault(); f.focus(); }
168
+ }
169
+ });
170
+ }
171
+
172
+ document.addEventListener('DOMContentLoaded', function() {
173
+ hookSearch();
174
+ hookFocus();
175
+ });
176
+ })();
@@ -0,0 +1,110 @@
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
+ /*
7
+ * SearchNavigation allows movement using the arrow keys through the search results.
8
+ *
9
+ * When using this library you will need to set scrollIntoView to the
10
+ * appropriate function for your layout. Use scrollInWindow if the container
11
+ * is not scrollable and scrollInElement if the container is a separate
12
+ * scrolling region.
13
+ */
14
+ SearchNavigation = new function() {
15
+ this.initNavigation = function() {
16
+ var _this = this;
17
+
18
+ document.addEventListener('keydown', function(e) {
19
+ _this.onkeydown(e);
20
+ });
21
+
22
+ this.navigationActive = true;
23
+ }
24
+
25
+ this.setNavigationActive = function(state) {
26
+ this.navigationActive = state;
27
+ }
28
+
29
+ this.onkeydown = function(e) {
30
+ if (!this.navigationActive) return;
31
+ switch(e.key) {
32
+ case 'ArrowLeft':
33
+ if (this.moveLeft()) e.preventDefault();
34
+ break;
35
+ case 'ArrowUp':
36
+ if (e.key == 'ArrowUp' || e.ctrlKey) {
37
+ if (this.moveUp()) e.preventDefault();
38
+ }
39
+ break;
40
+ case 'ArrowRight':
41
+ if (this.moveRight()) e.preventDefault();
42
+ break;
43
+ case 'ArrowDown':
44
+ if (e.key == 'ArrowDown' || e.ctrlKey) {
45
+ if (this.moveDown()) e.preventDefault();
46
+ }
47
+ break;
48
+ case 'Enter':
49
+ if (this.current) e.preventDefault();
50
+ this.select(this.current);
51
+ break;
52
+ }
53
+ if (e.ctrlKey && e.shiftKey) this.select(this.current);
54
+ }
55
+
56
+ this.moveRight = function() {
57
+ }
58
+
59
+ this.moveLeft = function() {
60
+ }
61
+
62
+ this.move = function(isDown) {
63
+ }
64
+
65
+ this.moveUp = function() {
66
+ return this.move(false);
67
+ }
68
+
69
+ this.moveDown = function() {
70
+ return this.move(true);
71
+ }
72
+
73
+ /*
74
+ * Scrolls to the given element in the scrollable element view.
75
+ */
76
+ this.scrollInElement = function(element, view) {
77
+ var offset, viewHeight, viewScroll, height;
78
+ offset = element.offsetTop;
79
+ height = element.offsetHeight;
80
+ viewHeight = view.offsetHeight;
81
+ viewScroll = view.scrollTop;
82
+
83
+ if (offset - viewScroll + height > viewHeight) {
84
+ view.scrollTop = offset - viewHeight + height;
85
+ }
86
+ if (offset < viewScroll) {
87
+ view.scrollTop = offset;
88
+ }
89
+ }
90
+
91
+ /*
92
+ * Scrolls to the given element in the window. The second argument is
93
+ * ignored
94
+ */
95
+ this.scrollInWindow = function(element, ignored) {
96
+ var offset, viewHeight, viewScroll, height;
97
+ offset = element.offsetTop;
98
+ height = element.offsetHeight;
99
+ viewHeight = window.innerHeight;
100
+ viewScroll = window.scrollY;
101
+
102
+ if (offset - viewScroll + height > viewHeight) {
103
+ window.scrollTo(window.scrollX, offset - viewHeight + height);
104
+ }
105
+ if (offset < viewScroll) {
106
+ window.scrollTo(window.scrollX, offset);
107
+ }
108
+ }
109
+ }
110
+
@@ -0,0 +1,202 @@
1
+ 'use strict';
2
+ /*
3
+ * Part of BitClust. Distributed under the Ruby License or the MIT License.
4
+ *
5
+ * Wires up the standalone cross-version search page (the static replacement
6
+ * for the server-backed rurema-search app at /ja/search/).
7
+ *
8
+ * The sibling files search_navigation.js, search_ranker.js and
9
+ * search_controller.js are vendored verbatim from RDoc's Aliki theme.
10
+ * They expect these globals to be defined before this script runs:
11
+ *
12
+ * search_data - defined by js/search_data.js. Each entry carries a
13
+ * `versions` array (ascending) added by
14
+ * SearchIndexGenerator.merge.
15
+ * search_versions - all indexed versions, ascending (inline in the page).
16
+ * search_version_base - relative path from this page to the directory that
17
+ * holds the per-version document roots ("../" when the
18
+ * page lives at /ja/search/).
19
+ */
20
+ (function() {
21
+ // BitClust extension (the vendored Aliki files above are kept verbatim).
22
+ //
23
+ // Same special-variable handling as search_init.js: keep "$"-prefixed
24
+ // queries literal (no "." -> "::" rewrite) and match them against
25
+ // full_name, where the "$" sigil lives.
26
+ // See https://github.com/rurema/bitclust/issues/194
27
+ //
28
+ // Same bitclust#279 handling as search_init.js too: fold "?." to ".#" so
29
+ // both module-function spellings ("Kernel.#open" pre-4.0, "Kernel?.open"
30
+ // 4.0+ — see #250) normalize the same way, and score dot-qualified
31
+ // entries via their match_name (added by SearchIndexGenerator) instead of
32
+ // full_name, since BitClust keeps a literal "." there for display while
33
+ // parseQuery's rewrite expects "::". See search_init.js for the full
34
+ // rationale; SearchIndexGenerator.merge keeps match_name on merged
35
+ // cross-version entries same as any other field.
36
+ if (typeof parseQuery === 'function') {
37
+ var alikiParseQuery = parseQuery;
38
+ parseQuery = function(query) {
39
+ var folded = query.indexOf('?.') === -1 ? query : query.replace(/\?\./g, '.#');
40
+ var q = alikiParseQuery(folded);
41
+ if (query.charAt(0) === '$') {
42
+ q.normalized = query.toLowerCase();
43
+ q.matchesFullName = true;
44
+ }
45
+ q.original = query;
46
+ return q;
47
+ };
48
+ }
49
+
50
+ if (typeof computeScore === 'function') {
51
+ var alikiComputeScore = computeScore;
52
+ computeScore = function(entry, q) {
53
+ if (entry && entry.match_name) {
54
+ return alikiComputeScore({ name: entry.name, full_name: entry.match_name, type: entry.type }, q);
55
+ }
56
+ return alikiComputeScore(entry, q);
57
+ };
58
+ }
59
+
60
+ // Same highlightMatch wrap as search_init.js: when the vendored code
61
+ // found nothing to mark (dot-qualified queries never contiguous-match a
62
+ // literal-dot full_name once "." has been rewritten to "::"), retry with
63
+ // the user's original query text, treating ".#" and "?." as the same
64
+ // module-function spelling. On this page the merged index only carries
65
+ // the "?." spelling (SearchIndexGenerator.merge folds ".#" away), so
66
+ // this is what lets a query typed in the pre-4.0 ".#" notation still
67
+ // light up the "?."-spelled row it matched. Both marks are two
68
+ // characters, so indexes into the folded strings map 1:1 onto the text.
69
+ if (typeof highlightMatch === 'function') {
70
+ var alikiHighlightMatch = highlightMatch;
71
+ highlightMatch = function(text, q) {
72
+ var marked = alikiHighlightMatch(text, q);
73
+ if (marked !== text) return marked; // the vendored code managed
74
+ if (!text || !q || !q.original) return marked;
75
+ var canonText = text.toLowerCase().replace(/\.#/g, '?.');
76
+ var canonQuery = q.original.toLowerCase().replace(/\.#/g, '?.');
77
+ if (!canonQuery) return marked;
78
+ var start = canonText.indexOf(canonQuery);
79
+ if (start === -1) return marked;
80
+ var end = start + canonQuery.length;
81
+ return text.substring(0, start) +
82
+ '\u0001' + text.substring(start, end) + '\u0002' +
83
+ text.substring(end);
84
+ };
85
+ }
86
+
87
+ function versionHref(version, path) {
88
+ return search_version_base + version + '/' + path;
89
+ }
90
+
91
+ function compareVersions(a, b) {
92
+ var as = a.split('.');
93
+ var bs = b.split('.');
94
+ for (var i = 0; i < Math.max(as.length, bs.length); i++) {
95
+ var d = parseInt(as[i] || '0', 10) - parseInt(bs[i] || '0', 10);
96
+ if (d) return d;
97
+ }
98
+ return 0;
99
+ }
100
+
101
+ // ranker の formatResult は {title, path, type} だけを結果に残し、統合 index の
102
+ // versions を落とす(vendored ファイルは verbatim 維持)。path から versions を
103
+ // 引けるよう対応表を作る。同じ path が複数エントリに現れる稀なケース
104
+ // (版によって type 等が違う)は、ページとしては存在する版の和集合が正しい
105
+ function buildVersionsByPath() {
106
+ var map = {};
107
+ for (var i = 0; i < search_data.index.length; i++) {
108
+ var e = search_data.index[i];
109
+ var list = map[e.path] || (map[e.path] = []);
110
+ var vs = e.versions || [];
111
+ for (var j = 0; j < vs.length; j++) {
112
+ if (list.indexOf(vs[j]) < 0) list.push(vs[j]);
113
+ }
114
+ }
115
+ return map;
116
+ }
117
+
118
+ // rurema-search compatibility: accept ?q=, its ?query= parameter, and its
119
+ // /query:<word>/ path form (the latter only reaches this page when the
120
+ // server rewrites unknown /ja/search/* paths here).
121
+ function initialQuery() {
122
+ var q = null;
123
+ if (typeof URLSearchParams !== 'undefined') {
124
+ var params = new URLSearchParams(window.location.search);
125
+ q = params.get('q') || params.get('query');
126
+ }
127
+ if (!q) {
128
+ var m = window.location.pathname.match(/\/query:([^\/]+)/);
129
+ if (m) {
130
+ try {
131
+ q = decodeURIComponent(m[1]);
132
+ } catch (e) {
133
+ q = m[1];
134
+ }
135
+ }
136
+ }
137
+ return q;
138
+ }
139
+
140
+ function init() {
141
+ var input = document.querySelector('#search-field');
142
+ var result = document.querySelector('#search-results');
143
+ if (!input || !result) return;
144
+
145
+ var search = new SearchController(search_data, input, result);
146
+ var versionsByPath = buildVersionsByPath();
147
+
148
+ search.renderItem = function(r) {
149
+ var li = document.createElement('li');
150
+ var versions = (versionsByPath[r.path] || [])
151
+ .slice().sort(compareVersions).reverse(); // newest first
152
+ var newest = versions[0] || '';
153
+ var html = '<p class="search-match"><a href="' +
154
+ this.escapeHTML(versionHref(newest, r.path)) + '">' +
155
+ this.hlt(r.title) + '</a>';
156
+ if (r.type) {
157
+ var typeClass = r.type.replace(/_/g, '-');
158
+ html += '<span class="search-type search-type-' +
159
+ this.escapeHTML(typeClass) + '">' + this.formatType(r.type) + '</span>';
160
+ }
161
+ html += '</p><p class="search-versions">';
162
+ for (var i = 0; i < versions.length; i++) {
163
+ html += '<a href="' + this.escapeHTML(versionHref(versions[i], r.path)) +
164
+ '">' + this.escapeHTML(versions[i]) + '</a>';
165
+ }
166
+ html += '</p>';
167
+ li.innerHTML = html;
168
+ return li;
169
+ };
170
+
171
+ search.formatType = function(type) {
172
+ var labels = {
173
+ 'class': 'class', 'module': 'module', 'object': 'object',
174
+ 'constant': 'const', 'instance_method': 'method', 'class_method': 'method',
175
+ 'variable': 'var', 'library': 'lib', 'document': 'doc', 'function': 'func',
176
+ 'heading': 'section'
177
+ };
178
+ return labels[type] || type;
179
+ };
180
+
181
+ search.select = function(selected) {
182
+ if (selected) window.location.href = selected.firstChild.firstChild.href;
183
+ };
184
+
185
+ // Keep the query shareable: mirror the box into the ?q= parameter.
186
+ input.addEventListener('input', function() {
187
+ var q = input.value.trim();
188
+ var url = q ? '?q=' + encodeURIComponent(q) : window.location.pathname;
189
+ window.history.replaceState(null, '', url);
190
+ });
191
+
192
+ var q = initialQuery();
193
+ if (q) {
194
+ input.value = q;
195
+ window.history.replaceState(null, '', '?q=' + encodeURIComponent(q));
196
+ search.search(q, false);
197
+ }
198
+ input.focus();
199
+ }
200
+
201
+ document.addEventListener('DOMContentLoaded', init);
202
+ })();