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 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
@@ -0,0 +1,4 @@
1
+ 0.1 2019-01-06
2
+ --------------
3
+
4
+ * First working version
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
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Rubyoshka
4
+ VERSION = '0.1'
5
+ 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: []