markdowndocs 0.1.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/README.md +291 -0
- data/Rakefile +10 -0
- data/app/assets/javascripts/markdowndocs/controllers/docs_mode_controller.js +58 -0
- data/app/controllers/markdowndocs/application_controller.rb +20 -0
- data/app/controllers/markdowndocs/docs_controller.rb +76 -0
- data/app/controllers/markdowndocs/preferences_controller.rb +33 -0
- data/app/helpers/markdowndocs/docs_helper.rb +65 -0
- data/app/models/markdowndocs/documentation.rb +176 -0
- data/app/services/markdowndocs/markdown_renderer.rb +117 -0
- data/app/views/markdowndocs/docs/_breadcrumb.html.erb +29 -0
- data/app/views/markdowndocs/docs/_card.html.erb +7 -0
- data/app/views/markdowndocs/docs/_mode_switcher.html.erb +73 -0
- data/app/views/markdowndocs/docs/_navigation.html.erb +39 -0
- data/app/views/markdowndocs/docs/index.html.erb +43 -0
- data/app/views/markdowndocs/docs/show.html.erb +62 -0
- data/config/locales/en.yml +11 -0
- data/config/routes.rb +7 -0
- data/lib/generators/markdowndocs/install/install_generator.rb +35 -0
- data/lib/generators/markdowndocs/install/templates/initializer.rb +37 -0
- data/lib/markdowndocs/configuration.rb +57 -0
- data/lib/markdowndocs/engine.rb +11 -0
- data/lib/markdowndocs/version.rb +5 -0
- data/lib/markdowndocs.rb +25 -0
- data/log/test.log +4 -0
- data/sig/markdowndocs.rbs +4 -0
- metadata +130 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 42524ce732ca270ae79839b4d26522b30fab9e088793df8c62e0c20ac0c5b1c3
|
|
4
|
+
data.tar.gz: c999713e3b504d54838db523a0f708110f9f28a049efd1568dacfbafa3b9d1c6
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 66583f10a3cf8c80eb651a0d3b0cbd99f86604313a36e91713d90569ee1ac1eb4889782969fcac16ad91dbb067a2a3853a20b5f99ddb7a7767529b7a489d7216
|
|
7
|
+
data.tar.gz: 705f2632e8a1429571b483d6a5ef6285af1a6380f428743b8adb5181342e2854027b327eceb3df001be4a6af3b932f713c2b6e48a5c81870569d6ae2dcfdb1fd
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [0.1.0] - 2026-02-20
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- Mountable Rails engine that serves markdown files as a browsable documentation site
|
|
13
|
+
- GitHub Flavored Markdown rendering via Commonmarker (tables, task lists, strikethrough, autolinks, footnotes)
|
|
14
|
+
- Syntax highlighting via Rouge with configurable theme
|
|
15
|
+
- YAML front matter support for per-document title, description, and mode availability
|
|
16
|
+
- Mode-based content filtering using HTML comment blocks
|
|
17
|
+
- Category organization for the index page
|
|
18
|
+
- Auto-generated table of contents from H2/H3 headings with anchor links
|
|
19
|
+
- Breadcrumb navigation and related-documents sidebar
|
|
20
|
+
- File-mtime-based cache invalidation using Rails.cache
|
|
21
|
+
- HTML sanitization via rails-html-sanitizer
|
|
22
|
+
- Directory traversal prevention via slug validation
|
|
23
|
+
- i18n support for all UI strings
|
|
24
|
+
- Install generator (`rails generate markdowndocs:install`)
|
|
25
|
+
|
|
26
|
+
[0.1.0]: https://github.com/dschmura/markdowndocs/releases/tag/v0.1.0
|
data/README.md
ADDED
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
# Markdowndocs
|
|
2
|
+
|
|
3
|
+
A drop-in mountable Rails engine that turns a folder of markdown files into a browsable documentation site with syntax highlighting, category grouping, and mode-based content filtering.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **GitHub Flavored Markdown** — Tables, task lists, strikethrough, footnotes, autolinks, and more via [Commonmarker](https://github.com/gjtorikian/commonmarker)
|
|
8
|
+
- **Syntax highlighting** — Code blocks highlighted with [Rouge](https://github.com/rouge-ruby/rouge) (configurable theme)
|
|
9
|
+
- **Category organization** — Group docs into named categories for the index page
|
|
10
|
+
- **Mode-based content filtering** — Show different content to different audiences (e.g., "User Guide" vs "Developer Guide")
|
|
11
|
+
- **Table of contents** — Auto-generated from H2/H3 headings with anchor links
|
|
12
|
+
- **YAML front matter** — Set title, description, and mode availability per document
|
|
13
|
+
- **Breadcrumb navigation** — Category-aware breadcrumbs on each doc page
|
|
14
|
+
- **Related documents** — Sidebar links to other docs in the same category
|
|
15
|
+
- **Responsive design** — Tailwind CSS with mobile support
|
|
16
|
+
- **Security** — HTML sanitization, slug validation, directory traversal prevention
|
|
17
|
+
- **Caching** — Rendered markdown is cached with file-mtime-based invalidation
|
|
18
|
+
- **i18n support** — All UI strings are translatable
|
|
19
|
+
|
|
20
|
+
## Requirements
|
|
21
|
+
|
|
22
|
+
- Ruby >= 3.2
|
|
23
|
+
- Rails >= 7.1
|
|
24
|
+
|
|
25
|
+
## Installation
|
|
26
|
+
|
|
27
|
+
Add the gem to your application's Gemfile:
|
|
28
|
+
|
|
29
|
+
```ruby
|
|
30
|
+
gem "markdowndocs"
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Then run:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
bundle install
|
|
37
|
+
rails generate markdowndocs:install
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
The generator will:
|
|
41
|
+
|
|
42
|
+
1. Create `config/initializers/markdowndocs.rb` with default configuration
|
|
43
|
+
2. Create the `app/docs/` directory for your markdown files
|
|
44
|
+
3. Mount the engine at `/docs` in your routes
|
|
45
|
+
|
|
46
|
+
## Configuration
|
|
47
|
+
|
|
48
|
+
Edit `config/initializers/markdowndocs.rb` to customize behavior:
|
|
49
|
+
|
|
50
|
+
```ruby
|
|
51
|
+
Markdowndocs.configure do |config|
|
|
52
|
+
# Path to markdown files (default: Rails.root.join("app/docs"))
|
|
53
|
+
# config.docs_path = Rails.root.join("app", "docs")
|
|
54
|
+
|
|
55
|
+
# Category → slug mapping
|
|
56
|
+
config.categories = {
|
|
57
|
+
"Getting Started" => %w[welcome quickstart],
|
|
58
|
+
"Guides" => %w[authentication deployment],
|
|
59
|
+
"Reference" => %w[api-reference configuration]
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
# Available documentation modes (default: %w[guide technical])
|
|
63
|
+
# config.modes = %w[guide technical]
|
|
64
|
+
|
|
65
|
+
# Default mode (default: "guide")
|
|
66
|
+
# config.default_mode = "guide"
|
|
67
|
+
|
|
68
|
+
# Rouge syntax highlighting theme (default: "github")
|
|
69
|
+
# config.rouge_theme = "github"
|
|
70
|
+
|
|
71
|
+
# Cache expiry for rendered markdown (default: 1.hour)
|
|
72
|
+
# config.cache_expiry = 1.hour
|
|
73
|
+
|
|
74
|
+
# Optional: Resolve current user's mode preference from database
|
|
75
|
+
# config.user_mode_resolver = ->(controller) {
|
|
76
|
+
# controller.send(:current_user)&.preferences&.docs_mode
|
|
77
|
+
# }
|
|
78
|
+
|
|
79
|
+
# Optional: Save user's mode preference to database
|
|
80
|
+
# config.user_mode_saver = ->(controller, mode) {
|
|
81
|
+
# controller.send(:current_user)&.preferences&.update!(docs_mode: mode)
|
|
82
|
+
# }
|
|
83
|
+
end
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Configuration Options
|
|
87
|
+
|
|
88
|
+
| Option | Default | Description |
|
|
89
|
+
| -------------------- | ------------------------------ | -------------------------------------------------------------- |
|
|
90
|
+
| `docs_path` | `Rails.root.join("app/docs")` | Directory containing your markdown files |
|
|
91
|
+
| `categories` | `{}` | Maps category names to arrays of document slugs |
|
|
92
|
+
| `modes` | `%w[guide technical]` | Available viewing modes |
|
|
93
|
+
| `default_mode` | `"guide"` | Mode shown by default |
|
|
94
|
+
| `rouge_theme` | `"github"` | Syntax highlighting color scheme |
|
|
95
|
+
| `cache_expiry` | `1.hour` | Cache duration for rendered markdown |
|
|
96
|
+
| `user_mode_resolver` | `nil` | Lambda to load a user's mode preference from the database |
|
|
97
|
+
| `user_mode_saver` | `nil` | Lambda to persist a user's mode preference to the database |
|
|
98
|
+
|
|
99
|
+
## Writing Documentation
|
|
100
|
+
|
|
101
|
+
Create markdown files in `app/docs/`. The filename (without `.md`) becomes the URL slug — `app/docs/quickstart.md` is served at `/docs/quickstart`.
|
|
102
|
+
|
|
103
|
+
### Front Matter
|
|
104
|
+
|
|
105
|
+
Add optional YAML front matter to set metadata:
|
|
106
|
+
|
|
107
|
+
```markdown
|
|
108
|
+
---
|
|
109
|
+
title: "Quick Start Guide"
|
|
110
|
+
description: "Get up and running in five minutes"
|
|
111
|
+
modes:
|
|
112
|
+
- guide
|
|
113
|
+
- technical
|
|
114
|
+
default_mode: guide
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
# Quick Start Guide
|
|
118
|
+
|
|
119
|
+
Your content here...
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
If front matter is omitted, the title is extracted from the first H1 heading and the description from the first paragraph.
|
|
123
|
+
|
|
124
|
+
### Mode Blocks
|
|
125
|
+
|
|
126
|
+
Use HTML comments to show content only in specific modes:
|
|
127
|
+
|
|
128
|
+
```markdown
|
|
129
|
+
## Setup
|
|
130
|
+
|
|
131
|
+
This paragraph appears in all modes.
|
|
132
|
+
|
|
133
|
+
<!-- mode: guide -->
|
|
134
|
+
Follow these steps to get started:
|
|
135
|
+
1. Click the "Install" button
|
|
136
|
+
2. Follow the on-screen prompts
|
|
137
|
+
<!-- /mode -->
|
|
138
|
+
|
|
139
|
+
<!-- mode: technical -->
|
|
140
|
+
Add the dependency to your Gemfile and run the install generator:
|
|
141
|
+
\`\`\`bash
|
|
142
|
+
bundle add markdowndocs
|
|
143
|
+
rails generate markdowndocs:install
|
|
144
|
+
\`\`\`
|
|
145
|
+
<!-- /mode -->
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### Syntax Highlighting
|
|
149
|
+
|
|
150
|
+
Code blocks are automatically syntax-highlighted. Specify the language after the opening fence:
|
|
151
|
+
|
|
152
|
+
````markdown
|
|
153
|
+
```ruby
|
|
154
|
+
def hello
|
|
155
|
+
puts "Hello, world!"
|
|
156
|
+
end
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
```javascript
|
|
160
|
+
function hello() {
|
|
161
|
+
console.log("Hello, world!");
|
|
162
|
+
}
|
|
163
|
+
```
|
|
164
|
+
````
|
|
165
|
+
|
|
166
|
+
Supported languages include Ruby, JavaScript, Python, Bash, YAML, JSON, HTML, CSS, SQL, and [many more](https://github.com/rouge-ruby/rouge/wiki/List-of-supported-languages-and-lexers).
|
|
167
|
+
|
|
168
|
+
### Categories
|
|
169
|
+
|
|
170
|
+
To organize docs on the index page, map category names to slugs in your configuration:
|
|
171
|
+
|
|
172
|
+
```ruby
|
|
173
|
+
config.categories = {
|
|
174
|
+
"Getting Started" => %w[welcome quickstart],
|
|
175
|
+
"Guides" => %w[authentication deployment]
|
|
176
|
+
}
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
Documents not assigned to a category will appear in an "Uncategorized" group.
|
|
180
|
+
|
|
181
|
+
## Rendering Pipeline
|
|
182
|
+
|
|
183
|
+
When a documentation page is requested, the markdown goes through these stages:
|
|
184
|
+
|
|
185
|
+
1. **File reading** — Load raw markdown from `app/docs/`
|
|
186
|
+
2. **Mode filtering** — Strip content blocks not matching the current viewing mode
|
|
187
|
+
3. **Commonmarker parsing** — Parse with GFM extensions (tables, strikethrough, autolinks, footnotes, task lists)
|
|
188
|
+
4. **Syntax highlighting** — Apply Rouge highlighting to fenced code blocks
|
|
189
|
+
5. **HTML sanitization** — Whitelist-based sanitization strips dangerous tags and attributes
|
|
190
|
+
6. **Heading anchors** — Inject `id` attributes on H2/H3 headings for TOC linking
|
|
191
|
+
7. **Caching** — Store rendered HTML keyed by file path, mtime, and mode
|
|
192
|
+
|
|
193
|
+
## Caching
|
|
194
|
+
|
|
195
|
+
Rendered HTML is cached using `Rails.cache` with a composite cache key based on file path, file modification time, and viewing mode. Cache is automatically invalidated when file content changes.
|
|
196
|
+
|
|
197
|
+
To manually clear documentation caches:
|
|
198
|
+
|
|
199
|
+
```ruby
|
|
200
|
+
# In Rails console
|
|
201
|
+
Rails.cache.clear
|
|
202
|
+
|
|
203
|
+
# Or delete matched keys
|
|
204
|
+
Rails.cache.delete_matched("markdown_*")
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
The default cache expiry is 1 hour, configurable via `config.cache_expiry`.
|
|
208
|
+
|
|
209
|
+
## Security
|
|
210
|
+
|
|
211
|
+
### Directory Traversal Prevention
|
|
212
|
+
|
|
213
|
+
Slugs are validated to contain only alphanumeric characters, hyphens, and underscores. Patterns like `../` and `/` are rejected, ensuring only files within `app/docs/` are accessible.
|
|
214
|
+
|
|
215
|
+
### HTML Sanitization
|
|
216
|
+
|
|
217
|
+
All rendered HTML is passed through a whitelist-based sanitizer. Safe tags (headings, paragraphs, code blocks, lists, links, images, tables) are allowed. Script tags, event handlers, and dangerous attributes are stripped.
|
|
218
|
+
|
|
219
|
+
### YAML Parsing
|
|
220
|
+
|
|
221
|
+
Front matter is parsed with `YAML.safe_load` to prevent code execution.
|
|
222
|
+
|
|
223
|
+
## Best Practices
|
|
224
|
+
|
|
225
|
+
1. **Start with H1** — Every document should have exactly one H1 heading at the top
|
|
226
|
+
2. **Write descriptive first paragraphs** — The first paragraph becomes the card description on the index page
|
|
227
|
+
3. **Use meaningful filenames** — The filename becomes the URL slug; use kebab-case (e.g., `api-reference.md`)
|
|
228
|
+
4. **Include code examples** — Use fenced code blocks with a language specifier for syntax highlighting
|
|
229
|
+
5. **Link between docs** — Reference other docs with relative links: `[See authentication](/docs/authentication)`
|
|
230
|
+
6. **Keep files focused** — Break large topics into multiple documents
|
|
231
|
+
7. **Use sequential headings** — Don't skip levels (e.g., H1 to H3); this ensures proper TOC generation
|
|
232
|
+
|
|
233
|
+
## Troubleshooting
|
|
234
|
+
|
|
235
|
+
### Document Not Appearing
|
|
236
|
+
|
|
237
|
+
1. Check the filename matches the slug in your category mapping
|
|
238
|
+
2. Verify the file has a `.md` extension
|
|
239
|
+
3. Ensure the file is in the `app/docs/` directory
|
|
240
|
+
4. Restart the server if you modified the initializer
|
|
241
|
+
|
|
242
|
+
### Syntax Highlighting Not Working
|
|
243
|
+
|
|
244
|
+
1. Verify the code fence has a language specified (e.g., `` ```ruby ``)
|
|
245
|
+
2. Check the Rouge theme is configured in the initializer
|
|
246
|
+
3. Clear the cache: `Rails.cache.clear`
|
|
247
|
+
|
|
248
|
+
### 404 Errors
|
|
249
|
+
|
|
250
|
+
1. Verify the slug matches the filename (use kebab-case)
|
|
251
|
+
2. Check the file exists in `app/docs/`
|
|
252
|
+
3. Look for typos in the slug or filename
|
|
253
|
+
|
|
254
|
+
## Development
|
|
255
|
+
|
|
256
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then run the tests:
|
|
257
|
+
|
|
258
|
+
```bash
|
|
259
|
+
bundle exec rspec
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
## Releasing
|
|
263
|
+
|
|
264
|
+
1. Update `CHANGELOG.md` with a new `## [x.y.z] - YYYY-MM-DD` section and add a comparison link at the bottom.
|
|
265
|
+
|
|
266
|
+
2. Bump the version in `lib/markdowndocs/version.rb`:
|
|
267
|
+
|
|
268
|
+
```ruby
|
|
269
|
+
module Markdowndocs
|
|
270
|
+
VERSION = "x.y.z"
|
|
271
|
+
end
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
3. Commit and tag the release:
|
|
275
|
+
|
|
276
|
+
```bash
|
|
277
|
+
git add lib/markdowndocs/version.rb CHANGELOG.md
|
|
278
|
+
git commit -m "Release vx.y.z"
|
|
279
|
+
git tag vx.y.z
|
|
280
|
+
git push origin main --tags
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
Pushing the tag triggers the GitHub Actions release workflow, which builds and publishes the gem to RubyGems automatically.
|
|
284
|
+
|
|
285
|
+
## Contributing
|
|
286
|
+
|
|
287
|
+
Bug reports and pull requests are welcome on GitHub at [github.com/dschmura/markdowndocs](https://github.com/dschmura/markdowndocs).
|
|
288
|
+
|
|
289
|
+
## License
|
|
290
|
+
|
|
291
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Documentation Mode Controller
|
|
5
|
+
*
|
|
6
|
+
* Handles localStorage persistence for guest users and provides
|
|
7
|
+
* optimistic UI updates for the documentation mode switcher.
|
|
8
|
+
*
|
|
9
|
+
* For authenticated users, the preference is stored in the database
|
|
10
|
+
* via the PreferencesController. For guests, we use localStorage
|
|
11
|
+
* as a fallback to persist their preference across sessions.
|
|
12
|
+
*/
|
|
13
|
+
export default class extends Controller {
|
|
14
|
+
static values = {
|
|
15
|
+
current: { type: String, default: "guide" }
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
static STORAGE_KEY = "markdowndocs_mode"
|
|
19
|
+
|
|
20
|
+
connect() {
|
|
21
|
+
if (!this.isAuthenticated()) {
|
|
22
|
+
this.restoreGuestMode()
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
isAuthenticated() {
|
|
27
|
+
const meta = document.querySelector('meta[name="user-authenticated"]')
|
|
28
|
+
return meta?.content === "true"
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
restoreGuestMode() {
|
|
32
|
+
try {
|
|
33
|
+
const savedMode = localStorage.getItem(this.constructor.STORAGE_KEY)
|
|
34
|
+
|
|
35
|
+
if (savedMode && savedMode !== this.currentValue) {
|
|
36
|
+
const url = new URL(window.location)
|
|
37
|
+
url.searchParams.set("mode", savedMode)
|
|
38
|
+
window.location.replace(url)
|
|
39
|
+
}
|
|
40
|
+
} catch (e) {
|
|
41
|
+
console.debug("localStorage unavailable for docs mode persistence")
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
saveGuestMode(mode) {
|
|
46
|
+
try {
|
|
47
|
+
localStorage.setItem(this.constructor.STORAGE_KEY, mode)
|
|
48
|
+
} catch (e) {
|
|
49
|
+
console.debug("localStorage unavailable for docs mode persistence")
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
currentValueChanged() {
|
|
54
|
+
if (!this.isAuthenticated()) {
|
|
55
|
+
this.saveGuestMode(this.currentValue)
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Markdowndocs
|
|
4
|
+
class ApplicationController < ::ApplicationController
|
|
5
|
+
protect_from_forgery with: :exception
|
|
6
|
+
|
|
7
|
+
# Make host app route helpers available in engine views
|
|
8
|
+
# (needed because isolate_namespace blocks host helpers by default)
|
|
9
|
+
helper Rails.application.routes.url_helpers
|
|
10
|
+
|
|
11
|
+
# Support Rails 8 built-in authentication (allow_unauthenticated_access)
|
|
12
|
+
# without requiring it — works with any auth system or none at all
|
|
13
|
+
if respond_to?(:allow_unauthenticated_access)
|
|
14
|
+
allow_unauthenticated_access
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Resume session if the host app supports it (Rails 8 auth)
|
|
18
|
+
before_action :resume_session, if: -> { respond_to?(:resume_session, true) }
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Markdowndocs
|
|
4
|
+
class DocsController < ApplicationController
|
|
5
|
+
before_action :validate_slug, only: :show
|
|
6
|
+
before_action :set_docs_mode
|
|
7
|
+
helper Markdowndocs::DocsHelper
|
|
8
|
+
|
|
9
|
+
SAFE_SLUG_PATTERN = /\A[a-zA-Z0-9_-]+\z/
|
|
10
|
+
|
|
11
|
+
def index
|
|
12
|
+
@docs_by_category = Documentation.grouped_by_category
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def show
|
|
16
|
+
@doc = Documentation.find_by_slug(params[:slug])
|
|
17
|
+
|
|
18
|
+
if @doc.nil?
|
|
19
|
+
render_not_found
|
|
20
|
+
return
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
rendered_html = MarkdownRenderer.render(
|
|
24
|
+
@doc.content,
|
|
25
|
+
cache_key: @doc.cache_key,
|
|
26
|
+
mode: @docs_mode
|
|
27
|
+
)
|
|
28
|
+
@rendered_content = helpers.add_heading_anchors(rendered_html)
|
|
29
|
+
@related_docs = Documentation.by_category(@doc.category).reject { |d| d.slug == @doc.slug }
|
|
30
|
+
@available_modes = @doc.available_modes
|
|
31
|
+
@toc_items = helpers.generate_table_of_contents(@rendered_content)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
def validate_slug
|
|
37
|
+
slug = params[:slug].to_s
|
|
38
|
+
|
|
39
|
+
unless slug.match?(SAFE_SLUG_PATTERN)
|
|
40
|
+
render_not_found
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def render_not_found
|
|
45
|
+
file_404 = Rails.public_path.join("404.html")
|
|
46
|
+
if file_404.exist?
|
|
47
|
+
render file: file_404, status: :not_found, layout: false
|
|
48
|
+
else
|
|
49
|
+
head :not_found
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def set_docs_mode
|
|
54
|
+
@docs_mode = determine_docs_mode
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def determine_docs_mode
|
|
58
|
+
mode = params[:mode] ||
|
|
59
|
+
resolve_user_mode ||
|
|
60
|
+
cookies[:markdowndocs_mode] ||
|
|
61
|
+
Markdowndocs.config.default_mode
|
|
62
|
+
|
|
63
|
+
valid_modes = Markdowndocs.config.modes
|
|
64
|
+
valid_modes.include?(mode) ? mode : Markdowndocs.config.default_mode
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def resolve_user_mode
|
|
68
|
+
resolver = Markdowndocs.config.user_mode_resolver
|
|
69
|
+
return nil unless resolver.respond_to?(:call)
|
|
70
|
+
|
|
71
|
+
resolver.call(self)
|
|
72
|
+
rescue
|
|
73
|
+
nil
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Markdowndocs
|
|
4
|
+
class PreferencesController < ApplicationController
|
|
5
|
+
def update
|
|
6
|
+
mode = params[:mode].to_s
|
|
7
|
+
|
|
8
|
+
unless Markdowndocs.config.modes.include?(mode)
|
|
9
|
+
head :unprocessable_entity
|
|
10
|
+
return
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Save to database via host app's lambda (if configured)
|
|
14
|
+
saver = Markdowndocs.config.user_mode_saver
|
|
15
|
+
if saver.respond_to?(:call)
|
|
16
|
+
begin
|
|
17
|
+
saver.call(self, mode)
|
|
18
|
+
rescue => e
|
|
19
|
+
Rails.logger.warn("Markdowndocs: user_mode_saver failed: #{e.message}")
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Always set cookie as fallback
|
|
24
|
+
cookies[:markdowndocs_mode] = {
|
|
25
|
+
value: mode,
|
|
26
|
+
expires: 1.year.from_now,
|
|
27
|
+
httponly: true
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
redirect_back(fallback_location: markdowndocs.root_path, status: :see_other)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Markdowndocs
|
|
4
|
+
module DocsHelper
|
|
5
|
+
def generate_table_of_contents(html)
|
|
6
|
+
return [] if html.blank?
|
|
7
|
+
|
|
8
|
+
doc = Nokogiri::HTML.fragment(html)
|
|
9
|
+
toc_items = []
|
|
10
|
+
|
|
11
|
+
doc.css("h2, h3").each do |heading|
|
|
12
|
+
text = heading.text.strip
|
|
13
|
+
next if text.blank?
|
|
14
|
+
|
|
15
|
+
slug = heading["id"].presence || slugify_heading(text)
|
|
16
|
+
|
|
17
|
+
toc_items << {
|
|
18
|
+
text: text,
|
|
19
|
+
slug: slug,
|
|
20
|
+
level: heading.name[1].to_i
|
|
21
|
+
}
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
toc_items
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def slugify_heading(text)
|
|
28
|
+
text.to_s
|
|
29
|
+
.downcase
|
|
30
|
+
.gsub(/[^\w\s-]/, "")
|
|
31
|
+
.gsub(/\s+/, "-").squeeze("-")
|
|
32
|
+
.gsub(/^-|-$/, "")
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def add_heading_anchors(html)
|
|
36
|
+
return html if html.blank?
|
|
37
|
+
|
|
38
|
+
doc = Nokogiri::HTML.fragment(html)
|
|
39
|
+
|
|
40
|
+
doc.css("h2, h3").each do |heading|
|
|
41
|
+
text = heading.text.strip
|
|
42
|
+
next if text.blank?
|
|
43
|
+
|
|
44
|
+
unless heading["id"].present?
|
|
45
|
+
slug = slugify_heading(text)
|
|
46
|
+
heading["id"] = slug
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
doc.css("a.anchor").each do |anchor|
|
|
51
|
+
anchor.remove if anchor.text.strip.empty?
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
doc.to_html
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def markdowndocs_format_breadcrumbs(category, title)
|
|
58
|
+
[
|
|
59
|
+
{name: "Docs", path: markdowndocs.root_path, current: false},
|
|
60
|
+
{name: category, path: nil, current: false},
|
|
61
|
+
{name: title, path: nil, current: true}
|
|
62
|
+
]
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|