markdowndocs 0.6.0 → 0.8.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 +4 -4
- data/CHANGELOG.md +115 -0
- data/README.md +95 -14
- data/app/controllers/markdowndocs/docs_controller.rb +29 -9
- data/app/controllers/markdowndocs/preferences_controller.rb +62 -3
- data/app/helpers/markdowndocs/docs_helper.rb +6 -2
- data/app/models/markdowndocs/documentation.rb +139 -23
- data/app/services/markdowndocs/markdown_renderer.rb +50 -19
- data/app/views/markdowndocs/docs/_mode_switcher.html.erb +8 -2
- data/app/views/markdowndocs/docs/show.html.erb +6 -1
- data/config/routes.rb +11 -0
- data/docs/superpowers/plans/2026-05-15-path-based-audience-routing.md +1619 -0
- data/docs/superpowers/specs/2026-05-15-path-based-audience-routing-design.md +311 -0
- data/lib/markdowndocs/configuration.rb +9 -1
- data/lib/markdowndocs/version.rb +1 -1
- data/lib/markdowndocs.rb +7 -0
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 75c53c564b04a6a6a957dc2b038121c542e924d97908f5001c100cbca060d3a6
|
|
4
|
+
data.tar.gz: f791c421dd96c2c81b9bd49019216b72bb796aef43965157e3a608309d25e47d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4af4914a66d3a2bcbdedcfb6e375573897dc1127199c6eabf6b0784f817741fcdb90ba732c4c8f1601bc56eb913781dc89dd59507c18c09ffea490ed211695e3
|
|
7
|
+
data.tar.gz: 3d733bca1efcdbb749a35491506f0d0f13ac4e7ed1f2f04ea6ee698cd94854cec7791e374ad02211a020a2d1012334639c49c5d87e1adb8b2b531a4f57d39be1
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,118 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.8.0] - 2026-05-29
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **Opt-in inline SVG (`config.allow_svg`).** When set to `true`, a curated,
|
|
13
|
+
safe subset of structural SVG tags and attributes is permitted in rendered
|
|
14
|
+
documents — useful for hand-authored diagrams. The `Rails::HTML5`
|
|
15
|
+
SafeListSanitizer remains the security boundary: `<script>`,
|
|
16
|
+
`<foreignObject>`, `on*` event handlers, and `javascript:` URIs are still
|
|
17
|
+
stripped. Defaults to `false`, so existing behavior is unchanged.
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
|
|
21
|
+
- Heading-anchor injection, table-of-contents extraction, and syntax
|
|
22
|
+
highlighting now parse with `Nokogiri::HTML5` instead of `Nokogiri::HTML`,
|
|
23
|
+
preserving case-sensitive SVG/MathML foreign-content attributes (e.g.
|
|
24
|
+
`viewBox`, `markerWidth`, `refX`) that were previously lowercased on
|
|
25
|
+
re-serialization, which silently broke any inline SVG.
|
|
26
|
+
|
|
27
|
+
## [0.7.0] - 2026-05-15
|
|
28
|
+
|
|
29
|
+
### Added
|
|
30
|
+
|
|
31
|
+
- **Path-based audience routing.** A first-level subdirectory of
|
|
32
|
+
`app/docs/` whose name matches an entry in `config.modes` is now
|
|
33
|
+
treated as an audience scope. Files inside `app/docs/technical/` are
|
|
34
|
+
visible only when the current mode is `technical`; files at the root
|
|
35
|
+
remain shared (visible in every mode). The new convention is the
|
|
36
|
+
recommended way to scope whole documents and replaces `audience:`
|
|
37
|
+
frontmatter (see Deprecated below).
|
|
38
|
+
- **`/docs/:mode/:slug` route.** Mode-scoped documents are served at
|
|
39
|
+
stable, RESTful URLs (e.g., `/docs/technical/architecture`). The
|
|
40
|
+
`:mode` segment is constrained to entries in `config.modes`; unknown
|
|
41
|
+
modes return 404.
|
|
42
|
+
- **Path-prefixed slugs in `config.categories`.** Slug entries may now
|
|
43
|
+
include a mode prefix (e.g., `"technical/architecture"`) to attach a
|
|
44
|
+
mode-scoped doc to a category. Bare slugs continue to match root
|
|
45
|
+
files. Example:
|
|
46
|
+
|
|
47
|
+
config.categories = {
|
|
48
|
+
"Architecture" => %w[technical/architecture]
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
- **Smart navigation in mode switcher.** Toggling the mode now attempts
|
|
52
|
+
to navigate to a same-slug document in the target mode's location,
|
|
53
|
+
falling back to the shared root sibling, then staying put. Sharing
|
|
54
|
+
links still works because URLs are stable.
|
|
55
|
+
- **`Markdowndocs.deprecator`** ActiveSupport::Deprecation instance for
|
|
56
|
+
emitting gem-specific deprecation warnings. Hosts can configure
|
|
57
|
+
behavior (silence / raise / log) via standard
|
|
58
|
+
`ActiveSupport::Deprecation` APIs.
|
|
59
|
+
|
|
60
|
+
### Changed
|
|
61
|
+
|
|
62
|
+
- `Documentation.all` walks both `app/docs/*.md` and
|
|
63
|
+
`app/docs/<mode>/*.md` for every configured mode.
|
|
64
|
+
- `Documentation` instances expose `#path_slug` (the file's path
|
|
65
|
+
relative to the docs root, sans `.md`).
|
|
66
|
+
- `Documentation.find_by_slug(slug, mode:)` prefers the mode-scoped
|
|
67
|
+
file first, then falls back to the root.
|
|
68
|
+
- `PreferencesController#update` now expects a `current_path` form
|
|
69
|
+
field (added in `_mode_switcher.html.erb`) and computes the smart-nav
|
|
70
|
+
target before redirecting. Hosts with custom forms targeting
|
|
71
|
+
`preference_path` should include `<input type="hidden"
|
|
72
|
+
name="current_path" value="<%= request.fullpath %>">` to opt into
|
|
73
|
+
smart navigation. Without it, the controller redirects to the docs
|
|
74
|
+
index (no behavior loss, just no smart-nav benefit).
|
|
75
|
+
|
|
76
|
+
### Deprecated
|
|
77
|
+
|
|
78
|
+
- **`audience:` frontmatter.** Still functional, but emits a one-shot
|
|
79
|
+
warning per file path per process boot. Will be removed in v1.0.0.
|
|
80
|
+
Migration: move the file into a matching mode subdirectory (or, for
|
|
81
|
+
multi-audience docs, drop the key — root files are shared). The
|
|
82
|
+
warning message includes the suggested target path.
|
|
83
|
+
|
|
84
|
+
### Migration notes
|
|
85
|
+
|
|
86
|
+
- See `README.md` ("Migrating from v0.6.x to v0.7.0") for full guidance.
|
|
87
|
+
- URL stability: every URL from v0.6.x continues to resolve unchanged.
|
|
88
|
+
- Subdirectories under `app/docs/` whose name doesn't match a
|
|
89
|
+
configured mode are now ignored (one-line warning at discovery). If
|
|
90
|
+
you've been using non-mode subdirectories for organization, either
|
|
91
|
+
flatten them or rename them to match a configured mode.
|
|
92
|
+
|
|
93
|
+
## [0.6.1] - 2026-05-13
|
|
94
|
+
|
|
95
|
+
### Fixed
|
|
96
|
+
|
|
97
|
+
- **Duplicate `id="docs-mode-switcher"` in the DOM** (issue #20). The
|
|
98
|
+
show layout renders `_navigation` (and therefore the mode switcher)
|
|
99
|
+
twice — once for the mobile sidebar, once for the desktop sidebar.
|
|
100
|
+
The hardcoded id on `_mode_switcher.html.erb` produced two elements
|
|
101
|
+
with the same id on every doc show page, a WCAG 4.1.1 violation.
|
|
102
|
+
|
|
103
|
+
Dropped the `id=` from the partial entirely. Stimulus already scopes
|
|
104
|
+
the controller via `data-controller="docs-mode"`, which can appear
|
|
105
|
+
N times in a document without colliding.
|
|
106
|
+
|
|
107
|
+
Host apps / tests / custom CSS selecting via `#docs-mode-switcher`
|
|
108
|
+
should switch to `[data-controller="docs-mode"]`. Note: any host app
|
|
109
|
+
relying on that id was already in a broken state (duplicate ids in
|
|
110
|
+
the DOM); this fix surfaces the issue rather than creating it.
|
|
111
|
+
|
|
112
|
+
### Migration notes
|
|
113
|
+
|
|
114
|
+
- If you have a CSS rule like `#docs-mode-switcher { ... }`, change it
|
|
115
|
+
to `[data-controller="docs-mode"] { ... }`.
|
|
116
|
+
- If you have a Capybara test using `within "#docs-mode-switcher"`,
|
|
117
|
+
change it to `within first("[data-controller='docs-mode']")` (or
|
|
118
|
+
similar — the partial may render twice depending on your layout).
|
|
119
|
+
|
|
8
120
|
## [0.6.0] - 2026-05-13
|
|
9
121
|
|
|
10
122
|
### Added
|
|
@@ -167,6 +279,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
167
279
|
- i18n support for all UI strings
|
|
168
280
|
- Install generator (`rails generate markdowndocs:install`)
|
|
169
281
|
|
|
282
|
+
[0.7.0]: https://github.com/dschmura/markdowndocs/releases/tag/v0.7.0
|
|
283
|
+
[0.6.1]: https://github.com/dschmura/markdowndocs/releases/tag/v0.6.1
|
|
284
|
+
[0.6.0]: https://github.com/dschmura/markdowndocs/releases/tag/v0.6.0
|
|
170
285
|
[0.5.0]: https://github.com/dschmura/markdowndocs/releases/tag/v0.5.0
|
|
171
286
|
[0.4.0]: https://github.com/dschmura/markdowndocs/releases/tag/v0.4.0
|
|
172
287
|
[0.3.1]: https://github.com/dschmura/markdowndocs/releases/tag/v0.3.1
|
data/README.md
CHANGED
|
@@ -55,8 +55,8 @@ Markdowndocs.configure do |config|
|
|
|
55
55
|
# Category → slug mapping
|
|
56
56
|
config.categories = {
|
|
57
57
|
"Getting Started" => %w[welcome quickstart],
|
|
58
|
-
"Guides" => %w[authentication
|
|
59
|
-
"
|
|
58
|
+
"Guides" => %w[authentication billing],
|
|
59
|
+
"Architecture" => %w[technical/architecture technical/billing]
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
# Available documentation modes (default: %w[guide technical])
|
|
@@ -71,6 +71,11 @@ Markdowndocs.configure do |config|
|
|
|
71
71
|
# Cache expiry for rendered markdown (default: 1.hour)
|
|
72
72
|
# config.cache_expiry = 1.hour
|
|
73
73
|
|
|
74
|
+
# Allow a curated, safe subset of inline SVG (for hand-authored diagrams).
|
|
75
|
+
# Scripts, event handlers, and javascript: URIs are still stripped by the
|
|
76
|
+
# sanitizer. Default: false. (default: false)
|
|
77
|
+
# config.allow_svg = true
|
|
78
|
+
|
|
74
79
|
# Optional: Resolve current user's mode preference from database
|
|
75
80
|
# config.user_mode_resolver = ->(controller) {
|
|
76
81
|
# controller.send(:current_user)&.preferences&.docs_mode
|
|
@@ -83,6 +88,11 @@ Markdowndocs.configure do |config|
|
|
|
83
88
|
end
|
|
84
89
|
```
|
|
85
90
|
|
|
91
|
+
> Bare slugs (e.g., `"welcome"`) match files at the docs root.
|
|
92
|
+
> Path-prefixed slugs (e.g., `"technical/architecture"`) match files
|
|
93
|
+
> inside the named mode subdirectory. The prefix segment must match
|
|
94
|
+
> an entry in `config.modes`.
|
|
95
|
+
|
|
86
96
|
### Configuration Options
|
|
87
97
|
|
|
88
98
|
| Option | Default | Description |
|
|
@@ -124,23 +134,43 @@ Your content here...
|
|
|
124
134
|
|
|
125
135
|
If front matter is omitted, the title is extracted from the first H1 heading and the description from the first paragraph.
|
|
126
136
|
|
|
127
|
-
### Audience Filtering
|
|
137
|
+
### Audience Filtering by Filesystem Path
|
|
138
|
+
|
|
139
|
+
The recommended way to scope a whole document to a single audience is to
|
|
140
|
+
place it inside a subdirectory whose name matches an entry in
|
|
141
|
+
`config.modes`. Files at the docs root are *shared* — visible in every
|
|
142
|
+
mode.
|
|
143
|
+
|
|
144
|
+
```text
|
|
145
|
+
app/docs/
|
|
146
|
+
├── getting_started.md → shared, visible in every mode
|
|
147
|
+
├── billing.md → shared
|
|
148
|
+
└── technical/
|
|
149
|
+
├── architecture.md → technical mode only
|
|
150
|
+
└── billing.md → technical mode only
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
URLs follow the filesystem layout: `app/docs/billing.md` is served at
|
|
154
|
+
`/docs/billing`; `app/docs/technical/billing.md` is served at
|
|
155
|
+
`/docs/technical/billing`. Both URLs are stable and shareable.
|
|
156
|
+
|
|
157
|
+
Subdirectories whose name does not match a configured mode are ignored
|
|
158
|
+
by document discovery, with a one-line warning at boot.
|
|
128
159
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
key
|
|
160
|
+
### Audience Filtering by Frontmatter (deprecated)
|
|
161
|
+
|
|
162
|
+
The `audience:` frontmatter key from v0.6.0 still works in v0.7.x but is
|
|
163
|
+
deprecated. A warning is logged the first time each affected file is
|
|
164
|
+
read. Move the file into the matching mode subdirectory and remove the
|
|
165
|
+
`audience:` key. See the migration guide below.
|
|
132
166
|
|
|
133
167
|
```yaml
|
|
134
|
-
audience: technical #
|
|
135
|
-
audience: [guide, technical] #
|
|
136
|
-
# omit `audience:` #
|
|
168
|
+
audience: technical # deprecated — move to app/docs/technical/
|
|
169
|
+
audience: [guide, technical] # deprecated — keep at root, drop the key
|
|
170
|
+
# omit `audience:` # still works for shared docs at root
|
|
137
171
|
```
|
|
138
172
|
|
|
139
|
-
|
|
140
|
-
hidden from the index AND unreachable via slug (404). This complements
|
|
141
|
-
the in-page `<!-- mode: -->` blocks below: use mode blocks when one doc
|
|
142
|
-
mixes audience-specific snippets; use `audience:` when whole docs are
|
|
143
|
-
audience-specific.
|
|
173
|
+
The `audience:` key is scheduled for removal in v1.0.0.
|
|
144
174
|
|
|
145
175
|
### Mode Blocks
|
|
146
176
|
|
|
@@ -303,6 +333,57 @@ bundle exec rspec
|
|
|
303
333
|
|
|
304
334
|
Pushing the tag triggers the GitHub Actions release workflow, which builds and publishes the gem to RubyGems automatically.
|
|
305
335
|
|
|
336
|
+
## Migrating from v0.6.x to v0.7.0
|
|
337
|
+
|
|
338
|
+
**URL stability.** Every URL from v0.6.x continues to resolve. Hosts
|
|
339
|
+
that upgrade without moving files see zero URL changes. Path-based
|
|
340
|
+
routing only introduces *new* URLs (`/docs/<mode>/<slug>`) when you
|
|
341
|
+
explicitly relocate files into mode subdirectories.
|
|
342
|
+
|
|
343
|
+
### If you don't use `audience:` today
|
|
344
|
+
|
|
345
|
+
No action required. Adopt the new convention at your leisure.
|
|
346
|
+
|
|
347
|
+
### If you use `audience: <single-mode>`
|
|
348
|
+
|
|
349
|
+
For each affected doc:
|
|
350
|
+
|
|
351
|
+
```diff
|
|
352
|
+
- app/docs/foo.md
|
|
353
|
+
- ---
|
|
354
|
+
- audience: technical
|
|
355
|
+
- ---
|
|
356
|
+
+ app/docs/technical/foo.md
|
|
357
|
+
+ (no `audience:` key)
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
The deprecation warning surfaces the suggested target path.
|
|
361
|
+
|
|
362
|
+
### If you use `audience: [guide, technical]`
|
|
363
|
+
|
|
364
|
+
The doc is multi-audience — drop the key, the root file is shared:
|
|
365
|
+
|
|
366
|
+
```diff
|
|
367
|
+
app/docs/foo.md
|
|
368
|
+
- ---
|
|
369
|
+
- audience: [guide, technical]
|
|
370
|
+
- ---
|
|
371
|
+
+ (no `audience:` key)
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
### `config.categories` for mode-scoped docs
|
|
375
|
+
|
|
376
|
+
Prefix slugs with the mode subdirectory:
|
|
377
|
+
|
|
378
|
+
```diff
|
|
379
|
+
config.categories = {
|
|
380
|
+
- "Architecture" => %w[architecture data_model]
|
|
381
|
+
+ "Architecture" => %w[technical/architecture data_model]
|
|
382
|
+
}
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
Bare slugs continue to mean "the doc at the root with this name."
|
|
386
|
+
|
|
306
387
|
## Contributing
|
|
307
388
|
|
|
308
389
|
Bug reports and pull requests are welcome on GitHub at [github.com/dschmura/markdowndocs](https://github.com/dschmura/markdowndocs).
|
|
@@ -26,7 +26,7 @@ module Markdowndocs
|
|
|
26
26
|
json = Rails.cache.fetch(cache_key, expires_in: Markdowndocs.config.cache_expiry) do
|
|
27
27
|
Documentation.all.map do |doc|
|
|
28
28
|
{
|
|
29
|
-
id: doc.
|
|
29
|
+
id: doc.path_slug,
|
|
30
30
|
title: doc.title,
|
|
31
31
|
description: doc.description,
|
|
32
32
|
content: doc.plain_text_content,
|
|
@@ -41,25 +41,29 @@ module Markdowndocs
|
|
|
41
41
|
end
|
|
42
42
|
|
|
43
43
|
def show
|
|
44
|
-
#
|
|
45
|
-
#
|
|
46
|
-
#
|
|
47
|
-
#
|
|
48
|
-
|
|
49
|
-
@doc = Documentation.find_by_slug(params[:slug], mode:
|
|
44
|
+
# `params[:mode]` here is the URL path segment (e.g. /docs/technical/foo → "technical")
|
|
45
|
+
# if the request matched the scoped route. Otherwise, fall back to the resolved
|
|
46
|
+
# current preference (@docs_mode) so root-mounted docs honor audience-frontmatter
|
|
47
|
+
# filtering.
|
|
48
|
+
lookup_mode = params[:mode].presence || @docs_mode
|
|
49
|
+
@doc = Documentation.find_by_slug(params[:slug], mode: lookup_mode)
|
|
50
50
|
|
|
51
51
|
if @doc.nil?
|
|
52
52
|
render_not_found
|
|
53
53
|
return
|
|
54
54
|
end
|
|
55
55
|
|
|
56
|
+
# `mode:` here controls in-doc <!-- mode: X --> block stripping,
|
|
57
|
+
# which should reflect the user's PREFERENCE (@docs_mode), not the
|
|
58
|
+
# URL-path mode (lookup_mode). A /technical/foo URL does not force
|
|
59
|
+
# guide-only blocks invisible.
|
|
56
60
|
rendered_html = MarkdownRenderer.render(
|
|
57
61
|
@doc.content,
|
|
58
62
|
cache_key: @doc.cache_key,
|
|
59
63
|
mode: @docs_mode
|
|
60
64
|
)
|
|
61
65
|
@rendered_content = helpers.add_heading_anchors(rendered_html)
|
|
62
|
-
@related_docs = Documentation.by_category(@doc.category).reject { |d| d.
|
|
66
|
+
@related_docs = Documentation.by_category(@doc.category).reject { |d| d.path_slug == @doc.path_slug }
|
|
63
67
|
@available_modes = @doc.available_modes
|
|
64
68
|
@toc_items = helpers.generate_table_of_contents(@rendered_content)
|
|
65
69
|
end
|
|
@@ -88,7 +92,17 @@ module Markdowndocs
|
|
|
88
92
|
end
|
|
89
93
|
|
|
90
94
|
def determine_docs_mode
|
|
91
|
-
|
|
95
|
+
# Only treat params[:mode] as a preference override on the root-mounted
|
|
96
|
+
# `/:slug` route. On the scoped `/:mode/:slug` route, params[:mode] is
|
|
97
|
+
# the URL path segment and is consumed by `show` for doc lookup, not as
|
|
98
|
+
# a preference override.
|
|
99
|
+
preference_param = if path_mode_in_request?
|
|
100
|
+
nil
|
|
101
|
+
else
|
|
102
|
+
params[:mode]
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
mode = preference_param ||
|
|
92
106
|
resolve_user_mode ||
|
|
93
107
|
cookies[:markdowndocs_mode] ||
|
|
94
108
|
Markdowndocs.config.default_mode
|
|
@@ -97,6 +111,12 @@ module Markdowndocs
|
|
|
97
111
|
valid_modes.include?(mode) ? mode : Markdowndocs.config.default_mode
|
|
98
112
|
end
|
|
99
113
|
|
|
114
|
+
def path_mode_in_request?
|
|
115
|
+
# The scoped route names `mode` as a path param. On the unscoped route,
|
|
116
|
+
# `mode` (when present) comes from the query string.
|
|
117
|
+
request.path_parameters[:mode].present?
|
|
118
|
+
end
|
|
119
|
+
|
|
100
120
|
def resolve_user_mode
|
|
101
121
|
resolver = Markdowndocs.config.user_mode_resolver
|
|
102
122
|
return nil unless resolver.respond_to?(:call)
|
|
@@ -10,7 +10,6 @@ module Markdowndocs
|
|
|
10
10
|
return
|
|
11
11
|
end
|
|
12
12
|
|
|
13
|
-
# Save to database via host app's lambda (if configured)
|
|
14
13
|
saver = Markdowndocs.config.user_mode_saver
|
|
15
14
|
if saver.respond_to?(:call)
|
|
16
15
|
begin
|
|
@@ -20,14 +19,74 @@ module Markdowndocs
|
|
|
20
19
|
end
|
|
21
20
|
end
|
|
22
21
|
|
|
23
|
-
# Always set cookie as fallback
|
|
24
22
|
cookies[:markdowndocs_mode] = {
|
|
25
23
|
value: mode,
|
|
26
24
|
expires: 1.year.from_now,
|
|
27
25
|
httponly: true
|
|
28
26
|
}
|
|
29
27
|
|
|
30
|
-
|
|
28
|
+
redirect_to(smart_nav_target(mode, params[:current_path]), status: :see_other)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
# Computes the post-toggle destination using the unified lookup rule:
|
|
34
|
+
# 1. /docs/<target_mode>/<slug> if the scoped file exists and is not current
|
|
35
|
+
# 2. /docs/<slug> (root) if it exists and is not current
|
|
36
|
+
# 3. current path (stay put)
|
|
37
|
+
# Falls back to the docs index when current_path is missing or doesn't
|
|
38
|
+
# match a recognizable doc URL.
|
|
39
|
+
def smart_nav_target(target_mode, current_path)
|
|
40
|
+
index_path = markdowndocs.root_path.chomp("/")
|
|
41
|
+
return index_path if current_path.blank?
|
|
42
|
+
|
|
43
|
+
slug = extract_slug_from_path(current_path)
|
|
44
|
+
return index_path if slug.nil?
|
|
45
|
+
|
|
46
|
+
docs_path = Markdowndocs.config.resolved_docs_path
|
|
47
|
+
scoped_file = docs_path.join(target_mode, "#{slug}.md")
|
|
48
|
+
root_file = docs_path.join("#{slug}.md")
|
|
49
|
+
|
|
50
|
+
scoped_url = markdowndocs.scoped_doc_path(mode: target_mode, slug: slug)
|
|
51
|
+
root_url = markdowndocs.doc_path(slug: slug)
|
|
52
|
+
|
|
53
|
+
if scoped_file.exist? && current_path != scoped_url
|
|
54
|
+
scoped_url
|
|
55
|
+
elsif root_file.exist? && current_path != root_url
|
|
56
|
+
root_url
|
|
57
|
+
else
|
|
58
|
+
current_path
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Pulls the slug from a docs path. Returns nil if the path is the index
|
|
63
|
+
# or doesn't match the docs URL shape. Recognizes both /docs/<slug> and
|
|
64
|
+
# /docs/<mode>/<slug>.
|
|
65
|
+
def extract_slug_from_path(path)
|
|
66
|
+
# Strip query string and trailing slash.
|
|
67
|
+
clean = path.split("?").first.to_s.chomp("/")
|
|
68
|
+
base = markdowndocs.root_path.chomp("/")
|
|
69
|
+
return nil unless clean.start_with?(base)
|
|
70
|
+
|
|
71
|
+
remainder = clean[base.length..]
|
|
72
|
+
return nil if remainder.blank? || remainder == "/"
|
|
73
|
+
|
|
74
|
+
segments = remainder.sub(%r{\A/}, "").split("/")
|
|
75
|
+
|
|
76
|
+
case segments.length
|
|
77
|
+
when 1
|
|
78
|
+
slug_candidate(segments.first)
|
|
79
|
+
when 2
|
|
80
|
+
# Could be /<mode>/<slug>. Only treat second segment as the slug
|
|
81
|
+
# if the first is a configured mode.
|
|
82
|
+
Markdowndocs.config.modes.include?(segments.first) ? slug_candidate(segments.last) : nil
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def slug_candidate(segment)
|
|
87
|
+
return nil if segment.blank?
|
|
88
|
+
return nil if segment.include?("..") || segment.include?("/")
|
|
89
|
+
segment
|
|
31
90
|
end
|
|
32
91
|
end
|
|
33
92
|
end
|
|
@@ -5,7 +5,9 @@ module Markdowndocs
|
|
|
5
5
|
def generate_table_of_contents(html)
|
|
6
6
|
return [] if html.blank?
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
# HTML5 parsing preserves case-sensitive foreign-content (SVG/MathML)
|
|
9
|
+
# attributes like viewBox/markerWidth; Nokogiri::HTML lowercases them.
|
|
10
|
+
doc = Nokogiri::HTML5.fragment(html)
|
|
9
11
|
toc_items = []
|
|
10
12
|
|
|
11
13
|
doc.css("h2, h3").each do |heading|
|
|
@@ -35,7 +37,9 @@ module Markdowndocs
|
|
|
35
37
|
def add_heading_anchors(html)
|
|
36
38
|
return html if html.blank?
|
|
37
39
|
|
|
38
|
-
|
|
40
|
+
# HTML5 parsing preserves case-sensitive foreign-content (SVG/MathML)
|
|
41
|
+
# attributes like viewBox/markerWidth; Nokogiri::HTML lowercases them.
|
|
42
|
+
doc = Nokogiri::HTML5.fragment(html)
|
|
39
43
|
|
|
40
44
|
doc.css("h2, h3").each do |heading|
|
|
41
45
|
text = heading.text.strip
|