docs-kit 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.
Files changed (89) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +46 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +939 -0
  5. data/app/components/docs_ui/brand_mark.rb +88 -0
  6. data/app/components/docs_ui/callout.rb +37 -0
  7. data/app/components/docs_ui/code.rb +123 -0
  8. data/app/components/docs_ui/endpoint.rb +44 -0
  9. data/app/components/docs_ui/error_table.rb +72 -0
  10. data/app/components/docs_ui/example.rb +102 -0
  11. data/app/components/docs_ui/field_table.rb +46 -0
  12. data/app/components/docs_ui/header.rb +30 -0
  13. data/app/components/docs_ui/icon.rb +65 -0
  14. data/app/components/docs_ui/json_response.rb +46 -0
  15. data/app/components/docs_ui/markdown.rb +187 -0
  16. data/app/components/docs_ui/markdown_action.rb +45 -0
  17. data/app/components/docs_ui/on_this_page.rb +104 -0
  18. data/app/components/docs_ui/open_api_operation.rb +126 -0
  19. data/app/components/docs_ui/page.rb +83 -0
  20. data/app/components/docs_ui/page_helpers.rb +52 -0
  21. data/app/components/docs_ui/prop_table.rb +43 -0
  22. data/app/components/docs_ui/prose.rb +30 -0
  23. data/app/components/docs_ui/request_example.rb +85 -0
  24. data/app/components/docs_ui/search_box.rb +106 -0
  25. data/app/components/docs_ui/search_results.rb +95 -0
  26. data/app/components/docs_ui/section.rb +94 -0
  27. data/app/components/docs_ui/shell.rb +161 -0
  28. data/app/components/docs_ui/sidebar.rb +106 -0
  29. data/app/components/docs_ui/table.rb +64 -0
  30. data/app/components/docs_ui/theme_switcher.rb +46 -0
  31. data/app/components/docs_ui/topbar_links.rb +42 -0
  32. data/app/controllers/docs_kit/llms_controller.rb +76 -0
  33. data/app/controllers/docs_kit/mcp_controller.rb +60 -0
  34. data/app/controllers/docs_kit/search_controller.rb +72 -0
  35. data/app/javascript/docs_kit/controllers/docs_nav_controller.js +619 -0
  36. data/config/importmap.rb +15 -0
  37. data/config/rubocop/docs_kit.yml +24 -0
  38. data/exe/docs-kit +80 -0
  39. data/lib/docs-kit.rb +5 -0
  40. data/lib/docs_kit/api_client.rb +52 -0
  41. data/lib/docs_kit/api_request.rb +66 -0
  42. data/lib/docs_kit/api_templates.rb +92 -0
  43. data/lib/docs_kit/configuration.rb +485 -0
  44. data/lib/docs_kit/controller.rb +47 -0
  45. data/lib/docs_kit/engine.rb +49 -0
  46. data/lib/docs_kit/llms_text.rb +105 -0
  47. data/lib/docs_kit/markdown_export/blocks.rb +160 -0
  48. data/lib/docs_kit/markdown_export/inline.rb +95 -0
  49. data/lib/docs_kit/markdown_export/table.rb +53 -0
  50. data/lib/docs_kit/markdown_export.rb +92 -0
  51. data/lib/docs_kit/mcp_server.rb +128 -0
  52. data/lib/docs_kit/mcp_tools.rb +118 -0
  53. data/lib/docs_kit/nav_item.rb +22 -0
  54. data/lib/docs_kit/open_api/document.rb +91 -0
  55. data/lib/docs_kit/open_api/operation.rb +213 -0
  56. data/lib/docs_kit/open_api/schema.rb +178 -0
  57. data/lib/docs_kit/open_api.rb +55 -0
  58. data/lib/docs_kit/registry.rb +152 -0
  59. data/lib/docs_kit/rubocop.rb +19 -0
  60. data/lib/docs_kit/search_hit.rb +28 -0
  61. data/lib/docs_kit/search_index/snippet.rb +65 -0
  62. data/lib/docs_kit/search_index.rb +169 -0
  63. data/lib/docs_kit/shortcut.rb +99 -0
  64. data/lib/docs_kit/templates/new_site.rb +175 -0
  65. data/lib/docs_kit/topbar_link.rb +39 -0
  66. data/lib/docs_kit/version.rb +5 -0
  67. data/lib/docs_kit.rb +72 -0
  68. data/lib/generators/docs_kit/install/USAGE +15 -0
  69. data/lib/generators/docs_kit/install/install_generator.rb +447 -0
  70. data/lib/generators/docs_kit/install/sync_report.rb +64 -0
  71. data/lib/generators/docs_kit/install/templates/agents_md.erb +105 -0
  72. data/lib/generators/docs_kit/install/templates/application.tailwind.css.erb +39 -0
  73. data/lib/generators/docs_kit/install/templates/build-css +34 -0
  74. data/lib/generators/docs_kit/install/templates/build_css.rake +13 -0
  75. data/lib/generators/docs_kit/install/templates/doc.rb.erb +17 -0
  76. data/lib/generators/docs_kit/install/templates/docs_controller.rb.erb +14 -0
  77. data/lib/generators/docs_kit/install/templates/docs_kit.rb.erb +91 -0
  78. data/lib/generators/docs_kit/install/templates/installation_page.rb.erb +37 -0
  79. data/lib/generators/docs_kit/install/templates/landing.rb.erb +25 -0
  80. data/lib/generators/docs_kit/install/templates/landings_controller.rb.erb +7 -0
  81. data/lib/generators/docs_kit/install/templates/phlex.rb.erb +14 -0
  82. data/lib/generators/docs_kit/install/templates/rails_icons.rb.erb +12 -0
  83. data/lib/generators/docs_kit/install/templates/skill.md.erb +88 -0
  84. data/lib/generators/docs_kit/page/USAGE +26 -0
  85. data/lib/generators/docs_kit/page/page_generator.rb +127 -0
  86. data/lib/generators/docs_kit/page/templates/page.rb.erb +21 -0
  87. data/lib/rubocop/cop/docs_kit/escaped_interpolation_in_heredoc.rb +119 -0
  88. data/lib/rubocop/cop/docs_kit/render_component_preferred.rb +123 -0
  89. metadata +253 -0
data/exe/docs-kit ADDED
@@ -0,0 +1,80 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # docs-kit CLI. One command scaffolds a complete, deployable docs site:
5
+ #
6
+ # docs-kit new my-docs
7
+ # docs-kit new my-docs --image mhenrixon/my-repo --service my-repo
8
+ #
9
+ # It runs `rails new` with the right minimal flags and applies docs-kit's
10
+ # application template (lib/docs_kit/templates/new_site.rb), which adds the gem,
11
+ # runs `docs_kit:install`, syncs icons, builds the CSS, and scaffolds Kamal +
12
+ # the reusable deploy workflow.
13
+
14
+ require "optparse"
15
+
16
+ TEMPLATE = File.expand_path("../lib/docs_kit/templates/new_site.rb", __dir__)
17
+
18
+ command = ARGV.shift
19
+
20
+ unless command == "new"
21
+ warn <<~USAGE
22
+ docs-kit — scaffold a docs site.
23
+
24
+ Usage:
25
+ docs-kit new NAME [--image OWNER/REPO] [--service NAME] [--gem-source SRC]
26
+
27
+ Options:
28
+ --image GHCR image (default mhenrixon/NAME) — use OWNER/REPO for the
29
+ repo-linked package so GITHUB_TOKEN can push+pull it.
30
+ --service Kamal service name (default NAME).
31
+ --gem-source How to depend on docs-kit: 'released' (default),
32
+ 'path:PATH', or 'github:OWNER/REPO'.
33
+ USAGE
34
+ exit 1
35
+ end
36
+
37
+ name = ARGV.shift
38
+ if name.nil? || name.start_with?("-")
39
+ warn "docs-kit new: NAME is required (e.g. docs-kit new my-docs)"
40
+ exit 1
41
+ end
42
+
43
+ opts = {}
44
+ OptionParser.new do |o|
45
+ o.on("--image IMAGE") { |v| opts[:image] = v }
46
+ o.on("--service SERVICE") { |v| opts[:service] = v }
47
+ o.on("--gem-source SRC") { |v| opts[:gem_source] = v }
48
+ end.parse!(ARGV)
49
+
50
+ # Map --gem-source to the Gemfile fragment the template injects.
51
+ gem_source =
52
+ case opts[:gem_source]
53
+ when nil, "released" then ""
54
+ when /\Apath:(.+)\z/ then %(path: "#{Regexp.last_match(1)}")
55
+ when /\Agithub:(.+)\z/ then %(github: "#{Regexp.last_match(1)}")
56
+ else opts[:gem_source]
57
+ end
58
+
59
+ env = {
60
+ "DOCS_KIT_GEM_SOURCE" => gem_source,
61
+ "DOCS_KIT_IMAGE" => opts[:image] || "mhenrixon/#{name}",
62
+ "DOCS_KIT_SERVICE" => opts[:service] || name
63
+ }
64
+
65
+ # A lean docs app: propshaft + importmap + turbo + stimulus (the shell uses
66
+ # javascript_importmap_tags and docs-kit's docs-nav Stimulus controller, so we do
67
+ # NOT pass --minimal — that strips JS entirely). Skip the DB/mailer/cable/test
68
+ # bits a docs site doesn't need.
69
+ rails_new = [
70
+ "rails", "new", name,
71
+ "-a", "propshaft", "-j", "importmap",
72
+ "--skip-active-record", "--skip-action-mailer", "--skip-action-cable",
73
+ "--skip-action-text", "--skip-active-job", "--skip-active-storage",
74
+ "--skip-test", "--skip-system-test", "--skip-jbuilder", "--skip-kamal",
75
+ "--skip-solid", "--skip-ci", "--skip-docker", "--skip-bootsnap",
76
+ "-m", TEMPLATE
77
+ ]
78
+
79
+ puts "→ #{rails_new.join(' ')}"
80
+ exit system(env, *rails_new) ? 0 : 1
data/lib/docs-kit.rb ADDED
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Bundler auto-requires the file matching the gem name ("docs-kit"); the real
4
+ # entrypoint is the underscored docs_kit.rb (DocsKit module + Docs Phlex kit).
5
+ require_relative "docs_kit"
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DocsKit
4
+ # One API-example language tab: a label, a Rouge lexer, a filename (a String or
5
+ # a `(request) -> String` proc), and a `template` — a `(DocsKit::ApiRequest) ->
6
+ # String` callable that renders the snippet. DocsUI::RequestExample turns each
7
+ # configured client into one DocsUI::Example tab.
8
+ #
9
+ # DocsKit::ApiClient.new(
10
+ # label: "cURL", lexer: :curl, filename: "request.sh",
11
+ # template: ->(req) { "curl -X #{req.http_method} '#{req.url}'" }
12
+ # )
13
+ #
14
+ # The gem ships four generic-HTTP defaults (DEFAULTS); a site overrides or
15
+ # extends them via DocsKit.configuration.api_clients (SDK-flavored snippets, a
16
+ # `cli` tab, ...).
17
+ ApiClient = Data.define(:label, :lexer, :filename, :template) do
18
+ def initialize(label:, lexer:, template:, filename: nil)
19
+ super
20
+ end
21
+
22
+ # The filename for this client's title bar: a static String, or the result of
23
+ # calling a proc filename with the request (so it can vary by verb/path).
24
+ def filename_for(request)
25
+ filename.respond_to?(:call) ? filename.call(request) : filename
26
+ end
27
+
28
+ # Render the snippet for this client from the request struct.
29
+ def render(request) = template.call(request)
30
+ end
31
+
32
+ # The four generic-HTTP clients every site starts from. Order is stable
33
+ # (curl → javascript → ruby → python) and preserved when a site merges its own.
34
+ ApiClient::DEFAULTS = {
35
+ curl: ApiClient.new(
36
+ label: "cURL", lexer: :curl, filename: "request.sh",
37
+ template: ApiTemplates.method(:curl)
38
+ ),
39
+ javascript: ApiClient.new(
40
+ label: "JavaScript", lexer: :javascript, filename: "request.js",
41
+ template: ApiTemplates.method(:javascript)
42
+ ),
43
+ ruby: ApiClient.new(
44
+ label: "Ruby", lexer: :ruby, filename: "request.rb",
45
+ template: ApiTemplates.method(:ruby)
46
+ ),
47
+ python: ApiClient.new(
48
+ label: "Python", lexer: :python, filename: "request.py",
49
+ template: ApiTemplates.method(:python)
50
+ )
51
+ }.freeze
52
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "uri"
5
+
6
+ module DocsKit
7
+ # A single API request, declared once and handed to every client template so a
8
+ # snippet is authored one place, not per language. DocsUI::RequestExample builds
9
+ # one from its args (merging DocsKit.configuration.api_base_url + the auth header)
10
+ # and each DocsKit::ApiClient#template receives it.
11
+ #
12
+ # req = DocsKit::ApiRequest.new(
13
+ # method: :post, path: "/v1/things",
14
+ # url: "https://api.example.com/v1/things",
15
+ # body: { name: "Acme" }
16
+ # )
17
+ # req.http_method # => "POST"
18
+ # req.pretty_body_json # => %({\n "name": "Acme"\n})
19
+ #
20
+ # Templates stay one short heredoc each: they read #http_method, #url,
21
+ # #url_with_query, #headers, #body?, and #pretty_body_json.
22
+ ApiRequest = Data.define(:method, :path, :url, :query, :headers, :body) do
23
+ def initialize(method:, path:, url:, query: {}, headers: {}, body: nil)
24
+ super
25
+ end
26
+
27
+ # The upcased HTTP verb for display in a snippet ("POST", "GET", ...).
28
+ def http_method = method.to_s.upcase
29
+
30
+ # Whether the request carries a payload body (drives whether a template emits
31
+ # its payload lines at all).
32
+ def body? = !body.nil?
33
+
34
+ # The body as pretty-printed JSON with string keys (deep-stringified), or nil
35
+ # when there is no body. A String body is passed through unchanged.
36
+ def pretty_body_json
37
+ return nil unless body?
38
+ return body if body.is_a?(String)
39
+
40
+ JSON.pretty_generate(deep_stringify(body))
41
+ end
42
+
43
+ # A URL-encoded "?a=1&b=2" query string, or "" when there is no query.
44
+ def query_string
45
+ return "" if query.nil? || query.empty?
46
+
47
+ "?#{URI.encode_www_form(query)}"
48
+ end
49
+
50
+ # The URL with the query string appended (the copy-pasteable request target).
51
+ def url_with_query = "#{url}#{query_string}"
52
+
53
+ private
54
+
55
+ # Recursively stringify Hash/Array keys and symbol values so JSON output reads
56
+ # like a real API response (no Ruby :symbol / => syntax leaking through).
57
+ def deep_stringify(value)
58
+ case value
59
+ when Hash then value.to_h { |k, v| [k.to_s, deep_stringify(v)] }
60
+ when Array then value.map { |v| deep_stringify(v) }
61
+ when Symbol then value.to_s
62
+ else value
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DocsKit
4
+ # The four generic-HTTP snippet templates the gem ships (curl, fetch, Net::HTTP,
5
+ # requests). Each is a `(DocsKit::ApiRequest) -> String` callable, referenced by
6
+ # DocsKit::ApiClient::DEFAULTS.
7
+ #
8
+ # The gem intentionally ships GENERIC HTTP snippets — it cannot know a site's
9
+ # SDK. A site swaps in SDK-flavored templates (or adds a `cli` client) by
10
+ # overriding c.api_clients; these are the fallback every site starts from.
11
+ #
12
+ # Every template guards its payload lines on request.body? so a body-less GET
13
+ # renders no dangling `-d`/`json=`/`request.body =` line.
14
+ module ApiTemplates
15
+ module_function
16
+
17
+ # curl -X METHOD 'url' [-H "Header: v"]... [-d '{json}']
18
+ def curl(request)
19
+ lines = ["curl -X #{request.http_method} '#{request.url_with_query}'"]
20
+ request.headers.each { |name, value| lines << %( -H "#{name}: #{value}") }
21
+ lines << %( -H "Content-Type: application/json") if request.body?
22
+ lines << " -d '#{request.pretty_body_json}'" if request.body?
23
+ lines.join(" \\\n")
24
+ end
25
+
26
+ # A fetch() call with a headers object and an optional JSON body.
27
+ def javascript(request)
28
+ headers = { "Content-Type" => "application/json" }.merge(request.headers)
29
+ header_lines = headers.map { |k, v| %( "#{k}": "#{v}") }.join(",\n")
30
+ body_line = request.body? ? %(\n body: JSON.stringify(#{compact_json(request)}),) : ""
31
+
32
+ <<~JS.strip
33
+ const response = await fetch("#{request.url_with_query}", {
34
+ method: "#{request.http_method}",
35
+ headers: {
36
+ #{header_lines}
37
+ },#{body_line}
38
+ });
39
+ const data = await response.json();
40
+ JS
41
+ end
42
+
43
+ # A Net::HTTP snippet: build the request, set headers, optional JSON body, send.
44
+ def ruby(request)
45
+ header_lines = request.headers.map { |k, v| %(request["#{k}"] = "#{v}") }
46
+ body_line = request.body? ? %(request.body = #{request.pretty_body_json}.to_json) : nil
47
+
48
+ <<~RUBY.strip
49
+ require "net/http"
50
+ require "json"
51
+
52
+ uri = URI("#{request.url_with_query}")
53
+ request = Net::HTTP::#{request.http_method.capitalize}.new(uri)
54
+ request["Content-Type"] = "application/json"
55
+ #{[*header_lines, body_line].compact.join("\n")}
56
+
57
+ response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == "https") do |http|
58
+ http.request(request)
59
+ end
60
+ RUBY
61
+ end
62
+
63
+ # A requests.<verb>(url, headers=..., json=...) call. Pretty JSON with
64
+ # double-quoted keys is valid Python dict syntax, so it drops straight into json=.
65
+ def python(request)
66
+ headers = { "Content-Type" => "application/json" }.merge(request.headers)
67
+ header_repr = headers.map { |k, v| %("#{k}": "#{v}") }.join(", ")
68
+ json_arg = request.body? ? ", json=#{request.pretty_body_json}" : ""
69
+
70
+ <<~PY.strip
71
+ import requests
72
+
73
+ response = requests.#{request.method.to_s.downcase}(
74
+ "#{request.url_with_query}",
75
+ headers={#{header_repr}}#{json_arg},
76
+ )
77
+ data = response.json()
78
+ PY
79
+ end
80
+
81
+ # --- helpers ------------------------------------------------------------
82
+
83
+ # The body as compact single-line JSON, for inlining in a JS literal.
84
+ def compact_json(request)
85
+ require "json"
86
+ json = request.pretty_body_json
87
+ JSON.generate(JSON.parse(json))
88
+ rescue JSON::ParserError
89
+ json
90
+ end
91
+ end
92
+ end