docs-kit 1.1.1 → 1.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 +4 -4
- data/CHANGELOG.md +116 -0
- data/README.md +109 -11
- data/app/components/docs_ui/code.rb +29 -10
- data/app/components/docs_ui/landing.rb +2 -2
- data/app/components/docs_ui/markdown.rb +8 -4
- data/app/components/docs_ui/markdown_images.rb +73 -0
- data/app/components/docs_ui/meta_tags.rb +2 -2
- data/app/components/docs_ui/page.rb +38 -11
- data/app/components/docs_ui/shell.rb +18 -4
- data/app/components/docs_ui/sidebar.rb +2 -2
- data/app/controllers/docs_kit/llms_controller.rb +5 -2
- data/lib/docs_kit/configuration.rb +67 -1
- data/lib/docs_kit/engine.rb +7 -0
- data/lib/docs_kit/llms_text.rb +1 -1
- data/lib/docs_kit/mcp_server.rb +2 -2
- data/lib/docs_kit/templates/new_site.rb +1 -1
- data/lib/docs_kit/version.rb +1 -1
- data/lib/docs_kit.rb +1 -1
- data/lib/generators/docs_kit/install/install_generator.rb +39 -4
- data/lib/generators/docs_kit/install/migration_registry.rb +12 -9
- data/lib/generators/docs_kit/install/migrations/v1_2_0.rb +120 -0
- data/lib/generators/docs_kit/install/sync_report.rb +87 -1
- data/lib/generators/docs_kit/install/templates/Dockerfile.tt +1 -1
- data/lib/generators/docs_kit/install/templates/agents_md.erb +3 -0
- data/lib/generators/docs_kit/page/page_generator.rb +10 -4
- metadata +6 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: feed239eb09deeafd1a9dc6e92e91d192c14f927fa562105139f4b16473d1a60
|
|
4
|
+
data.tar.gz: 6149bf7b9509fb611ce86e9c183a38c027ce77addee69669be41a0ce85fb292d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5af123f6815e81ccad67dcb7737836c0a2727fb60e963bfc16d6ff0a6540325ca1a67fa2aaaea2ff824ba3b36f2210d8fb4aaab43f097195ec7f020cbfc8c54e
|
|
7
|
+
data.tar.gz: 32e24110ca11cfba1c795d60ea93d6ecb2f1e42af912274ebc816718cd235d93e217bb1ac2a6082b08039eaefe162c6b8287f4ed328c685ab3c669c840e2e359
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,122 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [1.3.0] — 2026-10-13
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- **Markdown links drop a script-bearing href.** `[text](javascript:…)`,
|
|
10
|
+
`data:`, `vbscript:` and any other scheme outside http(s), `mailto:` and
|
|
11
|
+
`tel:` now render the link text with no `<a>` (Phlex already refused
|
|
12
|
+
`javascript:`; `data:` and the rest slipped through). Scheme-less hrefs are
|
|
13
|
+
unchanged. The same guard `DocsUI::Markdown` images have.
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
|
|
17
|
+
- **A per-request `brand` and `title_suffix`.** Both accept a zero-arity
|
|
18
|
+
callable (`c.brand = -> { Current.tenant_name }`), evaluated on every render:
|
|
19
|
+
the topbar, sidebar header, `<title>`, `og:site_name`, llms.txt and the MCP
|
|
20
|
+
server name show that request's value. A callable brand needs an explicit
|
|
21
|
+
`c.nav_storage_key`; `DocsKit.configure` raises without one. New readers
|
|
22
|
+
`Configuration#brand_text` / `#title_suffix_text`; a String brand renders
|
|
23
|
+
exactly as before. (#92)
|
|
24
|
+
- **Markdown images render.** `` in an `md` island now
|
|
25
|
+
renders a lazy-loaded `<img>` (it rendered nothing before). In a Rails view
|
|
26
|
+
a relative path resolves through the site's asset pipeline (`image_path`;
|
|
27
|
+
off a request it is kept as written); an absolute,
|
|
28
|
+
protocol-relative or root-relative URL is kept as written; the alt is the
|
|
29
|
+
description's plain text. A `javascript:`, `data:` or other non-http(s)
|
|
30
|
+
scheme renders no image. The page's `.md` twin exports it back to
|
|
31
|
+
``. (#94)
|
|
32
|
+
- **A dedicated docs import map.** `c.importmap_entry_point` (default
|
|
33
|
+
`"application"`) and `c.importmap` (an `Importmap::Map` or a callable
|
|
34
|
+
returning one, resolved per render) choose what the Shell's
|
|
35
|
+
`javascript_importmap_tags` loads, and `config.docs_kit.auto_pin = false` (in
|
|
36
|
+
`config/application.rb`) keeps the gem's `docs-nav` pin out of the host's
|
|
37
|
+
shared map. Unset, the head is unchanged. (#93)
|
|
38
|
+
- **`DocsUI::Page` title, description and eyebrow per instance.** The page now
|
|
39
|
+
reads `#title`, `#description` and `#eyebrow`, which default to the class
|
|
40
|
+
macros, so one page class can render many records (`def title = @widget.name`)
|
|
41
|
+
without an anonymous subclass per render. The `eyebrow`, `description` and
|
|
42
|
+
`on_page` macros inherit from a parent page when unset. Pages using only the
|
|
43
|
+
class macros render byte-identically. (#95)
|
|
44
|
+
|
|
45
|
+
## [1.2.1] — 2026-09-30 (with 1.2.0, same day)
|
|
46
|
+
|
|
47
|
+
### Upgrading from 1.1
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
bundle update docs-kit
|
|
51
|
+
bin/rails g docs_kit:install --sync # applies the 1.2 migration, prints what it can't
|
|
52
|
+
bun install && bun run build:css
|
|
53
|
+
bundle exec rspec
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
- **Ruby 3.3 or newer is required.** docs-kit 1.2 won't install on 3.2. Move
|
|
57
|
+
`.ruby-version` and the Dockerfile's `ARG RUBY_VERSION` first (`--sync` warns
|
|
58
|
+
about either one if it's still below 3.3).
|
|
59
|
+
- **`--sync` raises the toolchain it scaffolded.** In a Dockerfile that still
|
|
60
|
+
carries docs-kit's old `ARG BUN_VERSION=1.3.2`, it bumps that to 1.4.2. In
|
|
61
|
+
`package.json`, it raises docs-kit's old `tailwindcss` / `@tailwindcss/cli
|
|
62
|
+
^4.1.18` and `daisyui ^5.6.0` floors to `^4.3.3` / `^5.7.47`. Values you
|
|
63
|
+
picked yourself are never rewritten: an older one is printed as a warning,
|
|
64
|
+
and a newer one is left alone. Review the result with `git diff`.
|
|
65
|
+
- **Deploys pick up Node 24 actions and `ubuntu-26.04` automatically** when your
|
|
66
|
+
`deploy-docs.yml` calls `zoolutions/docs-kit/.github/workflows/deploy.yml@main`
|
|
67
|
+
(what `docs-kit new` scaffolds). `--sync` warns about a caller pinned to a tag
|
|
68
|
+
or SHA; move the pin forward.
|
|
69
|
+
- **Untrack `app/assets/stylesheets/tailwind.sources.css`** if it's committed:
|
|
70
|
+
`git rm --cached app/assets/stylesheets/tailwind.sources.css`. The generator
|
|
71
|
+
now gitignores it; `--sync` reminds you when it's still tracked.
|
|
72
|
+
|
|
73
|
+
### Removed
|
|
74
|
+
|
|
75
|
+
- **Ruby 3.2 support.** `required_ruby_version` is now `>= 3.3` (3.2 reached
|
|
76
|
+
end of life in March 2026), and CI no longer tests it.
|
|
77
|
+
|
|
78
|
+
### Changed
|
|
79
|
+
|
|
80
|
+
- **daisyui 2.x is allowed.** The dependency is now `>= 1.2, < 3`. 1.2.0 was
|
|
81
|
+
tagged but never reached RubyGems (its release run failed a stale spec), so
|
|
82
|
+
1.2.1 is the first published 1.2 release.
|
|
83
|
+
- **The reusable deploy workflow runs on Node 24 actions.** `deploy.yml` now
|
|
84
|
+
uses `actions/checkout@v7`, `docker/setup-buildx-action@v4`,
|
|
85
|
+
`docker/login-action@v4`, `docker/build-push-action@v7` and
|
|
86
|
+
`webfactory/ssh-agent@v0.10.0`, clearing the Node 20 deprecation warning for
|
|
87
|
+
every site that calls it. Its jobs run on `ubuntu-26.04` (ahead of the
|
|
88
|
+
`ubuntu-latest` migration).
|
|
89
|
+
- **`docs_kit:install` scaffolds current toolchain floors.** The `package.json`
|
|
90
|
+
stub requires `tailwindcss`/`@tailwindcss/cli` `^4.3.3` and `daisyui`
|
|
91
|
+
`^5.7.47` (a spec keeps them in lockstep with the gem's own docs site), and
|
|
92
|
+
the generated `Dockerfile` installs Bun 1.4.2 (and falls back to Ruby 3.4.11
|
|
93
|
+
when the host Ruby version can't be read).
|
|
94
|
+
- **`docs_kit:install` gitignores the generated `tailwind.sources.css`.**
|
|
95
|
+
`bin/build-css` rewrites it on every build with machine-specific gem paths, so
|
|
96
|
+
a committed copy only churns. `--sync` adds the `.gitignore` entry and warns
|
|
97
|
+
when the file is still tracked. (#72)
|
|
98
|
+
- **`--sync` runs its first release migration** (1.1 → 1.2, above). The
|
|
99
|
+
migration registry no longer ships empty.
|
|
100
|
+
|
|
101
|
+
### Fixed
|
|
102
|
+
|
|
103
|
+
- **`docs_kit:page` injected the registry line once per group.** In a registry
|
|
104
|
+
laid out as blank-line-separated groups (the layout `docs_kit:install`
|
|
105
|
+
produces), the "page line not followed by a page line" Regexp anchor matched
|
|
106
|
+
the end of *every* group and Thor's `inject_into_file` replaced every match.
|
|
107
|
+
The anchor is now the file's last `page` line as a String, so one run adds
|
|
108
|
+
exactly one line, at the end of the last group.
|
|
109
|
+
|
|
110
|
+
### Added
|
|
111
|
+
|
|
112
|
+
- **`DocsUI::Code` infers the language from `filename:`.**
|
|
113
|
+
`DocsUI::Code(<<~YAML, filename: "config/deploy.yml")` now highlights as YAML
|
|
114
|
+
instead of Ruby: with no `lexer:`, the lexer is guessed from Rouge's filename
|
|
115
|
+
globs (`*.yml` → yaml, `Dockerfile` → docker, `*.sh` → shell, …). An explicit
|
|
116
|
+
`lexer:` still wins, and a block with neither (or an unguessable filename)
|
|
117
|
+
stays Ruby, so existing output is unchanged.
|
|
118
|
+
|
|
119
|
+
## [1.1.1] and earlier
|
|
120
|
+
|
|
5
121
|
### Fixed
|
|
6
122
|
|
|
7
123
|
- **SEO `og:image` 404.** The og:image tag pointed at the raw config path
|
data/README.md
CHANGED
|
@@ -102,9 +102,17 @@ A site created before the stamp existed (no comment) is treated as the earliest
|
|
|
102
102
|
version, so it gets every migration. Migrations are **warn-only-safe** exactly
|
|
103
103
|
like the drift report: what a step can't safely automate it prints as a manual
|
|
104
104
|
checklist (`migration steps to apply by hand:`), never a destructive rewrite of
|
|
105
|
-
a line you've edited.
|
|
106
|
-
|
|
107
|
-
|
|
105
|
+
a line you've edited. A value is only rewritten when it is exactly what an older
|
|
106
|
+
docs-kit generated; anything you picked yourself is reported, never changed.
|
|
107
|
+
|
|
108
|
+
Every release that needs site changes lists them under **"Upgrading from X.Y"**
|
|
109
|
+
at the top of its [CHANGELOG](CHANGELOG.md) entry. That's what `--sync` automates
|
|
110
|
+
and what it leaves to you. Read it before a `bundle update docs-kit` that crosses
|
|
111
|
+
a minor version.
|
|
112
|
+
|
|
113
|
+
| Release | `--sync` does | You do |
|
|
114
|
+
|---------|---------------|--------|
|
|
115
|
+
| 1.2 | Raises the Bun and Tailwind/daisyUI floors that docs-kit generated. Warns about Ruby below 3.3, hand-picked older floors, and a deploy workflow pinned off `@main` | Move to Ruby >= 3.3 before updating. `git rm --cached` a tracked `tailwind.sources.css` |
|
|
108
116
|
|
|
109
117
|
### One-time cleanup for sites created before these landed
|
|
110
118
|
|
|
@@ -118,6 +126,7 @@ generators:
|
|
|
118
126
|
| `app/helpers/icon_helper.rb` | docs-kit renders icons via rails_icons (`DocsUI::Icon`) | Delete the file. |
|
|
119
127
|
| Hand-pinned docs-kit lines in `config/importmap.rb` | the engine auto-pins the `docs-nav` controller and its assets | Delete the manual `pin`/`pin_all_from` lines for docs-kit. |
|
|
120
128
|
| `Dockerfile` stamped by an older docs-kit (`# docs-kit Dockerfile vX.Y.Z`) | docs-kit ships an optimized, multi-stage Dockerfile; a stale copy misses image-size wins | Diff yours against the current template (`lib/generators/docs_kit/install/templates/Dockerfile.tt` in the gem), adopt the changes or replace it. See [Upgrading your Dockerfile](#upgrading-your-dockerfile). |
|
|
129
|
+
| `app/assets/stylesheets/tailwind.sources.css` committed to git | `bin/build-css` regenerates it on every build with machine-specific absolute gem paths — the committed copy churns per machine/Ruby and no build consumes it. The generator adds the `.gitignore` entry, but gitignoring doesn't untrack an already-committed copy. | `git rm --cached app/assets/stylesheets/tailwind.sources.css` and commit. |
|
|
121
130
|
|
|
122
131
|
### Upgrading your Dockerfile
|
|
123
132
|
|
|
@@ -184,6 +193,20 @@ DocsKit.configure do |c|
|
|
|
184
193
|
end
|
|
185
194
|
```
|
|
186
195
|
|
|
196
|
+
**One app, several hosts?** `brand` and `title_suffix` also take a zero-arity
|
|
197
|
+
callable, evaluated on every render — the topbar, the sidebar header, `<title>`,
|
|
198
|
+
`og:site_name`, llms.txt and the MCP server name all show that request's value.
|
|
199
|
+
A callable brand can't name the localStorage namespace (it must stay stable per
|
|
200
|
+
browser), so set `nav_storage_key` too; `DocsKit.configure` raises without it.
|
|
201
|
+
|
|
202
|
+
```ruby
|
|
203
|
+
DocsKit.configure do |c|
|
|
204
|
+
c.brand = -> { Current.tenant_name } # Current < ActiveSupport::CurrentAttributes
|
|
205
|
+
c.title_suffix = -> { "#{Current.tenant_name} docs" } # optional; unset follows the brand
|
|
206
|
+
c.nav_storage_key = "docs"
|
|
207
|
+
end
|
|
208
|
+
```
|
|
209
|
+
|
|
187
210
|
The nav is **derived from the registry**, so you never hand-write it. Each
|
|
188
211
|
registry maps a heading to its authored pages (`Doc.nav_items`); a page that
|
|
189
212
|
isn't written yet is skipped, so there are no dead links. Register a page with
|
|
@@ -422,6 +445,37 @@ end
|
|
|
422
445
|
`DocsUI::Page` includes the kit, so inside `#content` you call the components
|
|
423
446
|
directly — `DocsUI::Section(...)`, `DocsUI::Code(...)` — no `render … .new`.
|
|
424
447
|
|
|
448
|
+
### One page class, many records
|
|
449
|
+
|
|
450
|
+
`title`, `eyebrow` and `description` are class macros by default, but the page
|
|
451
|
+
reads them through **instance** methods — override those when one page class
|
|
452
|
+
renders many records (one reference page per item):
|
|
453
|
+
|
|
454
|
+
```ruby
|
|
455
|
+
class Views::Docs::Widget < DocsUI::Page
|
|
456
|
+
eyebrow "Widgets"
|
|
457
|
+
|
|
458
|
+
def initialize(widget:) = @widget = widget
|
|
459
|
+
def title = @widget.name # masthead <h1> and <title>
|
|
460
|
+
def description = @widget.summary # meta/OG description (default: class macro, then #lead)
|
|
461
|
+
def content = md(@widget.notes)
|
|
462
|
+
end
|
|
463
|
+
|
|
464
|
+
# controller
|
|
465
|
+
def show = render_page(Views::Docs::Widget.new(widget: Widget.find(params[:id])))
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
`eyebrow`, `description` and `on_page` inherit: a subclass that leaves one unset
|
|
469
|
+
uses its parent page's. `title` does not — each page names itself.
|
|
470
|
+
|
|
471
|
+
Such a page usually lives on its own route, outside the registry. If that route
|
|
472
|
+
has no `(.:format)` segment, the masthead's **Markdown** action (which links
|
|
473
|
+
`request.path + ".md"`) would 404 — hide it for that page:
|
|
474
|
+
|
|
475
|
+
```ruby
|
|
476
|
+
def markdown_action? = false
|
|
477
|
+
```
|
|
478
|
+
|
|
425
479
|
### The authoring convention
|
|
426
480
|
|
|
427
481
|
One rule covers the whole kit: **the primary argument is positional; modifiers
|
|
@@ -433,6 +487,10 @@ DocsUI::Section("Add the gem", id: "add", description: …) # title positional
|
|
|
433
487
|
DocsUI::Code(source, lexer: :ruby, filename: "Gemfile") # source positional
|
|
434
488
|
```
|
|
435
489
|
|
|
490
|
+
`DocsUI::Code` infers the language from `filename:` (`config/deploy.yml` →
|
|
491
|
+
YAML, `Dockerfile` → docker, `bin/setup.sh` → shell); `lexer:` overrides the
|
|
492
|
+
guess, and with neither the block is highlighted as Ruby.
|
|
493
|
+
|
|
436
494
|
For the two wrappers that take **no** positional argument — prose and a
|
|
437
495
|
multi-language example — `DocsUI::Page` gives you lowercase helpers so a block
|
|
438
496
|
needs no parens:
|
|
@@ -930,7 +988,7 @@ jobs:
|
|
|
930
988
|
```yaml
|
|
931
989
|
service: <repo>
|
|
932
990
|
image: zoolutions/<repo>
|
|
933
|
-
minimum_version: 4.0.
|
|
991
|
+
minimum_version: 4.0.7 # dash 4.0.7: doctor stale-proxy recovery + registry credential fail-fast
|
|
934
992
|
retain_containers: 2
|
|
935
993
|
error_pages_path: public # 502/503/504.html served during a deploy gap
|
|
936
994
|
registry: { server: ghcr.io, username: mhenrixon, password: [DASH_REGISTRY_PASSWORD] }
|
|
@@ -1038,6 +1096,42 @@ server round-trip:
|
|
|
1038
1096
|
of three placements, auto-hiding on short pages. No server-side knowledge of
|
|
1039
1097
|
the headings, no per-page wiring.
|
|
1040
1098
|
|
|
1099
|
+
### A dedicated docs import map
|
|
1100
|
+
|
|
1101
|
+
By default the Shell loads the host's `application` entry point from
|
|
1102
|
+
`Rails.application.importmap`, and the engine adds the `docs-nav` pin to that
|
|
1103
|
+
map. A host with several import maps can give its docs their own entry point
|
|
1104
|
+
and map, and keep the docs controller out of every other page's preloads:
|
|
1105
|
+
|
|
1106
|
+
```ruby
|
|
1107
|
+
# config/application.rb — skip the engine's auto-pin into the shared map
|
|
1108
|
+
config.docs_kit.auto_pin = false
|
|
1109
|
+
|
|
1110
|
+
# config/initializers/docs_kit.rb — build the second map (importmap-rails has no
|
|
1111
|
+
# registry for extra maps; the host owns it) and point the docs chrome at it
|
|
1112
|
+
DOCS_IMPORTMAP = Importmap::Map.new.draw(Rails.root.join("config/importmaps/docs.rb"))
|
|
1113
|
+
|
|
1114
|
+
DocsKit.configure do |c|
|
|
1115
|
+
c.importmap_entry_point = "docs" # default "application"
|
|
1116
|
+
c.importmap = -> { DOCS_IMPORTMAP } # default nil → Rails.application.importmap
|
|
1117
|
+
end
|
|
1118
|
+
```
|
|
1119
|
+
|
|
1120
|
+
```ruby
|
|
1121
|
+
# config/importmaps/docs.rb — drawn into the docs Importmap::Map above
|
|
1122
|
+
pin "docs"
|
|
1123
|
+
pin "@hotwired/turbo-rails", to: "turbo.min.js"
|
|
1124
|
+
pin "@hotwired/stimulus", to: "stimulus.min.js"
|
|
1125
|
+
pin "@hotwired/stimulus-loading", to: "stimulus-loading.js"
|
|
1126
|
+
pin_all_from DocsKit::Engine.root.join("app/javascript/docs_kit/controllers"),
|
|
1127
|
+
under: "docs_kit/controllers", to: "docs_kit/controllers"
|
|
1128
|
+
```
|
|
1129
|
+
|
|
1130
|
+
`c.importmap` takes an `Importmap::Map` or a callable returning one; a callable
|
|
1131
|
+
is resolved on every render, so a map rebuilt on code reload is picked up. The
|
|
1132
|
+
`docs` entry point registers the controllers with
|
|
1133
|
+
`eagerLoadControllersFrom("docs_kit/controllers", application)`.
|
|
1134
|
+
|
|
1041
1135
|
### On this page (auto-TOC)
|
|
1042
1136
|
|
|
1043
1137
|
`DocsUI::Page` renders it automatically. The placement is a strategy — set the
|
|
@@ -1082,17 +1176,21 @@ application.register("reactive", ReactiveController)
|
|
|
1082
1176
|
|
|
1083
1177
|
## Releasing (maintainers)
|
|
1084
1178
|
|
|
1085
|
-
Cut a release with
|
|
1179
|
+
Cut a release with `bin/release`. Never `gem push` by hand:
|
|
1086
1180
|
|
|
1087
1181
|
```bash
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1182
|
+
bin/release list # last releases + what each bump would give
|
|
1183
|
+
bin/release --dry-run # the version it would cut + the changes since the last tag
|
|
1184
|
+
bin/release # patch bump (minor / major / 1.3.0.rc1 for others)
|
|
1185
|
+
bin/release 1.2.0 --force # delete + re-create an existing tag/release
|
|
1091
1186
|
```
|
|
1092
1187
|
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1188
|
+
`bin/release` checks you're on a clean, up-to-date `main`, confirms, and hands off
|
|
1189
|
+
to `rake release[X.Y.Z]` (`rakelib/release.rake`). That task bumps
|
|
1190
|
+
`lib/docs_kit/version.rb` and the `docs-kit` pin in `docs/Gemfile.lock` (in place,
|
|
1191
|
+
no re-resolve), verifies `gem build --strict`, commits, pushes, and creates the
|
|
1192
|
+
GitHub Release. Both files belong to the zoolutions release kit that every gem
|
|
1193
|
+
shares; see [RELEASE_KIT.md](RELEASE_KIT.md) to adopt or upgrade it elsewhere. Publishing the tag fires
|
|
1096
1194
|
`.github/workflows/release.yml`, which runs the suite, rebuilds + content-checks
|
|
1097
1195
|
the gem, signs it with Sigstore, and pushes to RubyGems over **OIDC trusted
|
|
1098
1196
|
publishing** (no API token stored anywhere).
|
|
@@ -9,12 +9,16 @@ module DocsUI
|
|
|
9
9
|
# stylesheet asset is required.
|
|
10
10
|
#
|
|
11
11
|
# render DocsUI::Code.new(ruby_source) # ruby, no title
|
|
12
|
-
# render DocsUI::Code.new(
|
|
12
|
+
# render DocsUI::Code.new(yaml, filename: "config/deploy.yml") # yaml, inferred
|
|
13
|
+
# render DocsUI::Code.new(py, lexer: :python, filename: "a.py") # explicit wins
|
|
13
14
|
#
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
#
|
|
15
|
+
# The language resolves in order: an explicit `lexer:`; a guess from the
|
|
16
|
+
# `filename:` (Rouge's own filename globs — *.yml → yaml, Dockerfile → docker,
|
|
17
|
+
# *.sh → shell, …); else ruby. Any language Rouge knows (~200 lexers) works by
|
|
18
|
+
# its name or alias — python, go, rust, elixir, kotlin, swift, json,
|
|
19
|
+
# dockerfile, ... — no allowlist. Add friendly lexer aliases via
|
|
20
|
+
# DocsKit.configure (code_lexer_aliases). An unknown language falls back to
|
|
21
|
+
# plaintext (never raises). (Tab labels are a
|
|
18
22
|
# DocsUI::Example concern — set via code_language_labels, not here; Code has no
|
|
19
23
|
# label, only a filename.)
|
|
20
24
|
class Code < Phlex::HTML
|
|
@@ -22,7 +26,7 @@ module DocsUI
|
|
|
22
26
|
|
|
23
27
|
FORMATTER = Rouge::Formatters::HTML.new
|
|
24
28
|
|
|
25
|
-
def initialize(source, lexer:
|
|
29
|
+
def initialize(source, lexer: nil, filename: nil)
|
|
26
30
|
@source = source.to_s.strip
|
|
27
31
|
@lexer = lexer
|
|
28
32
|
@filename = filename
|
|
@@ -66,11 +70,26 @@ module DocsUI
|
|
|
66
70
|
end
|
|
67
71
|
end
|
|
68
72
|
|
|
69
|
-
# Resolve
|
|
70
|
-
# class/instance passed through; a configured friendly
|
|
71
|
-
# registry
|
|
73
|
+
# Resolve the lexer to a Rouge lexer instance. Order: an explicit Rouge::Lexer
|
|
74
|
+
# class/instance passed through; an explicit name via a configured friendly
|
|
75
|
+
# alias → Rouge's own registry → the configured fallback (plaintext); with no
|
|
76
|
+
# `lexer:` given, a guess from the filename; else the ruby default.
|
|
72
77
|
def lexer
|
|
73
|
-
explicit_lexer ||
|
|
78
|
+
explicit_lexer ||
|
|
79
|
+
(@lexer && (find_lexer(@lexer.to_s) || Rouge::Lexers::PlainText).new) ||
|
|
80
|
+
guessed_lexer ||
|
|
81
|
+
Rouge::Lexers::Ruby.new
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# A lexer inferred from @filename via Rouge's declared filename globs, or nil
|
|
85
|
+
# when there is no filename, nothing matches, or the match is ambiguous.
|
|
86
|
+
# (Lexer.guesses, not Lexer.guess: the latter answers PlainText for a
|
|
87
|
+
# no-match, which is indistinguishable from a real guess.)
|
|
88
|
+
def guessed_lexer
|
|
89
|
+
return nil if @filename.nil?
|
|
90
|
+
|
|
91
|
+
guesses = Rouge::Lexer.guesses(filename: @filename.to_s)
|
|
92
|
+
guesses.size == 1 ? guesses.first.new : nil
|
|
74
93
|
end
|
|
75
94
|
|
|
76
95
|
# A Rouge::Lexer instance passed directly (class or instance), else nil.
|
|
@@ -33,7 +33,7 @@ module DocsUI
|
|
|
33
33
|
include Phlex::Rails::Helpers::Request
|
|
34
34
|
|
|
35
35
|
def view_template
|
|
36
|
-
render DocsUI::Shell.new(title: landing.eyebrow || config.
|
|
36
|
+
render DocsUI::Shell.new(title: landing.eyebrow || config.brand_text) do
|
|
37
37
|
div(class: "mx-auto max-w-5xl") do
|
|
38
38
|
hero
|
|
39
39
|
feature_grid
|
|
@@ -78,7 +78,7 @@ module DocsUI
|
|
|
78
78
|
# (the one bit of markdown we honor, so a site can accent a word without HTML).
|
|
79
79
|
def heading
|
|
80
80
|
h1(class: "text-4xl font-bold tracking-tight md:text-5xl") do
|
|
81
|
-
(landing.title || config.
|
|
81
|
+
(landing.title || config.brand_text).to_s.split(/\*\*(.+?)\*\*/).each_with_index do |part, index|
|
|
82
82
|
next if part.empty?
|
|
83
83
|
|
|
84
84
|
index.odd? ? span(class: "text-primary") { part } : plain(part)
|
|
@@ -34,6 +34,9 @@ module DocsUI
|
|
|
34
34
|
TABLE_WRAPPER = "not-prose my-4 overflow-x-auto rounded-box border border-base-300"
|
|
35
35
|
TABLE_CLASSES = "table table-sm table-zebra"
|
|
36
36
|
|
|
37
|
+
#  → <img>, asset-pipeline resolved (see MarkdownImages).
|
|
38
|
+
include MarkdownImages
|
|
39
|
+
|
|
37
40
|
# Render source as INLINE markdown: no Prose wrapper div, and a single
|
|
38
41
|
# top-level paragraph is unwrapped so its inline children (strong/em/code/
|
|
39
42
|
# link) sit directly in the surrounding element. Used for a [:md, "…"] table
|
|
@@ -88,9 +91,7 @@ module DocsUI
|
|
|
88
91
|
|
|
89
92
|
def node_document(node) = visit_children(node)
|
|
90
93
|
|
|
91
|
-
def node_paragraph(node)
|
|
92
|
-
p { visit_children(node) }
|
|
93
|
-
end
|
|
94
|
+
def node_paragraph(node) = p { visit_children(node) }
|
|
94
95
|
|
|
95
96
|
# Demote so Markdown headings never collide with the page masthead/section
|
|
96
97
|
# headings: h1→h3, h2→h4, anything deeper caps at h4.
|
|
@@ -127,8 +128,11 @@ module DocsUI
|
|
|
127
128
|
render DocsUI::Code.new(node.string_content, lexer:)
|
|
128
129
|
end
|
|
129
130
|
|
|
131
|
+
# A link keeps its href only for an allowed scheme (MarkdownImages::
|
|
132
|
+
# LINK_SCHEMES) or none at all; otherwise the text renders bare.
|
|
130
133
|
def node_link(node)
|
|
131
|
-
|
|
134
|
+
href = link_href(node.url)
|
|
135
|
+
href ? a(href:) { visit_children(node) } : visit_children(node)
|
|
132
136
|
end
|
|
133
137
|
|
|
134
138
|
def node_list(node)
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DocsUI
|
|
4
|
+
# DocsUI::Markdown's image handler: `` → a lazy-loaded
|
|
5
|
+
# <img>. A relative src resolves through the site's asset pipeline
|
|
6
|
+
# (image_path → the digested /assets URL, Propshaft or Sprockets); an absolute,
|
|
7
|
+
# protocol-relative or root-relative URL is kept as written. Off a request (an
|
|
8
|
+
# isolated render) the src is used verbatim — the DocsUI::Logo posture.
|
|
9
|
+
module MarkdownImages
|
|
10
|
+
include Phlex::Rails::Helpers::ImagePath
|
|
11
|
+
|
|
12
|
+
# A Markdown image sits in the prose like the kit's other bordered boxes.
|
|
13
|
+
IMAGE_CLASSES = "my-4 rounded-box border border-base-300"
|
|
14
|
+
|
|
15
|
+
# An image src kept verbatim: absolute, protocol-relative or root-relative.
|
|
16
|
+
ABSOLUTE_SRC = %r{\A(?:https?:)?/}i
|
|
17
|
+
|
|
18
|
+
# Any URL scheme. An image src may only carry http(s); a link href one of
|
|
19
|
+
# LINK_SCHEMES. Scheme-less URLs (relative, root-relative, protocol-relative,
|
|
20
|
+
# #anchor) always pass; javascript:, data:, vbscript: and the rest never do.
|
|
21
|
+
URL_SCHEME = /\A[a-z][a-z0-9+.-]*:/i
|
|
22
|
+
LINK_SCHEMES = %w[http https mailto tel].freeze
|
|
23
|
+
|
|
24
|
+
# The descendants whose string_content makes up the alt text; a soft or hard
|
|
25
|
+
# line break between them becomes a space so words don't run together.
|
|
26
|
+
ALT_TEXT_NODES = %i[text code].freeze
|
|
27
|
+
ALT_BREAK_NODES = %i[softbreak linebreak].freeze
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
# An unsafe scheme renders nothing — the alt text is dropped with it rather
|
|
32
|
+
# than left behind as stray prose.
|
|
33
|
+
def node_image(node)
|
|
34
|
+
src = image_src(node.url)
|
|
35
|
+
return unless src
|
|
36
|
+
|
|
37
|
+
title = node.title.to_s
|
|
38
|
+
img(src:, alt: alt_text(node), title: (title unless title.empty?),
|
|
39
|
+
loading: "lazy", class: IMAGE_CLASSES)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Browsers strip ASCII whitespace/control characters before reading a
|
|
43
|
+
# scheme ("java\tscript:"), so the check does too.
|
|
44
|
+
def image_src(url)
|
|
45
|
+
bare = bare_url(url)
|
|
46
|
+
return url if bare.match?(ABSOLUTE_SRC)
|
|
47
|
+
return if bare.match?(URL_SCHEME)
|
|
48
|
+
|
|
49
|
+
view_context ? image_path(url) : url
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# A link's href as written, or nil when it carries a scheme outside
|
|
53
|
+
# LINK_SCHEMES (the same whitespace-stripped scheme read as image_src).
|
|
54
|
+
def link_href(url)
|
|
55
|
+
scheme = bare_url(url)[URL_SCHEME]
|
|
56
|
+
return url if scheme.nil? || LINK_SCHEMES.include?(scheme.chomp(":").downcase)
|
|
57
|
+
|
|
58
|
+
nil
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def bare_url(url) = url.to_s.gsub(/[\x00-\x20]/, "")
|
|
62
|
+
|
|
63
|
+
# Commonmarker 2 has no plain-text export, so the alt is the image's text
|
|
64
|
+
# (and inline code) descendants joined — emphasis markup is dropped.
|
|
65
|
+
def alt_text(node)
|
|
66
|
+
node.walk.filter_map do |n|
|
|
67
|
+
if ALT_TEXT_NODES.include?(n.type) then n.string_content
|
|
68
|
+
elsif ALT_BREAK_NODES.include?(n.type) then " "
|
|
69
|
+
end
|
|
70
|
+
end.join
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
@@ -52,7 +52,7 @@ module DocsUI
|
|
|
52
52
|
# The full title used for og:title, identical to what Shell puts in <title>:
|
|
53
53
|
# "Page · Suffix", or just the suffix on a title-less page (the home page).
|
|
54
54
|
def full_title
|
|
55
|
-
[@title, config.
|
|
55
|
+
[@title, config.title_suffix_text].compact.join(" · ")
|
|
56
56
|
end
|
|
57
57
|
|
|
58
58
|
def description_meta
|
|
@@ -67,7 +67,7 @@ module DocsUI
|
|
|
67
67
|
def open_graph
|
|
68
68
|
meta(property: "og:title", content: full_title)
|
|
69
69
|
meta(property: "og:type", content: seo.og_type)
|
|
70
|
-
meta(property: "og:site_name", content: config.
|
|
70
|
+
meta(property: "og:site_name", content: config.brand_text)
|
|
71
71
|
open_graph_optional
|
|
72
72
|
end
|
|
73
73
|
|
|
@@ -14,6 +14,16 @@ module DocsUI
|
|
|
14
14
|
# render DocsUI::Section.new("Add the gem") { … }
|
|
15
15
|
# end
|
|
16
16
|
# end
|
|
17
|
+
#
|
|
18
|
+
# A page class that renders many records overrides the INSTANCE readers
|
|
19
|
+
# (#title, #description, #eyebrow) instead — they default to the class macros:
|
|
20
|
+
#
|
|
21
|
+
# class Views::Docs::Widget < DocsUI::Page
|
|
22
|
+
# eyebrow "Widgets"
|
|
23
|
+
# def initialize(widget:) = @widget = widget
|
|
24
|
+
# def title = @widget.name
|
|
25
|
+
# def content = md(@widget.notes)
|
|
26
|
+
# end
|
|
17
27
|
class Page < Phlex::HTML
|
|
18
28
|
include Phlex::Rails::Helpers::Routes
|
|
19
29
|
# #request drives the "Markdown" masthead action's href (request.path + .md).
|
|
@@ -31,35 +41,42 @@ module DocsUI
|
|
|
31
41
|
@title
|
|
32
42
|
end
|
|
33
43
|
|
|
44
|
+
# Inherited: a subclass that sets no eyebrow shows its parent page's.
|
|
34
45
|
def eyebrow(value = nil)
|
|
35
46
|
@eyebrow = value if value
|
|
36
|
-
@eyebrow
|
|
47
|
+
@eyebrow || (superclass.eyebrow if parent_page?)
|
|
37
48
|
end
|
|
38
49
|
|
|
39
50
|
# The per-page SEO/social description → DocsUI::MetaTags (via Shell). Set it
|
|
40
51
|
# for a hand-tuned description; when unset, Page derives one from #lead, so
|
|
41
52
|
# an existing page gets a sensible description for free. nil (no
|
|
42
53
|
# description, no lead) falls back to config.seo.description in MetaTags.
|
|
54
|
+
# Inherited from a parent page when unset.
|
|
43
55
|
def description(value = nil)
|
|
44
56
|
@description = value if value
|
|
45
|
-
@description
|
|
57
|
+
@description || (superclass.description if parent_page?)
|
|
46
58
|
end
|
|
47
59
|
|
|
48
60
|
# The "On this page" auto-TOC placement for this page. Defaults to the
|
|
49
61
|
# configured DocsKit.configuration.on_page_default; set false to opt out,
|
|
50
|
-
# or :panel/:toggle/:sidebar to override per page.
|
|
62
|
+
# or :panel/:toggle/:sidebar to override per page. Inherited from a parent
|
|
63
|
+
# page when unset.
|
|
51
64
|
def on_page(value = :__unset__)
|
|
52
65
|
@on_page = value unless value == :__unset__
|
|
53
|
-
defined?(@on_page)
|
|
66
|
+
return @on_page if defined?(@on_page)
|
|
67
|
+
|
|
68
|
+
parent_page? ? superclass.on_page : DocsKit.configuration.on_page_default
|
|
54
69
|
end
|
|
70
|
+
|
|
71
|
+
private
|
|
72
|
+
|
|
73
|
+
# Whether the superclass is a page too (true below DocsUI::Page itself), so
|
|
74
|
+
# an unset macro falls back to it.
|
|
75
|
+
def parent_page? = superclass <= DocsUI::Page
|
|
55
76
|
end
|
|
56
77
|
|
|
57
78
|
def view_template
|
|
58
|
-
render DocsUI::Shell.new(
|
|
59
|
-
title: self.class.title,
|
|
60
|
-
description: self.class.description || lead,
|
|
61
|
-
on_page: self.class.on_page
|
|
62
|
-
) do
|
|
79
|
+
render DocsUI::Shell.new(title:, description:, on_page: self.class.on_page) do
|
|
63
80
|
# data-md-skip drops this nav from the Markdown export — it's chrome, not
|
|
64
81
|
# page content (DocsKit::MarkdownExport strips [data-md-skip]). The
|
|
65
82
|
# "Markdown" action sits opposite "← Home"; it's chrome too, so it lives
|
|
@@ -69,7 +86,7 @@ module DocsUI
|
|
|
69
86
|
render DocsUI::MarkdownAction.new(request.path) if markdown_action?
|
|
70
87
|
end
|
|
71
88
|
|
|
72
|
-
render DocsUI::Header.new(
|
|
89
|
+
render DocsUI::Header.new(title, eyebrow:) do
|
|
73
90
|
plain lead if lead
|
|
74
91
|
end
|
|
75
92
|
|
|
@@ -78,9 +95,19 @@ module DocsUI
|
|
|
78
95
|
end
|
|
79
96
|
|
|
80
97
|
# Whether to show the "Markdown" masthead action — the config knob
|
|
81
|
-
# (DocsKit.configuration.page_markdown_action, default true).
|
|
98
|
+
# (DocsKit.configuration.page_markdown_action, default true). The action links
|
|
99
|
+
# request.path + ".md"; override to false on a page whose route has no .md twin.
|
|
82
100
|
def markdown_action? = DocsKit.configuration.page_markdown_action
|
|
83
101
|
|
|
102
|
+
# The page title (masthead <h1> and <title>), eyebrow and SEO description.
|
|
103
|
+
# Each defaults to its class macro; override per instance when one page class
|
|
104
|
+
# renders many records. #title shadows Phlex's <title> element method, on
|
|
105
|
+
# pages only — the document <title> is DocsUI::Shell's, never a page body's.
|
|
106
|
+
def title = self.class.title
|
|
107
|
+
def eyebrow = self.class.eyebrow
|
|
108
|
+
# Falls back to #lead, then (in DocsUI::MetaTags) to config.seo.description.
|
|
109
|
+
def description = self.class.description || lead
|
|
110
|
+
|
|
84
111
|
# The lowercase authoring helpers md/prose/example come from DocsUI::PageHelpers
|
|
85
112
|
# (included above) — the parens-free path that never hits the constant-reference
|
|
86
113
|
# SyntaxError. The kit forms (DocsUI::Prose(), DocsUI::Example()) stay valid too.
|