diff --git a/lib/rdoc/generator/template/rails/_context.rhtml b/lib/rdoc/generator/template/rails/_context.rhtml index 503ece06..dce4c3e8 100644 --- a/lib/rdoc/generator/template/rails/_context.rhtml +++ b/lib/rdoc/generator/template/rails/_context.rhtml @@ -42,26 +42,6 @@ <% end %> - <% unless @context.method_list.empty? %> - -
Methods
-
- <% group_by_first_letter(@context.method_list).each do |letter, methods| %> -
<%= letter %>
-
- -
- <% end %> -
- <% end %> - <% @context.each_section do |section, constants, attributes| %> diff --git a/lib/rdoc/generator/template/rails/_file_nav.rhtml b/lib/rdoc/generator/template/rails/_file_nav.rhtml new file mode 100644 index 00000000..e91ba3aa --- /dev/null +++ b/lib/rdoc/generator/template/rails/_file_nav.rhtml @@ -0,0 +1,11 @@ +<% if @context.text? %> + +<% else %> + +<% end %> diff --git a/lib/rdoc/generator/template/rails/_head.rhtml b/lib/rdoc/generator/template/rails/_head.rhtml index f298ec24..28240449 100644 --- a/lib/rdoc/generator/template/rails/_head.rhtml +++ b/lib/rdoc/generator/template/rails/_head.rhtml @@ -1,15 +1,11 @@ <%= base_tag_for_context(@context) %> - - - - <% if canonical = canonical_url(@context) %> diff --git a/lib/rdoc/generator/template/rails/_index_nav.rhtml b/lib/rdoc/generator/template/rails/_index_nav.rhtml new file mode 100644 index 00000000..5e335ee5 --- /dev/null +++ b/lib/rdoc/generator/template/rails/_index_nav.rhtml @@ -0,0 +1,21 @@ + + +<% unless (modules = top_modules(@context.store)).empty? %> + + +<% end %> + +<% unless (extensions = core_extensions(@context.store)).empty? %> + + +<% end %> diff --git a/lib/rdoc/generator/template/rails/_module_nav.rhtml b/lib/rdoc/generator/template/rails/_module_nav.rhtml new file mode 100644 index 00000000..b9870238 --- /dev/null +++ b/lib/rdoc/generator/template/rails/_module_nav.rhtml @@ -0,0 +1,17 @@ +<%= button_to_search @context, display_name: short_name(@context) %> + +<% if outline = outline(@context) %> + +<% end %> + +<% unless (methods = module_methods(@context)).empty? %> + + +<% end %> diff --git a/lib/rdoc/generator/template/rails/_panel.rhtml b/lib/rdoc/generator/template/rails/_panel.rhtml index a9d8d04d..0d0390b6 100644 --- a/lib/rdoc/generator/template/rails/_panel.rhtml +++ b/lib/rdoc/generator/template/rails/_panel.rhtml @@ -13,18 +13,16 @@ -
+
+ placeholder="Search (/) for a class, method, ..." + data-turbo-permanent> -
+
-
-
-
    -
-
+
+ <% yield if block_given? %>
index diff --git a/lib/rdoc/generator/template/rails/class.rhtml b/lib/rdoc/generator/template/rails/class.rhtml index 18d4b1bf..d90ccf6e 100644 --- a/lib/rdoc/generator/template/rails/class.rhtml +++ b/lib/rdoc/generator/template/rails/class.rhtml @@ -4,7 +4,7 @@ <%= page_title @context.full_name %> - <% inline "_head.rhtml", { :tree_keys => @context.full_name.split('::') } %> + <% inline "_head.rhtml" %> @@ -13,9 +13,9 @@ Skip to Content Skip to Search - <% inline "_panel.rhtml" %> + <% inline("_panel.rhtml") { inline("_module_nav.rhtml") } %> -
+

<%= @context.type %> <%= module_breadcrumbs @context %>

diff --git a/lib/rdoc/generator/template/rails/file.rhtml b/lib/rdoc/generator/template/rails/file.rhtml index e0802e37..c930b353 100644 --- a/lib/rdoc/generator/template/rails/file.rhtml +++ b/lib/rdoc/generator/template/rails/file.rhtml @@ -4,7 +4,7 @@ <%= page_title @context.name %> - <% inline "_head.rhtml", { :tree_keys => [] } %> + <% inline "_head.rhtml" %> @@ -13,9 +13,9 @@ Skip to Content Skip to Search - <% inline "_panel.rhtml" %> + <% inline("_panel.rhtml") { inline("_file_nav.rhtml") } %> -
+
<%= "

" if @context.comment.empty? %> <%= full_name @context %> diff --git a/lib/rdoc/generator/template/rails/index.rhtml b/lib/rdoc/generator/template/rails/index.rhtml index e17b1008..fc806e25 100644 --- a/lib/rdoc/generator/template/rails/index.rhtml +++ b/lib/rdoc/generator/template/rails/index.rhtml @@ -4,16 +4,16 @@ <%= page_title %> - <% inline "_head.rhtml", {tree_keys: []} %> + <% inline "_head.rhtml" %> Skip to Content Skip to Search - <% inline "_panel.rhtml" %> + <% inline("_panel.rhtml") { inline("_index_nav.rhtml") } %> -
+
<% inline "_context.rhtml" %>
diff --git a/lib/rdoc/generator/template/rails/resources/css/main.css b/lib/rdoc/generator/template/rails/resources/css/main.css index 85b10fa6..a492bf78 100644 --- a/lib/rdoc/generator/template/rails/resources/css/main.css +++ b/lib/rdoc/generator/template/rails/resources/css/main.css @@ -67,6 +67,27 @@ a code { background-size: 1.1em; } +.query-button { + border: none; + text-align: left; + font-family: inherit; + font-size: 1em; + + background: url('../i/filter.svg') no-repeat; + background-position-y: 1px; + background-size: 1.1em; + padding: 0 0 0 1.3em; + color: var(--link-color); + + word-break: break-word; +} + +@media (hover: hover) { + .query-button:hover { + color: var(--link-hover-color); + } +} + .kind { font-family: monospace; } @@ -89,30 +110,6 @@ th font-weight: bold; } -.methods dt -{ - width: 1em; - font-size: 1.5em; - color:#AAA; - position: absolute; -} - -.methods dd -{ - margin-top: var(--space); - min-height: 1.8em; - -height: 1.8em; -} - - -.methods ul li -{ - margin-right: 0.7em; - margin-left: 0; - list-style: none; - display: inline; -} - .attr-rw { padding-right: 1em; text-align: center; @@ -289,7 +286,7 @@ html { box-shadow: 1px 1px 4px color-mix(in srgb, currentColor 50%, transparent) inset; } -.panel__results, .panel__tree { +.panel__results, .panel__nav { position: relative; height: calc(100dvh - var(--banner-height) - var(--search-height)); width: var(--panel-width); @@ -300,7 +297,7 @@ html { } /* Force scrolling in order to always contain scroll events (on mobile) */ -:is(.panel__results, .panel__tree)::after { +:is(.panel__results, .panel__nav)::after { content: ""; height: 1px; width: 1px; @@ -311,7 +308,7 @@ html { .panel__results:not(.active), .panel__search:placeholder-shown ~ .panel__results, -.panel__search:not(:placeholder-shown) ~ .panel__results.active ~ .panel__tree { +.panel__search:not(:placeholder-shown) ~ .panel__results.active ~ .panel__nav { /* `display: none` disables animations, so simulate it instead */ max-height: 0; max-width: 0; @@ -320,10 +317,6 @@ html { opacity: 0; } -.panel__tree { - overflow-x: hidden; -} - /* * Navigation panel - Search results @@ -388,6 +381,69 @@ html { } +/* + * Navigation panel - Page nav + */ + +.panel__nav { + padding: var(--space); + font-size: 0.95em; + line-height: calc(0.85 * var(--line-height)); +} + + +* + .nav__outline { + margin-top: var(--space-lg); +} + +.nav__outline ul { + margin-top: var(--space-sm); + padding-left: 1em; +} + +.nav__outline ul ul ul { + display: none; /* Only show two levels deep */ +} + +.nav__outline li { + word-break: break-word; +} + +.nav__outline a { + text-decoration: underline; +} + + +.nav__heading { + margin-top: var(--space-lg); + font-size: 1.3em; +} + +.nav__heading + .nav__list { + margin-top: var(--space-sm); +} + + +.nav__list { + padding: 0; + list-style: none; +} + +.nav__list li { + overflow: hidden; + text-overflow: ellipsis; +} + + +.nav__method-link code::before { + content: "#"; +} + +.nav__method-link--singleton code::before { + content: "::"; +} + + /* * Navigation panel on desktop */ @@ -411,7 +467,7 @@ html { width: 60ch; } - :is(.panel__results, .panel__tree)::after { + :is(.panel__results, .panel__nav)::after { display: none; } } @@ -434,29 +490,29 @@ html { } } -:where(#content) *, -:where(#content) :is(br, wbr) { +:where(.content) *, +:where(.content) :is(br, wbr) { margin: 0; } -:where(#content) * + * { +:where(.content) * + * { margin-top: var(--space); } -:where(#content) :is(ol, ul, dd) { +:where(.content) :is(ol, ul, dd) { padding: 0 0 0 1.25em; } -:where(#content) * + :is(li, dd) { +:where(.content) * + :is(li, dd) { margin-top: var(--space-sm); } /* Increase top margin for list items when any item has more than one paragraph */ -:where(#content :is(ol, ul):has(> li > p:not(:only-child))) * + li { +:where(.content :is(ol, ul):has(> li > p:not(:only-child))) * + li { margin-top: var(--space); } -:where(#content) code { +:where(.content) code { font-style: normal; } diff --git a/lib/rdoc/generator/template/rails/resources/css/panel.css b/lib/rdoc/generator/template/rails/resources/css/panel.css deleted file mode 100644 index 09fe1051..00000000 --- a/lib/rdoc/generator/template/rails/resources/css/panel.css +++ /dev/null @@ -1,166 +0,0 @@ -/* Panel (begin) */ -:root { - --panel-hover-background-color: #d0d0d0; - --panel-alternate-background-color: #F0EFEF; - --panel-current-background-color: #B61D1D; - --panel-highlight-color: #000; -} - - /* Tree (begin) */ /**/ - .panel .tree - { - font-family: "Helvetica Neue", Arial, sans-serif; - } - - .panel .tree ul:first-child - { - background: url(../i/tree_bg.svg); - background-size: 1px 60px; - } - - .panel .tree ul { - list-style: none; - margin: 0; - padding: 0; - } - - .panel .tree li - { - cursor: pointer; - height: 30px; - line-height: 100%; - margin: 0; - } - - - .panel .tree li .entry - { - padding-left: 18px; - padding-top: 5px; - height: 18px; - position: relative; - } - - .panel .tree li .icon - { - width: 10px; - height: 9px; - background: url(../i/arrow-down.svg); - background-size: 10px; - position: absolute; - left: 1px; - top: 8px; - cursor: default; - } - - .panel .tree li.closed .icon - { - background: url(../i/arrow-right.svg); - background-size: 10px; - } - - .panel .tree ul li h1 - { - font-size: 13px; - font-weight: normal; - color: var(--text-color); - margin-top: 0; - margin-bottom: 2px; - white-space: nowrap; - } - - .panel .tree ul li p - { - font-size: 11px; - color: #666; - margin-bottom: 2px; - white-space: nowrap; - } - - .panel .tree ul li h1 i - { - color: #999; - font-style: normal; - } - - .panel .tree ul li.current h1 i - { - color: #CCC; - } - - .panel .tree ul li.empty - { - cursor: text; - } - - .panel .tree ul li.empty h1, - .panel .tree ul li.empty p - { - color: #666; - font-style: italic; - } - - .panel .tree ul li.current - { - background: var(--panel-current-background-color); - } - - .panel .tree ul li.current .icon - { - background: url(../i/arrow-down-current.svg); - background-size: 10px; - } - - .panel .tree ul li.current.closed .icon - { - background: url(../i/arrow-right-current.svg); - background-size: 10px; - } - - .panel .tree ul li.current h1 - { - color: #FFF; - } - - .panel .tree ul li.current p - { - color: #CCC; - } - - .panel .tree ul li.current.empty h1, - .panel .tree ul li.current.empty p - { - color: #999; - } - - .panel .tree ul li:hover - { - background: var(--panel-hover-background-color); - } - - .panel .tree ul li.current:hover - { - background: var(--panel-current-background-color); - } - - .panel .tree .stopper - { - display: none; - } - - @media (prefers-color-scheme: dark) { - :root { - --panel-hover-background-color: #3b3b3b; - --panel-alternate-background-color: #000000; - --panel-highlight-color: #fff; - } - - .panel .tree ul:first-child { - background: url(../i/dark/tree_bg.svg); - background-size: 1px 60px; - } - } - - /* Tree (end) */ /**/ - -/* Panel (end) */ diff --git a/lib/rdoc/generator/template/rails/resources/i/arrow-down-current.svg b/lib/rdoc/generator/template/rails/resources/i/arrow-down-current.svg deleted file mode 100644 index abf5a99d..00000000 --- a/lib/rdoc/generator/template/rails/resources/i/arrow-down-current.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - - - diff --git a/lib/rdoc/generator/template/rails/resources/i/arrow-down.svg b/lib/rdoc/generator/template/rails/resources/i/arrow-down.svg deleted file mode 100644 index 88c5138e..00000000 --- a/lib/rdoc/generator/template/rails/resources/i/arrow-down.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - - - diff --git a/lib/rdoc/generator/template/rails/resources/i/arrow-right-current.svg b/lib/rdoc/generator/template/rails/resources/i/arrow-right-current.svg deleted file mode 100644 index 4fccc84f..00000000 --- a/lib/rdoc/generator/template/rails/resources/i/arrow-right-current.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - - - diff --git a/lib/rdoc/generator/template/rails/resources/i/arrow-right.svg b/lib/rdoc/generator/template/rails/resources/i/arrow-right.svg deleted file mode 100644 index 5b1569e4..00000000 --- a/lib/rdoc/generator/template/rails/resources/i/arrow-right.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - - - diff --git a/lib/rdoc/generator/template/rails/resources/i/dark/tree_bg.svg b/lib/rdoc/generator/template/rails/resources/i/dark/tree_bg.svg deleted file mode 100644 index 95dd2390..00000000 --- a/lib/rdoc/generator/template/rails/resources/i/dark/tree_bg.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - - - diff --git a/lib/rdoc/generator/template/rails/resources/i/filter.svg b/lib/rdoc/generator/template/rails/resources/i/filter.svg new file mode 100644 index 00000000..0e7eafa5 --- /dev/null +++ b/lib/rdoc/generator/template/rails/resources/i/filter.svg @@ -0,0 +1 @@ + diff --git a/lib/rdoc/generator/template/rails/resources/i/tree_bg.svg b/lib/rdoc/generator/template/rails/resources/i/tree_bg.svg deleted file mode 100644 index fd6a2137..00000000 --- a/lib/rdoc/generator/template/rails/resources/i/tree_bg.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - - - diff --git a/lib/rdoc/generator/template/rails/resources/js/main.js b/lib/rdoc/generator/template/rails/resources/js/main.js index 1851f58b..61604817 100644 --- a/lib/rdoc/generator/template/rails/resources/js/main.js +++ b/lib/rdoc/generator/template/rails/resources/js/main.js @@ -33,6 +33,15 @@ document.addEventListener("turbo:load", () => {

` ); + // Handle query button clicks. + document.addEventListener("click", ({ target }) => { + const query = target.closest(".query-button")?.dataset?.query; + if (query) { + search.query = query; + search.focus(); + } + }); + const query = new URL(document.location).searchParams.get("q"); if (query) { search.feelingLucky(query); @@ -40,14 +49,17 @@ document.addEventListener("turbo:load", () => { }, { once: true }); document.addEventListener("turbo:load", function() { - // Only initialize panel if not yet initialized - if(!$('#panel .tree ul li').length) { - $('#links').hide(); - var panel = new Searchdoc.Panel($('#panel'), tree); - panel.toggle(JSON.parse($('meta[name="data-tree-keys"]').attr("content"))); - } + $('#links').hide(); +}); + + +// Hide menu on mobile when navigating to a named anchor on the current page. +// For example, when clicking on a method in the method list. +window.addEventListener("hashchange", () => { + document.getElementById("panel__state").checked = false; }); + // Because search results are in a `data-turbo-permanent` element, manually blur // to hide them when navigating. document.addEventListener("turbo:click", ({ target }) => { @@ -56,19 +68,21 @@ document.addEventListener("turbo:click", ({ target }) => { } }); -// Keep scroll position for panel + +// Keep scroll position for search results across Turbo page loads. (function() { var scrollTop = 0; addEventListener("turbo:before-render", function() { - scrollTop = document.querySelector(".panel__tree").scrollTop + scrollTop = document.getElementById("results").scrollTop }) addEventListener("turbo:render", function() { - document.querySelector(".panel__tree").scrollTop = scrollTop + document.getElementById("results").scrollTop = scrollTop }) })() + document.addEventListener("turbo:load", function () { var backToTop = $("a.back-to-top"); diff --git a/lib/rdoc/generator/template/rails/resources/js/search.js b/lib/rdoc/generator/template/rails/resources/js/search.js index 3ea893f0..af7529a6 100644 --- a/lib/rdoc/generator/template/rails/resources/js/search.js +++ b/lib/rdoc/generator/template/rails/resources/js/search.js @@ -18,7 +18,7 @@ export class Search { } search() { - const bitPositions = this.compileQuery(this.inputEl.value); + const bitPositions = this.compileQuery(this.query); let worst; this.clearResults(); @@ -97,8 +97,7 @@ export class Search { } feelingLucky(query) { - this.inputEl.value = query; - this.search(); + this.query = query; this.clickCursor(); } @@ -119,6 +118,15 @@ export class Search { this.outputEl.classList.toggle("active", value); } + get query() { + return this.inputEl.value; + } + + set query(value) { + this.inputEl.value = value; + this.search(); + } + get cursorEl() { return this._cursorEl; } diff --git a/lib/rdoc/generator/template/rails/resources/js/searchdoc.js b/lib/rdoc/generator/template/rails/resources/js/searchdoc.js deleted file mode 100644 index 6680d064..00000000 --- a/lib/rdoc/generator/template/rails/resources/js/searchdoc.js +++ /dev/null @@ -1,323 +0,0 @@ -Searchdoc = {}; - -// navigation.js ------------------------------------------ - -Searchdoc.Navigation = new function() { - this.initNavigation = function() { - var _this = this; - - $(document).keydown(function(e) { - _this.onkeydown(e); - }).keyup(function(e) { - _this.onkeyup(e); - }); - }; - - this.navigationActive = function() { - this.$searchInput ??= document.getElementById("search"); - this.$searchOutput ??= document.getElementById("results"); - return document.activeElement !== this.$searchInput && - !this.$searchOutput.contains(document.activeElement); - }; - - - this.onkeyup = function(e) { - if (!this.navigationActive()) return; - switch (e.keyCode) { - case 37: //Event.KEY_LEFT: - case 38: //Event.KEY_UP: - case 39: //Event.KEY_RIGHT: - case 40: //Event.KEY_DOWN: - this.clearMoveTimeout(); - break; - } - }; - - this.onkeydown = function(e) { - if (!this.navigationActive()) return; - - switch (e.keyCode) { - case 37: //Event.KEY_LEFT: - if (this.moveLeft()) e.preventDefault(); - break; - case 38: //Event.KEY_UP: - if (this.moveUp()) e.preventDefault(); - this.startMoveTimeout(false); - break; - case 39: //Event.KEY_RIGHT: - if (this.moveRight()) e.preventDefault(); - break; - case 40: //Event.KEY_DOWN: - if (this.moveDown()) e.preventDefault(); - this.startMoveTimeout(true); - break; - case 13: //Event.KEY_RETURN: - if(e.target.dataset["turbo"]) { break; } - if (this.$current) this.select(this.$current); - break; - } - }; - - this.clearMoveTimeout = function() { - clearTimeout(this.moveTimeout); - this.moveTimeout = null; - }; - - this.startMoveTimeout = function(isDown) { - if (this.moveTimeout) this.clearMoveTimeout(); - var _this = this; - - var go = function() { - if (!_this.moveTimeout) return; - _this[isDown ? 'moveDown' : 'moveUp'](); - _this.moveTimout = setTimeout(go, 100); - }; - this.moveTimeout = setTimeout(go, 200); - }; - - this.moveRight = function() {}; - - this.moveLeft = function() {}; - - this.move = function(isDown) {}; - - this.moveUp = function() { - return this.move(false); - }; - - this.moveDown = function() { - return this.move(true); - }; -}; - - -// scrollIntoView.js -------------------------------------- - -function scrollIntoView(element, view) { - var offset, viewHeight, viewScroll, height; - offset = element.offsetTop; - height = element.offsetHeight; - viewHeight = view.offsetHeight; - viewScroll = view.scrollTop; - if (offset - viewScroll + height > viewHeight) { - view.scrollTop = offset - viewHeight + height; - } - if (offset < viewScroll) { - view.scrollTop = offset; - } -} - -// panel.js ----------------------------------------------- - -Searchdoc.Panel = function(element, tree) { - this.$element = $(element); - this.$current = null; - this.tree = new Searchdoc.Tree($('.tree', element), tree, this); -}; - -Searchdoc.Panel.prototype = $.extend({}, Searchdoc.Navigation, new function() { - this.toggle = function(keys) { - var keysIndex = 0; - var _tree = this.tree; - var _this = this; - $.each(_tree.$list[0].children, function(i, li) { - if(keys.length > keysIndex) { - $li = $(li); - if($li.text().split(" ")[0] == keys[keysIndex]) { - if($li.find('.icon').length > 0) { - _tree.toggle($li); - } - keysIndex += 1; - if(keysIndex == keys.length) { - _tree.highlight($li) - } - } - } - }); - }; - - this.open = function(src) { - var location = $("base").attr("href") + src; - Turbo.visit(location); - if (this.highlight) this.highlight(src); - }; -}); - -// tree.js ------------------------------------------------ - -Searchdoc.Tree = function(element, tree, panel) { - this.$element = $(element); - this.$list = $('ul', element); - this.tree = tree; - this.panel = panel; - this.init(); -}; - -Searchdoc.Tree.prototype = $.extend({}, Searchdoc.Navigation, new function() { - this.init = function() { - var stopper = document.createElement('li'); - stopper.className = 'stopper'; - this.$list[0].appendChild(stopper); - for (var i = 0, l = this.tree.length; i < l; i++) { - buildAndAppendItem.call(this, this.tree[i], 0, stopper); - } - var _this = this; - this.$list.click(function(e) { - var $target = $(e.target), - $li = $target.closest('li'); - if ($target.hasClass('icon')) { - _this.toggle($li); - } else { - _this.select($li); - } - }); - - this.initNavigation(); - }; - - this.select = function($li) { - this.highlight($li); - var path = $li[0].searchdoc_tree_data.path; - if (path) this.panel.open(path); - }; - - this.highlight = function($li) { - if (this.$current) this.$current.removeClass('current'); - this.$current = $li.addClass('current'); - }; - - this.toggle = function($li) { - var closed = !$li.hasClass('closed'), - children = $li[0].searchdoc_tree_data.children; - $li.toggleClass('closed'); - for (var i = 0, l = children.length; i < l; i++) { - toggleVis.call(this, $(children[i].li), !closed); - } - }; - - this.moveRight = function() { - if (!this.$current) { - this.highlight(this.$list.find('li:first')); - return; - } - if (this.$current.hasClass('closed')) { - this.toggle(this.$current); - } - }; - - this.moveLeft = function() { - if (!this.$current) { - this.highlight(this.$list.find('li:first')); - return; - } - if (!this.$current.hasClass('closed')) { - this.toggle(this.$current); - } else { - var level = this.$current[0].searchdoc_tree_data.level; - if (level === 0) return; - var $next = this.$current.prevAll('li.level_' + (level - 1) + ':visible:first'); - this.$current.removeClass('current'); - $next.addClass('current'); - scrollIntoView($next[0], this.$element[0]); - this.$current = $next; - } - }; - - this.move = function(isDown) { - if (!this.$current) { - this.highlight(this.$list.find('li:first')); - return true; - } - var next = this.$current[0]; - if (isDown) { - do { - next = next.nextSibling; - if (next && next.style && next.style.display != 'none') break; - } while (next); - } else { - do { - next = next.previousSibling; - if (next && next.style && next.style.display != 'none') break; - } while (next); - } - if (next && next.className.indexOf('stopper') == -1) { - this.$current.removeClass('current'); - $(next).addClass('current'); - scrollIntoView(next, this.$element[0]); - this.$current = $(next); - } - return true; - }; - - function toggleVis($li, show) { - var closed = $li.hasClass('closed'), - children = $li[0].searchdoc_tree_data.children; - $li.css('display', show ? '' : 'none'); - if (!show && this.$current && $li[0] == this.$current[0]) { - this.$current.removeClass('current'); - this.$current = null; - } - for (var i = 0, l = children.length; i < l; i++) { - toggleVis.call(this, $(children[i].li), show && !closed); - } - } - - function buildAndAppendItem(item, level, before) { - var li = renderItem(item, level), - list = this.$list[0]; - item.li = li; - list.insertBefore(li, before); - for (var i = 0, l = item[3].length; i < l; i++) { - buildAndAppendItem.call(this, item[3][i], level + 1, before); - } - return li; - } - - function renderItem(item, level) { - var li = document.createElement('li'), - cnt = document.createElement('div'), - h1 = document.createElement('h1'), - p = document.createElement('p'), - icon, i; - - li.appendChild(cnt); - li.style.paddingLeft = getOffset(level); - cnt.className = 'entry'; - if (!item[1]) li.className = 'empty '; - cnt.appendChild(h1); - // cnt.appendChild(p); - h1.appendChild(document.createTextNode(item[0])); - // p.appendChild(document.createTextNode(item[4])); - if (item[2]) { - i = document.createElement('i'); - i.appendChild(document.createTextNode(item[2])); - h1.appendChild(i); - } - if (item[3].length > 0) { - icon = document.createElement('div'); - icon.className = 'icon'; - cnt.appendChild(icon); - } - - // user direct assignement instead of $() - // it's 8x faster - // $(li).data('path', item[1]) - // .data('children', item[3]) - // .data('level', level) - // .css('display', level == 0 ? '' : 'none') - // .addClass('level_' + level) - // .addClass('closed'); - li.searchdoc_tree_data = { - path: item[1], - children: item[3], - level: level - }; - li.style.display = level === 0 ? '' : 'none'; - li.className += 'level_' + level + ' closed'; - return li; - } - - function getOffset(level) { - return 5 + 18 * level + 'px'; - } -}); diff --git a/lib/sdoc/generator.rb b/lib/sdoc/generator.rb index fd61cd66..7b70fb03 100644 --- a/lib/sdoc/generator.rb +++ b/lib/sdoc/generator.rb @@ -10,13 +10,13 @@ require "sdoc/search_index" require "sdoc/version" -class RDoc::ClassModule - def with_documentation? - document_self_or_methods || classes_and_modules.any?{ |c| c.with_documentation? } +class RDoc::Options + attr_writer :core_ext_pattern + + def core_ext_pattern + @core_ext_pattern ||= /core_ext/ end -end -class RDoc::Options attr_accessor :github attr_accessor :search_index end @@ -26,8 +26,6 @@ class RDoc::Generator::SDoc DESCRIPTION = 'Searchable HTML documentation' - TREE_FILE = File.join 'panel', 'tree.js' - FILE_DIR = 'files' CLASS_DIR = 'classes' @@ -46,6 +44,12 @@ def self.setup_options(options) opt.separator nil opt.separator "SDoc generator options:" + opt.separator nil + opt.on("--core-ext=PATTERN", Regexp, "Regexp pattern indicating files that define core extensions. " \ + "Defaults to 'core_ext'.") do |pattern| + options.core_ext_pattern = pattern + end + opt.separator nil opt.on("--github", "-g", "Generate links to github.") do |value| @@ -86,7 +90,6 @@ def generate copy_resources generate_search_index generate_file_links - generate_class_tree generate_index_file generate_file_files @@ -153,36 +156,6 @@ def generate_file_links render_file("file_links.rhtml", "panel/file_links.html", @files) end - ### Create class tree structure and write it as json - def generate_class_tree - debug_msg "Generating class tree" - topclasses = @classes.select {|klass| !(RDoc::ClassModule === klass.parent) } - tree = generate_file_tree + generate_class_tree_level(topclasses) - file = @output_dir + TREE_FILE - debug_msg " writing class tree to %s" % file - File.open(file, "w", 0644) do |f| - f.write('var tree = '); f.write(tree.to_json(:max_nesting => 0)) - end unless @options.dry_run - end - - ### Recursivly build class tree structure - def generate_class_tree_level(classes, visited = {}) - tree = [] - classes.select do |klass| - !visited[klass] && klass.with_documentation? - end.sort.each do |klass| - visited[klass] = true - item = [ - klass.name, - klass.document_self_or_methods ? klass.path : '', - klass.module? ? '' : (klass.superclass ? " < #{String === klass.superclass ? klass.superclass : klass.superclass.full_name}" : ''), - generate_class_tree_level(klass.classes_and_modules, visited) - ] - tree << item - end - tree - end - def generate_search_index debug_msg "Generating search index" unless @options.dry_run @@ -202,41 +175,4 @@ def copy_resources debug_msg "Copying #{resources_path}/** to #{@output_dir}/**" FileUtils.cp_r resources_path.to_s, @output_dir.to_s unless @options.dry_run end - - class FilesTree - attr_reader :children - def add(path, url) - path = path.split(File::SEPARATOR) unless Array === path - @children ||= {} - if path.length == 1 - @children[path.first] = url - else - @children[path.first] ||= FilesTree.new - @children[path.first].add(path[1, path.length], url) - end - end - end - - def generate_file_tree - if @files.length > 1 - @files_tree = FilesTree.new - @files.each do |file| - @files_tree.add(file.relative_name, file.path) - end - [['', '', 'files', generate_file_tree_level(@files_tree)]] - else - [] - end - end - - def generate_file_tree_level(tree) - tree.children.keys.sort.map do |name| - child = tree.children[name] - if String === child - [name, child, '', []] - else - ['', '', name, generate_file_tree_level(child)] - end - end - end end diff --git a/lib/sdoc/helpers.rb b/lib/sdoc/helpers.rb index 96879ce8..a994852f 100644 --- a/lib/sdoc/helpers.rb +++ b/lib/sdoc/helpers.rb @@ -37,6 +37,11 @@ def link_to_external(text, url, html_attributes = {}) link_to(text, url, html_attributes) end + def button_to_search(query, display_name: full_name(query)) + query = query.full_name if query.is_a?(RDoc::CodeObject) + %() + end + def full_name(named) named = named.full_name if named.is_a?(RDoc::CodeObject) "#{named.split(%r"(?<=./|.::)").map { |part| h part }.join("")}" @@ -97,10 +102,29 @@ def page_description(leading_html, max_length: 160) h text end - def group_by_first_letter(rdoc_objects) - rdoc_objects.sort_by(&:name).group_by do |object| - object.name[/^[a-z]/i]&.upcase || "#" + def outline(context) + comment = context.respond_to?(:comment_location) ? context.comment_location : context.comment + return if comment.empty? + + headings = context.parse(comment).table_of_contents + headings.shift if headings.one? { |heading| heading.level == 1 } && headings[0].level == 1 + + _outline_list(context, headings) + end + + def _outline_list(context, headings, following: 0) + items = [] + while headings[0] && headings[0].level > following + items << _outline_list_item(context, headings) end + "
    #{items.join}
" unless items.empty? + end + + def _outline_list_item(context, headings) + heading = headings.shift + link = link_to(heading.plain_html, "##{heading.label(context)}") + sublist = _outline_list(context, headings, following: heading.level) + "
  • #{link}#{sublist}
  • " end def more_less_ul(items, limit) @@ -123,6 +147,31 @@ def more_less_ul(items, limit) end end + def top_modules(rdoc_store) + _top_modules(rdoc_store).reject { |rdoc_module| _core_ext?(rdoc_module) } + end + + def core_extensions(rdoc_store) + _top_modules(rdoc_store).select { |rdoc_module| _core_ext?(rdoc_module) } + end + + def _top_modules(rdoc_store) + rdoc_store.all_classes_and_modules.select do |rdoc_module| + !rdoc_module.full_name.include?("::") + end.sort + end + + def _core_ext?(rdoc_module) + # HACK There is currently a bug in RDoc v6.5.0 that causes the value of + # RDoc::ClassModule#in_files for `Object` to become polluted. The cause is + # unclear, but it might be related to setting global constants (for example, + # setting `APP_PATH = "..."` outside of a class or module). To work around + # this bug, we always treat `Object` as a core extension. + rdoc_module.full_name == "Object" || + + rdoc_module.in_files.all? { |rdoc_file| @options.core_ext_pattern.match?(rdoc_file.full_name) } + end + def module_breadcrumbs(rdoc_module) parent_names = rdoc_module.full_name.split("::")[0...-1] @@ -145,6 +194,12 @@ def module_ancestors(rdoc_module) ancestors end + def module_methods(rdoc_module) + rdoc_module.each_method.sort_by do |rdoc_method| + [rdoc_method.singleton ? 0 : 1, rdoc_method.name] + end + end + def method_signature(rdoc_method) if rdoc_method.call_seq # Support specifying a call-seq like `to_s -> string` diff --git a/lib/sdoc/rdoc_monkey_patches.rb b/lib/sdoc/rdoc_monkey_patches.rb index 8040f5cc..b57486ec 100644 --- a/lib/sdoc/rdoc_monkey_patches.rb +++ b/lib/sdoc/rdoc_monkey_patches.rb @@ -22,3 +22,19 @@ def cross_reference(name, text = nil, code = true) super end end) + + +RDoc::Parser::Ruby.prepend(Module.new do + def get_class_or_module(container, ignore_constants = false) + @ignoring_constants ||= nil + original_ignoring_constants, @ignoring_constants = @ignoring_constants, ignore_constants + super + ensure + @ignoring_constants = original_ignoring_constants + end + + def record_location(*) + @ignoring_constants ||= nil + super unless @ignoring_constants + end +end) diff --git a/spec/helpers_spec.rb b/spec/helpers_spec.rb index bd014ae1..375e04dd 100644 --- a/spec/helpers_spec.rb +++ b/spec/helpers_spec.rb @@ -191,6 +191,30 @@ module Foo; class Bar; end; end end end + describe "#button_to_search" do + it "renders a button with the given query" do + _(@helpers.button_to_search("Foo#<<")).must_equal <<~HTML.chomp + + HTML + end + + it "uses RDoc::CodeObject#full_name for the query when given an RDoc::CodeObject" do + rdoc_method = rdoc_top_level_for(<<~RUBY).find_module_named("Foo").find_method("<<", false) + module Foo; def <<(*); end; end + RUBY + + _(@helpers.button_to_search(rdoc_method)).must_equal <<~HTML.chomp + + HTML + end + + it "supports overriding the displayed name" do + _(@helpers.button_to_search("Foo::Bar", display_name: "Bar")).must_equal <<~HTML.chomp + + HTML + end + end + describe "#full_name" do it "wraps name in " do _(@helpers.full_name("Foo")).must_equal "Foo" @@ -470,27 +494,96 @@ module Foo; module Bar; module Qux; end; end; end end end - describe "#group_by_first_letter" do - it "groups RDoc objects by the first letter of their #name" do + describe "#outline" do + def expected(html, context:) + html.gsub(/\s/, "").gsub(/
  • ([^<]+)/, '
  • \1') + end + + it "renders a nested list of heading links" do context = rdoc_top_level_for(<<~RUBY).find_module_named("Foo") - module Foo - def bar; end - def _bar; end - def baa; end + # == L2-1 + # == L2-2 + # === L3-1 + # ==== L4-1 + # ===== L5-1 + # ====== L6-1 + # == L2-3 + module Foo; end + RUBY - def qux; end - def _qux; end - def Qux; end - end + _(@helpers.outline(context)).must_equal expected(<<~HTML, context: context) +
      +
    • L2-1
    • +
    • L2-2
        +
      • L3-1
          +
        • L4-1
            +
          • L5-1
              +
            • L6-1
            • +
          • +
        • +
      • +
    • +
    • L2-3
    • +
    + HTML + end + + it "handles skipped heading levels" do + context = rdoc_top_level_for(<<~RUBY).find_module_named("Foo") + # === L3-1 + # ===== L5-1 + # == L2-1 + # ==== L4-1 + module Foo; end RUBY - expected = { - "#" => [context.find_method("_bar", false), context.find_method("_qux", false)], - "B" => [context.find_method("baa", false), context.find_method("bar", false)], - "Q" => [context.find_method("Qux", false), context.find_method("qux", false)], - } + _(@helpers.outline(context)).must_equal expected(<<~HTML, context: context) +
      +
    • L3-1
        +
      • L5-1
      • +
    • +
    • L2-1
        +
      • L4-1
      • +
    • +
    + HTML + end - _(@helpers.group_by_first_letter(context.method_list)).must_equal expected + it "omits the h1 heading when it is primary" do + context = rdoc_top_level_for(<<~RUBY).find_module_named("Foo") + # = L1-1 + # == L2-1 + # === L3-1 + # == L2-2 + module Foo; end + RUBY + + _(@helpers.outline(context)).must_equal expected(<<~HTML, context: context) +
      +
    • L2-1
        +
      • L3-1
      • +
    • +
    • L2-2
    • +
    + HTML + end + + it "preserves all h1 headings when any are non-primary" do + context = rdoc_top_level_for(<<~RUBY).find_module_named("Foo") + # = L1-1 + # == L2-1 + # = L1-2 + module Foo; end + RUBY + + _(@helpers.outline(context)).must_equal expected(<<~HTML, context: context) +
      +
    • L1-1
        +
      • L2-1
      • +
    • +
    • L1-2
    • +
    + HTML end end @@ -527,6 +620,54 @@ def ul(items) end end + describe "#top_modules" do + it "returns top-level classes and modules in sorted order" do + top_level = rdoc_top_level_for <<~RUBY + class Foo; module Hoge; end; end + module Bar; class Fuga; end; end + RUBY + + _(@helpers.top_modules(top_level.store)). + must_equal [top_level.find_module_named("Bar"), top_level.find_module_named("Foo")] + end + + it "handles flattened class and module declarations" do + top_level = rdoc_top_level_for <<~RUBY + class Foo::Hoge; end + module Bar::Fuga; end + RUBY + + _(@helpers.top_modules(top_level.store)). + must_equal [top_level.find_module_named("Bar"), top_level.find_module_named("Foo")] + end + + it "excludes core extensions (based on options.core_ext_pattern)" do + top_level = rdoc_top_level_for <<~RUBY + module Foo; end + RUBY + + @helpers.options.core_ext_pattern = /#{Regexp.escape top_level.name}/ + + _(@helpers.top_modules(top_level.store)).must_be_empty + end + end + + describe "#core_extensions" do + it "returns top-level core extensions in sorted order (based on options.core_ext_pattern)" do + top_level = rdoc_top_level_for <<~RUBY + class Foo; module Hoge; end; end + module Bar; class Fuga; end; end + RUBY + + _(@helpers.core_extensions(top_level.store)).must_be_empty + + @helpers.options.core_ext_pattern = /#{Regexp.escape top_level.name}/ + + _(@helpers.core_extensions(top_level.store)). + must_equal [top_level.find_module_named("Bar"), top_level.find_module_named("Foo")] + end + end + describe "#module_breadcrumbs" do it "renders links for each of the module's parents" do top_level = rdoc_top_level_for <<~RUBY @@ -599,6 +740,28 @@ class Foo; include M1; end end end + describe "#module_methods" do + it "returns all methods of a given module, sorted by definition scope and name" do + rdoc_module = rdoc_top_level_for(<<~RUBY).find_module_named("Foo") + module Foo + def foo; end + class << self + private def foo; end + end + private def bar; end + def self.bar; end + end + RUBY + + _(@helpers.module_methods(rdoc_module)).must_equal [ + rdoc_module.find_method("bar", true), + rdoc_module.find_method("foo", true), + rdoc_module.find_method("bar", false), + rdoc_module.find_method("foo", false), + ] + end + end + describe "#method_signature" do it "returns the method signature wrapped in " do method = rdoc_top_level_for(<<~RUBY).find_module_named("Foo").find_method("bar", false) diff --git a/spec/rdoc_generator_spec.rb b/spec/rdoc_generator_spec.rb index 6b12e569..34e1d384 100644 --- a/spec/rdoc_generator_spec.rb +++ b/spec/rdoc_generator_spec.rb @@ -55,6 +55,16 @@ def parse_options(*options) end end + describe "options.core_ext_pattern" do + it "is /core_ext/ by default" do + _(parse_options().core_ext_pattern).must_equal %r"core_ext" + end + + it "can be set via --core-ext" do + _(parse_options("--core-ext", "foo.*bar").core_ext_pattern).must_equal %r"foo.*bar" + end + end + describe "options.github" do it "is disabled by default" do _(parse_options().github).must_be_nil diff --git a/spec/rdoc_monkey_patches_spec.rb b/spec/rdoc_monkey_patches_spec.rb index 51c70e8e..e112e2e9 100644 --- a/spec/rdoc_monkey_patches_spec.rb +++ b/spec/rdoc_monkey_patches_spec.rb @@ -47,4 +47,32 @@ def qux; end _(description).must_match %r"and qux \(also\) cannot" end end + + describe RDoc::Parser::Ruby do + it "does not pollute RDoc::ClassModule#in_files when parsing constants" do + Dir.mktmpdir do |dir| + Dir.chdir(dir) do + File.write("float_ext.rb", <<~RUBY) + class Float + def ext; end + end + RUBY + + File.write("foo.rb", <<~RUBY) + class Foo + def foo; Float::INFINITY; end + end + RUBY + + rdoc_store = rdoc_dry_run("--files", "float_ext.rb", "foo.rb").store + + _(rdoc_store.find_class_or_module("Float").in_files). + must_equal [rdoc_store.find_file_named("float_ext.rb")] + + _(rdoc_store.find_file_named("foo.rb").classes_and_modules). + must_equal [rdoc_store.find_class_or_module("Foo")] + end + end + end + end end