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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +26 -0
- data/CONTRIBUTING.md +31 -0
- data/LICENSE.txt +21 -0
- data/README.md +341 -0
- data/Rakefile +10 -0
- data/app/controllers/infodocs/application_controller.rb +68 -0
- data/app/controllers/infodocs/assets_controller.rb +15 -0
- data/app/controllers/infodocs/editor_controller.rb +36 -0
- data/app/controllers/infodocs/indexes_controller.rb +22 -0
- data/app/controllers/infodocs/pages_controller.rb +48 -0
- data/app/controllers/infodocs/raws_controller.rb +13 -0
- data/app/controllers/infodocs/searches_controller.rb +18 -0
- data/app/controllers/infodocs/tags_controller.rb +11 -0
- data/app/controllers/infodocs/trees_controller.rb +40 -0
- data/app/helpers/infodocs/application_helper.rb +70 -0
- data/app/javascript/infodocs/application.js +74 -0
- data/app/javascript/infodocs/controllers/editor_controller.js +135 -0
- data/app/javascript/infodocs/controllers/highlight_controller.js +44 -0
- data/app/javascript/infodocs/controllers/mermaid_controller.js +25 -0
- data/app/javascript/infodocs/controllers/search_controller.js +125 -0
- data/app/javascript/infodocs/controllers/sidebar_controller.js +34 -0
- data/app/javascript/infodocs/controllers/toc_controller.js +18 -0
- data/app/javascript/infodocs/controllers/tree_editor_controller.js +65 -0
- data/app/javascript/infodocs/controllers/tree_state_controller.js +37 -0
- data/app/javascript/infodocs/vendor/sortable.esm.js +3378 -0
- data/app/views/infodocs/_delete_button.html.erb +6 -0
- data/app/views/infodocs/_handle.html.erb +5 -0
- data/app/views/infodocs/_tree.html.erb +35 -0
- data/app/views/infodocs/editor/edit.html.erb +61 -0
- data/app/views/infodocs/pages/empty_folder.html.erb +6 -0
- data/app/views/infodocs/pages/not_found.html.erb +11 -0
- data/app/views/infodocs/pages/show.html.erb +92 -0
- data/app/views/infodocs/searches/index.html.erb +20 -0
- data/app/views/infodocs/tags/index.html.erb +26 -0
- data/app/views/layouts/infodocs/application.html.erb +104 -0
- data/config/importmap.rb +6 -0
- data/config/routes.rb +26 -0
- data/lib/generators/infodocs/install/install_generator.rb +59 -0
- data/lib/generators/infodocs/install/templates/getting-started.md.tt +8 -0
- data/lib/generators/infodocs/install/templates/initializer.rb.tt +51 -0
- data/lib/generators/infodocs/views/views_generator.rb +13 -0
- data/lib/infodocs/code_theme.rb +130 -0
- data/lib/infodocs/configuration.rb +50 -0
- data/lib/infodocs/engine.rb +39 -0
- data/lib/infodocs/link_checker.rb +43 -0
- data/lib/infodocs/linter.rb +48 -0
- data/lib/infodocs/page.rb +124 -0
- data/lib/infodocs/rails_lexer.rb +39 -0
- data/lib/infodocs/renderer.rb +231 -0
- data/lib/infodocs/search.rb +131 -0
- data/lib/infodocs/source.rb +35 -0
- data/lib/infodocs/tree.rb +215 -0
- data/lib/infodocs/tree_editor.rb +179 -0
- data/lib/infodocs/url_helper.rb +23 -0
- data/lib/infodocs/version.rb +5 -0
- data/lib/infodocs.rb +74 -0
- data/lib/tasks/infodocs.rake +63 -0
- data/sig/infodocs.rbs +4 -0
- metadata +192 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 533feb13c1f6b783ea3c35e75c9ddee6eb8aeb75824211ba63b14abbef9ff365
|
|
4
|
+
data.tar.gz: 1fa1dbcbfe09aa223887c25f3531da22c85345554c4c7a82ff9c35f15501d8be
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 51b65648f39c3aa0ee255d2f6d69d5260ad1078eef149642a7f49a9bf20934bd3a7186a82259f1b6904c3394bae0bedd38f9cdf7fec67837ade5c66fbdf58e2d
|
|
7
|
+
data.tar.gz: 0746e6d98ee89475c1517ea068320d7b843a3d2a476ee1d0ee69c3c74839b4541bfc2572c0e1a85464c6d1ede421a7e0b7f90015a1aedc41a745ff73e01755a8
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.3.0 - 2026-10-28
|
|
4
|
+
|
|
5
|
+
- `infodocs:lint` content checks; `CONTRIBUTING.md` with a browser checklist
|
|
6
|
+
- "Copy Markdown" button and `/docs/_raw/<page>`
|
|
7
|
+
- "On this page" on small screens, sidebar remembers open folders and scrolls to the current page, search terms highlighted on the target page, SortableJS bundled, `config.mermaid_url`
|
|
8
|
+
- Footnotes, heading anchors, code embeds from files (`file=app/x.rb#L1-20`)
|
|
9
|
+
- Branding (`logo`, `favicon`, `header_links`, `head_html`) and `infodocs_page_path` / `infodocs_page_url` helpers for the host app
|
|
10
|
+
- `redirect_from` frontmatter (301); moving pages in edit mode leaves redirects behind
|
|
11
|
+
- `draft: true` frontmatter (development only) and `config.page_authorize` for per-page visibility
|
|
12
|
+
|
|
13
|
+
## 0.2.0 - 2026-10-19
|
|
14
|
+
|
|
15
|
+
- Read-only docs browser: tree sidebar (`_tree.yml` ordering), FTS5 search with advanced syntax, Rouge highlighting, Mermaid, auth modes (none/basic/devise/lambda), several named sources
|
|
16
|
+
- Development-only edit mode: split Markdown editor with live preview, create, move/reorder, delete
|
|
17
|
+
- Code palettes (`one`, `github`, `vscode`, `catppuccin`), code block header with copy button, line numbers and highlighted lines
|
|
18
|
+
- GitHub-style alerts (`> [!NOTE]`, `TIP`, `IMPORTANT`, `WARNING`, `CAUTION`)
|
|
19
|
+
- Images and files next to pages are served by the engine; `infodocs:check_links` rake task
|
|
20
|
+
- Page footer: last updated (git) and optional "Edit on GitHub" (`repo_url`, `repo_branch`)
|
|
21
|
+
- Page tags (frontmatter or `_tree.yml`), sidebar tag filter, tag pages, `tag:` search, tags field in the editor
|
|
22
|
+
- Live search dropdown with history; render cache
|
|
23
|
+
- `llms.txt`, `sitemap.xml`, `infodocs:export` static export
|
|
24
|
+
- 404 page suggests matching pages; print stylesheet; skip-to-content link
|
|
25
|
+
- Fix: duplicate heading `id` attributes, crash on headings with inline code/emphasis
|
|
26
|
+
- CI workflow, `rubygems_mfa_required`
|
data/CONTRIBUTING.md
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Contributing
|
|
2
|
+
|
|
3
|
+
```sh
|
|
4
|
+
bundle install
|
|
5
|
+
bundle exec rake test
|
|
6
|
+
```
|
|
7
|
+
|
|
8
|
+
The tests cover the Ruby side and the rendered HTML. The JavaScript (Stimulus controllers) and the look of the pages are **not** covered by automated tests, so before a release click through the checklist below in a real browser, in a host app with the gem installed (`bin/rails g infodocs:install`).
|
|
9
|
+
|
|
10
|
+
## Browser checklist
|
|
11
|
+
|
|
12
|
+
Desktop (≥ 1280px) and a phone-sized window (≈ 390px), light and dark mode:
|
|
13
|
+
|
|
14
|
+
- [ ] Sidebar: folders open/close, the state survives a reload, the current page is scrolled into view; on a phone it is a drawer.
|
|
15
|
+
- [ ] Header: logo, links and title look right; search field is usable on a phone.
|
|
16
|
+
- [ ] Search: `⌘K` / `/` focuses the field; typing shows results; arrows + Enter open one; the page marks the matching words; an empty field shows recent searches.
|
|
17
|
+
- [ ] Page: breadcrumbs, tags, last updated, previous/next; "On this page" is collapsible below 1280px and a sticky list above.
|
|
18
|
+
- [ ] Code: highlighting for Ruby/JS, Copy button, line numbers and highlighted lines (`{2,4-6} showLineNumbers`), `file=` embeds.
|
|
19
|
+
- [ ] Mermaid diagram renders and follows dark mode.
|
|
20
|
+
- [ ] Alerts (`> [!NOTE]`), footnotes, heading `#` links, images next to a page.
|
|
21
|
+
- [ ] Copy Markdown puts the page source on the clipboard.
|
|
22
|
+
- [ ] Tags: chips filter the sidebar, `/docs/_tags` and `/docs/_tags/<tag>` work.
|
|
23
|
+
- [ ] Draft pages show only in development (with a badge); `page_authorize` hides pages from the sidebar, search and by URL.
|
|
24
|
+
- [ ] Edit mode (development only): editor toolbar and ⌘S, live preview, Tags field, New page/folder, drag & drop reorder/move (a redirect is left behind), delete with confirmation.
|
|
25
|
+
- [ ] Printing a page hides the sidebar, header and buttons.
|
|
26
|
+
|
|
27
|
+
## Before a release
|
|
28
|
+
|
|
29
|
+
1. Update `lib/infodocs/version.rb` and `CHANGELOG.md`.
|
|
30
|
+
2. `bundle exec rake test`, `bin/rails infodocs:check_links` and `bin/rails infodocs:lint` in a host app.
|
|
31
|
+
3. `gem build infodocs.gemspec`, inspect the file list, then tag and push.
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Jakub Cieślar
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
# Infodocs
|
|
2
|
+
|
|
3
|
+
A mountable Rails engine that turns the Markdown files in your repository into a modern documentation site: a sidebar tree, powerful full-text search, syntax highlighting, Mermaid diagrams and a layout that works well on phones. Built for apps using **Tailwind**, **Stimulus** and **importmap**.
|
|
4
|
+
|
|
5
|
+
There is no database and no build step. Your `.md` files are the source of truth, so docs are versioned, reviewed and deployed together with your code.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- **Sidebar tree** built from your folders, with collapsible sections and the current page highlighted.
|
|
10
|
+
- **Explicit ordering** through a small `_tree.yml` per folder (title, order, hidden entries, tags).
|
|
11
|
+
- **Full-text search** powered by SQLite FTS5: ranking, snippets with highlighted matches, phrases, `OR`, exclusions, prefixes and field filters; live results while typing. Diacritics are folded (Polish `ł`, `ą`, `ę`… work).
|
|
12
|
+
- **GitHub-flavoured Markdown**: tables, task lists, strikethrough, autolinks, `> [!NOTE]` style alerts.
|
|
13
|
+
- **Server-side syntax highlighting** with Rouge (Ruby, Rails DSL, JS, and every other Rouge language), your choice of palette, copy button, optional line numbers and highlighted lines.
|
|
14
|
+
- **Mermaid diagrams** from ` ```mermaid ` blocks, loaded lazily only on pages that contain one, dark-mode aware.
|
|
15
|
+
- **Relative links and images**: `[setup](../guides/setup.md#install)` and `` work on GitHub and on the docs site; `infodocs:check_links` finds broken ones.
|
|
16
|
+
- **Tags**: several per page, filter chips in the sidebar, tag pages, `tag:` search.
|
|
17
|
+
- **Table of contents** (h2/h3) with scroll-spy (a collapsible "On this page" on small screens), the sidebar remembers open folders and scrolls to the current page, breadcrumbs, previous/next navigation, last-updated date and an optional "Edit on GitHub" link.
|
|
18
|
+
- **Mobile friendly**: the sidebar becomes a drawer, tap targets are 44px, code and tables scroll horizontally.
|
|
19
|
+
- **Dark mode** through Tailwind's `dark:` variants (follows the system setting).
|
|
20
|
+
- **Access control**: public, HTTP basic auth, Devise (with optional role check) or your own lambda.
|
|
21
|
+
- **Keyboard shortcuts**: `⌘K` / `Ctrl+K` or `/` focuses search.
|
|
22
|
+
- **Multiple source folders** from anywhere in the app (or a git submodule / checkout), mounted under one site with their own URL names and titles.
|
|
23
|
+
- **Edit mode (development only)**: Markdown editor with live preview, create, drag & drop reorder/move and delete of pages and folders, all written to your `.md` files.
|
|
24
|
+
- **`llms.txt`, `sitemap.xml` and static export** for hosting the docs elsewhere.
|
|
25
|
+
- **Live pickup of changes** in development: edit a file, refresh the page.
|
|
26
|
+
|
|
27
|
+
## Requirements
|
|
28
|
+
|
|
29
|
+
Ruby 3.2+, Rails 7.1+ with `tailwindcss-rails` (Tailwind 4), `importmap-rails` and `stimulus-rails` in the host app. SQLite is used for the search index even if your app uses another database. Devise is only needed for `auth = :devise`.
|
|
30
|
+
|
|
31
|
+
## Installation
|
|
32
|
+
|
|
33
|
+
Add the gem to your `Gemfile` (not published on RubyGems yet, so use the repository or a local path):
|
|
34
|
+
|
|
35
|
+
```ruby
|
|
36
|
+
gem "infodocs", github: "jcieslar/infodocs"
|
|
37
|
+
# or: gem "infodocs", path: "../infodocs"
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Install and run the generator:
|
|
41
|
+
|
|
42
|
+
```sh
|
|
43
|
+
bundle install
|
|
44
|
+
bin/rails g infodocs:install
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Options: `--path=docs` (folder with your Markdown, relative to the app root) and `--mount=/docs` (URL prefix).
|
|
48
|
+
|
|
49
|
+
The generator:
|
|
50
|
+
|
|
51
|
+
1. creates `config/initializers/infodocs.rb`,
|
|
52
|
+
2. mounts the engine in `config/routes.rb`,
|
|
53
|
+
3. creates the docs folder with `_tree.yml` and a sample page,
|
|
54
|
+
4. adds `@plugin "@tailwindcss/typography"` and an `@source` line for the engine's views to your Tailwind entry CSS (`app/assets/tailwind/application.css`),
|
|
55
|
+
5. adds `/tmp/infodocs/` to `.gitignore`.
|
|
56
|
+
|
|
57
|
+
Then start the app and open `/docs`. It is safe to re-run the generator; it will not duplicate the route or CSS lines.
|
|
58
|
+
|
|
59
|
+
> The `@source` line contains the absolute path of the installed gem. Re-run `bin/rails g infodocs:install` after the gem path changes (for example after a version bump with a git or path source).
|
|
60
|
+
|
|
61
|
+
## Writing docs
|
|
62
|
+
|
|
63
|
+
```
|
|
64
|
+
docs/
|
|
65
|
+
_tree.yml # optional: title, order, hidden, tags
|
|
66
|
+
index.md # landing page of the folder (index.md or README.md)
|
|
67
|
+
getting-started.md
|
|
68
|
+
guides/
|
|
69
|
+
_tree.yml
|
|
70
|
+
setup.md
|
|
71
|
+
deploy.md
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Files and folders starting with `_` or `.` are ignored.
|
|
75
|
+
|
|
76
|
+
`_tree.yml`:
|
|
77
|
+
|
|
78
|
+
```yaml
|
|
79
|
+
title: Guides # name of the folder in the sidebar
|
|
80
|
+
order: [setup, deploy] # file names without .md, or subfolder names; the rest follows alphabetically
|
|
81
|
+
hidden: [drafts] # reachable by URL, not shown in the sidebar or prev/next
|
|
82
|
+
tags: # optional, see "Tags"
|
|
83
|
+
setup: [guide, rails]
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Page title: the `title:` key in YAML frontmatter, otherwise the first `# Heading`, otherwise the file name.
|
|
87
|
+
|
|
88
|
+
A page:
|
|
89
|
+
|
|
90
|
+
````markdown
|
|
91
|
+
---
|
|
92
|
+
title: Setup guide
|
|
93
|
+
tags: [guide, rails]
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
# Setup
|
|
97
|
+
|
|
98
|
+
## Install
|
|
99
|
+
|
|
100
|
+
```ruby
|
|
101
|
+
puts "highlighted by Rouge"
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
```mermaid
|
|
105
|
+
graph LR
|
|
106
|
+
Markdown --> Infodocs --> Docs
|
|
107
|
+
```
|
|
108
|
+
````
|
|
109
|
+
|
|
110
|
+
### Alerts
|
|
111
|
+
|
|
112
|
+
GitHub-style alerts: put `[!NOTE]`, `[!TIP]`, `[!IMPORTANT]`, `[!WARNING]` or `[!CAUTION]` on the first line of a blockquote:
|
|
113
|
+
|
|
114
|
+
```markdown
|
|
115
|
+
> [!WARNING]
|
|
116
|
+
> Back up the database first.
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### Images and files
|
|
120
|
+
|
|
121
|
+
Relative paths work next to a page: ``. The engine serves png, jpg, gif, webp, avif, svg, pdf, mp4 and webm from inside the docs directories only (never `_`/`.` entries).
|
|
122
|
+
|
|
123
|
+
### Code blocks
|
|
124
|
+
|
|
125
|
+
Rouge highlights fenced blocks by language: `ruby`, `rails` (Ruby plus Rails DSL: associations, callbacks such as `after_create_commit`, routes, migrations), `erb`, `js`/`javascript`, `yaml`, `shell`, `json`, `html`, `css`, `sql` and everything else Rouge supports. Highlighting happens on the server, so there is no flash of unstyled code.
|
|
126
|
+
|
|
127
|
+
Each block has a header with the language, an optional file name and a **Copy** button. After the language you can add line highlights, `showLineNumbers` and a file name, in any order:
|
|
128
|
+
|
|
129
|
+
````markdown
|
|
130
|
+
```ruby {2,4-6} showLineNumbers app/models/note.rb
|
|
131
|
+
class Note < ApplicationRecord
|
|
132
|
+
end
|
|
133
|
+
```
|
|
134
|
+
````
|
|
135
|
+
|
|
136
|
+
To show a real file from your app instead of pasting it, use `file=` (optionally with a line range). The block body is ignored:
|
|
137
|
+
|
|
138
|
+
````markdown
|
|
139
|
+
```ruby file=app/models/note.rb#L3-12 showLineNumbers
|
|
140
|
+
```
|
|
141
|
+
````
|
|
142
|
+
|
|
143
|
+
Only files inside `Rails.root` can be embedded; dotfiles, credentials, keys and `..` paths are refused (the block shows an error and `infodocs:check_links` reports it). The language is guessed from the file name when you leave it out.
|
|
144
|
+
|
|
145
|
+
Headings get a `#` link on hover, and footnotes (`text[^1]` with `[^1]: note`) are supported.
|
|
146
|
+
|
|
147
|
+
Pick a palette in the initializer; every palette has a light and a dark variant (dark follows `prefers-color-scheme` or a `.dark` class):
|
|
148
|
+
|
|
149
|
+
```ruby
|
|
150
|
+
config.code_theme = :one # :one (default), :github, :vscode, :catppuccin
|
|
151
|
+
|
|
152
|
+
# or tweak colours of a base palette (roles: fg bg comment keyword string number
|
|
153
|
+
# function type variable builtin operator punctuation tag attribute escape regex symbol)
|
|
154
|
+
config.code_theme = { base: :one, dark: { keyword: "#ff79c6" } }
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### Tags
|
|
158
|
+
|
|
159
|
+
A page can have several tags, from either place (they are merged and lower-cased):
|
|
160
|
+
|
|
161
|
+
```markdown
|
|
162
|
+
---
|
|
163
|
+
tags: [guide, deploy]
|
|
164
|
+
---
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
```yaml
|
|
168
|
+
# guides/_tree.yml, which is what the edit page's "Tags" field writes (development only)
|
|
169
|
+
tags:
|
|
170
|
+
setup: [guide, rails] # page name => tags
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
When any tag exists the sidebar shows tag chips: click one to filter the tree (`?tag=guide`, kept while you navigate), click again to clear. `/docs/_tags` lists all tags, `/docs/_tags/guide` the pages with that tag. Tags follow a page when it is moved in the tree.
|
|
174
|
+
|
|
175
|
+
## Search
|
|
176
|
+
|
|
177
|
+
Results link to the page with `?q=…`, which marks the matching words on the page and scrolls to the first one. Typing in the header field shows live results (arrow keys + Enter); with an empty field the last searches are offered. `GET /docs/search.json?q=…` returns the same results as JSON.
|
|
178
|
+
|
|
179
|
+
| Query | Meaning |
|
|
180
|
+
| --- | --- |
|
|
181
|
+
| `deploy kamal` | both words (AND) |
|
|
182
|
+
| `"exact phrase"` | phrase |
|
|
183
|
+
| `deploy OR release` | either word |
|
|
184
|
+
| `deploy -docker` | exclude a word |
|
|
185
|
+
| `depl*` | prefix |
|
|
186
|
+
| `title:setup` | match in the page title only |
|
|
187
|
+
| `heading:install` | match in headings only |
|
|
188
|
+
| `path:guides` | match in the URL path |
|
|
189
|
+
| `tag:guide` | pages with the tag (alone or combined with words) |
|
|
190
|
+
|
|
191
|
+
Results are ranked with BM25 (title and headings weigh more than body text) and show a snippet with the matching terms marked. The index is stored in `tmp/infodocs/index.sqlite3` and updated from file modification times.
|
|
192
|
+
|
|
193
|
+
## Configuration
|
|
194
|
+
|
|
195
|
+
`config/initializers/infodocs.rb`:
|
|
196
|
+
|
|
197
|
+
```ruby
|
|
198
|
+
Infodocs.configure do |config|
|
|
199
|
+
# One or more directories. With several, each becomes a top-level folder
|
|
200
|
+
# named after the directory...
|
|
201
|
+
config.paths = [Rails.root.join("docs"), Rails.root.join("guides")]
|
|
202
|
+
|
|
203
|
+
# ...or name them yourself (slug = URL segment, title = sidebar label):
|
|
204
|
+
config.paths = [
|
|
205
|
+
{ path: Rails.root.join("docs"), slug: "app", title: "Application" },
|
|
206
|
+
{ path: Rails.root.join("vendor/shared-docs/docs"), slug: "shared" } # e.g. a git submodule
|
|
207
|
+
]
|
|
208
|
+
|
|
209
|
+
config.title = "Docs" # header + <title>
|
|
210
|
+
config.editable = Rails.env.development? # edit mode, development only (see below)
|
|
211
|
+
config.code_theme = :one # see "Code blocks"
|
|
212
|
+
config.stylesheets = %w[tailwind] # stylesheet tags in the docs layout
|
|
213
|
+
config.index_path = Rails.root.join("tmp/infodocs/index.sqlite3")
|
|
214
|
+
|
|
215
|
+
config.repo_url = "https://github.com/acme/app" # optional: "Edit on GitHub" link in the footer
|
|
216
|
+
config.repo_branch = "main"
|
|
217
|
+
|
|
218
|
+
config.auth = :none # see below
|
|
219
|
+
end
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
### Copy as Markdown
|
|
223
|
+
|
|
224
|
+
Every page has a **Copy Markdown** button that copies the source (without frontmatter) to the clipboard, handy for pasting into an AI assistant. The same text is served at `/docs/_raw/<page>` under the page's normal access rules.
|
|
225
|
+
|
|
226
|
+
### Redirects
|
|
227
|
+
|
|
228
|
+
When a page gets a new address, list the old ones in its frontmatter and they answer with a 301 to the new page:
|
|
229
|
+
|
|
230
|
+
```yaml
|
|
231
|
+
---
|
|
232
|
+
redirect_from: [old/path, getting-started]
|
|
233
|
+
---
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
Moving a page or a folder in edit mode adds the old addresses for you (the page's frontmatter is rewritten as YAML, so comments inside it are not kept). A real page at the old address always wins over a redirect.
|
|
237
|
+
|
|
238
|
+
### Drafts and per-page visibility
|
|
239
|
+
|
|
240
|
+
`draft: true` in a page's frontmatter keeps it out of the docs everywhere except development (where it is marked with a *draft* badge): no URL, no sidebar entry, no search result, not in `llms.txt` or `sitemap.xml`.
|
|
241
|
+
|
|
242
|
+
To show some pages only to some visitors, return false from `page_authorize`. A hidden page behaves as if it did not exist (404, missing from the sidebar, search, tags, prev/next, `llms.txt` and `sitemap.xml`):
|
|
243
|
+
|
|
244
|
+
```ruby
|
|
245
|
+
config.page_authorize = ->(page, user) { !page.tags.include?("internal") || user&.admin? }
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
`user` is the signed-in Devise user (`auth = :devise`) or `current_user` if your controllers define it, otherwise `nil`. This is independent of the global `auth` check below, which gates the whole site.
|
|
249
|
+
|
|
250
|
+
### Access control
|
|
251
|
+
|
|
252
|
+
```ruby
|
|
253
|
+
config.auth = :none # public (default)
|
|
254
|
+
|
|
255
|
+
config.auth = :basic # HTTP basic auth
|
|
256
|
+
config.basic_auth = { user: ENV["DOCS_USER"], password: ENV["DOCS_PASSWORD"] }
|
|
257
|
+
|
|
258
|
+
config.auth = :devise # require a signed-in Devise user
|
|
259
|
+
config.devise = { scope: :user, authorize: ->(user) { user.admin? } }
|
|
260
|
+
|
|
261
|
+
config.auth = -> { current_user&.staff? } # any lambda, evaluated in the controller;
|
|
262
|
+
# truthy allows, falsy returns 403
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
## Edit mode (development only)
|
|
266
|
+
|
|
267
|
+
In `Rails.env.development`, with `config.editable = true`, the docs get editing tools (only on this machine, only for users who pass the docs `auth`):
|
|
268
|
+
|
|
269
|
+
- **Edit** on a page opens a Markdown editor with a toolbar (bold, italic, code, link, headings, lists, Ruby/JS/Mermaid blocks), a **Tags** field and a live server-rendered preview (tabs on mobile). `⌘S` / **Save** writes the `.md` file.
|
|
270
|
+
- **New page** in the sidebar creates a page or a folder (a folder needs no `index.md`).
|
|
271
|
+
- **Reorder** lets you drag pages and folders to reorder or move them; `_tree.yml` files are updated and files are moved with `git mv`.
|
|
272
|
+
- The trash icon deletes a page or a folder with its contents (`git rm`, with a confirmation).
|
|
273
|
+
|
|
274
|
+
Everything lands in your working tree: review and commit it with git.
|
|
275
|
+
|
|
276
|
+
Safeguards: the routes return 404 unless the app runs in development, the request comes from this machine and `editable` is on. Users must also pass the docs `auth` check. Only files inside the configured docs folders can be written, moved or deleted.
|
|
277
|
+
|
|
278
|
+
## Tools
|
|
279
|
+
|
|
280
|
+
```sh
|
|
281
|
+
bin/rails infodocs:check_links # broken relative links, anchors, images and embeds (exit 1 if any, for CI)
|
|
282
|
+
bin/rails infodocs:lint # content checks, see below (exit 1 if any)
|
|
283
|
+
bin/rails "infodocs:export[tmp/site]" # static HTML export
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
`infodocs:lint` reports pages without a title (`# Heading` or `title:`), unknown frontmatter keys (known: `title`, `description`, `tags`, `draft`, `redirect_from`), hidden pages that no page links to, and `redirect_from` entries that are shadowed by a real page or claimed twice.
|
|
287
|
+
|
|
288
|
+
The export includes images, `llms.txt`, `sitemap.xml` and `public/assets` if present; run `assets:precompile` first and set `auth = :none`. Live search and edit mode need the Rails server, so they do not work in the export.
|
|
289
|
+
|
|
290
|
+
The engine also serves `/docs/llms.txt` and `/docs/sitemap.xml` behind the same auth as the docs (URLs use the request host). `config.repo_url` adds an "Edit on GitHub" link to the page footer.
|
|
291
|
+
|
|
292
|
+
## Branding and linking from your app
|
|
293
|
+
|
|
294
|
+
```ruby
|
|
295
|
+
config.logo = "/logo.svg" # shown before the title
|
|
296
|
+
config.favicon = "/favicon.ico"
|
|
297
|
+
config.header_links = [{ label: "App", url: "/" }, { label: "Status", url: "https://status.example.com" }]
|
|
298
|
+
config.head_html = '<script defer src="https://analytics.example.com/script.js"></script>' # trusted HTML for <head>
|
|
299
|
+
|
|
300
|
+
# logo, favicon and head_html can also be lambdas that receive the view:
|
|
301
|
+
config.logo = ->(view) { view.image_path("logo.svg") }
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
Mermaid is loaded from a CDN by default (SortableJS for drag & drop ships inside the gem). To self-host Mermaid, e.g. for a strict CSP or an offline network, point `config.mermaid_url` at your copy of `mermaid.esm.min.mjs` (its chunk files must sit next to it).
|
|
305
|
+
|
|
306
|
+
Header links collapse into the sidebar drawer on phones. In your own views and controllers link to a docs page with `infodocs_page_path("guides/setup")` (also `infodocs_page_url`); without an argument it points to the docs home. (`infodocs_path` is the route helper Rails creates for the mount itself and always points to the docs root.)
|
|
307
|
+
|
|
308
|
+
## Customising the look
|
|
309
|
+
|
|
310
|
+
Everything is plain ERB with Tailwind utility classes. Copy the views into your app and edit them:
|
|
311
|
+
|
|
312
|
+
```sh
|
|
313
|
+
bin/rails g infodocs:views
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
Views then live in `app/views/infodocs` and `app/views/layouts/infodocs`.
|
|
317
|
+
|
|
318
|
+
## How it works
|
|
319
|
+
|
|
320
|
+
- `Infodocs::Tree` scans the configured folders and `_tree.yml` files.
|
|
321
|
+
- `Infodocs::Renderer` converts Markdown with commonmarker, highlights code with Rouge, builds the TOC and rewrites links.
|
|
322
|
+
- `Infodocs::Search` maintains the FTS5 index.
|
|
323
|
+
- The engine ships its own small Stimulus application (`infodocs/application`) with sidebar, search shortcut, TOC and Mermaid controllers, pinned through importmap, so it does not touch your own JavaScript.
|
|
324
|
+
|
|
325
|
+
## Development
|
|
326
|
+
|
|
327
|
+
```sh
|
|
328
|
+
bundle install
|
|
329
|
+
bundle exec rake test
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
## Roadmap
|
|
333
|
+
|
|
334
|
+
- Sources from remote git repositories.
|
|
335
|
+
- Translations (`docs/pl`, `docs/en`) with a language switcher.
|
|
336
|
+
- A visual (WYSIWYG) editor mode.
|
|
337
|
+
- Browser (system) tests; for now see `CONTRIBUTING.md` for the manual checklist.
|
|
338
|
+
|
|
339
|
+
## License
|
|
340
|
+
|
|
341
|
+
MIT.
|
data/Rakefile
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Infodocs
|
|
4
|
+
class ApplicationController < ActionController::Base
|
|
5
|
+
layout "infodocs/application"
|
|
6
|
+
|
|
7
|
+
before_action :authenticate_docs!
|
|
8
|
+
helper_method :docs_tree, :page_allowed?, :visible_pages, :visible_tags
|
|
9
|
+
|
|
10
|
+
private
|
|
11
|
+
|
|
12
|
+
# Editing endpoints: development only, this machine only, plus docs auth.
|
|
13
|
+
def ensure_editable!
|
|
14
|
+
head :not_found unless Infodocs.editing_enabled? && request.local?
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def docs_tree
|
|
18
|
+
Infodocs.tree
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# The signed-in user handed to `config.page_authorize` (nil without one).
|
|
22
|
+
def docs_user
|
|
23
|
+
auth = Infodocs.config.auth
|
|
24
|
+
if auth == :devise
|
|
25
|
+
send(:"current_#{Infodocs.config.devise.fetch(:scope, :user)}")
|
|
26
|
+
elsif respond_to?(:current_user, true)
|
|
27
|
+
current_user
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def page_allowed?(page)
|
|
32
|
+
authorize = Infodocs.config.page_authorize
|
|
33
|
+
authorize.nil? || page.nil? || !!authorize.call(page, docs_user)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def visible_pages
|
|
37
|
+
@visible_pages ||= docs_tree.pages.select { |page| page_allowed?(page) }
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def visible_tags
|
|
41
|
+
@visible_tags ||= visible_pages.flat_map(&:tags).tally.sort.to_h
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def authenticate_docs!
|
|
45
|
+
auth = Infodocs.config.auth
|
|
46
|
+
|
|
47
|
+
if auth.respond_to?(:call)
|
|
48
|
+
instance_exec(&auth) || head(:forbidden)
|
|
49
|
+
elsif auth == :basic
|
|
50
|
+
credentials = Infodocs.config.basic_auth
|
|
51
|
+
authenticate_or_request_with_http_basic("Docs") do |user, password|
|
|
52
|
+
ActiveSupport::SecurityUtils.secure_compare(user, credentials[:user].to_s) &
|
|
53
|
+
ActiveSupport::SecurityUtils.secure_compare(password, credentials[:password].to_s)
|
|
54
|
+
end
|
|
55
|
+
elsif auth == :devise
|
|
56
|
+
authenticate_devise_user!
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def authenticate_devise_user!
|
|
61
|
+
scope = Infodocs.config.devise.fetch(:scope, :user)
|
|
62
|
+
send(:"authenticate_#{scope}!")
|
|
63
|
+
user = send(:"current_#{scope}")
|
|
64
|
+
authorize = Infodocs.config.devise[:authorize]
|
|
65
|
+
head :forbidden if authorize && !authorize.call(user)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Infodocs
|
|
4
|
+
# Images and other files that sit next to the markdown pages.
|
|
5
|
+
class AssetsController < ApplicationController
|
|
6
|
+
def show
|
|
7
|
+
file = docs_tree.asset(params[:path])
|
|
8
|
+
return head(:not_found) unless file
|
|
9
|
+
|
|
10
|
+
# SVG may carry scripts; the sandbox keeps them inert when opened directly.
|
|
11
|
+
response.set_header("Content-Security-Policy", "sandbox")
|
|
12
|
+
send_file file, type: Tree::ASSET_TYPES.fetch(file.extname.downcase), disposition: "inline"
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Infodocs
|
|
4
|
+
# Writes edited Markdown straight to the source file (to be committed by the
|
|
5
|
+
# developer). Only reachable in development, from this machine, and only for
|
|
6
|
+
# users who already pass the docs authentication.
|
|
7
|
+
class EditorController < ApplicationController
|
|
8
|
+
before_action :ensure_editable!
|
|
9
|
+
before_action :load_page
|
|
10
|
+
|
|
11
|
+
def edit
|
|
12
|
+
@content = @page.raw
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def update
|
|
16
|
+
@page.write(params.require(:content))
|
|
17
|
+
Infodocs.reload!
|
|
18
|
+
render json: { saved: true, path: @page.relative_path, title: Infodocs.tree.find(@page.slug)&.title }
|
|
19
|
+
rescue ActionController::ParameterMissing
|
|
20
|
+
render json: { saved: false, error: "content missing" }, status: :unprocessable_entity
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def preview
|
|
24
|
+
draft = Page.new(file: @page.file, slug: @page.slug, root: @page.root, raw: params[:content].to_s)
|
|
25
|
+
rendered = Renderer.new(draft, mount_path: root_path).call
|
|
26
|
+
render html: rendered.html.html_safe, layout: false
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def load_page
|
|
32
|
+
@page = docs_tree.find(params[:path].to_s)
|
|
33
|
+
head(:not_found) unless @page && page_allowed?(@page)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Infodocs
|
|
4
|
+
# Machine-readable indexes: llms.txt (https://llmstxt.org) and sitemap.xml.
|
|
5
|
+
class IndexesController < ApplicationController
|
|
6
|
+
def llms
|
|
7
|
+
lines = ["# #{Infodocs.config.title}", ""]
|
|
8
|
+
visible_pages.each do |page|
|
|
9
|
+
note = page.description
|
|
10
|
+
lines << "- [#{page.title}](#{page_url(page.slug)})#{": #{note}" if note.present?}"
|
|
11
|
+
end
|
|
12
|
+
render plain: lines.join("\n") + "\n", content_type: "text/plain"
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def sitemap
|
|
16
|
+
urls = visible_pages.map do |page|
|
|
17
|
+
" <url><loc>#{ERB::Util.html_escape(page_url(page.slug))}</loc><lastmod>#{page.updated_at.utc.iso8601}</lastmod></url>"
|
|
18
|
+
end
|
|
19
|
+
render xml: %(<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n#{urls.join("\n")}\n</urlset>\n)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Infodocs
|
|
4
|
+
class PagesController < ApplicationController
|
|
5
|
+
def show
|
|
6
|
+
@page = docs_tree.find(params[:path].to_s) || first_page
|
|
7
|
+
return show_folder if @page.nil?
|
|
8
|
+
return render_not_found unless page_allowed?(@page)
|
|
9
|
+
|
|
10
|
+
@rendered = Infodocs.render(@page, mount_path: root_path)
|
|
11
|
+
@trail = docs_tree.trail(@page.slug)
|
|
12
|
+
index = visible_pages.index(@page)
|
|
13
|
+
@previous = visible_pages[index - 1] if index&.positive?
|
|
14
|
+
@next = visible_pages[index + 1] if index
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
# A folder without a landing page opens its first page; an empty one says so.
|
|
20
|
+
def show_folder
|
|
21
|
+
moved = docs_tree.redirect_for(params[:path].to_s)
|
|
22
|
+
moved_page = moved && docs_tree.find(moved)
|
|
23
|
+
return redirect_to(page_path(moved), status: :moved_permanently) if moved_page && page_allowed?(moved_page)
|
|
24
|
+
|
|
25
|
+
folder = docs_tree.node(params[:path].to_s)
|
|
26
|
+
return render_not_found unless folder&.folder?
|
|
27
|
+
|
|
28
|
+
first = docs_tree.first_page_under(folder)
|
|
29
|
+
first = visible_pages.find { |page| page.slug.start_with?("#{folder.slug}/") } unless first.nil? || page_allowed?(first)
|
|
30
|
+
return redirect_to(page_path(first.slug)) if first
|
|
31
|
+
|
|
32
|
+
@folder = folder
|
|
33
|
+
@trail = docs_tree.trail(folder.slug)
|
|
34
|
+
render "infodocs/pages/empty_folder"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def first_page
|
|
38
|
+
visible_pages.first if params[:path].blank?
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Unknown path: offer pages whose text matches the words in the URL.
|
|
42
|
+
def render_not_found
|
|
43
|
+
words = params[:path].to_s.split(/[^\p{L}\p{N}]+/).select { |word| word.length > 2 }.first(5)
|
|
44
|
+
@suggestions = words.any? ? Infodocs.search.search(words.join(" OR "), limit: 5) : []
|
|
45
|
+
render "infodocs/pages/not_found", status: :not_found
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Infodocs
|
|
4
|
+
# The Markdown source of a page (without frontmatter), for "Copy Markdown".
|
|
5
|
+
class RawsController < ApplicationController
|
|
6
|
+
def show
|
|
7
|
+
page = docs_tree.find(params[:path].to_s) || (docs_tree.pages.first if params[:path].blank?)
|
|
8
|
+
return head(:not_found) unless page && page_allowed?(page)
|
|
9
|
+
|
|
10
|
+
render plain: page.body, content_type: "text/plain"
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Infodocs
|
|
4
|
+
class SearchesController < ApplicationController
|
|
5
|
+
def index
|
|
6
|
+
@query = params[:q].to_s.strip
|
|
7
|
+
@results = @query.empty? ? [] : Infodocs.search.search(@query, limit: 100).select { |r| page_allowed?(docs_tree.find(r.slug)) }
|
|
8
|
+
@results = @results.first(request.format.json? ? 8 : 20)
|
|
9
|
+
|
|
10
|
+
respond_to do |format|
|
|
11
|
+
format.html
|
|
12
|
+
format.json do
|
|
13
|
+
render json: @results.map { |r| { title: r.title, path: r.slug, url: page_path(r.slug), snippet: r.snippet } }
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|