infodocs 0.3.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 (60) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +26 -0
  3. data/CONTRIBUTING.md +31 -0
  4. data/LICENSE.txt +21 -0
  5. data/README.md +341 -0
  6. data/Rakefile +10 -0
  7. data/app/controllers/infodocs/application_controller.rb +68 -0
  8. data/app/controllers/infodocs/assets_controller.rb +15 -0
  9. data/app/controllers/infodocs/editor_controller.rb +36 -0
  10. data/app/controllers/infodocs/indexes_controller.rb +22 -0
  11. data/app/controllers/infodocs/pages_controller.rb +48 -0
  12. data/app/controllers/infodocs/raws_controller.rb +13 -0
  13. data/app/controllers/infodocs/searches_controller.rb +18 -0
  14. data/app/controllers/infodocs/tags_controller.rb +11 -0
  15. data/app/controllers/infodocs/trees_controller.rb +40 -0
  16. data/app/helpers/infodocs/application_helper.rb +70 -0
  17. data/app/javascript/infodocs/application.js +74 -0
  18. data/app/javascript/infodocs/controllers/editor_controller.js +135 -0
  19. data/app/javascript/infodocs/controllers/highlight_controller.js +44 -0
  20. data/app/javascript/infodocs/controllers/mermaid_controller.js +25 -0
  21. data/app/javascript/infodocs/controllers/search_controller.js +125 -0
  22. data/app/javascript/infodocs/controllers/sidebar_controller.js +34 -0
  23. data/app/javascript/infodocs/controllers/toc_controller.js +18 -0
  24. data/app/javascript/infodocs/controllers/tree_editor_controller.js +65 -0
  25. data/app/javascript/infodocs/controllers/tree_state_controller.js +37 -0
  26. data/app/javascript/infodocs/vendor/sortable.esm.js +3378 -0
  27. data/app/views/infodocs/_delete_button.html.erb +6 -0
  28. data/app/views/infodocs/_handle.html.erb +5 -0
  29. data/app/views/infodocs/_tree.html.erb +35 -0
  30. data/app/views/infodocs/editor/edit.html.erb +61 -0
  31. data/app/views/infodocs/pages/empty_folder.html.erb +6 -0
  32. data/app/views/infodocs/pages/not_found.html.erb +11 -0
  33. data/app/views/infodocs/pages/show.html.erb +92 -0
  34. data/app/views/infodocs/searches/index.html.erb +20 -0
  35. data/app/views/infodocs/tags/index.html.erb +26 -0
  36. data/app/views/layouts/infodocs/application.html.erb +104 -0
  37. data/config/importmap.rb +6 -0
  38. data/config/routes.rb +26 -0
  39. data/lib/generators/infodocs/install/install_generator.rb +59 -0
  40. data/lib/generators/infodocs/install/templates/getting-started.md.tt +8 -0
  41. data/lib/generators/infodocs/install/templates/initializer.rb.tt +51 -0
  42. data/lib/generators/infodocs/views/views_generator.rb +13 -0
  43. data/lib/infodocs/code_theme.rb +130 -0
  44. data/lib/infodocs/configuration.rb +50 -0
  45. data/lib/infodocs/engine.rb +39 -0
  46. data/lib/infodocs/link_checker.rb +43 -0
  47. data/lib/infodocs/linter.rb +48 -0
  48. data/lib/infodocs/page.rb +124 -0
  49. data/lib/infodocs/rails_lexer.rb +39 -0
  50. data/lib/infodocs/renderer.rb +231 -0
  51. data/lib/infodocs/search.rb +131 -0
  52. data/lib/infodocs/source.rb +35 -0
  53. data/lib/infodocs/tree.rb +215 -0
  54. data/lib/infodocs/tree_editor.rb +179 -0
  55. data/lib/infodocs/url_helper.rb +23 -0
  56. data/lib/infodocs/version.rb +5 -0
  57. data/lib/infodocs.rb +74 -0
  58. data/lib/tasks/infodocs.rake +63 -0
  59. data/sig/infodocs.rbs +4 -0
  60. metadata +192 -0
@@ -0,0 +1,6 @@
1
+ <% if infodocs_deletable?(node) %>
2
+ <button type="button" title="Delete <%= node.title %>" aria-label="Delete <%= node.title %>" <%= tag.attributes(infodocs_delete_attributes(node)) %>
3
+ class="inline-flex size-8 shrink-0 items-center justify-center rounded text-gray-400 hover:bg-red-50 hover:text-red-600 dark:hover:bg-red-950">
4
+ <svg class="size-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 7h16M10 11v6M14 11v6M6 7l1 13h10l1-13M9 7V4h6v3"/></svg>
5
+ </button>
6
+ <% end %>
@@ -0,0 +1,5 @@
1
+ <% if infodocs_editable? %>
2
+ <span class="infodocs-handle hidden size-6 shrink-0 cursor-grab touch-none items-center justify-center text-gray-400 group-data-[reordering=true]/nav:inline-flex" aria-hidden="true">
3
+ <svg class="size-4" viewBox="0 0 16 16" fill="currentColor"><circle cx="5" cy="3" r="1.2"/><circle cx="11" cy="3" r="1.2"/><circle cx="5" cy="8" r="1.2"/><circle cx="11" cy="8" r="1.2"/><circle cx="5" cy="13" r="1.2"/><circle cx="11" cy="13" r="1.2"/></svg>
4
+ </span>
5
+ <% end %>
@@ -0,0 +1,35 @@
1
+ <% parent = local_assigns.fetch(:parent, "") %>
2
+ <ul class="space-y-0.5" data-parent-slug="<%= parent %>" data-infodocs-tree-editor-target="list">
3
+ <% tag = infodocs_active_tag %>
4
+ <% infodocs_filter_nodes(nodes, tag).each do |node| %>
5
+ <% active = current == node.slug %>
6
+ <% link_classes = "flex min-h-11 min-w-0 flex-1 items-center rounded-md px-2 text-sm lg:min-h-8 #{active ? 'bg-blue-50 font-medium text-blue-700 dark:bg-blue-950 dark:text-blue-300' : 'text-gray-600 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-800'}" %>
7
+ <li data-slug="<%= node.slug %>">
8
+ <% if node.folder? %>
9
+ <details <%= "open" if current.to_s == node.slug || current.to_s.start_with?("#{node.slug}/") %> class="group/folder">
10
+ <summary class="flex min-h-11 cursor-pointer list-none items-center gap-2 rounded-md px-2 text-sm font-medium hover:bg-gray-100 lg:min-h-8 dark:hover:bg-gray-800">
11
+ <%= render "infodocs/handle" %>
12
+ <svg class="size-3 shrink-0 transition-transform group-open/folder:rotate-90" viewBox="0 0 12 12" fill="currentColor"><path d="M4 2l5 4-5 4z"/></svg>
13
+ <span class="min-w-0 flex-1 truncate"><%= node.title %></span>
14
+ <%= render "infodocs/delete_button", node: node %>
15
+ </summary>
16
+ <div class="ml-3 border-l border-gray-200 pl-2 dark:border-gray-800">
17
+ <% if node.page %>
18
+ <%= link_to "Overview", page_path(node.slug, tag: tag), class: link_classes %>
19
+ <% end %>
20
+ <%= render "infodocs/tree", nodes: node.children.reject(&:hidden), current: current, parent: node.slug %>
21
+ </div>
22
+ </details>
23
+ <% else %>
24
+ <div class="flex items-center gap-1">
25
+ <%= render "infodocs/handle" %>
26
+ <%= link_to page_path(node.slug, tag: tag), aria: { current: (active ? "page" : nil) }, class: link_classes do %>
27
+ <span class="truncate"><%= node.title %></span>
28
+ <% if node.page&.draft? %><span class="ml-2 shrink-0 rounded bg-amber-100 px-1.5 text-[10px] font-medium uppercase text-amber-800 dark:bg-amber-900 dark:text-amber-200">draft</span><% end %>
29
+ <% end %>
30
+ <%= render "infodocs/delete_button", node: node %>
31
+ </div>
32
+ <% end %>
33
+ </li>
34
+ <% end %>
35
+ </ul>
@@ -0,0 +1,61 @@
1
+ <% back = @page.slug.empty? ? root_path : page_path(@page.slug) %>
2
+ <div data-controller="infodocs-editor"
3
+ data-infodocs-editor-save-url-value="<%= edit_page_path(@page.slug.presence) %>"
4
+ data-infodocs-editor-preview-url-value="<%= preview_page_path(@page.slug.presence) %>"
5
+ data-infodocs-editor-back-url-value="<%= back %>"
6
+ data-infodocs-editor-tags-url-value="<%= tag_tree_page_path %>"
7
+ data-infodocs-editor-slug-value="<%= @page.slug %>"
8
+ class="flex min-h-[calc(100dvh-7rem)] flex-col">
9
+ <div class="mb-3 flex flex-wrap items-center gap-2">
10
+ <div class="min-w-0 flex-1">
11
+ <h1 class="truncate text-lg font-semibold">Editing <%= @page.title %></h1>
12
+ <p class="truncate font-mono text-xs text-gray-500"><%= @page.relative_path %></p>
13
+ </div>
14
+ <span class="text-sm text-gray-500" role="status" data-infodocs-editor-target="status"></span>
15
+ <% page_node = docs_tree.node(@page.slug) %>
16
+ <% if page_node&.leaf? && infodocs_deletable?(page_node) %>
17
+ <button type="button" <%= tag.attributes(infodocs_delete_attributes(page_node)) %>
18
+ class="inline-flex min-h-11 items-center rounded-md px-3 text-sm font-medium text-red-600 hover:bg-red-50 dark:hover:bg-red-950">Delete</button>
19
+ <% end %>
20
+ <%= link_to "Close", back, class: "inline-flex min-h-11 items-center rounded-md px-3 text-sm font-medium text-gray-600 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-800" %>
21
+ <button type="button" data-action="infodocs-editor#save"
22
+ class="inline-flex min-h-11 items-center rounded-md bg-blue-600 px-4 text-sm font-medium text-white hover:bg-blue-500">Save <kbd class="ml-2 hidden text-xs opacity-70 sm:inline">⌘S</kbd></button>
23
+ </div>
24
+
25
+ <div class="mb-3 flex flex-wrap items-center gap-2 text-sm">
26
+ <label for="infodocs-tags" class="font-medium">Tags</label>
27
+ <input id="infodocs-tags" type="text" list="infodocs-tag-list" autocomplete="off" placeholder="guide, api"
28
+ value="<%= @page.manifest_tags.join(', ') %>" data-infodocs-editor-target="tags" data-action="input->infodocs-editor#changed"
29
+ class="min-w-48 flex-1 rounded-md border border-gray-300 bg-white px-2 py-1.5 text-base focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500 sm:text-sm dark:border-gray-700 dark:bg-gray-900">
30
+ <datalist id="infodocs-tag-list"><% visible_tags.each_key do |tag| %><option value="<%= tag %>"><% end %></datalist>
31
+ <% frontmatter_only = @page.tags - @page.manifest_tags %>
32
+ <% if frontmatter_only.any? %><span class="text-xs text-gray-500">also in frontmatter: <%= frontmatter_only.join(", ") %></span><% end %>
33
+ </div>
34
+
35
+ <div class="flex flex-wrap items-center gap-0.5 rounded-t-md border border-b-0 border-gray-300 bg-gray-50 px-1 py-1 dark:border-gray-700 dark:bg-gray-900" role="toolbar" aria-label="Formatting">
36
+ <% [["bold", "B", "Bold"], ["italic", "I", "Italic"], ["code", "<>", "Inline code"], ["link", "Link", "Link"], ["h1", "H1", "Heading 1"], ["h2", "H2", "Heading 2"], ["h3", "H3", "Heading 3"],
37
+ ["list", "• List", "Bullet list"], ["task", "☑ Task", "Task list"], ["quote", "❝", "Quote"], ["ruby", "Ruby", "Ruby code block"], ["js", "JS", "JavaScript code block"],
38
+ ["mermaid", "Mermaid", "Mermaid diagram"]].each do |action, label, title| %>
39
+ <button type="button" title="<%= title %>" data-action="infodocs-editor#format" data-infodocs-editor-kind-param="<%= action %>"
40
+ class="inline-flex min-h-9 min-w-9 items-center justify-center rounded px-2 text-sm font-medium text-gray-700 hover:bg-gray-200 dark:text-gray-200 dark:hover:bg-gray-800"><%= label %></button>
41
+ <% end %>
42
+ <div class="ml-auto flex gap-1 lg:hidden" role="tablist">
43
+ <button type="button" role="tab" aria-selected="true" data-infodocs-editor-target="tab" data-pane="write" data-action="infodocs-editor#show" data-infodocs-editor-pane-param="write"
44
+ class="min-h-9 rounded px-3 text-sm aria-selected:bg-white aria-selected:font-medium dark:aria-selected:bg-gray-800">Write</button>
45
+ <button type="button" role="tab" aria-selected="false" data-infodocs-editor-target="tab" data-pane="preview" data-action="infodocs-editor#show" data-infodocs-editor-pane-param="preview"
46
+ class="min-h-9 rounded px-3 text-sm aria-selected:bg-white aria-selected:font-medium dark:aria-selected:bg-gray-800">Preview</button>
47
+ </div>
48
+ </div>
49
+
50
+ <div class="grid min-h-0 flex-1 gap-0 rounded-b-md border border-gray-300 lg:grid-cols-2 dark:border-gray-700">
51
+ <textarea data-infodocs-editor-target="input" data-action="input->infodocs-editor#changed keydown->infodocs-editor#keydown"
52
+ spellcheck="false" aria-label="Markdown source"
53
+ class="min-h-[60dvh] w-full resize-none border-0 bg-white p-4 font-mono text-sm leading-6 focus:outline-none focus:ring-0 lg:border-r lg:border-gray-300 dark:bg-gray-950 dark:lg:border-gray-700"><%= @content %></textarea>
54
+ <div data-infodocs-editor-target="pane" data-pane="preview" class="hidden min-h-[60dvh] overflow-y-auto p-4 lg:block" data-controller="infodocs-mermaid">
55
+ <div data-infodocs-editor-target="preview"
56
+ class="prose prose-slate max-w-none dark:prose-invert prose-table:block prose-table:overflow-x-auto [&_:not(pre)>code]:rounded-md [&_:not(pre)>code]:bg-gray-100 [&_:not(pre)>code]:px-1.5 [&_:not(pre)>code]:py-0.5 [&_:not(pre)>code]:text-[0.875em] [&_:not(pre)>code]:font-normal [&_:not(pre)>code]:before:content-none [&_:not(pre)>code]:after:content-none dark:[&_:not(pre)>code]:bg-gray-800"></div>
57
+ </div>
58
+ </div>
59
+
60
+ <p class="mt-3 text-sm text-gray-500">Saving writes <code><%= @page.relative_path %></code> in your working tree — review and commit the change with git.</p>
61
+ </div>
@@ -0,0 +1,6 @@
1
+ <h1 class="text-2xl font-bold"><%= @folder.title %></h1>
2
+ <p class="mt-2 text-gray-600 dark:text-gray-400">This folder has no pages yet.</p>
3
+ <% if infodocs_editable? %>
4
+ <p class="mt-2 text-sm text-gray-500">Use <strong>New page</strong> in the sidebar and pick this folder.</p>
5
+ <% end %>
6
+ <%= link_to "Back to docs", root_path, class: "mt-4 inline-block text-blue-600 underline" %>
@@ -0,0 +1,11 @@
1
+ <h1 class="text-2xl font-bold">Page not found</h1>
2
+ <p class="mt-2 text-gray-600 dark:text-gray-400">There is no document at <code><%= params[:path] %></code>.</p>
3
+ <%= link_to "Back to docs", root_path, class: "mt-4 inline-block text-blue-600 underline" %>
4
+ <% if @suggestions.present? %>
5
+ <h2 class="mt-8 text-sm font-medium">Did you mean</h2>
6
+ <ul class="mt-2 space-y-1">
7
+ <% @suggestions.each do |result| %>
8
+ <li><%= link_to result.title, page_path(result.slug), class: "text-blue-600 hover:underline" %></li>
9
+ <% end %>
10
+ </ul>
11
+ <% end %>
@@ -0,0 +1,92 @@
1
+ <div class="flex gap-10" data-controller="infodocs-mermaid">
2
+ <article class="min-w-0 flex-1">
3
+ <div class="mb-4 flex items-start justify-between gap-3">
4
+ <nav class="flex flex-wrap items-center gap-1 text-sm text-gray-500" aria-label="Breadcrumb">
5
+ <%= link_to Infodocs.config.title, root_path, class: "hover:underline" %>
6
+ <% @trail.each do |node| %>
7
+ <span aria-hidden="true">/</span>
8
+ <% if node.slug == @page.slug %>
9
+ <span class="text-gray-900 dark:text-gray-100"><%= node.title %></span>
10
+ <% else %>
11
+ <%= link_to node.title, page_path(node.slug), class: "hover:underline" %>
12
+ <% end %>
13
+ <% end %>
14
+ </nav>
15
+
16
+ <div class="flex shrink-0 items-center gap-2">
17
+ <button type="button" data-infodocs-copy-markdown="<%= raw_page_path(@page.slug.presence) %>" title="Copy this page as Markdown"
18
+ class="inline-flex min-h-9 shrink-0 items-center gap-1.5 rounded-md border border-gray-300 px-3 text-sm font-medium text-gray-700 hover:bg-gray-100 dark:border-gray-700 dark:text-gray-200 dark:hover:bg-gray-800">
19
+ <svg class="size-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="11" height="11" rx="2"/><path d="M5 15V6a2 2 0 012-2h9"/></svg>
20
+ <span data-label>Copy Markdown</span>
21
+ </button>
22
+ <% if infodocs_editable? %>
23
+ <%= link_to edit_page_path(@page.slug.presence), title: "Edit #{@page.relative_path}",
24
+ class: "inline-flex min-h-9 shrink-0 items-center gap-1.5 rounded-md border border-gray-300 px-3 text-sm font-medium text-gray-700 hover:bg-gray-100 dark:border-gray-700 dark:text-gray-200 dark:hover:bg-gray-800" do %>
25
+ <svg class="size-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 20h9M16.5 3.5a2.1 2.1 0 013 3L7 19l-4 1 1-4z"/></svg>
26
+ Edit
27
+ <% end %>
28
+ <% page_node = docs_tree.node(@page.slug) %>
29
+ <% if page_node&.leaf? && infodocs_deletable?(page_node) %>
30
+ <button type="button" <%= tag.attributes(infodocs_delete_attributes(page_node)) %> title="Delete this page"
31
+ class="inline-flex min-h-9 shrink-0 items-center gap-1.5 rounded-md border border-red-200 px-3 text-sm font-medium text-red-600 hover:bg-red-50 dark:border-red-900 dark:hover:bg-red-950">Delete</button>
32
+ <% end %>
33
+ <% end %>
34
+ </div>
35
+ </div>
36
+
37
+ <% if @rendered.toc.any? %>
38
+ <details class="mb-4 rounded-md border border-gray-200 text-sm xl:hidden dark:border-gray-800">
39
+ <summary class="flex min-h-11 cursor-pointer items-center px-3 font-medium">On this page</summary>
40
+ <ul class="space-y-1 border-t border-gray-200 px-3 py-2 dark:border-gray-800">
41
+ <% @rendered.toc.each do |heading| %>
42
+ <li class="<%= 'pl-4' if heading.level == 3 %>"><a href="#<%= heading.id %>" class="block py-1 text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-100"><%= heading.text %></a></li>
43
+ <% end %>
44
+ </ul>
45
+ </details>
46
+ <% end %>
47
+ <% if @page.tags.any? %>
48
+ <div class="mb-4 flex flex-wrap gap-1.5">
49
+ <% @page.tags.each do |tag| %>
50
+ <%= link_to tag, tags_path(tag), class: "rounded-full border border-gray-300 px-2.5 py-0.5 text-xs text-gray-600 hover:bg-gray-100 dark:border-gray-700 dark:text-gray-400 dark:hover:bg-gray-800" %>
51
+ <% end %>
52
+ </div>
53
+ <% end %>
54
+ <div data-controller="infodocs-highlight" class="prose prose-slate max-w-none dark:prose-invert prose-table:block prose-table:overflow-x-auto [&_:not(pre)>code]:rounded-md [&_:not(pre)>code]:bg-gray-100 [&_:not(pre)>code]:px-1.5 [&_:not(pre)>code]:py-0.5 [&_:not(pre)>code]:text-[0.875em] [&_:not(pre)>code]:font-normal [&_:not(pre)>code]:before:content-none [&_:not(pre)>code]:after:content-none dark:[&_:not(pre)>code]:bg-gray-800">
55
+ <%= @rendered.html.html_safe %>
56
+ </div>
57
+
58
+ <% repo_url = @page.repo_edit_url(Infodocs.config.repo_url, Infodocs.config.repo_branch, Rails.root) %>
59
+ <div class="mt-12 flex flex-wrap items-center justify-between gap-2 text-xs text-gray-500">
60
+ <span>Last updated <time datetime="<%= @page.updated_at.iso8601 %>"><%= @page.updated_at.strftime("%-d %b %Y") %></time></span>
61
+ <% if repo_url %><%= link_to "Edit on GitHub", repo_url, class: "hover:text-gray-900 hover:underline dark:hover:text-gray-100", target: "_blank", rel: "noopener" %><% end %>
62
+ </div>
63
+ <nav class="mt-4 grid gap-3 border-t border-gray-200 pt-6 sm:grid-cols-2 dark:border-gray-800" aria-label="Pagination">
64
+ <% if @previous %>
65
+ <%= link_to page_path(@previous.slug), class: "rounded-md border border-gray-200 p-4 hover:bg-gray-50 dark:border-gray-800 dark:hover:bg-gray-900" do %>
66
+ <span class="block text-xs text-gray-500">← Previous</span><%= @previous.title %>
67
+ <% end %>
68
+ <% else %><span></span><% end %>
69
+ <% if @next %>
70
+ <%= link_to page_path(@next.slug), class: "rounded-md border border-gray-200 p-4 text-right hover:bg-gray-50 dark:border-gray-800 dark:hover:bg-gray-900" do %>
71
+ <span class="block text-xs text-gray-500">Next →</span><%= @next.title %>
72
+ <% end %>
73
+ <% end %>
74
+ </nav>
75
+ </article>
76
+
77
+ <% if @rendered.toc.any? %>
78
+ <aside class="hidden w-56 shrink-0 xl:block" data-controller="infodocs-toc">
79
+ <div class="sticky top-20 text-sm">
80
+ <p class="mb-2 font-medium">On this page</p>
81
+ <ul class="space-y-1 border-l border-gray-200 dark:border-gray-800">
82
+ <% @rendered.toc.each do |heading| %>
83
+ <li class="<%= 'pl-6' if heading.level == 3 %> <%= 'pl-3' if heading.level == 2 %>">
84
+ <a href="#<%= heading.id %>" data-infodocs-toc-target="link"
85
+ class="block py-0.5 text-gray-500 hover:text-gray-900 aria-[current]:font-medium aria-[current]:text-blue-600 dark:hover:text-gray-100"><%= heading.text %></a>
86
+ </li>
87
+ <% end %>
88
+ </ul>
89
+ </div>
90
+ </aside>
91
+ <% end %>
92
+ </div>
@@ -0,0 +1,20 @@
1
+ <h1 class="text-2xl font-bold">Search</h1>
2
+ <% if @query.empty? %>
3
+ <p class="mt-2 text-gray-600 dark:text-gray-400">
4
+ Syntax: <code>word word</code> (AND), <code>"exact phrase"</code>, <code>a OR b</code>, <code>-exclude</code>,
5
+ <code>pref*</code>, <code>title:setup</code>, <code>path:guides</code>, <code>heading:install</code>.
6
+ </p>
7
+ <% elsif @results.empty? %>
8
+ <p class="mt-2 text-gray-600 dark:text-gray-400">No results for <strong><%= @query %></strong>.</p>
9
+ <% else %>
10
+ <p class="mt-2 text-sm text-gray-500"><%= pluralize(@results.size, "result") %> for <strong><%= @query %></strong></p>
11
+ <ol class="mt-4 divide-y divide-gray-200 dark:divide-gray-800">
12
+ <% @results.each do |result| %>
13
+ <li class="py-4">
14
+ <%= link_to result.title, page_path(result.slug, q: @query), class: "text-lg font-medium text-blue-700 hover:underline dark:text-blue-300" %>
15
+ <p class="text-xs text-gray-500">/<%= result.slug %></p>
16
+ <p class="mt-1 text-sm text-gray-700 [&_mark]:rounded [&_mark]:bg-yellow-200 [&_mark]:px-0.5 dark:text-gray-300 dark:[&_mark]:bg-yellow-700"><%= result.snippet.html_safe %></p>
17
+ </li>
18
+ <% end %>
19
+ </ol>
20
+ <% end %>
@@ -0,0 +1,26 @@
1
+ <h1 class="text-2xl font-bold"><%= @tag ? "Tag: #{@tag}" : "Tags" %></h1>
2
+ <% if @tags.empty? %>
3
+ <p class="mt-2 text-gray-600 dark:text-gray-400">No tags yet. Add <code>tags: [guide, api]</code> to a page's frontmatter<%= " or use the tags field in the editor" if infodocs_editable? %>.</p>
4
+ <% else %>
5
+ <div class="mt-4 flex flex-wrap gap-2">
6
+ <% @tags.each do |tag, count| %>
7
+ <%= link_to "#{tag} · #{count}", (tag == @tag ? tags_path : tags_path(tag)),
8
+ class: "rounded-full border px-3 py-1 text-sm #{tag == @tag ? 'border-blue-600 bg-blue-600 text-white' : 'border-gray-300 text-gray-700 hover:bg-gray-100 dark:border-gray-700 dark:text-gray-300 dark:hover:bg-gray-800'}" %>
9
+ <% end %>
10
+ </div>
11
+ <% if @tag %>
12
+ <% if @pages.empty? %>
13
+ <p class="mt-6 text-gray-600 dark:text-gray-400">No pages tagged <strong><%= @tag %></strong>.</p>
14
+ <% else %>
15
+ <ul class="mt-6 divide-y divide-gray-200 dark:divide-gray-800">
16
+ <% @pages.each do |page| %>
17
+ <li class="py-3">
18
+ <%= link_to page.title, page_path(page.slug), class: "font-medium text-blue-700 hover:underline dark:text-blue-300" %>
19
+ <p class="text-xs text-gray-500">/<%= page.slug %></p>
20
+ <% if page.description.present? %><p class="mt-1 text-sm text-gray-700 dark:text-gray-300"><%= page.description %></p><% end %>
21
+ </li>
22
+ <% end %>
23
+ </ul>
24
+ <% end %>
25
+ <% end %>
26
+ <% end %>
@@ -0,0 +1,104 @@
1
+ <!DOCTYPE html>
2
+ <html lang="<%= I18n.locale %>" class="h-full">
3
+ <head>
4
+ <title><%= [@page&.title, Infodocs.config.title].compact.join(" · ") %></title>
5
+ <meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">
6
+ <%= csrf_meta_tags %>
7
+ <%= csp_meta_tag %>
8
+ <% if Infodocs.config.mermaid_url.present? %><meta name="infodocs-mermaid-url" content="<%= Infodocs.config.mermaid_url %>"><% end %>
9
+ <% Infodocs.config.stylesheets.each do |sheet| %>
10
+ <%= stylesheet_link_tag sheet, "data-turbo-track": "reload" %>
11
+ <% end %>
12
+ <style <%= tag.attributes(nonce: content_security_policy_nonce) %>><%= raw infodocs_highlight_css %></style>
13
+ <%= javascript_importmap_tags "infodocs/application" %>
14
+ <% if (favicon = infodocs_option(:favicon)).present? %><link rel="icon" href="<%= favicon %>"><% end %>
15
+ <%= raw infodocs_option(:head_html) %>
16
+ </head>
17
+
18
+ <body class="h-full bg-white text-gray-900 antialiased dark:bg-gray-950 dark:text-gray-100"
19
+ data-controller="infodocs-sidebar infodocs-search">
20
+ <a href="#infodocs-main" class="sr-only focus:not-sr-only focus:absolute focus:left-2 focus:top-2 focus:z-50 focus:rounded focus:bg-white focus:px-3 focus:py-2 focus:text-sm focus:shadow dark:focus:bg-gray-900">Skip to content</a>
21
+ <header class="sticky top-0 z-30 flex h-14 items-center gap-3 border-b border-gray-200 bg-white/90 px-4 backdrop-blur dark:border-gray-800 dark:bg-gray-950/90">
22
+ <button type="button" class="-ml-2 inline-flex size-11 items-center justify-center rounded-md hover:bg-gray-100 dark:hover:bg-gray-800"
23
+ aria-label="Toggle navigation" aria-controls="infodocs-sidebar" data-action="infodocs-sidebar#toggle">
24
+ <svg class="size-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 6h16M4 12h16M4 18h16"/></svg>
25
+ </button>
26
+ <%= link_to root_path, class: "flex items-center gap-2 font-semibold" do %>
27
+ <% if (logo = infodocs_option(:logo)).present? %><img src="<%= logo %>" alt="" class="h-6 w-auto"><% end %>
28
+ <span><%= Infodocs.config.title %></span>
29
+ <% end %>
30
+ <nav aria-label="Site links" class="ml-2 hidden items-center gap-1 md:flex">
31
+ <% infodocs_header_links.each do |link| %>
32
+ <%= link_to link[:label], link[:url], class: "rounded-md px-2 py-1 text-sm text-gray-600 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-800" %>
33
+ <% end %>
34
+ </nav>
35
+
36
+ <%= form_with url: search_path, method: :get, class: "relative ml-auto w-full max-w-sm", data: { turbo: false, infodocs_search_url: search_path(format: :json) } do %>
37
+ <input type="search" name="q" value="<%= params[:q] %>" placeholder="Search… ⌘K"
38
+ autocomplete="off" data-infodocs-search-target="input"
39
+ class="w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-base focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500 sm:text-sm dark:border-gray-700 dark:bg-gray-900">
40
+ <ul hidden role="listbox" data-infodocs-search-target="results"
41
+ class="absolute inset-x-0 top-full z-40 mt-1 max-h-96 overflow-y-auto rounded-md border border-gray-200 bg-white py-1 shadow-lg dark:border-gray-700 dark:bg-gray-900"></ul>
42
+ <% end %>
43
+ </header>
44
+
45
+ <div class="flex w-full">
46
+ <div class="fixed inset-0 z-30 hidden bg-black/40 lg:hidden" data-infodocs-sidebar-target="backdrop" data-action="click->infodocs-sidebar#close"></div>
47
+
48
+ <aside id="infodocs-sidebar" data-infodocs-sidebar-target="panel" data-open="false" data-collapsed="false"
49
+ class="fixed inset-y-0 left-0 z-40 w-72 -translate-x-full overflow-y-auto border-r border-gray-200 bg-white px-3 pb-8 pt-16 transition-transform data-[open=true]:translate-x-0 lg:sticky lg:top-14 lg:z-0 lg:h-[calc(100dvh-3.5rem)] lg:shrink-0 lg:translate-x-0 lg:pt-4 lg:data-[collapsed=true]:hidden dark:border-gray-800 dark:bg-gray-950">
50
+ <div data-controller="infodocs-tree-editor" data-infodocs-tree-editor-url-value="<%= move_tree_path %>">
51
+ <% if infodocs_editable? %>
52
+ <div class="mb-2 flex items-center justify-between gap-2 px-2">
53
+ <span class="text-xs font-medium uppercase tracking-wide text-gray-400">Pages</span>
54
+ <button type="button" data-action="infodocs-tree-editor#toggle" data-infodocs-tree-editor-target="toggle" aria-pressed="false"
55
+ class="min-h-8 rounded-md px-2 text-xs font-medium text-gray-600 hover:bg-gray-100 aria-pressed:bg-blue-600 aria-pressed:text-white dark:text-gray-300 dark:hover:bg-gray-800">Reorder</button>
56
+ </div>
57
+ <p class="mb-2 hidden px-2 text-xs text-gray-500" data-infodocs-tree-editor-target="status" role="status"></p>
58
+ <details class="mb-3 rounded-md border border-gray-200 dark:border-gray-800">
59
+ <summary class="flex min-h-9 cursor-pointer list-none items-center gap-1.5 px-2 text-sm font-medium text-gray-700 hover:bg-gray-100 dark:text-gray-200 dark:hover:bg-gray-800">
60
+ <svg class="size-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 5v14M5 12h14"/></svg> New page
61
+ </summary>
62
+ <%= form_with url: new_tree_page_path, class: "space-y-2 border-t border-gray-200 p-2 dark:border-gray-800", data: { turbo: false } do %>
63
+ <input type="text" name="title" required placeholder="Title" autocomplete="off" aria-label="Title"
64
+ class="w-full rounded-md border border-gray-300 bg-white px-2 py-2 text-base focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500 sm:text-sm dark:border-gray-700 dark:bg-gray-900">
65
+ <select name="parent" aria-label="Folder" class="w-full rounded-md border border-gray-300 bg-white px-2 py-2 text-base sm:text-sm dark:border-gray-700 dark:bg-gray-900">
66
+ <%= options_for_select(infodocs_folder_options, @page&.directory_slug) %>
67
+ </select>
68
+ <select name="kind" aria-label="Type" class="w-full rounded-md border border-gray-300 bg-white px-2 py-2 text-base sm:text-sm dark:border-gray-700 dark:bg-gray-900">
69
+ <option value="page">Page (.md file)</option>
70
+ <option value="folder">Folder</option>
71
+ </select>
72
+ <button type="submit" class="min-h-9 w-full rounded-md bg-blue-600 px-3 text-sm font-medium text-white hover:bg-blue-500">Create &amp; edit</button>
73
+ <% end %>
74
+ </details>
75
+ <% end %>
76
+ <% if infodocs_header_links.any? %>
77
+ <nav aria-label="Site links" class="mb-3 flex flex-wrap gap-1 px-2 md:hidden">
78
+ <% infodocs_header_links.each do |link| %>
79
+ <%= link_to link[:label], link[:url], class: "rounded-md border border-gray-200 px-2 py-1 text-xs text-gray-600 dark:border-gray-700 dark:text-gray-400" %>
80
+ <% end %>
81
+ </nav>
82
+ <% end %>
83
+ <% if visible_tags.any? %>
84
+ <div class="mb-3 flex flex-wrap items-center gap-1.5 px-2" aria-label="Filter by tag" role="group">
85
+ <% visible_tags.each do |tag, count| %>
86
+ <% on = infodocs_active_tag == tag %>
87
+ <%= link_to "#{tag} #{count}", (on ? request.path : "#{request.path}?#{{ tag: tag }.to_query}"), aria: { pressed: on },
88
+ class: "rounded-full border px-2 py-0.5 text-xs #{on ? 'border-blue-600 bg-blue-600 text-white' : 'border-gray-300 text-gray-600 hover:bg-gray-100 dark:border-gray-700 dark:text-gray-400 dark:hover:bg-gray-800'}" %>
89
+ <% end %>
90
+ <%= link_to "all tags", tags_path, class: "text-xs text-gray-500 hover:underline" %>
91
+ </div>
92
+ <% end %>
93
+ <nav aria-label="Docs" class="group/nav" data-reordering="false" data-infodocs-tree-editor-target="nav" data-controller="infodocs-tree-state">
94
+ <%= render "infodocs/tree", nodes: docs_tree.children.reject(&:hidden), current: @page&.slug %>
95
+ </nav>
96
+ </div>
97
+ </aside>
98
+
99
+ <main id="infodocs-main" class="min-w-0 flex-1 px-4 py-6 pb-[max(1.5rem,env(safe-area-inset-bottom))] md:px-8">
100
+ <%= yield %>
101
+ </main>
102
+ </div>
103
+ </body>
104
+ </html>
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ pin "infodocs/application", to: "infodocs/application.js"
4
+ pin_all_from File.expand_path("../app/javascript/infodocs/controllers", __dir__), under: "infodocs/controllers"
5
+ pin "mermaid", to: "https://cdn.jsdelivr.net/npm/mermaid@11.4.1/dist/mermaid.esm.min.mjs"
6
+ pin "sortablejs", to: "infodocs/vendor/sortable.esm.js" # 1.15.6, MIT
data/config/routes.rb ADDED
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ Infodocs::Engine.routes.draw do
4
+ get "search", to: "searches#index", as: :search
5
+
6
+ # Editing (development only, guarded in EditorController). The `_` prefix
7
+ # cannot clash with docs pages: the tree ignores names starting with `_`.
8
+ get "_edit(/*path)", to: "editor#edit", as: :edit_page, format: false
9
+ patch "_edit(/*path)", to: "editor#update", format: false
10
+ post "_preview(/*path)", to: "editor#preview", as: :preview_page, format: false
11
+
12
+ post "_tree/new", to: "trees#create", as: :new_tree_page, format: false
13
+ delete "_tree", to: "trees#destroy", as: :delete_tree, format: false
14
+ post "_tree/tags", to: "trees#tags", as: :tag_tree_page, format: false
15
+ get "_tags(/:tag)", to: "tags#index", as: :tags, constraints: { tag: %r{[^/]+} }, format: false
16
+ post "_tree/move", to: "trees#move", as: :move_tree, format: false
17
+
18
+ get "llms.txt", to: "indexes#llms", format: false
19
+ get "sitemap.xml", to: "indexes#sitemap", format: false
20
+
21
+ get "_raw(/*path)", to: "raws#show", as: :raw_page, format: false
22
+ get "_asset/*path", to: "assets#show", as: :asset, format: false
23
+
24
+ root to: "pages#show"
25
+ get "*path", to: "pages#show", as: :page, format: false
26
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Infodocs
4
+ module Generators
5
+ class InstallGenerator < Rails::Generators::Base
6
+ source_root File.expand_path("templates", __dir__)
7
+
8
+ class_option :path, type: :string, default: "docs", desc: "Directory (relative to the app root) holding your .md files"
9
+ class_option :mount, type: :string, default: "/docs", desc: "URL path to mount the docs at"
10
+
11
+ def create_initializer
12
+ template "initializer.rb.tt", "config/initializers/infodocs.rb"
13
+ end
14
+
15
+ def mount_engine
16
+ routes = File.join(destination_root, "config/routes.rb")
17
+ return if File.exist?(routes) && File.read(routes).include?("Infodocs::Engine")
18
+
19
+ route %(mount Infodocs::Engine => "#{options[:mount]}")
20
+ end
21
+
22
+ def create_sample_docs
23
+ empty_directory options[:path]
24
+ create_file File.join(options[:path], "_tree.yml"), "title: Docs\norder: [getting-started]\n"
25
+ template "getting-started.md.tt", File.join(options[:path], "getting-started.md")
26
+ end
27
+
28
+ def configure_tailwind
29
+ css = tailwind_entry
30
+ return say("Tailwind entry CSS not found; add `@source \"#{engine_root}/app\";` and `@plugin \"@tailwindcss/typography\";` to it manually.", :yellow) unless css
31
+
32
+ content = File.read(File.join(destination_root, css))
33
+ append_to_file css, %(\n@plugin "@tailwindcss/typography";\n) unless content.include?("@tailwindcss/typography")
34
+ return if content.include?(engine_root)
35
+
36
+ append_to_file css, %(\n/* Infodocs: scan engine views for utility classes. Re-run `rails g infodocs:install` if the gem path changes. */\n@source "#{engine_root}/app";\n)
37
+ end
38
+
39
+ def ignore_index
40
+ append_to_file ".gitignore", "\n/tmp/infodocs/\n" if File.exist?(File.join(destination_root, ".gitignore"))
41
+ end
42
+
43
+ def show_readme
44
+ say "\nInfodocs installed. Start the server and open #{options[:mount]}.", :green
45
+ end
46
+
47
+ private
48
+
49
+ def tailwind_entry
50
+ %w[app/assets/tailwind/application.css app/assets/stylesheets/application.tailwind.css app/assets/stylesheets/application.css]
51
+ .find { |file| File.exist?(File.join(destination_root, file)) && File.read(File.join(destination_root, file)).include?("tailwindcss") }
52
+ end
53
+
54
+ def engine_root
55
+ Infodocs::Engine.root.to_s
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,8 @@
1
+ # Getting started
2
+
3
+ Welcome. Files in this folder are shown in the sidebar; order them with `_tree.yml`.
4
+
5
+ ```mermaid
6
+ graph LR
7
+ Markdown --> Infodocs --> Docs
8
+ ```
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ Infodocs.configure do |config|
4
+ # One or more directories with .md files. Several become top-level folders.
5
+ # Give a directory its own URL name and sidebar title with a hash:
6
+ # config.paths = [
7
+ # { path: Rails.root.join("docs"), slug: "app", title: "Application" },
8
+ # { path: Rails.root.join("lib/payments/docs"), slug: "payments", title: "Payments" }
9
+ # ]
10
+ config.paths = [Rails.root.join("<%= options[:path] %>")]
11
+
12
+ config.title = "Docs"
13
+
14
+ # :none (public), :basic, :devise or a lambda evaluated in the controller
15
+ # (return truthy to allow), e.g. -> { current_user&.admin? }
16
+ config.auth = :none
17
+
18
+ # Branding (each can be a value or `->(view) { view.image_path("logo.svg") }`).
19
+ # config.logo = "/logo.svg"
20
+ # config.favicon = "/favicon.ico"
21
+ # config.header_links = [{ label: "App", url: "/" }, { label: "Status", url: "https://status.example.com" }]
22
+ # config.head_html = '<script defer src="https://analytics.example.com/script.js"></script>'
23
+
24
+ # Mermaid is loaded from a CDN by default; point it at your own copy if you need to.
25
+ # config.mermaid_url = "/vendor/mermaid.esm.min.mjs"
26
+
27
+ # Hide individual pages from some visitors (tree, search, llms.txt, sitemap and
28
+ # direct URLs). `user` is the signed-in Devise user, or current_user if defined.
29
+ # config.page_authorize = ->(page, user) { !page.tags.include?("internal") || user&.admin? }
30
+
31
+ # Edit/create/move/delete pages from the browser. Only works in development,
32
+ # on this machine; the changes are written to the .md files for you to commit.
33
+ config.editable = Rails.env.development?
34
+
35
+ # config.auth = :basic
36
+ # config.basic_auth = { user: ENV["DOCS_USER"], password: ENV["DOCS_PASSWORD"] }
37
+
38
+ # config.auth = :devise
39
+ # config.devise = { scope: :user, authorize: ->(user) { user.admin? } }
40
+
41
+ # Code colours: :one (default), :github, :vscode, :catppuccin, or tweak one:
42
+ # config.code_theme = { base: :one, dark: { keyword: "#ff79c6" } }
43
+ config.code_theme = :one
44
+
45
+ # "Edit on GitHub" link in the page footer (paths are relative to Rails.root).
46
+ # config.repo_url = "https://github.com/your-org/your-app"
47
+ # config.repo_branch = "main"
48
+
49
+ # Stylesheet tags rendered in the docs layout (tailwindcss-rails builds "tailwind").
50
+ # config.stylesheets = %w[tailwind]
51
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Infodocs
4
+ module Generators
5
+ class ViewsGenerator < Rails::Generators::Base
6
+ desc "Copies Infodocs views into app/views for customisation"
7
+
8
+ def copy_views
9
+ directory Infodocs::Engine.root.join("app/views").to_s, "app/views"
10
+ end
11
+ end
12
+ end
13
+ end