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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +46 -0
- data/LICENSE.txt +21 -0
- data/README.md +939 -0
- data/app/components/docs_ui/brand_mark.rb +88 -0
- data/app/components/docs_ui/callout.rb +37 -0
- data/app/components/docs_ui/code.rb +123 -0
- data/app/components/docs_ui/endpoint.rb +44 -0
- data/app/components/docs_ui/error_table.rb +72 -0
- data/app/components/docs_ui/example.rb +102 -0
- data/app/components/docs_ui/field_table.rb +46 -0
- data/app/components/docs_ui/header.rb +30 -0
- data/app/components/docs_ui/icon.rb +65 -0
- data/app/components/docs_ui/json_response.rb +46 -0
- data/app/components/docs_ui/markdown.rb +187 -0
- data/app/components/docs_ui/markdown_action.rb +45 -0
- data/app/components/docs_ui/on_this_page.rb +104 -0
- data/app/components/docs_ui/open_api_operation.rb +126 -0
- data/app/components/docs_ui/page.rb +83 -0
- data/app/components/docs_ui/page_helpers.rb +52 -0
- data/app/components/docs_ui/prop_table.rb +43 -0
- data/app/components/docs_ui/prose.rb +30 -0
- data/app/components/docs_ui/request_example.rb +85 -0
- data/app/components/docs_ui/search_box.rb +106 -0
- data/app/components/docs_ui/search_results.rb +95 -0
- data/app/components/docs_ui/section.rb +94 -0
- data/app/components/docs_ui/shell.rb +161 -0
- data/app/components/docs_ui/sidebar.rb +106 -0
- data/app/components/docs_ui/table.rb +64 -0
- data/app/components/docs_ui/theme_switcher.rb +46 -0
- data/app/components/docs_ui/topbar_links.rb +42 -0
- data/app/controllers/docs_kit/llms_controller.rb +76 -0
- data/app/controllers/docs_kit/mcp_controller.rb +60 -0
- data/app/controllers/docs_kit/search_controller.rb +72 -0
- data/app/javascript/docs_kit/controllers/docs_nav_controller.js +619 -0
- data/config/importmap.rb +15 -0
- data/config/rubocop/docs_kit.yml +24 -0
- data/exe/docs-kit +80 -0
- data/lib/docs-kit.rb +5 -0
- data/lib/docs_kit/api_client.rb +52 -0
- data/lib/docs_kit/api_request.rb +66 -0
- data/lib/docs_kit/api_templates.rb +92 -0
- data/lib/docs_kit/configuration.rb +485 -0
- data/lib/docs_kit/controller.rb +47 -0
- data/lib/docs_kit/engine.rb +49 -0
- data/lib/docs_kit/llms_text.rb +105 -0
- data/lib/docs_kit/markdown_export/blocks.rb +160 -0
- data/lib/docs_kit/markdown_export/inline.rb +95 -0
- data/lib/docs_kit/markdown_export/table.rb +53 -0
- data/lib/docs_kit/markdown_export.rb +92 -0
- data/lib/docs_kit/mcp_server.rb +128 -0
- data/lib/docs_kit/mcp_tools.rb +118 -0
- data/lib/docs_kit/nav_item.rb +22 -0
- data/lib/docs_kit/open_api/document.rb +91 -0
- data/lib/docs_kit/open_api/operation.rb +213 -0
- data/lib/docs_kit/open_api/schema.rb +178 -0
- data/lib/docs_kit/open_api.rb +55 -0
- data/lib/docs_kit/registry.rb +152 -0
- data/lib/docs_kit/rubocop.rb +19 -0
- data/lib/docs_kit/search_hit.rb +28 -0
- data/lib/docs_kit/search_index/snippet.rb +65 -0
- data/lib/docs_kit/search_index.rb +169 -0
- data/lib/docs_kit/shortcut.rb +99 -0
- data/lib/docs_kit/templates/new_site.rb +175 -0
- data/lib/docs_kit/topbar_link.rb +39 -0
- data/lib/docs_kit/version.rb +5 -0
- data/lib/docs_kit.rb +72 -0
- data/lib/generators/docs_kit/install/USAGE +15 -0
- data/lib/generators/docs_kit/install/install_generator.rb +447 -0
- data/lib/generators/docs_kit/install/sync_report.rb +64 -0
- data/lib/generators/docs_kit/install/templates/agents_md.erb +105 -0
- data/lib/generators/docs_kit/install/templates/application.tailwind.css.erb +39 -0
- data/lib/generators/docs_kit/install/templates/build-css +34 -0
- data/lib/generators/docs_kit/install/templates/build_css.rake +13 -0
- data/lib/generators/docs_kit/install/templates/doc.rb.erb +17 -0
- data/lib/generators/docs_kit/install/templates/docs_controller.rb.erb +14 -0
- data/lib/generators/docs_kit/install/templates/docs_kit.rb.erb +91 -0
- data/lib/generators/docs_kit/install/templates/installation_page.rb.erb +37 -0
- data/lib/generators/docs_kit/install/templates/landing.rb.erb +25 -0
- data/lib/generators/docs_kit/install/templates/landings_controller.rb.erb +7 -0
- data/lib/generators/docs_kit/install/templates/phlex.rb.erb +14 -0
- data/lib/generators/docs_kit/install/templates/rails_icons.rb.erb +12 -0
- data/lib/generators/docs_kit/install/templates/skill.md.erb +88 -0
- data/lib/generators/docs_kit/page/USAGE +26 -0
- data/lib/generators/docs_kit/page/page_generator.rb +127 -0
- data/lib/generators/docs_kit/page/templates/page.rb.erb +21 -0
- data/lib/rubocop/cop/docs_kit/escaped_interpolation_in_heredoc.rb +119 -0
- data/lib/rubocop/cop/docs_kit/render_component_preferred.rb +123 -0
- 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,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
|