stimulus_plumbers_mcp 0.4.4 → 0.4.8
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/README.md +120 -0
- data/bin/bump-version +26 -0
- data/bin/mcp-query +107 -0
- data/bin/server +8 -0
- data/bin/stimulus-plumbers-mcp +7 -0
- data/lib/stimulus_plumbers/mcp/cli.rb +13 -0
- data/lib/stimulus_plumbers/mcp/loaders/aria_loader.rb +31 -0
- data/lib/stimulus_plumbers/mcp/loaders/component_docs_loader.rb +38 -0
- data/lib/stimulus_plumbers/mcp/loaders/component_requirements.rb +41 -0
- data/lib/stimulus_plumbers/mcp/loaders/component_schema_loader.rb +49 -0
- data/lib/stimulus_plumbers/mcp/loaders/component_theme_loader.rb +41 -0
- data/lib/stimulus_plumbers/mcp/loaders/controller_docs_loader.rb +31 -0
- data/lib/stimulus_plumbers/mcp/loaders/controller_schema_loader.rb +49 -0
- data/lib/stimulus_plumbers/mcp/loaders/guide.md +48 -0
- data/lib/stimulus_plumbers/mcp/loaders/guide_loader.rb +40 -4
- data/lib/stimulus_plumbers/mcp/loaders/icons_loader.rb +34 -0
- data/lib/stimulus_plumbers/mcp/loaders/support/docs_table_parser.rb +112 -0
- data/lib/stimulus_plumbers/mcp/loaders/support/gem_vendor_path.rb +18 -0
- data/lib/stimulus_plumbers/mcp/loaders/tailwind_loader.rb +35 -0
- data/lib/stimulus_plumbers/mcp/loaders/versions_loader.rb +110 -0
- data/lib/stimulus_plumbers/mcp/plugins/aria.rb +34 -0
- data/lib/stimulus_plumbers/mcp/plugins/base.rb +40 -31
- data/lib/stimulus_plumbers/mcp/plugins/component_docs.rb +96 -0
- data/lib/stimulus_plumbers/mcp/plugins/component_schema.rb +133 -0
- data/lib/stimulus_plumbers/mcp/plugins/component_theme.rb +90 -0
- data/lib/stimulus_plumbers/mcp/plugins/controller_docs.rb +91 -0
- data/lib/stimulus_plumbers/mcp/plugins/controller_schema.rb +81 -0
- data/lib/stimulus_plumbers/mcp/plugins/guide.rb +70 -16
- data/lib/stimulus_plumbers/mcp/plugins/icons.rb +42 -0
- data/lib/stimulus_plumbers/mcp/plugins/tailwind.rb +69 -45
- data/lib/stimulus_plumbers/mcp/plugins/versions.rb +44 -0
- data/lib/stimulus_plumbers/mcp/server.rb +26 -9
- data/lib/stimulus_plumbers/mcp/version.rb +1 -1
- data/lib/stimulus_plumbers_mcp.rb +32 -11
- metadata +36 -13
- data/lib/stimulus_plumbers/mcp/loaders/component_controller_map.rb +0 -38
- data/lib/stimulus_plumbers/mcp/loaders/docs_loader.rb +0 -122
- data/lib/stimulus_plumbers/mcp/loaders/schema_loader.rb +0 -45
- data/lib/stimulus_plumbers/mcp/loaders/stimulus_manifest.rb +0 -23
- data/lib/stimulus_plumbers/mcp/loaders/tailwind_theme_loader.rb +0 -29
- data/lib/stimulus_plumbers/mcp/loaders/theme_loader.rb +0 -97
- data/lib/stimulus_plumbers/mcp/plugins/docs.rb +0 -82
- data/lib/stimulus_plumbers/mcp/plugins/schema.rb +0 -110
- data/lib/stimulus_plumbers/mcp/plugins/stimulus.rb +0 -66
- data/lib/stimulus_plumbers/mcp/plugins/theme.rb +0 -66
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module StimulusPlumbers
|
|
4
|
+
module MCP
|
|
5
|
+
module Plugins
|
|
6
|
+
class Versions < Base
|
|
7
|
+
class << self
|
|
8
|
+
def loader_key = :versions
|
|
9
|
+
|
|
10
|
+
def loader = VersionsLoader
|
|
11
|
+
|
|
12
|
+
def static_resources
|
|
13
|
+
[
|
|
14
|
+
::MCP::Resource.new(
|
|
15
|
+
uri: "versions://sources",
|
|
16
|
+
name: "source-versions",
|
|
17
|
+
description: "Resolved version (and resolution path, for stimulus) of each gem/package " \
|
|
18
|
+
"backing this server's data — use to spot version drift between sources",
|
|
19
|
+
mime_type: "application/json"
|
|
20
|
+
)
|
|
21
|
+
].freeze
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def read(uri, store)
|
|
25
|
+
return unless uri == "versions://sources"
|
|
26
|
+
|
|
27
|
+
json_resource(uri, store[:versions])
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def register_tools(server, store)
|
|
31
|
+
text_tool(
|
|
32
|
+
server,
|
|
33
|
+
name: "get_source_versions",
|
|
34
|
+
description: "Returns the resolved version (and resolution path, for stimulus) of each " \
|
|
35
|
+
"gem/package backing this server's data — use to spot version drift between sources"
|
|
36
|
+
) do
|
|
37
|
+
JSON.generate(store[:versions])
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -4,12 +4,19 @@ module StimulusPlumbers
|
|
|
4
4
|
module MCP
|
|
5
5
|
module Server
|
|
6
6
|
PLUGINS = [
|
|
7
|
+
Plugins::ComponentDocs,
|
|
8
|
+
Plugins::ComponentSchema,
|
|
9
|
+
Plugins::ComponentTheme,
|
|
10
|
+
|
|
11
|
+
Plugins::ControllerDocs,
|
|
12
|
+
Plugins::ControllerSchema,
|
|
13
|
+
|
|
14
|
+
Plugins::Icons,
|
|
15
|
+
Plugins::Tailwind,
|
|
16
|
+
|
|
17
|
+
Plugins::Aria,
|
|
7
18
|
Plugins::Guide,
|
|
8
|
-
Plugins::
|
|
9
|
-
Plugins::Docs,
|
|
10
|
-
Plugins::Stimulus,
|
|
11
|
-
Plugins::Theme,
|
|
12
|
-
Plugins::Tailwind
|
|
19
|
+
Plugins::Versions
|
|
13
20
|
].freeze
|
|
14
21
|
|
|
15
22
|
INSTRUCTIONS = "Use these resources and tools for accurate API references when " \
|
|
@@ -17,24 +24,34 @@ module StimulusPlumbers
|
|
|
17
24
|
"Read guide://overview first for a map of the form/view/stimulus API."
|
|
18
25
|
|
|
19
26
|
def self.build
|
|
20
|
-
store =
|
|
27
|
+
store = build_store
|
|
21
28
|
report_sources(store)
|
|
22
29
|
|
|
23
30
|
server = new_server
|
|
24
31
|
server.resources_read_handler do |params|
|
|
25
|
-
PLUGINS.lazy.filter_map { |plugin| plugin.read(params[:uri], store) }.first ||
|
|
32
|
+
PLUGINS.lazy.filter_map { |plugin| plugin.read(params[:uri], store) }.first ||
|
|
33
|
+
unknown_resource(params[:uri])
|
|
26
34
|
end
|
|
27
35
|
PLUGINS.each { |plugin| plugin.register_tools(server, store) }
|
|
28
36
|
server
|
|
29
37
|
end
|
|
30
38
|
|
|
39
|
+
def self.build_store
|
|
40
|
+
PLUGINS.to_h { |plugin| [plugin.loader_key, plugin.loader.call] }
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def self.unknown_resource(uri)
|
|
44
|
+
message = "unknown resource: #{uri} — read guide://overview for a map of available resources"
|
|
45
|
+
[{ uri: uri, mimeType: "application/json", text: JSON.generate(error: message) }]
|
|
46
|
+
end
|
|
47
|
+
|
|
31
48
|
def self.new_server
|
|
32
49
|
::MCP::Server.new(
|
|
33
50
|
name: "stimulus-plumbers",
|
|
34
51
|
version: StimulusPlumbers::MCP::VERSION,
|
|
35
52
|
instructions: INSTRUCTIONS,
|
|
36
|
-
resources: PLUGINS.flat_map
|
|
37
|
-
resource_templates: PLUGINS.flat_map
|
|
53
|
+
resources: PLUGINS.flat_map(&:static_resources),
|
|
54
|
+
resource_templates: PLUGINS.flat_map(&:dynamic_resource_templates)
|
|
38
55
|
)
|
|
39
56
|
end
|
|
40
57
|
|
|
@@ -9,18 +9,39 @@ require "stimulus_plumbers"
|
|
|
9
9
|
require "stimulus_plumbers_tailwind"
|
|
10
10
|
|
|
11
11
|
require_relative "stimulus_plumbers/mcp/version"
|
|
12
|
+
require_relative "stimulus_plumbers/mcp/loaders/support/docs_table_parser"
|
|
13
|
+
require_relative "stimulus_plumbers/mcp/loaders/support/gem_vendor_path"
|
|
14
|
+
|
|
15
|
+
require_relative "stimulus_plumbers/mcp/loaders/component_docs_loader"
|
|
16
|
+
require_relative "stimulus_plumbers/mcp/loaders/component_requirements"
|
|
17
|
+
require_relative "stimulus_plumbers/mcp/loaders/component_schema_loader"
|
|
18
|
+
require_relative "stimulus_plumbers/mcp/loaders/component_theme_loader"
|
|
19
|
+
|
|
20
|
+
require_relative "stimulus_plumbers/mcp/loaders/controller_docs_loader"
|
|
21
|
+
require_relative "stimulus_plumbers/mcp/loaders/controller_schema_loader"
|
|
22
|
+
|
|
23
|
+
require_relative "stimulus_plumbers/mcp/loaders/icons_loader"
|
|
24
|
+
require_relative "stimulus_plumbers/mcp/loaders/tailwind_loader"
|
|
25
|
+
|
|
26
|
+
require_relative "stimulus_plumbers/mcp/loaders/aria_loader"
|
|
12
27
|
require_relative "stimulus_plumbers/mcp/loaders/guide_loader"
|
|
13
|
-
require_relative "stimulus_plumbers/mcp/loaders/
|
|
14
|
-
|
|
15
|
-
require_relative "stimulus_plumbers/mcp/loaders/stimulus_manifest"
|
|
16
|
-
require_relative "stimulus_plumbers/mcp/loaders/component_controller_map"
|
|
17
|
-
require_relative "stimulus_plumbers/mcp/loaders/theme_loader"
|
|
18
|
-
require_relative "stimulus_plumbers/mcp/loaders/tailwind_theme_loader"
|
|
28
|
+
require_relative "stimulus_plumbers/mcp/loaders/versions_loader"
|
|
29
|
+
|
|
19
30
|
require_relative "stimulus_plumbers/mcp/plugins/base"
|
|
20
|
-
|
|
21
|
-
require_relative "stimulus_plumbers/mcp/plugins/
|
|
22
|
-
require_relative "stimulus_plumbers/mcp/plugins/
|
|
23
|
-
require_relative "stimulus_plumbers/mcp/plugins/
|
|
24
|
-
|
|
31
|
+
|
|
32
|
+
require_relative "stimulus_plumbers/mcp/plugins/component_docs"
|
|
33
|
+
require_relative "stimulus_plumbers/mcp/plugins/component_schema"
|
|
34
|
+
require_relative "stimulus_plumbers/mcp/plugins/component_theme"
|
|
35
|
+
|
|
36
|
+
require_relative "stimulus_plumbers/mcp/plugins/controller_docs"
|
|
37
|
+
require_relative "stimulus_plumbers/mcp/plugins/controller_schema"
|
|
38
|
+
|
|
39
|
+
require_relative "stimulus_plumbers/mcp/plugins/icons"
|
|
25
40
|
require_relative "stimulus_plumbers/mcp/plugins/tailwind"
|
|
41
|
+
|
|
42
|
+
require_relative "stimulus_plumbers/mcp/plugins/aria"
|
|
43
|
+
require_relative "stimulus_plumbers/mcp/plugins/guide"
|
|
44
|
+
require_relative "stimulus_plumbers/mcp/plugins/versions"
|
|
45
|
+
|
|
26
46
|
require_relative "stimulus_plumbers/mcp/server"
|
|
47
|
+
require_relative "stimulus_plumbers/mcp/cli"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: stimulus_plumbers_mcp
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.4.
|
|
4
|
+
version: 0.4.8
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ryan Chang
|
|
@@ -69,31 +69,54 @@ description: A local MCP server that exposes the stimulus-plumbers API schema an
|
|
|
69
69
|
documentation to LLM-powered IDEs
|
|
70
70
|
email:
|
|
71
71
|
- ryancyq@gmail.com
|
|
72
|
-
executables:
|
|
72
|
+
executables:
|
|
73
|
+
- stimulus-plumbers-mcp
|
|
73
74
|
extensions: []
|
|
74
75
|
extra_rdoc_files: []
|
|
75
76
|
files:
|
|
76
|
-
-
|
|
77
|
-
-
|
|
77
|
+
- README.md
|
|
78
|
+
- bin/bump-version
|
|
79
|
+
- bin/mcp-query
|
|
80
|
+
- bin/server
|
|
81
|
+
- bin/stimulus-plumbers-mcp
|
|
82
|
+
- lib/stimulus_plumbers/mcp/cli.rb
|
|
83
|
+
- lib/stimulus_plumbers/mcp/loaders/aria_loader.rb
|
|
84
|
+
- lib/stimulus_plumbers/mcp/loaders/component_docs_loader.rb
|
|
85
|
+
- lib/stimulus_plumbers/mcp/loaders/component_requirements.rb
|
|
86
|
+
- lib/stimulus_plumbers/mcp/loaders/component_schema_loader.rb
|
|
87
|
+
- lib/stimulus_plumbers/mcp/loaders/component_theme_loader.rb
|
|
88
|
+
- lib/stimulus_plumbers/mcp/loaders/controller_docs_loader.rb
|
|
89
|
+
- lib/stimulus_plumbers/mcp/loaders/controller_schema_loader.rb
|
|
90
|
+
- lib/stimulus_plumbers/mcp/loaders/guide.md
|
|
78
91
|
- lib/stimulus_plumbers/mcp/loaders/guide_loader.rb
|
|
79
|
-
- lib/stimulus_plumbers/mcp/loaders/
|
|
80
|
-
- lib/stimulus_plumbers/mcp/loaders/
|
|
81
|
-
- lib/stimulus_plumbers/mcp/loaders/
|
|
82
|
-
- lib/stimulus_plumbers/mcp/loaders/
|
|
92
|
+
- lib/stimulus_plumbers/mcp/loaders/icons_loader.rb
|
|
93
|
+
- lib/stimulus_plumbers/mcp/loaders/support/docs_table_parser.rb
|
|
94
|
+
- lib/stimulus_plumbers/mcp/loaders/support/gem_vendor_path.rb
|
|
95
|
+
- lib/stimulus_plumbers/mcp/loaders/tailwind_loader.rb
|
|
96
|
+
- lib/stimulus_plumbers/mcp/loaders/versions_loader.rb
|
|
97
|
+
- lib/stimulus_plumbers/mcp/plugins/aria.rb
|
|
83
98
|
- lib/stimulus_plumbers/mcp/plugins/base.rb
|
|
84
|
-
- lib/stimulus_plumbers/mcp/plugins/
|
|
99
|
+
- lib/stimulus_plumbers/mcp/plugins/component_docs.rb
|
|
100
|
+
- lib/stimulus_plumbers/mcp/plugins/component_schema.rb
|
|
101
|
+
- lib/stimulus_plumbers/mcp/plugins/component_theme.rb
|
|
102
|
+
- lib/stimulus_plumbers/mcp/plugins/controller_docs.rb
|
|
103
|
+
- lib/stimulus_plumbers/mcp/plugins/controller_schema.rb
|
|
85
104
|
- lib/stimulus_plumbers/mcp/plugins/guide.rb
|
|
86
|
-
- lib/stimulus_plumbers/mcp/plugins/
|
|
87
|
-
- lib/stimulus_plumbers/mcp/plugins/stimulus.rb
|
|
105
|
+
- lib/stimulus_plumbers/mcp/plugins/icons.rb
|
|
88
106
|
- lib/stimulus_plumbers/mcp/plugins/tailwind.rb
|
|
89
|
-
- lib/stimulus_plumbers/mcp/plugins/
|
|
107
|
+
- lib/stimulus_plumbers/mcp/plugins/versions.rb
|
|
90
108
|
- lib/stimulus_plumbers/mcp/server.rb
|
|
91
109
|
- lib/stimulus_plumbers/mcp/version.rb
|
|
92
110
|
- lib/stimulus_plumbers_mcp.rb
|
|
93
111
|
homepage: https://github.com/ryancyq/stimulus-plumbers
|
|
94
112
|
licenses:
|
|
95
113
|
- MIT
|
|
96
|
-
metadata:
|
|
114
|
+
metadata:
|
|
115
|
+
rubygems_mfa_required: 'true'
|
|
116
|
+
allowed_push_host: https://rubygems.org
|
|
117
|
+
changelog_uri: https://github.com/ryancyq/stimulus-plumbers/blob/main/CHANGELOG.md
|
|
118
|
+
homepage_uri: https://github.com/ryancyq/stimulus-plumbers
|
|
119
|
+
source_code_uri: https://github.com/ryancyq/stimulus-plumbers/tree/main/stimulus-plumbers-mcp
|
|
97
120
|
rdoc_options: []
|
|
98
121
|
require_paths:
|
|
99
122
|
- lib
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module StimulusPlumbers
|
|
4
|
-
module MCP
|
|
5
|
-
class ComponentControllerMap
|
|
6
|
-
def self.call
|
|
7
|
-
Components.constants
|
|
8
|
-
.map { |c| Components.const_get(c) }
|
|
9
|
-
.grep(Class)
|
|
10
|
-
.to_h { |klass| [component_key(klass), controllers_for(klass).uniq] }
|
|
11
|
-
end
|
|
12
|
-
|
|
13
|
-
# Controllers from a component class plus its nested sub-components (e.g. Combobox::Date),
|
|
14
|
-
# keyed to the top-level component. Skips references to sibling components.
|
|
15
|
-
def self.controllers_for(mod)
|
|
16
|
-
mod.constants(false).flat_map do |const|
|
|
17
|
-
value = mod.const_get(const)
|
|
18
|
-
if const.to_s.end_with?("CONTROLLER")
|
|
19
|
-
Array(value).grep(String).flat_map(&:split)
|
|
20
|
-
elsif nested?(mod, value)
|
|
21
|
-
controllers_for(value)
|
|
22
|
-
else
|
|
23
|
-
[]
|
|
24
|
-
end
|
|
25
|
-
end
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
def self.nested?(mod, value)
|
|
29
|
-
value.is_a?(Module) && !value.name.nil? && value.name.start_with?("#{mod.name}::")
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
def self.component_key(klass)
|
|
33
|
-
klass.name.demodulize.underscore.to_sym
|
|
34
|
-
end
|
|
35
|
-
private_class_method :controllers_for, :nested?, :component_key
|
|
36
|
-
end
|
|
37
|
-
end
|
|
38
|
-
end
|
|
@@ -1,122 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module StimulusPlumbers
|
|
4
|
-
module MCP
|
|
5
|
-
class DocsLoader
|
|
6
|
-
DOCS_DIR = File.expand_path(
|
|
7
|
-
"../../../../../stimulus-plumbers-rails/docs/component",
|
|
8
|
-
__dir__
|
|
9
|
-
)
|
|
10
|
-
|
|
11
|
-
def self.call
|
|
12
|
-
Dir[File.join(DOCS_DIR, "*.md")].each_with_object({}) do |path, result|
|
|
13
|
-
name = File.basename(path, ".md").to_sym
|
|
14
|
-
content = File.read(path)
|
|
15
|
-
result[name] = {
|
|
16
|
-
content: content,
|
|
17
|
-
examples: extract_erb_examples(content),
|
|
18
|
-
signature: extract_signature(content)
|
|
19
|
-
}
|
|
20
|
-
end
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
def self.extract_erb_examples(content)
|
|
24
|
-
content.scan(%r{```erb\n(.*?)```}m).map(&:first).map(&:strip)
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
# Parse `| Option |` / `| Slot method |` doc tables into the helper surface.
|
|
28
|
-
# Options are grouped under the heading (sub-helper signature) above them.
|
|
29
|
-
def self.extract_signature(content)
|
|
30
|
-
tables = tables_with_headings(content)
|
|
31
|
-
{ helpers: option_helpers(tables), slots: slot_methods(tables) }
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
def self.option_helpers(tables)
|
|
35
|
-
tables.select { |t| t[:header].first == "Option" }
|
|
36
|
-
.filter_map do |t|
|
|
37
|
-
options = t[:rows].map { |r| option_row(r) }
|
|
38
|
-
{ signature: t[:heading], options: options } unless options.empty?
|
|
39
|
-
end
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
def self.slot_methods(tables)
|
|
43
|
-
tables.select { |t| t[:header].first == "Slot method" }
|
|
44
|
-
.flat_map { |t| t[:rows].map { |r| slot_row(r) } }
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
def self.option_row(cells)
|
|
48
|
-
{ option: clean(cells[0]), default: clean(cells[1]), description: cells[2].to_s }
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
def self.slot_row(cells)
|
|
52
|
-
slot = clean(cells[0])
|
|
53
|
-
description = cells[1].to_s
|
|
54
|
-
{ slot: slot, description: description, block: block_required?(slot, description) }
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
def self.block_required?(slot, description)
|
|
58
|
-
slot.include?("{") || description.match?(%r{block required}i)
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
# Tag each table with the heading above it; skip fenced code (so ```ruby
|
|
62
|
-
# comments aren't read as headings).
|
|
63
|
-
def self.tables_with_headings(content)
|
|
64
|
-
state = { heading: nil, fenced: false, buffer: [], tables: [] }
|
|
65
|
-
content.each_line { |line| scan_line(line, state) }
|
|
66
|
-
flush_table(state)
|
|
67
|
-
state[:tables]
|
|
68
|
-
end
|
|
69
|
-
|
|
70
|
-
def self.scan_line(line, state)
|
|
71
|
-
if line.start_with?("```")
|
|
72
|
-
flush_table(state)
|
|
73
|
-
state[:fenced] = !state[:fenced]
|
|
74
|
-
elsif state[:fenced]
|
|
75
|
-
nil
|
|
76
|
-
elsif (heading = line[%r{\A#+\s+(.+)}, 1])
|
|
77
|
-
flush_table(state)
|
|
78
|
-
state[:heading] = clean(heading)
|
|
79
|
-
elsif line.lstrip.start_with?("|")
|
|
80
|
-
state[:buffer] << line
|
|
81
|
-
else
|
|
82
|
-
flush_table(state)
|
|
83
|
-
end
|
|
84
|
-
end
|
|
85
|
-
|
|
86
|
-
def self.flush_table(state)
|
|
87
|
-
return if state[:buffer].empty?
|
|
88
|
-
|
|
89
|
-
state[:tables] << build_table(state[:buffer]).merge(heading: state[:heading])
|
|
90
|
-
state[:buffer] = []
|
|
91
|
-
end
|
|
92
|
-
|
|
93
|
-
def self.build_table(lines)
|
|
94
|
-
rows = lines.map { |l| split_row(l) }.reject { |cells| cells.all? { |c| c.match?(%r{\A:?-+:?\z}) } }
|
|
95
|
-
{ header: rows.first, rows: rows.drop(1) }
|
|
96
|
-
end
|
|
97
|
-
|
|
98
|
-
# Split a markdown table row, honouring escaped pipes (`\|`) inside cells.
|
|
99
|
-
def self.split_row(line)
|
|
100
|
-
line.strip.delete_prefix("|").delete_suffix("|")
|
|
101
|
-
.split(%r{(?<!\\)\|})
|
|
102
|
-
.map { |c| c.gsub('\|', "|").strip }
|
|
103
|
-
end
|
|
104
|
-
|
|
105
|
-
def self.clean(cell)
|
|
106
|
-
cell.to_s.gsub(%r{[`*]}, "").sub(%r{:\z}, "").strip
|
|
107
|
-
end
|
|
108
|
-
|
|
109
|
-
private_class_method :option_helpers,
|
|
110
|
-
:slot_methods,
|
|
111
|
-
:option_row,
|
|
112
|
-
:slot_row,
|
|
113
|
-
:block_required?,
|
|
114
|
-
:tables_with_headings,
|
|
115
|
-
:scan_line,
|
|
116
|
-
:flush_table,
|
|
117
|
-
:build_table,
|
|
118
|
-
:split_row,
|
|
119
|
-
:clean
|
|
120
|
-
end
|
|
121
|
-
end
|
|
122
|
-
end
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module StimulusPlumbers
|
|
4
|
-
module MCP
|
|
5
|
-
class SchemaLoader
|
|
6
|
-
def self.call
|
|
7
|
-
{
|
|
8
|
-
components: extract_schema,
|
|
9
|
-
field_as: extract_field_as,
|
|
10
|
-
icons: extract_icons,
|
|
11
|
-
stimulus: ComponentControllerMap.call
|
|
12
|
-
}
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
def self.extract_schema
|
|
16
|
-
Themes::Base::SCHEMA.transform_values do |param_schema|
|
|
17
|
-
param_schema.transform_values do |meta|
|
|
18
|
-
v = meta[:validate]
|
|
19
|
-
{ default: meta[:default], valid: v.respond_to?(:to_a) ? v.to_a : v.inspect }
|
|
20
|
-
end
|
|
21
|
-
end
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
def self.extract_field_as
|
|
25
|
-
{
|
|
26
|
-
field: Form::Fields::Renderer::FIELD.keys,
|
|
27
|
-
collection_field: Form::Fields::Renderer::COLLECTION.keys,
|
|
28
|
-
choice: Form::Fields::Renderer::CHOICE.keys
|
|
29
|
-
}
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
def self.extract_icons
|
|
33
|
-
heroicon_dir = Themes::Tailwind::Icons::Heroicon.send(:svg_dir)
|
|
34
|
-
custom_dir = Themes::Tailwind::Icons::Custom.send(:svg_dir)
|
|
35
|
-
|
|
36
|
-
outline = Dir[File.join(heroicon_dir, "outline", "*.svg")].map { |f| File.basename(f, ".svg") }
|
|
37
|
-
solid = Dir[File.join(heroicon_dir, "solid", "*.svg")].map { |f| "#{File.basename(f, ".svg")}/solid" }
|
|
38
|
-
customs = Dir[File.join(custom_dir, "*.svg")].map { |f| File.basename(f, ".svg") }
|
|
39
|
-
aliases = Themes::Tailwind::Icon::ALIASES.keys
|
|
40
|
-
|
|
41
|
-
(outline + solid + customs + aliases).uniq.sort
|
|
42
|
-
end
|
|
43
|
-
end
|
|
44
|
-
end
|
|
45
|
-
end
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module StimulusPlumbers
|
|
4
|
-
module MCP
|
|
5
|
-
class StimulusManifest
|
|
6
|
-
MANIFEST_PATH = File.expand_path(
|
|
7
|
-
File.join(__dir__, "../../../../..", "stimulus-plumbers", "dist", "controllers.manifest.json")
|
|
8
|
-
).freeze
|
|
9
|
-
|
|
10
|
-
def self.call
|
|
11
|
-
unless File.exist?(MANIFEST_PATH)
|
|
12
|
-
StimulusPlumbers::Logger.warn(
|
|
13
|
-
"controllers.manifest.json not found at #{MANIFEST_PATH}. " \
|
|
14
|
-
"Run `node --run build:manifest` in stimulus-plumbers/ to generate it."
|
|
15
|
-
)
|
|
16
|
-
return {}
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
JSON.parse(File.read(MANIFEST_PATH))
|
|
20
|
-
end
|
|
21
|
-
end
|
|
22
|
-
end
|
|
23
|
-
end
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module StimulusPlumbers
|
|
4
|
-
module MCP
|
|
5
|
-
class TailwindThemeLoader
|
|
6
|
-
def self.call
|
|
7
|
-
theme = Themes::TailwindTheme.new
|
|
8
|
-
|
|
9
|
-
Themes::Base::SCHEMA.each_with_object({}) do |(key, params), result|
|
|
10
|
-
# Skip keys with no _classes method — calling resolve would trigger Logger.warn
|
|
11
|
-
next unless theme.respond_to?(:"#{key}_classes", true)
|
|
12
|
-
|
|
13
|
-
result[key] = {}
|
|
14
|
-
result[key][:default] = theme.resolve(key)[:classes].to_s
|
|
15
|
-
|
|
16
|
-
params.each do |param, meta|
|
|
17
|
-
valid = meta[:validate]
|
|
18
|
-
next unless valid.respond_to?(:to_a)
|
|
19
|
-
|
|
20
|
-
valid.to_a.each do |val|
|
|
21
|
-
classes = theme.resolve(key, param => val)[:classes].to_s
|
|
22
|
-
result[key]["#{param}:#{val}"] = classes
|
|
23
|
-
end
|
|
24
|
-
end
|
|
25
|
-
end
|
|
26
|
-
end
|
|
27
|
-
end
|
|
28
|
-
end
|
|
29
|
-
end
|
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module StimulusPlumbers
|
|
4
|
-
module MCP
|
|
5
|
-
class ThemeLoader
|
|
6
|
-
BASE_DOC = <<~MARKDOWN
|
|
7
|
-
# Custom Theme Implementation Guide
|
|
8
|
-
|
|
9
|
-
A custom theme is a Ruby class that extends `StimulusPlumbers::Themes::Base` and defines
|
|
10
|
-
`{component_key}_classes(**args)` methods for the components you want to style.
|
|
11
|
-
|
|
12
|
-
## Method Convention
|
|
13
|
-
|
|
14
|
-
- **Name:** `{component_key}_classes` — e.g. `button_classes`, `form_group_classes`
|
|
15
|
-
- **Params:** keyword arguments matching the schema params for that component
|
|
16
|
-
- **Return:** a Hash with a `:classes` key containing a space-separated CSS class string
|
|
17
|
-
|
|
18
|
-
```ruby
|
|
19
|
-
def button_classes(type: :default, variant: :default, size: :md)
|
|
20
|
-
{ classes: "..." }
|
|
21
|
-
end
|
|
22
|
-
```
|
|
23
|
-
|
|
24
|
-
Components with no params still receive empty kwargs:
|
|
25
|
-
|
|
26
|
-
```ruby
|
|
27
|
-
def form_group_classes
|
|
28
|
-
{ classes: "..." }
|
|
29
|
-
end
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
## Minimal Example
|
|
33
|
-
|
|
34
|
-
```ruby
|
|
35
|
-
class MyTheme < StimulusPlumbers::Themes::Base
|
|
36
|
-
private
|
|
37
|
-
|
|
38
|
-
def button_classes(type: :default, variant: :default, size: :md)
|
|
39
|
-
base = "inline-flex items-center gap-2 rounded px-3 py-2"
|
|
40
|
-
variant_class = { primary: "bg-blue-600 text-white", destructive: "bg-red-600 text-white" }.fetch(variant, "bg-gray-100")
|
|
41
|
-
{ classes: [base, variant_class].join(" ") }
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
def link_classes(type: :default, variant: :default)
|
|
45
|
-
{ classes: "underline text-blue-600 hover:text-blue-800" }
|
|
46
|
-
end
|
|
47
|
-
end
|
|
48
|
-
```
|
|
49
|
-
|
|
50
|
-
## Registration
|
|
51
|
-
|
|
52
|
-
```ruby
|
|
53
|
-
StimulusPlumbers.configure do |config|
|
|
54
|
-
config.theme.register(:my_theme, MyTheme)
|
|
55
|
-
config.theme = :my_theme
|
|
56
|
-
end
|
|
57
|
-
```
|
|
58
|
-
|
|
59
|
-
## Component Keys
|
|
60
|
-
|
|
61
|
-
See `theme://components` for the full list of component keys that can be themed.
|
|
62
|
-
Use `theme://components/{name}` for the method signature and param details per component.
|
|
63
|
-
|
|
64
|
-
## Partial Implementation
|
|
65
|
-
|
|
66
|
-
You only need to define methods for components you want to style — unimplemented keys
|
|
67
|
-
return an empty hash, which renders the component with no CSS classes.
|
|
68
|
-
MARKDOWN
|
|
69
|
-
|
|
70
|
-
def self.call
|
|
71
|
-
{
|
|
72
|
-
base_doc: BASE_DOC,
|
|
73
|
-
components: extract_interface
|
|
74
|
-
}
|
|
75
|
-
end
|
|
76
|
-
|
|
77
|
-
def self.extract_interface
|
|
78
|
-
Themes::Base::SCHEMA.each_with_object({}) do |(key, params), result|
|
|
79
|
-
result[key] = {
|
|
80
|
-
method: "#{key}_classes",
|
|
81
|
-
params: params.transform_values { |meta| format_param(meta) },
|
|
82
|
-
returns: "{ classes: String }"
|
|
83
|
-
}
|
|
84
|
-
end
|
|
85
|
-
end
|
|
86
|
-
|
|
87
|
-
def self.format_param(meta)
|
|
88
|
-
valid = meta[:validate]
|
|
89
|
-
entry = { default: meta[:default] }
|
|
90
|
-
entry[:valid] = valid.to_a if valid.respond_to?(:to_a)
|
|
91
|
-
entry
|
|
92
|
-
end
|
|
93
|
-
|
|
94
|
-
private_class_method :extract_interface, :format_param
|
|
95
|
-
end
|
|
96
|
-
end
|
|
97
|
-
end
|