span_view 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/README.md +35 -0
- data/Rakefile +11 -0
- data/lib/span_view/errors.rb +9 -0
- data/lib/span_view/parser.rb +30 -0
- data/lib/span_view/span/compiler.rb +72 -0
- data/lib/span_view/span/grammar.rb +80 -0
- data/lib/span_view/template_handler.rb +15 -0
- data/lib/span_view/version.rb +5 -0
- data/lib/span_view.rb +7 -0
- data/sig/span_view.rbs +4 -0
- metadata +80 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: a817462b757b21ce691f3c8c6a69db01a42ac1dc1a441fbca983c16621fbc76f
|
|
4
|
+
data.tar.gz: bcf9b53f48db929ee28596a65de0512adcad12cc920ead5714191e894bc1dcc6
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 144d54323855f13488f79ca44e41274eb814ff4ee8fa50506909f599b267611cbcd61951ebe281ee9d3d35b962cddfea3817a4ded0cce47c6a8a5fde834796df
|
|
7
|
+
data.tar.gz: f499383736eab6bc7967c3b9415c5c353fd956b841f63256086f19cf96b303f62b3185cba0da9928e79c71e433f765ea0b821e420eb60326fc3881b4a284bd46
|
data/README.md
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# SpanView
|
|
2
|
+
|
|
3
|
+
TODO: Delete this and the text below, and describe your gem
|
|
4
|
+
|
|
5
|
+
Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/span_view`. To experiment with that code, run `bin/console` for an interactive prompt.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
TODO: Replace `UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG` with your gem name right after releasing it to RubyGems.org. Please do not do it earlier due to security reasons. Alternatively, replace this section with instructions to install your gem from git if you don't plan to release to RubyGems.org.
|
|
10
|
+
|
|
11
|
+
Install the gem and add to the application's Gemfile by executing:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
bundle add UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
If bundler is not being used to manage dependencies, install the gem by executing:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
gem install UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Usage
|
|
24
|
+
|
|
25
|
+
TODO: Write usage instructions here
|
|
26
|
+
|
|
27
|
+
## Development
|
|
28
|
+
|
|
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
|
+
|
|
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
|
+
|
|
33
|
+
## Contributing
|
|
34
|
+
|
|
35
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/fapapa/league.
|
data/Rakefile
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "parslet"
|
|
4
|
+
require_relative "errors"
|
|
5
|
+
require_relative "span/grammar"
|
|
6
|
+
require_relative "span/compiler"
|
|
7
|
+
|
|
8
|
+
module SpanView
|
|
9
|
+
# Public interface: `.span` source string in → compiled ERB string out.
|
|
10
|
+
# Pure function, no Rails coupling. Parses with the Span::Grammar, compiles with
|
|
11
|
+
# the Span::Compiler transform.
|
|
12
|
+
module Parser
|
|
13
|
+
def self.call(source)
|
|
14
|
+
Span::Compiler.new.apply(Span::Grammar.new.parse(source))
|
|
15
|
+
rescue Parslet::ParseFailed => e
|
|
16
|
+
raise ParseError, "Invalid .span 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 SpanView
|
|
7
|
+
module Span
|
|
8
|
+
# Compiles an Span::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 SpanView::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 SpanView::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 SpanView::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 SpanView::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 SpanView
|
|
6
|
+
module Span
|
|
7
|
+
# PEG grammar for the `.span` template language (structure only; codegen lives
|
|
8
|
+
# in Span::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 SpanView
|
|
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(:span, SpanView::TemplateHandler)
|
data/lib/span_view.rb
ADDED
data/sig/span_view.rbs
ADDED
metadata
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: span_view
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Fabio Papa
|
|
8
|
+
bindir: exe
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 2026-07-19 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: actionview
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '7.1'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '7.1'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: parslet
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - "~>"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '2.0'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '2.0'
|
|
40
|
+
description: Span the gap. ActionView .span template handler for the League system.
|
|
41
|
+
email:
|
|
42
|
+
- me@fabiopapa.dev
|
|
43
|
+
executables: []
|
|
44
|
+
extensions: []
|
|
45
|
+
extra_rdoc_files: []
|
|
46
|
+
files:
|
|
47
|
+
- README.md
|
|
48
|
+
- Rakefile
|
|
49
|
+
- lib/span_view.rb
|
|
50
|
+
- lib/span_view/errors.rb
|
|
51
|
+
- lib/span_view/parser.rb
|
|
52
|
+
- lib/span_view/span/compiler.rb
|
|
53
|
+
- lib/span_view/span/grammar.rb
|
|
54
|
+
- lib/span_view/template_handler.rb
|
|
55
|
+
- lib/span_view/version.rb
|
|
56
|
+
- sig/span_view.rbs
|
|
57
|
+
homepage: https://github.com/fapapa/league/tree/main/gems/span_view
|
|
58
|
+
licenses: []
|
|
59
|
+
metadata:
|
|
60
|
+
allowed_push_host: https://rubygems.org
|
|
61
|
+
homepage_uri: https://github.com/fapapa/league/tree/main/gems/span_view
|
|
62
|
+
source_code_uri: https://github.com/fapapa/league/tree/main/gems/span_view
|
|
63
|
+
rdoc_options: []
|
|
64
|
+
require_paths:
|
|
65
|
+
- lib
|
|
66
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
67
|
+
requirements:
|
|
68
|
+
- - ">="
|
|
69
|
+
- !ruby/object:Gem::Version
|
|
70
|
+
version: 3.2.0
|
|
71
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
72
|
+
requirements:
|
|
73
|
+
- - ">="
|
|
74
|
+
- !ruby/object:Gem::Version
|
|
75
|
+
version: '0'
|
|
76
|
+
requirements: []
|
|
77
|
+
rubygems_version: 3.6.2
|
|
78
|
+
specification_version: 4
|
|
79
|
+
summary: Span the gap.
|
|
80
|
+
test_files: []
|