rubyoshka 0.1
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 +4 -0
- data/README.md +187 -0
- data/lib/rubyoshka.rb +189 -0
- data/lib/rubyoshka/version.rb +5 -0
- metadata +123 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: dcac0e7abbbb0bad4f11b3c3b500ed7c14f506d06c6aa8ecbcec4cfc3503ce16
|
4
|
+
data.tar.gz: 8fd105613444edd272644aff2cb054b4c0a540789f5e63f41a4d9c033fc36577
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 4f693a62532ae3235383068ed789cc144fd38ec7723c5ff728ace5b0a5b0ae267e61fc3e8140ac8972420a8c35fa0731b543dbd84016bda049843038ae5bafa4
|
7
|
+
data.tar.gz: 3ca48bcd081c077635cffa99d36823e2f19c1ac832a26426ae510e233d1054880a0e74b3047aeef389dcb88b4905a95339e90145cd131efcd2a8edba23599dea
|
data/CHANGELOG.md
ADDED
data/README.md
ADDED
@@ -0,0 +1,187 @@
|
|
1
|
+
# Rubyoshka - Composable HTML templating for Ruby
|
2
|
+
|
3
|
+
[INSTALL](#installing-rubyoshka) |
|
4
|
+
[TUTORIAL](#getting-started) |
|
5
|
+
[EXAMPLES](examples)
|
6
|
+
|
7
|
+
## What is Rubyoshka
|
8
|
+
|
9
|
+
Rubyoshka is an HTML templating engine for Ruby that offers the following
|
10
|
+
features:
|
11
|
+
|
12
|
+
- HTML templating using plain Ruby syntax
|
13
|
+
- Minimal boilerplate
|
14
|
+
- Automatic HTML escaping
|
15
|
+
- Composable nested components
|
16
|
+
- Access to a global context from anywhere in the component hierarchy
|
17
|
+
- High performance
|
18
|
+
|
19
|
+
With Rubyoshka you can structure your templates like a Russian doll, each
|
20
|
+
component containing any number of nested components, in a similar fashion to
|
21
|
+
React. The name *Rubyoshka* is a nod to Matryoshka, the Russian nesting doll.
|
22
|
+
|
23
|
+
## Installing Rubyoshka
|
24
|
+
|
25
|
+
```bash
|
26
|
+
$ gem install polyphony
|
27
|
+
```
|
28
|
+
|
29
|
+
## Getting started
|
30
|
+
|
31
|
+
To use Rubyoshka in your code just require it:
|
32
|
+
|
33
|
+
```ruby
|
34
|
+
require 'rubyoshka'
|
35
|
+
```
|
36
|
+
|
37
|
+
Alternatively, you can import it using [Modulation](https://github.com/digital-fabric/modulation):
|
38
|
+
|
39
|
+
```ruby
|
40
|
+
Rubyoshka = import('rubyoshka')
|
41
|
+
```
|
42
|
+
|
43
|
+
To create a template use `Rubyoshka.new` or the global method `Kernel#H`:
|
44
|
+
|
45
|
+
```ruby
|
46
|
+
# can also use Rubyoshka.new
|
47
|
+
html = H {
|
48
|
+
div { p 'hello' }
|
49
|
+
}
|
50
|
+
```
|
51
|
+
|
52
|
+
To render the template use `render`:
|
53
|
+
|
54
|
+
```ruby
|
55
|
+
H { span 'best span' }.render #=> "<span>best span</span>"
|
56
|
+
```
|
57
|
+
|
58
|
+
The render method accepts an arbitrary context variable:
|
59
|
+
|
60
|
+
```ruby
|
61
|
+
html = H {
|
62
|
+
h1 context[:title]
|
63
|
+
}
|
64
|
+
|
65
|
+
html.render(title: 'My title') #=> "<h1>My title</h1>"
|
66
|
+
```
|
67
|
+
|
68
|
+
## All about tags
|
69
|
+
|
70
|
+
Tags are added using unqualified method calls, and are nested using blocks:
|
71
|
+
|
72
|
+
```ruby
|
73
|
+
H {
|
74
|
+
html {
|
75
|
+
head {
|
76
|
+
title 'page title'
|
77
|
+
}
|
78
|
+
body {
|
79
|
+
article {
|
80
|
+
h1 'article title'
|
81
|
+
}
|
82
|
+
}
|
83
|
+
}
|
84
|
+
}
|
85
|
+
```
|
86
|
+
|
87
|
+
Tag methods accept a string argument, a block, or no argument at all:
|
88
|
+
|
89
|
+
```ruby
|
90
|
+
H { p 'hello' }.render #=> "<p>hello</p>"
|
91
|
+
|
92
|
+
H { p { span '1'; span '2' } }.render #=> "<p><span>1</span><span>2</span></p>"
|
93
|
+
|
94
|
+
H { hr() }.render #=> "<hr/>"
|
95
|
+
```
|
96
|
+
|
97
|
+
Tag methods also accept tag attributes, given as a hash:
|
98
|
+
|
99
|
+
```ruby
|
100
|
+
H { img src: '/my.gif' }.render #=> "<img src="/my.gif"/>
|
101
|
+
|
102
|
+
H { p "foobar", class: 'important' }.render #=> "<p class=\"important\">foobar</p>"
|
103
|
+
```
|
104
|
+
|
105
|
+
## Templates as components
|
106
|
+
|
107
|
+
Rubyoshka makes it easy to compose multiple templates into a whole HTML
|
108
|
+
document. Each template can be defined as a self-contained component that can
|
109
|
+
be reused inside other components. Components should be defined as constants,
|
110
|
+
either in the global namespace, or on the Rubyoshka namespace:
|
111
|
+
|
112
|
+
```ruby
|
113
|
+
# Item is actually a Proc that returns a template
|
114
|
+
Item = ->(id:, text:, checked:) {
|
115
|
+
H {
|
116
|
+
li {
|
117
|
+
input name: id, type: 'checkbox', checked: checked
|
118
|
+
label text, for: id
|
119
|
+
}
|
120
|
+
}
|
121
|
+
}
|
122
|
+
|
123
|
+
def render_items(items)
|
124
|
+
html = H {
|
125
|
+
ul {
|
126
|
+
items.each { |id, attributes|
|
127
|
+
Item id: id, text: attributes[:text], checked: attributes[:active]
|
128
|
+
}
|
129
|
+
}
|
130
|
+
}.render
|
131
|
+
end
|
132
|
+
```
|
133
|
+
|
134
|
+
## Wrapping arbitrary HTML
|
135
|
+
|
136
|
+
Components can be used to wrap arbitrary HTML content by defining them as procs
|
137
|
+
that accept blocks:
|
138
|
+
|
139
|
+
```ruby
|
140
|
+
Header = ->(&inner_html) {
|
141
|
+
header {
|
142
|
+
h1 'title'
|
143
|
+
emit inner_html
|
144
|
+
}
|
145
|
+
}
|
146
|
+
|
147
|
+
H { Header { button 'OK'} }.render #=> "<header><h1>title</h1><button>OK</button></header>"
|
148
|
+
```
|
149
|
+
|
150
|
+
## Some interesting use cases
|
151
|
+
|
152
|
+
Rubyoshka opens up all kinds of new possibilities when it comes to putting
|
153
|
+
together pieces of HTML. Feel free to explore the possibilities!
|
154
|
+
|
155
|
+
### Routing in the view
|
156
|
+
|
157
|
+
The following example demonstrates a router component implemented as a pure
|
158
|
+
function. The router simply returns the correct component for the given path:
|
159
|
+
|
160
|
+
```ruby
|
161
|
+
Router = ->(path) {
|
162
|
+
case path
|
163
|
+
when '/'
|
164
|
+
PostIndex()
|
165
|
+
when /^posts\/(.+)$/
|
166
|
+
post = get_post($1)
|
167
|
+
if post
|
168
|
+
Post(post)
|
169
|
+
else
|
170
|
+
ErrorPage(404)
|
171
|
+
end
|
172
|
+
end
|
173
|
+
}
|
174
|
+
|
175
|
+
Blog = H {
|
176
|
+
html {
|
177
|
+
head {
|
178
|
+
title: 'My blog'
|
179
|
+
}
|
180
|
+
body {
|
181
|
+
Topbar()
|
182
|
+
Sidebar()
|
183
|
+
div id: 'content' { Router(context[:path]) }
|
184
|
+
}
|
185
|
+
}
|
186
|
+
}
|
187
|
+
```
|
data/lib/rubyoshka.rb
ADDED
@@ -0,0 +1,189 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'modulation/gem'
|
4
|
+
require 'escape_utils'
|
5
|
+
|
6
|
+
export_default :Rubyoshka
|
7
|
+
|
8
|
+
# A Rubyoshka is a template representing a piece of HTML
|
9
|
+
class Rubyoshka
|
10
|
+
# A Rendering is a rendering of a Rubyoshka
|
11
|
+
class Rendering
|
12
|
+
attr_reader :context
|
13
|
+
|
14
|
+
# Initializes attributes and renders the given block
|
15
|
+
# @param context [Hash] rendering context
|
16
|
+
# @param block [Proc] template block
|
17
|
+
# @return [void]
|
18
|
+
def initialize(context, &block)
|
19
|
+
@context = context
|
20
|
+
@buffer = +''
|
21
|
+
instance_eval(&block)
|
22
|
+
end
|
23
|
+
|
24
|
+
# Returns the result of the rendering
|
25
|
+
# @return [String]
|
26
|
+
def to_s
|
27
|
+
@buffer
|
28
|
+
end
|
29
|
+
|
30
|
+
S_TAG_METHOD = <<~EOF
|
31
|
+
def %1$s(*args, &block)
|
32
|
+
tag(:%1$s, *args, &block)
|
33
|
+
end
|
34
|
+
EOF
|
35
|
+
|
36
|
+
R_CONST_SYM = /^[A-Z]/
|
37
|
+
|
38
|
+
# Catches undefined tag method call and handles them by defining the method
|
39
|
+
# @param sym [Symbol] HTML tag or component identifier
|
40
|
+
# @param args [Array] method call arguments
|
41
|
+
# @param block [Proc] block passed to method call
|
42
|
+
# @return [void]
|
43
|
+
def method_missing(sym, *args, &block)
|
44
|
+
if sym =~ R_CONST_SYM
|
45
|
+
o = instance_eval(sym.to_s) rescue Rubyoshka.const_get(sym) \
|
46
|
+
rescue Object.const_get(sym)
|
47
|
+
case o
|
48
|
+
when ::Proc
|
49
|
+
self.class.define_method(sym) { |*a, &b| emit o.(*a, &b) }
|
50
|
+
emit o.(*args, &block)
|
51
|
+
when Rubyoshka
|
52
|
+
self.class.define_method(sym) { emit o }
|
53
|
+
emit(o)
|
54
|
+
when ::String
|
55
|
+
@buffer << o
|
56
|
+
else
|
57
|
+
e = StandardError.new "Cannot render #{o.inspect}"
|
58
|
+
e.set_backtrace(caller)
|
59
|
+
raise e
|
60
|
+
end
|
61
|
+
else
|
62
|
+
self.class.class_eval(S_TAG_METHOD % sym)
|
63
|
+
tag(sym, *args, &block)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
# Emits the given object into the rendering buffer
|
68
|
+
# @param o [Proc, Rubyoshka, String] emitted object
|
69
|
+
# @return [void]
|
70
|
+
def emit(o)
|
71
|
+
case o
|
72
|
+
when ::Proc
|
73
|
+
instance_eval(&o)
|
74
|
+
when Rubyoshka
|
75
|
+
instance_eval(&o.block)
|
76
|
+
else
|
77
|
+
@buffer << o.to_s
|
78
|
+
end
|
79
|
+
end
|
80
|
+
alias_method :e, :emit
|
81
|
+
|
82
|
+
S_LT = '<'
|
83
|
+
S_GT = '>'
|
84
|
+
S_LT_SLASH = '</'
|
85
|
+
S_SPACE_LT_SLASH = ' </'
|
86
|
+
S_SLASH_GT = '/>'
|
87
|
+
S_SPACE = ' '
|
88
|
+
S_EQUAL_QUOTE = '="'
|
89
|
+
S_QUOTE = '"'
|
90
|
+
|
91
|
+
E = EscapeUtils
|
92
|
+
|
93
|
+
# Emits an HTML tag
|
94
|
+
# @param sym [Symbol] HTML tag
|
95
|
+
# @param text [String] text content of tag
|
96
|
+
# @param props [Hash] tag attributes
|
97
|
+
# @param block [Proc] nested HTML block
|
98
|
+
# @return [void]
|
99
|
+
def tag(sym, text = nil, **props, &block)
|
100
|
+
sym = sym.to_s
|
101
|
+
|
102
|
+
@buffer << S_LT << sym
|
103
|
+
emit_props(props) unless props.empty?
|
104
|
+
|
105
|
+
if block
|
106
|
+
@buffer << S_GT
|
107
|
+
instance_eval(&block)
|
108
|
+
@buffer << S_LT_SLASH << sym << S_GT
|
109
|
+
elsif Rubyoshka === text
|
110
|
+
@buffer << S_GT
|
111
|
+
emit(text)
|
112
|
+
@buffer << S_LT_SLASH << sym << S_GT
|
113
|
+
elsif text
|
114
|
+
@buffer << S_GT << E.escape_html(text.to_s) <<
|
115
|
+
S_LT_SLASH << sym << S_GT
|
116
|
+
else
|
117
|
+
@buffer << S_SLASH_GT
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
# Emits tag attributes into the rendering buffer
|
122
|
+
# @param props [Hash] tag attributes
|
123
|
+
# @return [void]
|
124
|
+
def emit_props(props)
|
125
|
+
props.each { |k, v|
|
126
|
+
case k
|
127
|
+
when :text
|
128
|
+
when :src, :href
|
129
|
+
@buffer << S_SPACE << k.to_s << S_EQUAL_QUOTE <<
|
130
|
+
E.escape_uri(v) << S_QUOTE
|
131
|
+
else
|
132
|
+
if v == true
|
133
|
+
@buffer << S_SPACE << k.to_s
|
134
|
+
else
|
135
|
+
@buffer << S_SPACE << k.to_s << S_EQUAL_QUOTE << v << S_QUOTE
|
136
|
+
end
|
137
|
+
end
|
138
|
+
}
|
139
|
+
end
|
140
|
+
|
141
|
+
# Emits the p tag
|
142
|
+
# @param text [String] text content of tag
|
143
|
+
# @param props [Hash] tag attributes
|
144
|
+
# @para block [Proc] nested HTML block
|
145
|
+
# @return [void]
|
146
|
+
def p(text = nil, **props, &block)
|
147
|
+
tag(:p, text, **props, &block)
|
148
|
+
end
|
149
|
+
|
150
|
+
S_HTML5_DOCTYPE = '<!DOCTYPE html>'
|
151
|
+
|
152
|
+
# Emits an HTML5 doctype tag and an html tag with the given block
|
153
|
+
# @param block [Proc] nested HTML block
|
154
|
+
# @return [void]
|
155
|
+
def html5(&block)
|
156
|
+
@buffer << S_HTML5_DOCTYPE
|
157
|
+
self.html(&block)
|
158
|
+
end
|
159
|
+
|
160
|
+
# Emits text into the rendering buffer
|
161
|
+
# @param data [String] text
|
162
|
+
def text(data)
|
163
|
+
@buffer << E.escape_html(text)
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
attr_reader :block
|
168
|
+
|
169
|
+
# Initializes a Rubyoshka with the given block
|
170
|
+
# @param block [Proc] nested HTML block
|
171
|
+
# @param [void]
|
172
|
+
def initialize(&block)
|
173
|
+
@block = block
|
174
|
+
end
|
175
|
+
|
176
|
+
# Renders the associated block and returns the string result
|
177
|
+
# @param context [Hash] context
|
178
|
+
# @return [String]
|
179
|
+
def render(context = {})
|
180
|
+
Rendering.new(context, &block).to_s
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
module ::Kernel
|
185
|
+
# Convenience method for creating a new Rubyoshka
|
186
|
+
def H(&block)
|
187
|
+
Rubyoshka.new(&block)
|
188
|
+
end
|
189
|
+
end
|
metadata
ADDED
@@ -0,0 +1,123 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: rubyoshka
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: '0.1'
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Sharon Rosner
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2019-01-06 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: modulation
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - '='
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0.18'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - '='
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0.18'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: escape_utils
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - '='
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 1.2.1
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - '='
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 1.2.1
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: minitest
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - '='
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: 5.11.3
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - '='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 5.11.3
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: benchmark-ips
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - '='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 2.7.2
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - '='
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: 2.7.2
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: erubis
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - '='
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: 2.7.0
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - '='
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: 2.7.0
|
83
|
+
description:
|
84
|
+
email: ciconia@gmail.com
|
85
|
+
executables: []
|
86
|
+
extensions: []
|
87
|
+
extra_rdoc_files:
|
88
|
+
- README.md
|
89
|
+
files:
|
90
|
+
- CHANGELOG.md
|
91
|
+
- README.md
|
92
|
+
- lib/rubyoshka.rb
|
93
|
+
- lib/rubyoshka/version.rb
|
94
|
+
homepage: http://github.com/digital-fabric/rubyoshka
|
95
|
+
licenses:
|
96
|
+
- MIT
|
97
|
+
metadata:
|
98
|
+
source_code_uri: https://github.com/digital-fabric/rubyoshka
|
99
|
+
post_install_message:
|
100
|
+
rdoc_options:
|
101
|
+
- "--title"
|
102
|
+
- rubyoshka
|
103
|
+
- "--main"
|
104
|
+
- README.md
|
105
|
+
require_paths:
|
106
|
+
- lib
|
107
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
108
|
+
requirements:
|
109
|
+
- - ">="
|
110
|
+
- !ruby/object:Gem::Version
|
111
|
+
version: '0'
|
112
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
113
|
+
requirements:
|
114
|
+
- - ">="
|
115
|
+
- !ruby/object:Gem::Version
|
116
|
+
version: '0'
|
117
|
+
requirements: []
|
118
|
+
rubyforge_project:
|
119
|
+
rubygems_version: 2.7.3
|
120
|
+
signing_key:
|
121
|
+
specification_version: 4
|
122
|
+
summary: 'Rubyoshka: composable HTML templating for Ruby'
|
123
|
+
test_files: []
|