sugar_bee 0.1.0 → 0.2.2

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2682b6199fd26509a94f89eb0dba48f359b06e6dff5f4fd63a2151daaeedc3e2
4
- data.tar.gz: 1c80ab38f1ad79eb1deb0c85d4a00f63ebf0aa4f26b818d70af9ce3bbdea8b3b
3
+ metadata.gz: 0f2cc41e7f639d90d331659d7eaafad8e852e512ef11384b7129bb40e3810634
4
+ data.tar.gz: 6c99912c8d11226b9023a1c0bc597111379db1c49844e115d8a9fe4cb8bae414
5
5
  SHA512:
6
- metadata.gz: 8b723db24c2bdeb06a2b701b585356c10efd7a9247e75b109027783c63db163eb6a58469fe65c2d1b74bd02ea5002592efc0465bc47652a9bd5071496e7e8c52
7
- data.tar.gz: 3dd3b0ab9a5fb864631a5057ac653ca5ee85414dbae1a61f48ad7e90c8eac73c736a684301041a4fc0f946085a8c0e229af51c66a07d60c35df5ce71c4165a60
6
+ metadata.gz: 223214a0a2d6c7c32b92bf2da03194c86c9c7bfbc2b7aef90b72ba239702eee2ddef4988d9c5d1a72326d7c00af3a8392473c4f55476c18b6ae6a907c1ffa24d
7
+ data.tar.gz: 25c117707750a4e3c0ca4f6d8635e3f74142ab337057a7a635c8daf50f71c9070e42eb20a01b1efb2a8152ac0efa6fcff6b5d6e13a5714b84234a387efe4f361
data/README.md CHANGED
@@ -26,7 +26,7 @@ TODO: Write usage instructions here
26
26
 
27
27
  ## Development
28
28
 
29
- After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
29
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
30
30
 
31
31
  To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
32
32
 
data/Rakefile CHANGED
@@ -1,10 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "bundler/gem_tasks"
4
- require "rspec/core/rake_task"
4
+ Bundler::GemHelper.tag_prefix = "sugar_bee-"
5
+ require "minitest/test_task"
5
6
 
6
- RSpec::Core::RakeTask.new(:spec)
7
+ Minitest::TestTask.create
7
8
 
8
9
  require "standard/rake"
9
10
 
10
- task default: %i[spec standard]
11
+ task default: %i[test standard]
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SugarBee
4
+ class Error < StandardError; end
5
+
6
+ class UnknownComponentError < Error; end
7
+
8
+ class ParseError < Error; end
9
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "parslet"
4
+ require_relative "errors"
5
+ require_relative "srb/grammar"
6
+ require_relative "srb/compiler"
7
+
8
+ module SugarBee
9
+ # Public interface: `.srb` source string in → compiled ERB string out.
10
+ # Pure function, no Rails coupling. Parses with the SRB::Grammar, compiles with
11
+ # the SRB::Compiler transform.
12
+ module Parser
13
+ def self.call(source)
14
+ SRB::Compiler.new.apply(SRB::Grammar.new.parse(source))
15
+ rescue Parslet::ParseFailed => e
16
+ raise ParseError, "Invalid .srb template: #{deepest_failure_line(e)}"
17
+ end
18
+
19
+ # The root of `parse_failure_cause.ascii_tree` reports where the top-level
20
+ # repetition gave up (often "line 1 char 1"); the line that progressed
21
+ # furthest into the input is the one that names the actual problem.
22
+ def self.deepest_failure_line(error)
23
+ error.parse_failure_cause.ascii_tree.lines
24
+ .map { |line| line.strip.sub(/\A[|`\s-]+/, "") }
25
+ .reverse # ties resolve to the deepest (most specific) tree node
26
+ .max_by { |line| line.match(/line (\d+) char (\d+)\.\z/) { |m| [m[1].to_i, m[2].to_i] } || [0, 0] }
27
+ end
28
+ private_class_method :deepest_failure_line
29
+ end
30
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "parslet"
4
+ require_relative "../errors"
5
+
6
+ module SugarBee
7
+ module SRB
8
+ # Compiles an SRB::Grammar parse tree into an ERB string.
9
+ # `tokens:` is ALWAYS emitted (even empty → `%i[]`); tag pairs emit `do |c|`
10
+ # (the block param is the slot receiver Story 4.4 will use). Self-closing tags
11
+ # emit no block.
12
+ class Compiler < Parslet::Transform
13
+ # Marks a kv-pair kwarg so render_call can partition it away from plain
14
+ # token strings; `code` is the already-formatted Ruby value (quoted or verbatim).
15
+ KvPair = Struct.new(:key, :code)
16
+
17
+ rule(text: simple(:t)) { t.to_s }
18
+ rule(token: simple(:t)) { t.to_s }
19
+ rule(source: sequence(:nodes)) { nodes.join }
20
+
21
+ rule(grouped: {prefix: simple(:prefix), inner: subtree(:inner)}) do
22
+ Array(inner).map { |tok| "#{prefix}:#{tok}" }
23
+ end
24
+
25
+ rule(kv: {key: simple(:key), dq: subtree(:val)}) do
26
+ content = val.is_a?(Array) ? "" : val.to_s
27
+ KvPair.new(key.to_s, "\"#{content}\"")
28
+ end
29
+ rule(kv: {key: simple(:key), bare: simple(:val)}) { KvPair.new(key.to_s, val.to_s) }
30
+
31
+ rule(self_close: {name: simple(:name), args: subtree(:args)}) do
32
+ "<%= render(#{Compiler.render_call(name, args)}) %>"
33
+ end
34
+
35
+ rule(tag_pair: {name: simple(:name), args: subtree(:args), source: subtree(:children), close: simple(:close)}) do
36
+ unless name.to_s == close.to_s
37
+ raise SugarBee::ParseError, "Mismatched tags: <#{name}> closed by </#{close}>"
38
+ end
39
+ "<%= render(#{Compiler.render_call(name, args)}) do |c| %>#{Array(children).join}<% end %>"
40
+ end
41
+
42
+ rule(slot: {sname: simple(:sname), source: subtree(:children), close: simple(:close)}) do
43
+ unless sname.to_s == close.to_s
44
+ raise SugarBee::ParseError, "Mismatched slot tags: <:#{sname}> closed by </:#{close}>"
45
+ end
46
+ "<% c.with_#{sname} do %>#{Array(children).join}<% end %>"
47
+ end
48
+
49
+ def self.render_call(name, args)
50
+ const = "#{name}Component"
51
+ unless Object.const_defined?(const)
52
+ raise SugarBee::UnknownComponentError, "Unknown component <#{name}>: no #{const} class is defined"
53
+ end
54
+
55
+ token_strings, kv_pairs = [], []
56
+ Array(args).each do |item|
57
+ case item
58
+ when KvPair
59
+ unless item.key.match?(/\A[a-z_][a-zA-Z0-9_]*\z/)
60
+ raise SugarBee::ParseError, "Invalid attribute key '#{item.key}': not a valid Ruby keyword argument"
61
+ end
62
+ kv_pairs << "#{item.key}: #{item.code}"
63
+ when Array then token_strings.concat(item)
64
+ else token_strings << item
65
+ end
66
+ end
67
+ kwargs = kv_pairs.empty? ? "" : ", #{kv_pairs.join(", ")}"
68
+ "#{const}.new(tokens: %i[#{token_strings.join(" ")}]#{kwargs})"
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "parslet"
4
+
5
+ module SugarBee
6
+ module SRB
7
+ # PEG grammar for the `.srb` template language (structure only; codegen lives
8
+ # in SRB::Compiler).
9
+ class Grammar < Parslet::Parser
10
+ root(:source)
11
+
12
+ rule(:source) { (erb | component | slot | passthrough).repeat.as(:source) }
13
+
14
+ # ERB tags are atomic: the whole `<% ... %>` / `<%= ... %>` run is one
15
+ # opaque verbatim unit, terminated by the first subsequent `%>`. The
16
+ # parser never looks inside — no interpretation of Ruby content (FR-25/26).
17
+ rule(:erb) { (erb_edge >> (str("%>").absent? >> any).repeat >> str("%>")).as(:text) }
18
+
19
+ # Verbatim text: plain HTML, lowercase tags — anything not opening/closing a component, slot, or ERB tag.
20
+ rule(:passthrough) { (node_edge.absent? >> any).repeat(1).as(:text) }
21
+ rule(:node_edge) { component_edge | slot_edge | erb_edge }
22
+ rule(:component_edge) { (str("<") >> match["A-Z"]) | (str("</") >> sp? >> match["A-Z"]) }
23
+ # Only a lowercase-initial name opens a slot, so `<:` before anything else
24
+ # (uppercase, space, punctuation) falls through to passthrough as verbatim
25
+ # text instead of hard-failing the whole parse.
26
+ rule(:slot_edge) { (str("<:") | str("</:")) >> match["a-z"] }
27
+ # Stops passthrough at `<%` so the `erb` rule claims the tag whole —
28
+ # without this, passthrough would consume into an ERB tag and could
29
+ # hard-stop at a `<`+uppercase inside it (e.g. `<%= "<Box>" %>`).
30
+ # `<%%` (ERB's literal-escape) is excluded: it is not an ERB opener, so
31
+ # neither the erb rule claims it nor does passthrough stop at it — the
32
+ # sequence falls through to passthrough and emits verbatim.
33
+ rule(:erb_edge) { str("<%") >> str("%").absent? }
34
+
35
+ rule(:component) { self_close | tag_pair }
36
+ rule(:self_close) { (str("<") >> cname >> args >> sp? >> str("/>")).as(:self_close) }
37
+ rule(:tag_pair) do
38
+ (str("<") >> cname >> args >> sp? >> str(">") >>
39
+ source >> str("</") >> sp? >> cname_close.as(:close) >> sp? >> str(">")).as(:tag_pair)
40
+ end
41
+
42
+ rule(:cname) { (match["A-Z"] >> match["A-Za-z"].repeat).as(:name) }
43
+ rule(:cname_close) { match["A-Z"] >> match["A-Za-z"].repeat }
44
+
45
+ rule(:slot) do
46
+ (str("<:") >> sname >> sp? >> str(">") >>
47
+ source >> str("</:") >> sp? >> sname_close.as(:close) >> sp? >> str(">")).as(:slot)
48
+ end
49
+ rule(:sname) { (match["a-z"] >> match["a-zA-Z0-9_"].repeat).as(:sname) }
50
+ rule(:sname_close) { match["a-z"] >> match["a-zA-Z0-9_"].repeat }
51
+
52
+ # Mixed bag: tokens (bare/grouped) AND kv pairs. Compiler partitions them apart.
53
+ rule(:args) { (sp >> attr).repeat.as(:args) }
54
+ rule(:attr) { grouped | kvattr | token }
55
+
56
+ rule(:grouped) do
57
+ (match["^:\\[\\s"].repeat(1).as(:prefix) >> str(":[") >>
58
+ (group_token >> (sp >> group_token).repeat).as(:inner) >> str("]")).as(:grouped)
59
+ end
60
+ # Same charset as `token`, but a bare "]" ends the token so the group's
61
+ # closing bracket is never swallowed. A balanced "[...]" run (arbitrary
62
+ # values like gap-[13px]) is consumed whole, so its inner "]" doesn't
63
+ # terminate the group prematurely.
64
+ rule(:group_token) do
65
+ (bracketed | (str("/") >> str(">").absent?) | match["^\\s/>\\[\\]"]).repeat(1).as(:token)
66
+ end
67
+ rule(:bracketed) { str("[") >> match["^\\]"].repeat >> str("]") }
68
+
69
+ rule(:kvattr) { (key.as(:key) >> str("=") >> (dquoted | unquoted)).as(:kv) }
70
+ rule(:key) { match["a-zA-Z_\\-"].repeat(1) }
71
+ rule(:dquoted) { str('"') >> match['^"'].repeat.as(:dq) >> str('"') }
72
+ rule(:unquoted) { match["^\\s/>"].repeat(1).as(:bare) }
73
+
74
+ rule(:token) { ((str("/") >> str(">").absent?) | match["^\\s/>"]).repeat(1).as(:token) }
75
+
76
+ rule(:sp) { match["\\s"].repeat(1) }
77
+ rule(:sp?) { sp.maybe }
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "action_view"
4
+ require_relative "parser"
5
+
6
+ module SugarBee
7
+ module TemplateHandler
8
+ def self.call(template, source)
9
+ compiled_erb = Parser.call(source)
10
+ ActionView::Template.handler_for_extension(:erb).call(template, compiled_erb)
11
+ end
12
+ end
13
+ end
14
+
15
+ ActionView::Template.register_template_handler(:srb, SugarBee::TemplateHandler)
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module SugarBee
2
- VERSION = "0.1.0"
4
+ VERSION = "0.2.2"
3
5
  end
data/lib/sugar_bee.rb CHANGED
@@ -1,9 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "action_view"
3
4
  require_relative "sugar_bee/version"
4
- require_relative "sugar_bee/handler"
5
- require_relative "sugar_bee/railtie" if defined?(Rails)
6
-
7
- module SugarBee
8
- class Error < StandardError; end
9
- end
5
+ require_relative "sugar_bee/errors"
6
+ require_relative "sugar_bee/parser"
7
+ require_relative "sugar_bee/template_handler"
metadata CHANGED
@@ -1,34 +1,28 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sugar_bee
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Fabio Papa
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 1980-01-02 00:00:00.000000000 Z
10
+ date: 2026-07-18 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
- name: rails
13
+ name: actionview
14
14
  requirement: !ruby/object:Gem::Requirement
15
15
  requirements:
16
16
  - - ">="
17
17
  - !ruby/object:Gem::Version
18
- version: 7.0.0
19
- - - "<"
20
- - !ruby/object:Gem::Version
21
- version: 9.0.0
18
+ version: '7.1'
22
19
  type: :runtime
23
20
  prerelease: false
24
21
  version_requirements: !ruby/object:Gem::Requirement
25
22
  requirements:
26
23
  - - ">="
27
24
  - !ruby/object:Gem::Version
28
- version: 7.0.0
29
- - - "<"
30
- - !ruby/object:Gem::Version
31
- version: 9.0.0
25
+ version: '7.1'
32
26
  - !ruby/object:Gem::Dependency
33
27
  name: parslet
34
28
  requirement: !ruby/object:Gem::Requirement
@@ -43,45 +37,29 @@ dependencies:
43
37
  - - "~>"
44
38
  - !ruby/object:Gem::Version
45
39
  version: '2.0'
46
- - !ruby/object:Gem::Dependency
47
- name: rspec
48
- requirement: !ruby/object:Gem::Requirement
49
- requirements:
50
- - - "~>"
51
- - !ruby/object:Gem::Version
52
- version: '3.0'
53
- type: :development
54
- prerelease: false
55
- version_requirements: !ruby/object:Gem::Requirement
56
- requirements:
57
- - - "~>"
58
- - !ruby/object:Gem::Version
59
- version: '3.0'
60
- description: Handles a new SRB template type that builds on ERB, allowing JSX-like
61
- tags that render ViewComponents.`<Card></Card>` renders `CardComponent` for example.
40
+ description: ActionView .srb template handler for Sugar Cube.
62
41
  email:
63
42
  - me@fabiopapa.dev
64
43
  executables: []
65
44
  extensions: []
66
45
  extra_rdoc_files: []
67
46
  files:
68
- - ".rspec"
69
- - ".standard.yml"
70
47
  - README.md
71
48
  - Rakefile
72
49
  - lib/sugar_bee.rb
73
- - lib/sugar_bee/handler.rb
74
- - lib/sugar_bee/railtie.rb
75
- - lib/sugar_bee/srb.rb
76
- - lib/sugar_bee/srb_to_erb.rb
50
+ - lib/sugar_bee/errors.rb
51
+ - lib/sugar_bee/parser.rb
52
+ - lib/sugar_bee/srb/compiler.rb
53
+ - lib/sugar_bee/srb/grammar.rb
54
+ - lib/sugar_bee/template_handler.rb
77
55
  - lib/sugar_bee/version.rb
78
56
  - sig/sugar_bee.rbs
79
- homepage: https://github.com/fapapa/sugar_bee
57
+ homepage: https://github.com/fapapa/sugar_cube_system/tree/main/gems/sugar_bee
80
58
  licenses: []
81
59
  metadata:
82
- homepage_uri: https://github.com/fapapa/sugar_bee
83
- source_code_uri: https://github.com/fapapa/sugar_bee
84
- changelog_uri: https://github.com/fapapa/sugar_bee/changelog.md
60
+ allowed_push_host: https://rubygems.org
61
+ homepage_uri: https://github.com/fapapa/sugar_cube_system/tree/main/gems/sugar_bee
62
+ source_code_uri: https://github.com/fapapa/sugar_cube_system/tree/main/gems/sugar_bee
85
63
  rdoc_options: []
86
64
  require_paths:
87
65
  - lib
@@ -89,14 +67,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
89
67
  requirements:
90
68
  - - ">="
91
69
  - !ruby/object:Gem::Version
92
- version: 3.1.0
70
+ version: 3.2.0
93
71
  required_rubygems_version: !ruby/object:Gem::Requirement
94
72
  requirements:
95
73
  - - ">="
96
74
  - !ruby/object:Gem::Version
97
75
  version: '0'
98
76
  requirements: []
99
- rubygems_version: 3.6.8
77
+ rubygems_version: 3.6.2
100
78
  specification_version: 4
101
- summary: Syntactic sugar on top of ERB to render ViewComponents with JSX-like tags.
79
+ summary: ActionView .srb template handler for Sugar Cube.
102
80
  test_files: []
data/.rspec DELETED
@@ -1,3 +0,0 @@
1
- --format documentation
2
- --color
3
- --require spec_helper
data/.standard.yml DELETED
@@ -1,3 +0,0 @@
1
- # For available configuration options, see:
2
- # https://github.com/standardrb/standard
3
- ruby_version: 3.1
@@ -1,18 +0,0 @@
1
- require "action_view"
2
- require "action_view/template"
3
- require_relative "srb"
4
- require_relative "srb_to_erb"
5
-
6
- module SugarBee
7
- class Handler
8
- def self.call(template, source)
9
- processed = process(source)
10
- ActionView::Template.registered_template_handler(:erb).call(template, processed)
11
- end
12
-
13
- def self.process(source)
14
- srb = Srb.new.parse(source)
15
- SrbToErb.new.apply(srb)
16
- end
17
- end
18
- end
@@ -1,9 +0,0 @@
1
- module SugarBee
2
- class Railtie < Rails::Railtie
3
- initializer "sugar_bee.register_template_handler" do
4
- ActiveSupport.on_load(:action_view) do
5
- ActionView::Template.register_default_template_handler :srb, SugarBee::Handler
6
- end
7
- end
8
- end
9
- end
data/lib/sugar_bee/srb.rb DELETED
@@ -1,28 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "parslet"
4
-
5
- class SugarBee::Srb < Parslet::Parser
6
- root(:source)
7
- rule(:source) { (non_srb | srb).repeat.as(:source) }
8
-
9
- rule(:non_srb) { (srb_detector.absent? >> any).repeat(1).as(:erb) }
10
- rule(:srb) { (self_closing_tag | tag_pair).as(:srb) }
11
- rule(:self_closing_tag) do
12
- str("<") >> srb_self_close_detector.absent? >> (component_name >> vattrs.maybe).as(:simple_component) >> space? >> srb_self_close_detector
13
- end
14
- rule(:tag_pair) { (opening_tag >> source >> closing_tag).as(:tag_pair) }
15
-
16
- rule(:opening_tag) { (str("<") >> (component_name >> vattrs.maybe).as(:open) >> space? >> str(">")) }
17
- rule(:closing_tag) { (str("</") >> space? >> component_name.as(:close) >> str(">")) }
18
- rule(:component_name) { (match["A-Z"] >> match["A-Za-z"].repeat).as(:name) }
19
- rule(:vattrs) { space? >> (vattr >> space?).repeat.as(:vattrs) }
20
- rule(:vattr) { match["-a-z0-9"].repeat(1).as(:val) }
21
-
22
- rule(:srb_detector) { (srb_open_detector | srb_close_detector) }
23
- rule(:srb_open_detector) { str("<") >> match["A-Z"] }
24
- rule(:srb_close_detector) { str("</") >> space? >> match["A-Z"] }
25
- rule(:srb_self_close_detector) { str("/>") }
26
- rule(:space) { str("\s") }
27
- rule(:space?) { space.maybe }
28
- end
@@ -1,32 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "parslet"
4
-
5
- class SugarBee::SrbToErb < Parslet::Transform
6
- rule(source: sequence(:erb_fragments)) { erb_fragments.join }
7
- rule(
8
- srb: {
9
- simple_component: {
10
- name: simple(:component_name),
11
- vattrs: sequence(:values)
12
- }
13
- }
14
- ) do
15
- args = "(vattrs: %w[#{values.join(" ")}])" if values.any?
16
- "<%= render(#{component_name}Component.new#{args}) %>"
17
- end
18
- rule(
19
- srb: {
20
- tag_pair: {
21
- open: {name: simple(:component_name), vattrs: sequence(:values)},
22
- source: sequence(:erb_fragments),
23
- close: {name: simple(:component_name)}
24
- }
25
- }
26
- ) do
27
- args = "(vattrs: %w[#{values.join(" ")}])" if values.any?
28
- "<%= render(#{component_name}Component.new#{args}) do %>#{erb_fragments.join}<% end %>"
29
- end
30
- rule(erb: simple(:content)) { content }
31
- rule(val: simple(:vattr)) { vattr }
32
- end