rdoc 6.15.1 → 6.16.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 (48) hide show
  1. checksums.yaml +4 -4
  2. data/History.rdoc +1 -1
  3. data/lib/rdoc/code_object/top_level.rb +18 -17
  4. data/lib/rdoc/comment.rb +190 -8
  5. data/lib/rdoc/generator/aliki.rb +42 -0
  6. data/lib/rdoc/generator/template/aliki/_aside_toc.rhtml +8 -0
  7. data/lib/rdoc/generator/template/aliki/_footer.rhtml +23 -0
  8. data/lib/rdoc/generator/template/aliki/_head.rhtml +91 -0
  9. data/lib/rdoc/generator/template/aliki/_header.rhtml +56 -0
  10. data/lib/rdoc/generator/template/aliki/_sidebar_ancestors.rhtml +6 -0
  11. data/lib/rdoc/generator/template/aliki/_sidebar_classes.rhtml +5 -0
  12. data/lib/rdoc/generator/template/aliki/_sidebar_extends.rhtml +15 -0
  13. data/lib/rdoc/generator/template/aliki/_sidebar_includes.rhtml +15 -0
  14. data/lib/rdoc/generator/template/aliki/_sidebar_installed.rhtml +16 -0
  15. data/lib/rdoc/generator/template/aliki/_sidebar_methods.rhtml +21 -0
  16. data/lib/rdoc/generator/template/aliki/_sidebar_pages.rhtml +37 -0
  17. data/lib/rdoc/generator/template/aliki/_sidebar_search.rhtml +15 -0
  18. data/lib/rdoc/generator/template/aliki/_sidebar_sections.rhtml +11 -0
  19. data/lib/rdoc/generator/template/aliki/_sidebar_toggle.rhtml +3 -0
  20. data/lib/rdoc/generator/template/aliki/class.rhtml +219 -0
  21. data/lib/rdoc/generator/template/aliki/css/rdoc.css +1612 -0
  22. data/lib/rdoc/generator/template/aliki/index.rhtml +21 -0
  23. data/lib/rdoc/generator/template/aliki/js/aliki.js +483 -0
  24. data/lib/rdoc/generator/template/aliki/js/c_highlighter.js +299 -0
  25. data/lib/rdoc/generator/template/aliki/js/search.js +120 -0
  26. data/lib/rdoc/generator/template/aliki/js/theme-toggle.js +112 -0
  27. data/lib/rdoc/generator/template/aliki/page.rhtml +17 -0
  28. data/lib/rdoc/generator/template/aliki/servlet_not_found.rhtml +14 -0
  29. data/lib/rdoc/generator/template/aliki/servlet_root.rhtml +65 -0
  30. data/lib/rdoc/generator/template/darkfish/_head.rhtml +2 -7
  31. data/lib/rdoc/generator/template/darkfish/_sidebar_search.rhtml +1 -0
  32. data/lib/rdoc/generator/template/darkfish/table_of_contents.rhtml +1 -1
  33. data/lib/rdoc/generator/template/json_index/js/searcher.js +5 -1
  34. data/lib/rdoc/generator.rb +1 -0
  35. data/lib/rdoc/markup/pre_process.rb +34 -10
  36. data/lib/rdoc/markup/to_html.rb +6 -4
  37. data/lib/rdoc/options.rb +21 -10
  38. data/lib/rdoc/parser/c.rb +15 -46
  39. data/lib/rdoc/parser/prism_ruby.rb +121 -113
  40. data/lib/rdoc/parser/ruby.rb +8 -8
  41. data/lib/rdoc/parser/ruby_tools.rb +5 -7
  42. data/lib/rdoc/parser/simple.rb +4 -21
  43. data/lib/rdoc/rdoc.rb +1 -0
  44. data/lib/rdoc/text.rb +1 -1
  45. data/lib/rdoc/token_stream.rb +13 -1
  46. data/lib/rdoc/tom_doc.rb +1 -1
  47. data/lib/rdoc/version.rb +1 -1
  48. metadata +27 -2
@@ -0,0 +1,21 @@
1
+ <body role="document" class="file has-toc">
2
+ <%= render '_header.rhtml' %>
3
+ <%= render '_sidebar_toggle.rhtml' %>
4
+
5
+ <nav id="navigation" role="navigation">
6
+ <%= render '_sidebar_pages.rhtml' %>
7
+ <%= render '_sidebar_classes.rhtml' %>
8
+ </nav>
9
+
10
+ <main role="main">
11
+ <%- if @main_page %>
12
+ <%= @main_page.description %>
13
+ <%- else %>
14
+ <p>This is the API documentation for <%= h @title %>.</p>
15
+ <%- end %>
16
+ </main>
17
+
18
+ <%= render '_aside_toc.rhtml' %>
19
+
20
+ <%= render '_footer.rhtml' %>
21
+ </body>
@@ -0,0 +1,483 @@
1
+ 'use strict';
2
+
3
+ /* ===== Method Source Code Toggling ===== */
4
+
5
+ function showSource(e) {
6
+ let target = e.target;
7
+ while (!target.classList.contains('method-detail')) {
8
+ target = target.parentNode;
9
+ }
10
+ if (typeof target !== "undefined" && target !== null) {
11
+ target = target.querySelector('.method-source-code');
12
+ }
13
+ if (typeof target !== "undefined" && target !== null) {
14
+ target.classList.toggle('active-menu')
15
+ }
16
+ }
17
+
18
+ function hookSourceViews() {
19
+ document.querySelectorAll('.method-source-toggle').forEach((codeObject) => {
20
+ codeObject.addEventListener('click', showSource);
21
+ });
22
+ }
23
+
24
+ /* ===== Search Functionality ===== */
25
+
26
+ function createSearchInstance(input, result) {
27
+ if (!input || !result) return null;
28
+
29
+ result.classList.remove("initially-hidden");
30
+
31
+ const search = new Search(search_data, input, result);
32
+
33
+ search.renderItem = function(result) {
34
+ const li = document.createElement('li');
35
+ let html = '';
36
+
37
+ // TODO add relative path to <script> per-page
38
+ html += `<p class="search-match"><a href="${index_rel_prefix}${this.escapeHTML(result.path)}">${this.hlt(result.title)}`;
39
+ if (result.params)
40
+ html += `<span class="params">${result.params}</span>`;
41
+ html += '</a>';
42
+
43
+ if (result.namespace)
44
+ html += `<p class="search-namespace">${this.hlt(result.namespace)}`;
45
+
46
+ if (result.snippet)
47
+ html += `<div class="search-snippet">${result.snippet}</div>`;
48
+
49
+ li.innerHTML = html;
50
+
51
+ return li;
52
+ }
53
+
54
+ search.select = function(result) {
55
+ let href = result.firstChild.firstChild.href;
56
+ const query = this.input.value;
57
+ if (query) {
58
+ const url = new URL(href, window.location.origin);
59
+ url.searchParams.set('q', query);
60
+ url.searchParams.set('nav', '0');
61
+ href = url.toString();
62
+ }
63
+ window.location.href = href;
64
+ }
65
+
66
+ search.scrollIntoView = search.scrollInWindow;
67
+
68
+ return search;
69
+ }
70
+
71
+ function hookSearch() {
72
+ const input = document.querySelector('#search-field');
73
+ const result = document.querySelector('#search-results');
74
+
75
+ if (!input || !result) return; // Exit if search elements not found
76
+
77
+ const search_section = document.querySelector('#search-section');
78
+ if (search_section) {
79
+ search_section.classList.remove("initially-hidden");
80
+ }
81
+
82
+ const search = createSearchInstance(input, result);
83
+ if (!search) return;
84
+
85
+ // Check for ?q= URL parameter and trigger search automatically
86
+ if (typeof URLSearchParams !== 'undefined') {
87
+ const urlParams = new URLSearchParams(window.location.search);
88
+ const queryParam = urlParams.get('q');
89
+ if (queryParam) {
90
+ const navParam = urlParams.get('nav');
91
+ const autoSelect = navParam !== '0';
92
+ input.value = queryParam;
93
+ search.search(queryParam, autoSelect);
94
+ }
95
+ }
96
+ }
97
+
98
+ /* ===== Keyboard Shortcuts ===== */
99
+
100
+ function hookFocus() {
101
+ document.addEventListener("keydown", (event) => {
102
+ if (document.activeElement.tagName === 'INPUT') {
103
+ return;
104
+ }
105
+ if (event.key === "/") {
106
+ event.preventDefault();
107
+ document.querySelector('#search-field').focus();
108
+ }
109
+ });
110
+ }
111
+
112
+ /* ===== Mobile Navigation ===== */
113
+
114
+ function hookSidebar() {
115
+ const navigation = document.querySelector('#navigation');
116
+ const navigationToggle = document.querySelector('#navigation-toggle');
117
+
118
+ if (!navigation || !navigationToggle) return;
119
+
120
+ const closeNav = () => {
121
+ navigation.hidden = true;
122
+ navigationToggle.ariaExpanded = 'false';
123
+ document.body.classList.remove('nav-open');
124
+ };
125
+
126
+ const openNav = () => {
127
+ navigation.hidden = false;
128
+ navigationToggle.ariaExpanded = 'true';
129
+ document.body.classList.add('nav-open');
130
+ };
131
+
132
+ const toggleNav = () => {
133
+ if (navigation.hidden) {
134
+ openNav();
135
+ } else {
136
+ closeNav();
137
+ }
138
+ };
139
+
140
+ navigationToggle.addEventListener('click', (e) => {
141
+ e.stopPropagation();
142
+ toggleNav();
143
+ });
144
+
145
+ const isSmallViewport = window.matchMedia("(max-width: 1023px)").matches;
146
+ if (isSmallViewport) {
147
+ closeNav();
148
+
149
+ // Close nav when clicking links inside it
150
+ document.addEventListener('click', (e) => {
151
+ if (e.target.closest('#navigation a')) {
152
+ closeNav();
153
+ }
154
+ });
155
+
156
+ // Close nav when clicking backdrop
157
+ document.addEventListener('click', (e) => {
158
+ if (!navigation.hidden &&
159
+ !e.target.closest('#navigation') &&
160
+ !e.target.closest('#navigation-toggle')) {
161
+ closeNav();
162
+ }
163
+ });
164
+ }
165
+ }
166
+
167
+ /* ===== Right Sidebar Table of Contents ===== */
168
+
169
+ function generateToc() {
170
+ const tocNav = document.querySelector('#toc-nav');
171
+ if (!tocNav) return; // Exit if TOC nav doesn't exist
172
+
173
+ const main = document.querySelector('main');
174
+ if (!main) return;
175
+
176
+ // Find all h2 and h3 headings in the main content
177
+ const headings = main.querySelectorAll('h1, h2, h3');
178
+ if (headings.length === 0) return;
179
+
180
+ const tocList = document.createElement('ul');
181
+ tocList.className = 'toc-list';
182
+
183
+ headings.forEach((heading) => {
184
+ // Skip if heading doesn't have an id
185
+ if (!heading.id) return;
186
+
187
+ const li = document.createElement('li');
188
+ const level = heading.tagName.toLowerCase();
189
+ li.className = `toc-item toc-${level}`;
190
+
191
+ const link = document.createElement('a');
192
+ link.href = `#${heading.id}`;
193
+ link.className = 'toc-link';
194
+ link.textContent = heading.textContent.trim();
195
+ link.setAttribute('data-target', heading.id);
196
+
197
+ li.appendChild(link);
198
+ setHeadingScrollHandler(heading, link);
199
+ tocList.appendChild(li);
200
+ });
201
+
202
+ if (tocList.children.length > 0) {
203
+ tocNav.appendChild(tocList);
204
+ } else {
205
+ // Hide TOC if no headings found
206
+ const tocContainer = document.querySelector('.table-of-contents');
207
+ if (tocContainer) {
208
+ tocContainer.style.display = 'none';
209
+ }
210
+ }
211
+ }
212
+
213
+ function hookTocActiveHighlighting() {
214
+ const tocLinks = document.querySelectorAll('.toc-link');
215
+ const targetHeadings = [];
216
+ tocLinks.forEach((link) => {
217
+ const targetId = link.getAttribute('data-target');
218
+ const heading = document.getElementById(targetId);
219
+ if (heading) {
220
+ targetHeadings.push(heading);
221
+ }
222
+ });
223
+
224
+ if (targetHeadings.length === 0) return;
225
+
226
+ const observerOptions = {
227
+ root: null,
228
+ rootMargin: '0% 0px -35% 0px',
229
+ threshold: 0
230
+ };
231
+
232
+ const intersectingHeadings = new Set();
233
+ const update = () => {
234
+ const firstIntersectingHeading = targetHeadings.find((heading) => {
235
+ return intersectingHeadings.has(heading);
236
+ });
237
+ if (!firstIntersectingHeading) return;
238
+ const correspondingLink = document.querySelector(`.toc-link[data-target="${firstIntersectingHeading.id}"]`);
239
+ if (!correspondingLink) return;
240
+
241
+ // Remove active class from all links
242
+ tocLinks.forEach((link) => {
243
+ link.classList.remove('active');
244
+ });
245
+
246
+ // Add active class to current link
247
+ correspondingLink.classList.add('active');
248
+
249
+ // Scroll link into view if needed
250
+ const tocNav = document.querySelector('#toc-nav');
251
+ if (tocNav) {
252
+ const linkRect = correspondingLink.getBoundingClientRect();
253
+ const navRect = tocNav.getBoundingClientRect();
254
+
255
+ if (linkRect.top < navRect.top || linkRect.bottom > navRect.bottom) {
256
+ correspondingLink.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
257
+ }
258
+ }
259
+ };
260
+ const observer = new IntersectionObserver((entries) => {
261
+ entries.forEach((entry) => {
262
+ if (entry.isIntersecting) {
263
+ intersectingHeadings.add(entry.target);
264
+ } else {
265
+ intersectingHeadings.delete(entry.target);
266
+ }
267
+ });
268
+ update();
269
+ }, observerOptions);
270
+
271
+ // Observe all headings that have corresponding TOC links
272
+ targetHeadings.forEach((heading) => {
273
+ observer.observe(heading);
274
+ });
275
+ }
276
+
277
+ function setHeadingScrollHandler(heading, link) {
278
+ // Smooth scroll to heading when clicking link
279
+ if (!heading.id) return;
280
+
281
+ link.addEventListener('click', (e) => {
282
+ e.preventDefault();
283
+ heading.scrollIntoView({ behavior: 'smooth', block: 'start' });
284
+ history.pushState(null, '', `#${heading.id}`);
285
+ });
286
+ }
287
+
288
+ function setHeadingSelfLinkScrollHandlers() {
289
+ // Clicking link inside heading scrolls smoothly to heading itself
290
+ const headings = document.querySelectorAll('h1, h2, h3, h4, h5, h6');
291
+ headings.forEach((heading) => {
292
+ if (!heading.id) return;
293
+
294
+ const link = heading.querySelector(`a[href^="#${heading.id}"]`);
295
+ if (link) setHeadingScrollHandler(heading, link);
296
+ })
297
+ }
298
+
299
+ /* ===== Mobile Search Modal ===== */
300
+
301
+ function hookSearchModal() {
302
+ const searchToggle = document.querySelector('#search-toggle');
303
+ const searchModal = document.querySelector('#search-modal');
304
+ const searchModalClose = document.querySelector('#search-modal-close');
305
+ const searchModalBackdrop = document.querySelector('.search-modal-backdrop');
306
+ const searchInput = document.querySelector('#search-field-mobile');
307
+ const searchResults = document.querySelector('#search-results-mobile');
308
+ const searchEmpty = document.querySelector('.search-modal-empty');
309
+
310
+ if (!searchToggle || !searchModal) return;
311
+
312
+ // Initialize search for mobile modal
313
+ const mobileSearch = createSearchInstance(searchInput, searchResults);
314
+ if (!mobileSearch) return;
315
+
316
+ // Hide empty state when there are results
317
+ const originalRenderItem = mobileSearch.renderItem;
318
+ mobileSearch.renderItem = function(result) {
319
+ if (searchEmpty) searchEmpty.style.display = 'none';
320
+ return originalRenderItem.call(this, result);
321
+ };
322
+
323
+ const openSearchModal = () => {
324
+ searchModal.hidden = false;
325
+ document.body.style.overflow = 'hidden';
326
+ // Focus input after animation
327
+ setTimeout(() => {
328
+ if (searchInput) searchInput.focus();
329
+ }, 100);
330
+ };
331
+
332
+ const closeSearchModal = () => {
333
+ searchModal.hidden = true;
334
+ document.body.style.overflow = '';
335
+ };
336
+
337
+ // Open on button click
338
+ searchToggle.addEventListener('click', openSearchModal);
339
+
340
+ // Close on close button click
341
+ if (searchModalClose) {
342
+ searchModalClose.addEventListener('click', closeSearchModal);
343
+ }
344
+
345
+ // Close on backdrop click
346
+ if (searchModalBackdrop) {
347
+ searchModalBackdrop.addEventListener('click', closeSearchModal);
348
+ }
349
+
350
+ // Close on Escape key
351
+ document.addEventListener('keydown', (e) => {
352
+ if (e.key === 'Escape' && !searchModal.hidden) {
353
+ closeSearchModal();
354
+ }
355
+ });
356
+
357
+ // Check for ?q= URL parameter on mobile and open modal
358
+ if (typeof URLSearchParams !== 'undefined') {
359
+ const urlParams = new URLSearchParams(window.location.search);
360
+ const queryParam = urlParams.get('q');
361
+ const isSmallViewport = window.matchMedia("(max-width: 1023px)").matches;
362
+
363
+ if (queryParam && isSmallViewport) {
364
+ openSearchModal();
365
+ searchInput.value = queryParam;
366
+ const navParam = urlParams.get('nav');
367
+ const autoSelect = navParam !== '0';
368
+ mobileSearch.search(queryParam, autoSelect);
369
+ }
370
+ }
371
+ }
372
+
373
+ /* ===== Code Block Copy Functionality ===== */
374
+
375
+ function createCopyButton() {
376
+ const button = document.createElement('button');
377
+ button.className = 'copy-code-button';
378
+ button.type = 'button';
379
+ button.setAttribute('aria-label', 'Copy code to clipboard');
380
+ button.setAttribute('title', 'Copy code');
381
+
382
+ // Create clipboard icon SVG
383
+ const clipboardIcon = `
384
+ <svg viewBox="0 0 24 24">
385
+ <rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
386
+ <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
387
+ </svg>
388
+ `;
389
+
390
+ // Create checkmark icon SVG (for copied state)
391
+ const checkIcon = `
392
+ <svg viewBox="0 0 24 24">
393
+ <polyline points="20 6 9 17 4 12"></polyline>
394
+ </svg>
395
+ `;
396
+
397
+ button.innerHTML = clipboardIcon;
398
+ button.dataset.clipboardIcon = clipboardIcon;
399
+ button.dataset.checkIcon = checkIcon;
400
+
401
+ return button;
402
+ }
403
+
404
+ function wrapCodeBlocksWithCopyButton() {
405
+ // Copy buttons are generated dynamically rather than statically in rhtml templates because:
406
+ // - Code blocks are generated by RDoc's markup formatter (RDoc::Markup::ToHtml),
407
+ // not directly in rhtml templates
408
+ // - Modifying the formatter would require extending RDoc's core internals
409
+
410
+ // Find all pre elements that are not already wrapped
411
+ const preElements = document.querySelectorAll('main pre:not(.code-block-wrapper pre)');
412
+
413
+ preElements.forEach((pre) => {
414
+ // Skip if already wrapped
415
+ if (pre.parentElement.classList.contains('code-block-wrapper')) {
416
+ return;
417
+ }
418
+
419
+ // Create wrapper
420
+ const wrapper = document.createElement('div');
421
+ wrapper.className = 'code-block-wrapper';
422
+
423
+ // Insert wrapper before pre
424
+ pre.parentNode.insertBefore(wrapper, pre);
425
+
426
+ // Move pre into wrapper
427
+ wrapper.appendChild(pre);
428
+
429
+ // Create and add copy button
430
+ const copyButton = createCopyButton();
431
+ wrapper.appendChild(copyButton);
432
+
433
+ // Add click handler
434
+ copyButton.addEventListener('click', () => {
435
+ copyCodeToClipboard(pre, copyButton);
436
+ });
437
+ });
438
+ }
439
+
440
+ function copyCodeToClipboard(preElement, button) {
441
+ const code = preElement.textContent;
442
+
443
+ // Use the Clipboard API (supported by all modern browsers)
444
+ if (navigator.clipboard && navigator.clipboard.writeText) {
445
+ navigator.clipboard.writeText(code).then(() => {
446
+ showCopySuccess(button);
447
+ }).catch(() => {
448
+ alert('Failed to copy code.');
449
+ });
450
+ } else {
451
+ alert('Failed to copy code.');
452
+ }
453
+ }
454
+
455
+ function showCopySuccess(button) {
456
+ // Change icon to checkmark
457
+ button.innerHTML = button.dataset.checkIcon;
458
+ button.classList.add('copied');
459
+ button.setAttribute('aria-label', 'Copied!');
460
+ button.setAttribute('title', 'Copied!');
461
+
462
+ // Revert back after 2 seconds
463
+ setTimeout(() => {
464
+ button.innerHTML = button.dataset.clipboardIcon;
465
+ button.classList.remove('copied');
466
+ button.setAttribute('aria-label', 'Copy code to clipboard');
467
+ button.setAttribute('title', 'Copy code');
468
+ }, 2000);
469
+ }
470
+
471
+ /* ===== Initialization ===== */
472
+
473
+ document.addEventListener('DOMContentLoaded', () => {
474
+ hookSourceViews();
475
+ hookSearch();
476
+ hookFocus();
477
+ hookSidebar();
478
+ generateToc();
479
+ setHeadingSelfLinkScrollHandlers();
480
+ hookTocActiveHighlighting();
481
+ hookSearchModal();
482
+ wrapCodeBlocksWithCopyButton();
483
+ });