Class: DocsKit::Configuration

Inherits:
Object
  • Object
show all
Defined in:
lib/docs_kit/configuration.rb

Overview

Per-site configuration for the shared docs chrome. Everything that differs between two otherwise-identical docs sites lives here, so the Phlex shell (DocsUI::Shell, DocsUI::Sidebar, DocsUI::ThemeSwitcher) is byte-identical across sites and only the config changes.

DocsKit.configure do |c|
c.brand        = "phlex-reactive"
c.title_suffix = "phlex-reactive"
c.themes       = %w[dark light synthwave ...]
c.nav          = -> { { "Demos" => Demo.grouped, "Docs" => Doc.grouped } }
end

Constant Summary collapse

DEFAULT_NAV =

The sentinel "no explicit nav" lambda. #nav_groups compares against this identity to decide whether to derive the sidebar from #nav_registries.

-> { {} }
DEFAULT_SEARCH_SHORTCUTS =

The search-palette shortcuts before this was configurable — "/" and the platform command chord — so a site that never sets #search_shortcuts keeps exactly the previous behavior.

["/", "mod+k"].freeze
DEFAULT_DARK_THEMES =

The built-in daisyUI theme names that are dark. #dark_themes defaults to this; #dark_themes_shipped intersects it with the site's #themes so only shipped themes ever generate dark code CSS. A site with custom dark themes overrides #dark_themes (docs-kit can't see the compiled daisyUI CSS to detect darkness at render time, so an honest static list + override wins).

%w[
  dark synthwave halloween forest black luxury dracula
  business night coffee dim sunset abyss
].freeze
DEFAULT_LEXER_ALIASES =

Built-in friendly aliases (kept small — Rouge resolves most names itself).

{ curl: "console", console: "console" }.freeze
DEFAULT_LANGUAGE_LABELS =

Built-in tab labels for the common languages that don't just capitalize.

{
  javascript: "JavaScript", typescript: "TypeScript", php: "PHP",
  curl: "cURL", json: "JSON", yaml: "YAML", html: "HTML", css: "CSS",
  erb: "ERB", jsx: "JSX", tsx: "TSX", sql: "SQL", graphql: "GraphQL"
}.freeze
ON_PAGE_MODES =

The auto-TOC placements, all driven by the same docs-nav Stimulus controller (it reads the page's headings from the DOM):

:panel   — a sticky card in the top-right of the content column
:toggle  — a sticky floating button (top-right) opening a dropdown
:sidebar — nested under the active nav item in the left sidebar
%i[panel toggle sidebar].freeze
DEFAULT_CODE_THEME =

The default Rouge theme both #code_theme_class and #code_theme_dark_class fall back to when a configured theme name can't be resolved — so a typo'd theme name degrades gracefully instead of raising on every code block.

"Rouge::Themes::Monokai"

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initializeConfiguration

Returns a new instance of Configuration.



232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
# File 'lib/docs_kit/configuration.rb', line 232

def initialize
  @brand = "Docs"
  @tagline = nil
  @brand_href = "/"
  @title_suffix = nil
  @themes = %w[dark light]
  @default_theme = nil
  # The default nav lambda. Until a site assigns #nav (which sets
  # @nav_explicit), #nav_groups treats nav as "unset" and derives the sidebar
  # from #nav_registries instead; an explicit c.nav (any lambda) then wins.
  @nav = DEFAULT_NAV
  @nav_explicit = false
  @nav_registries = {}
  @version_badge = nil
  @stylesheets = %w[application]
  @code_theme = "Rouge::Themes::Monokai"
  @code_theme_dark = nil
  @dark_themes = DEFAULT_DARK_THEMES
  @default_group_icon = "file-text"
  @icon_library = "lucide"
  @nav_storage_key = nil
  @on_page_default = :panel
  @code_lexer_aliases = {}
  @code_lexer_fallback = "plaintext"
  @code_language_labels = {}
  @page_markdown_action = true
  @mcp = true
  @search = true
  @search_path = "/docs/search"
  @search_shortcuts = DEFAULT_SEARCH_SHORTCUTS
  @api_base_url = "https://api.example.com"
  @api_auth_header = nil
  @api_clients = {}
  @topbar_links = []
  @openapi = nil
end

Instance Attribute Details

#api_auth_headerObject

An example Authorization header line merged into every generated request snippet (e.g. "Authorization: Bearer sk_live_..."). Defaults to nil → no auth line, so a site with no auth example renders clean snippets.



178
179
180
# File 'lib/docs_kit/configuration.rb', line 178

def api_auth_header
  @api_auth_header
end

#api_base_urlObject

The API base URL prefixed onto a DocsUI::RequestExample path so copy-pasted snippets point at a real host. Defaults to a neutral example host; a site sets its own (e.g. "https://api.acme.com").



173
174
175
# File 'lib/docs_kit/configuration.rb', line 173

def api_base_url
  @api_base_url
end

#api_clientsObject

The effective client map for DocsUI::RequestExample: the four shipped defaults with site overrides/extensions merged over them. Hash#merge keeps a reused token in its original position and appends new tokens in declaration order, so tab order is stable and predictable.



313
314
315
# File 'lib/docs_kit/configuration.rb', line 313

def api_clients
  DocsKit::ApiClient::DEFAULTS.merge(@api_clients || {})
end

#brandObject

The brand text shown in the topbar and sidebar header.



20
21
22
# File 'lib/docs_kit/configuration.rb', line 20

def brand
  @brand
end

#brand_hrefObject

The href the topbar brand link points at. Defaults to "/" (site root). A site whose docs live under a subpath sets its own (e.g. "/docs") so the brand link is a one-line config change, not a Shell subclass.



31
32
33
# File 'lib/docs_kit/configuration.rb', line 31

def brand_href
  @brand_href
end

#code_language_labelsObject

Human labels for language tabs in DocsUI::Example, merged over the built-ins (e.g. { elixir: "Elixir", curl: "cURL" }). Unknown tokens humanize.



134
135
136
# File 'lib/docs_kit/configuration.rb', line 134

def code_language_labels
  @code_language_labels
end

#code_lexer_aliasesObject

Friendly-name → Rouge lexer aliases for code blocks, merged over the built-in defaults. Any language Rouge knows (~200) already works by its own name/alias; use this only to add or override (e.g. { curl: "console", dockerfile: "docker" }). Value is anything Rouge::Lexer.find accepts.



126
127
128
# File 'lib/docs_kit/configuration.rb', line 126

def code_lexer_aliases
  @code_lexer_aliases
end

#code_lexer_fallbackObject

The lexer used when a requested language can't be resolved. Default "plaintext" (no highlighting, never raises).



130
131
132
# File 'lib/docs_kit/configuration.rb', line 130

def code_lexer_fallback
  @code_lexer_fallback
end

#code_themeObject

The Rouge theme class used by DocsUI::Code for inline syntax-highlight CSS. This is the BASE (light) theme, emitted un-scoped so it applies to every theme unless a dark override wins (see #code_theme_dark).



84
85
86
# File 'lib/docs_kit/configuration.rb', line 84

def code_theme
  @code_theme
end

#code_theme_darkObject

An optional second Rouge theme (String name or Class) used for the site's DARK daisyUI themes. Default nil → single-theme behavior, fully backwards compatible. When set, DocsUI::Code additionally emits this theme's CSS scoped under [data-theme=X] .code-highlight for each shipped dark theme (see #dark_themes), so code blocks stay readable when the switcher flips to a dark theme — CSS-only, no JS, no flash.



92
93
94
# File 'lib/docs_kit/configuration.rb', line 92

def code_theme_dark
  @code_theme_dark
end

#dark_themesObject

The theme names treated as DARK for #code_theme_dark scoping. Defaults to the built-in daisyUI dark themes (DEFAULT_DARK_THEMES). Intersected with #themes at render time (see #dark_themes_shipped) so only shipped themes generate CSS. Override to name custom dark themes (e.g. %w).



98
99
100
# File 'lib/docs_kit/configuration.rb', line 98

def dark_themes
  @dark_themes
end

#default_group_iconObject

The lucide icon name used for a nav group with no explicit icon.



101
102
103
# File 'lib/docs_kit/configuration.rb', line 101

def default_group_icon
  @default_group_icon
end

#default_themeObject



434
435
436
# File 'lib/docs_kit/configuration.rb', line 434

def default_theme
  @default_theme || Array(@themes).first
end

#icon_libraryObject

The RailsIcons library docs-kit renders its OWN chrome icons from (the sidebar carets, search glyph, theme toggle, etc.). Defaults to "lucide" to match the lucide icon names docs-kit ships. This is independent of the host app's RailsIcons.configuration.default_library — a host whose default is phosphor/heroicons can leave this at "lucide" so the chrome keeps rendering, without flipping its global default. Set to nil to defer to the host's default_library.



110
111
112
# File 'lib/docs_kit/configuration.rb', line 110

def icon_library
  @icon_library
end

#mcpObject

Whether the built-in read-only MCP endpoint (DocsKit::McpController, a POST /mcp JSON-RPC server exposing list_pages / get_page / search_docs over the same registry the docs render from) is active. Defaults to true, but the endpoint only turns on when the optional mcp gem is ALSO present and the host draws the route — #mcp_enabled? gates on both. Set false to keep the endpoint off even on a site that bundles the gem. See DocsKit::McpServer.



148
149
150
# File 'lib/docs_kit/configuration.rb', line 148

def mcp
  @mcp
end

A callable returning the sidebar nav as an ordered Hash of { "Heading" => { "Subgroup" => [items] } }. Each item must respond to the duck type the Sidebar renders (see DocsUI::Sidebar#nav_link): #href, #label, and optional #icon. Defaults to an empty nav.

Prefer #nav_registries for the common case — an explicit #nav lambda is only needed for bespoke nav (multiple registries interleaved, custom subgroups). When #nav is left at its default, the sidebar derives from #nav_registries instead.



55
56
57
# File 'lib/docs_kit/configuration.rb', line 55

def nav
  @nav
end

An ordered { "Heading" => registry_class } map. Each registry responds to .nav_items (Registry v2) → { group => [NavItem] } for its authored pages. #nav_groups derives the whole sidebar from this with zero site code, so a site never hand-writes the nav lambda. Defaults to {}. An explicit #nav lambda still wins (full backwards compatibility).



70
71
72
# File 'lib/docs_kit/configuration.rb', line 70

def nav_registries
  @nav_registries
end


403
404
405
# File 'lib/docs_kit/configuration.rb', line 403

def nav_storage_key
  @nav_storage_key || @brand.to_s.downcase.gsub(/[^a-z0-9]+/, "-").gsub(/\A-+|-+\z/, "")
end

#on_page_defaultObject

The resolved default placement (a mode symbol or false). A bare true default means :panel (the canonical default), never a self-reference.



336
337
338
339
340
# File 'lib/docs_kit/configuration.rb', line 336

def on_page_default
  raw = @on_page_default
  raw = :panel if raw == true
  coerce_on_page_mode(raw)
end

#openapiObject

The OpenAPI spec the operation page helper / DocsUI::OpenApiOperation read from: a String/Pathname path (.json parsed as JSON, else YAML) or an already-parsed Hash. Defaults to nil → the bridge is off and a site that doesn't set it is byte-identical to before. Read the loaded model via #openapi_document (which memoizes + reloads on file change), never @openapi.



201
202
203
# File 'lib/docs_kit/configuration.rb', line 201

def openapi
  @openapi
end

#page_markdown_actionObject

Whether DocsUI::Page shows the "Markdown" masthead action — a link to the page's .md twin that docs-nav enhances into copy-to-clipboard. Defaults to true; set false to hide the affordance site-wide (the .md route still works). See DocsKit::MarkdownExport / DocsKit::Controller#render_page.



140
141
142
# File 'lib/docs_kit/configuration.rb', line 140

def page_markdown_action
  @page_markdown_action
end

#searchObject

Whether the topbar renders the docs-search form (and the docs-nav palette markup). Defaults to true. Set false to hide search site-wide — the route can stay drawn, but no affordance points at it. Gated together with a present #search_path by #search_enabled?, which the Shell reads.



154
155
156
# File 'lib/docs_kit/configuration.rb', line 154

def search
  @search
end

#search_pathObject

The path the topbar search form submits to (GET ?q=), and the base the palette fetches .json from. Defaults to "/docs/search" — the route the install generator draws. A site that mounts search elsewhere sets its own; blank it to disable the affordance without touching #search.



160
161
162
# File 'lib/docs_kit/configuration.rb', line 160

def search_path
  @search_path
end

#search_shortcutsObject

The parsed search-palette shortcuts (DocsKit::Shortcut list), with anything unparseable dropped. The topbar renders one per entry and docs-nav binds each; an empty list means no keyboard shortcut (the form still works).



430
431
432
# File 'lib/docs_kit/configuration.rb', line 430

def search_shortcuts
  DocsKit::Shortcut.parse_list(@search_shortcuts)
end

#stylesheetsObject

The stylesheet logical names linked in , in order. Defaults to ["application"] (the Bun/Tailwind-compiled build). A site that ships extra stylesheets (e.g. a separate rouge theme) lists them here.



79
80
81
# File 'lib/docs_kit/configuration.rb', line 79

def stylesheets
  @stylesheets
end

#taglineObject

A one-line site summary, rendered as the llms.txt blockquote (> {tagline}) under the H1. Defaults to nil → the blockquote line is omitted, so a site that never sets it still gets a valid llms.txt. Purely for the AI-readable index (DocsKit::LlmsText); the chrome never shows it.



26
27
28
# File 'lib/docs_kit/configuration.rb', line 26

def tagline
  @tagline
end

#themesObject

The themes offered by the ThemeSwitcher. The first is the page default unless #default_theme is set. Must match the themes enabled in the site's Tailwind @plugin "daisyui" { themes: ... } block.



40
41
42
# File 'lib/docs_kit/configuration.rb', line 40

def themes
  @themes
end

#title_suffixObject



407
408
409
# File 'lib/docs_kit/configuration.rb', line 407

def title_suffix
  @title_suffix || @brand
end

The normalized topbar links (DocsKit::TopbarLink list), in declaration order. Each configured Hash/TopbarLink is coerced via TopbarLink.from, so the Shell only ever sees value objects. Blank/nil config yields [].



272
273
274
# File 'lib/docs_kit/configuration.rb', line 272

def topbar_links
  Array(@topbar_links).map { |link| DocsKit::TopbarLink.from(link) }
end

#version_badgeObject

Optional callable returning a short version-badge string for the sidebar header (e.g. -> { "v#DaisyUI::VERSION" }). nil renders no badge.



74
75
76
# File 'lib/docs_kit/configuration.rb', line 74

def version_badge
  @version_badge
end

Instance Method Details

#code_theme_classObject

The Rouge theme class resolved from #code_theme (String or class). A String name that doesn't resolve degrades to the default theme rather than raising NameError on every DocsUI::Code render.



470
471
472
473
474
# File 'lib/docs_kit/configuration.rb', line 470

def code_theme_class
  return @code_theme if @code_theme.is_a?(Class)

  resolve_theme(@code_theme) || Object.const_get(DEFAULT_CODE_THEME)
end

#code_theme_dark_classObject

The dark Rouge theme class resolved from #code_theme_dark (String or class), or nil when unset — mirrors #code_theme_class. DocsUI::Code emits dark code CSS only when this is non-nil, so an unresolvable name degrades to nil (no dark restyle) rather than raising.



480
481
482
483
484
485
# File 'lib/docs_kit/configuration.rb', line 480

def code_theme_dark_class
  return if @code_theme_dark.nil?
  return @code_theme_dark if @code_theme_dark.is_a?(Class)

  resolve_theme(@code_theme_dark)
end

#dark_themes_shippedObject

The dark themes the site actually ships: #dark_themes intersected with #themes, in #themes declaration order. DocsUI::Code scopes the dark theme's CSS under [data-theme=X] for each of these, so a dark theme that isn't in the Tailwind build never emits dead CSS.



491
492
493
# File 'lib/docs_kit/configuration.rb', line 491

def dark_themes_shipped
  Array(@themes) & Array(@dark_themes)
end

#landingObject

The landing-page knobs (DocsKit::LandingConfig), read by DocsUI::Landing. Lazily built and memoized so a c.landing.title = ... block mutates the one instance the component later reads. A site that never touches it still gets a minimal hero + the doc index (see LandingConfig), so DocsUI::Landing is safe to render with zero landing config.



290
291
292
# File 'lib/docs_kit/configuration.rb', line 290

def landing
  @landing ||= DocsKit::LandingConfig.new
end

#language_labelsObject

The effective label map (built-ins + site overrides), symbol-keyed.



323
324
325
# File 'lib/docs_kit/configuration.rb', line 323

def language_labels
  DEFAULT_LANGUAGE_LABELS.merge((@code_language_labels || {}).transform_keys(&:to_sym))
end

#lexer_aliasesObject

The effective alias map (built-ins + site overrides), symbol-keyed.



318
319
320
# File 'lib/docs_kit/configuration.rb', line 318

def lexer_aliases
  DEFAULT_LEXER_ALIASES.merge((@code_lexer_aliases || {}).transform_keys(&:to_sym))
end

#mcp_enabled?Boolean

Whether the built-in MCP endpoint is active: the #mcp toggle is on AND the optional mcp gem is loadable — the same "toggle AND capability" shape as #search_enabled?. Read by DocsKit::LlmsText (to advertise /mcp in llms.txt) and DocsKit::McpController (to 404 when off), so a site without the gem, or one that set c.mcp = false, is byte-identical to before this feature.

Returns:

  • (Boolean)


416
417
418
# File 'lib/docs_kit/configuration.rb', line 416

def mcp_enabled?
  !!@mcp && mcp_gem_present?
end

The resolved nav Hash for this request. Always returns a Hash.

An explicit #nav lambda wins. Otherwise the sidebar derives from #nav_registries: each heading maps to its registry's .nav_items, and a heading whose pages are all unauthored (empty nav_items) is dropped so no empty group renders.



444
445
446
447
448
449
# File 'lib/docs_kit/configuration.rb', line 444

def nav_groups
  return nav_groups_from_registries unless @nav_explicit

  result = @nav.respond_to?(:call) ? @nav.call : @nav
  result || {}
end

#normalize_on_page(value) ⇒ Object

Coerce a per-page on_page: value to a valid mode symbol or false. true means "use the configured default".



344
345
346
347
348
# File 'lib/docs_kit/configuration.rb', line 344

def normalize_on_page(value)
  return on_page_default if value == true

  coerce_on_page_mode(value)
end

#openapi_documentObject

The loaded DocsKit::OpenApi::Document for #openapi. Memoized; when #openapi is a file path, the memo is invalidated on an mtime change so editing the spec in development is picked up without a server restart. Raises a DocsKit::Error naming the knob when read while #openapi is unset — a missing spec has nothing useful to degrade to.

Raises:



299
300
301
302
303
304
305
306
307
# File 'lib/docs_kit/configuration.rb', line 299

def openapi_document
  raise DocsKit::Error, "no OpenAPI spec configured — set c.openapi to a path or Hash" if @openapi.nil?

  mtime = openapi_source_mtime
  return @openapi_document if defined?(@openapi_document) && @openapi_document_mtime == mtime

  @openapi_document_mtime = mtime
  @openapi_document = DocsKit::OpenApi.load(@openapi)
end

#search_enabled?Boolean

Whether the Shell renders the search affordance: search is on AND a path is set to submit to. A site with @search_path blanked (or nil) gets no form even if @search is true — there'd be nothing to submit to.

Returns:

  • (Boolean)


423
424
425
# File 'lib/docs_kit/configuration.rb', line 423

def search_enabled?
  !!@search && !@search_path.to_s.empty?
end

#seoObject

The SEO / social-share knobs (DocsKit::SeoConfig), read by DocsUI::MetaTags. Lazily built and memoized so a c.seo.description = ... block mutates the one instance the Shell later reads. A site that never touches it gets the backwards-safe defaults (see SeoConfig), so the head is a strict superset of the pre-SEO markup.



281
282
283
# File 'lib/docs_kit/configuration.rb', line 281

def seo
  @seo ||= DocsKit::SeoConfig.new
end

#version_badge_textObject

The resolved version badge string, or nil. The rendered version badge. A callable is invoked; a plain String (or any non-nil value) is coerced to its string form — so c.version_badge = "v1.2" renders, not only a lambda.



455
456
457
458
459
460
# File 'lib/docs_kit/configuration.rb', line 455

def version_badge_text
  return if @version_badge.nil?
  return @version_badge.call if @version_badge.respond_to?(:call)

  @version_badge.to_s
end