ruex 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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 13224922b7e3d224c8519f480b1cd5ba0b0c5838330af7f240520a703c8f149f
4
+ data.tar.gz: aee7340b08113ad97b628bcb056c5069428e51e982d74af4645f0849858ab7cd
5
+ SHA512:
6
+ metadata.gz: 224c71be4526088840e75d8440a8ce07ca8b5e6a1a76f922f2a1700af635114b3f3f0b0603956dbd2688665796cb1c16c74fdc06e1da0aca49bae6d318f2fb92
7
+ data.tar.gz: c615a249f8a61ea13e6d5b50851a725c97c0c16a6b2f870e6f1befd7f3d445c8bb3e93f64dc0699aa29a8cc04a7ca8b2ad3693d75d35933a271c180b592e26f3
data/LICENSE ADDED
@@ -0,0 +1,24 @@
1
+ BSD 2-Clause License
2
+
3
+ Copyright (c) 2026, takanobu maekawa
4
+
5
+ Redistribution and use in source and binary forms, with or without
6
+ modification, are permitted provided that the following conditions are met:
7
+
8
+ 1. Redistributions of source code must retain the above copyright notice, this
9
+ list of conditions and the following disclaimer.
10
+
11
+ 2. Redistributions in binary form must reproduce the above copyright notice,
12
+ this list of conditions and the following disclaimer in the documentation
13
+ and/or other materials provided with the distribution.
14
+
15
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
16
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
17
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
18
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
19
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
20
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
21
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
22
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
23
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
24
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
data/README.md ADDED
@@ -0,0 +1,205 @@
1
+ # ruex
2
+
3
+ A library and CLI tool that generates HTML using plain Ruby expressions.
4
+ It is intended for static site or page generation, and is not suitable as a dynamic web page renderer.
5
+
6
+ Technically, you only need to know HTML, CSS, and Ruby — nothing more.
7
+ Easy to extend when needed.
8
+
9
+ ---
10
+
11
+ ## Installation
12
+
13
+ ```sh
14
+ gem install ruex
15
+ ```
16
+
17
+ Or add to your Gemfile:
18
+
19
+ ```ruby
20
+ gem "ruex"
21
+ ```
22
+
23
+ ---
24
+
25
+ ## Quick Example
26
+
27
+ ```shell
28
+ $ ruex -e 'div { p "hello" }'
29
+ <div><p>hello</p></div>
30
+
31
+ # same thing by the command pipeline
32
+ $ echo 'div { p "hello" }' | ruex
33
+ <div><p>hello</p></div>
34
+ ```
35
+
36
+ ## How it works
37
+
38
+ Every standard HTML tag is available as a Ruby method.
39
+
40
+ ```shell
41
+ $ ruex -e 'p "hello"'
42
+ <p>hello</p>
43
+ ```
44
+
45
+ ### Attributes
46
+
47
+ ```shell
48
+ $ ruex -e 'p "Hi", class: "msg"'
49
+ <p class="msg">Hi</p>
50
+ ```
51
+
52
+ ### Blocks
53
+
54
+ You can write child elements inside blocks:
55
+
56
+ ```shell
57
+ $ ruex -e 'div { p "Hello" }'
58
+ <div><p>Hello</p></div>
59
+ ```
60
+
61
+ ### Mixing Ruby code
62
+
63
+ You can freely mix Ruby code.
64
+
65
+ ```shell
66
+ $ cat list.html.rx
67
+ ul{
68
+ %w[Foo Bar].each do |name|
69
+ li(name)
70
+ end
71
+ }
72
+
73
+ $ cat list.html.rx | ruex
74
+ <ul><li>Foo</li><li>Bar</li></ul>
75
+ ```
76
+
77
+ ### Variable binding
78
+
79
+ ```shell
80
+ $ cat table.html.rx
81
+ table {
82
+ people.each do |person|
83
+ tr{ td(person[:name]) }
84
+ end
85
+ }
86
+
87
+ $ cat table.html.rx | ruex -b '{people: [{name: "hoge"}, {name: "bar"}]}'
88
+ <table><tr><td>hoge</td></tr><tr><td>bar</td></tr></table>
89
+ ```
90
+
91
+ ### Embedding text
92
+
93
+ `text` and `_` functions embed texts in HTML:
94
+
95
+ ```shell
96
+ $ ruex -e 'text "Today is a good day"'
97
+ Today is a good day
98
+
99
+ $ ruex -e '_"Today is a good day"'
100
+ Today is a good day
101
+
102
+ $ ruex -e 'div{ _"Today is a good day" }'
103
+ <div>Today is a good day</div>
104
+ ```
105
+
106
+ ### HTML comments
107
+
108
+ ruex does not provide a comment function.
109
+ If you want to include an HTML comment, write it directly as a string:
110
+
111
+ ```shell
112
+ $ ruex -e 'div { _"<!-- note -->" }'
113
+ <div><!-- note --></div>
114
+ ```
115
+
116
+ ### Custom Tags
117
+
118
+ Just compose HTML using ruex expressions.
119
+
120
+ ```ruby
121
+ # my_tags.rb
122
+
123
+ require 'ruex'
124
+
125
+ module MyTag
126
+ include Ruex
127
+
128
+ def card(title, &block)
129
+ div(class: "card"){
130
+ h2 title
131
+ block.call if block_given?
132
+ }
133
+ end
134
+ end
135
+ ```
136
+
137
+ There are a few things you should know:
138
+
139
+ - you must provide your custom tag library as a module.
140
+ - Your module name must correspond to its file name (PascalCase → snake_case).
141
+ ex) MyTag -> my_tag.rb
142
+
143
+
144
+ You can use your library throuth `ruex` command, of course.
145
+
146
+ ```shell
147
+ $ ruex -I /your/ruby/load/path -r my_tags.rb -e 'card("Hello") { p "world" }'
148
+ <div class="card"><h2>Hello</h2><p>world</p></div>
149
+ ```
150
+
151
+ If you make your custom tag library as Ruby gems, you don't need to specify `-I`.
152
+
153
+ ### CLI options
154
+
155
+ ```shell
156
+ $ ruex -h
157
+ Render ruex expressions as HTML.
158
+
159
+ Usage:
160
+ echo 'p "hello"' | ruex
161
+ ruex -e 'p "hello"'
162
+ ruex -f template.ruex
163
+
164
+ Options:
165
+ -I, --include-path PATH Add PATH to $LOAD_PATH
166
+ -r, --require LIB Require LIB and include its module into Ruex::Core
167
+ -e, --expr EXPR Evaluate EXPR instead of reading from stdin
168
+ -f, --file FILE Read Ruex DSL from FILE
169
+ -b, --bind KEY=VALUE Bind a variable available inside Ruex DSL
170
+ -c, --context-file FILE Load variables from YAML or JSON file
171
+ -v, --version Show Ruex version
172
+ -h, --help Show this help message
173
+ ```
174
+
175
+ ### Using in program code
176
+
177
+ 1. To use ruex in Ruby code, include `Ruex`
178
+ 2. use `render` method
179
+
180
+ ```ruby
181
+ require 'ruex'
182
+
183
+ include Ruex
184
+
185
+ render 'div { _"Hello, World!!" }'
186
+ ```
187
+
188
+ All tag name methods such as `div`, `p` and so on are **not intended to use outside the string literals.**
189
+
190
+ ## Intended Use / Safety Notes
191
+ ruex evaluates Ruby expressions directly.
192
+ Use it with trusted, developer-authored templates such as static site content.
193
+ It is not intended for rendering untrusted input in web applications.
194
+
195
+ ## License
196
+
197
+ ruex is distributed under the BSD 2-Clause License (SPDX: BSD-2-Clause).
198
+ See the LICENSE file for details.
199
+
200
+ ## Author
201
+
202
+ Takanobu Maekawa
203
+
204
+ https://github.com/tmkw
205
+
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.1.0
data/bin/ruex ADDED
@@ -0,0 +1,82 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "optparse"
4
+ require "stringio"
5
+ require "ruex/cli"
6
+ require "ruex/version"
7
+ require "ruex/helper/cli"
8
+
9
+ include Ruex::Helper::CLI
10
+
11
+ load_paths = []
12
+ require_files = []
13
+ expr = nil
14
+ file_path = nil
15
+ bindings = {}
16
+ context_file = nil
17
+
18
+ opts = OptionParser.new do |opt|
19
+ opt.on("-I", "--include-path PATH") { |path| load_paths << path }
20
+ opt.on("-r", "--require LIB") { |lib| require_files << lib }
21
+ opt.on("-e", "--expr EXPR") { |e| expr = e }
22
+ opt.on("-f", "--file FILE") { |path| file_path = path }
23
+
24
+ opt.on("-b", "--bind RUBY_HASH") do |str|
25
+ begin
26
+ hash = ContextFile.read(StringIO.new(str))
27
+ bindings.merge!(hash)
28
+ rescue => e
29
+ warn e.message
30
+ exit(1)
31
+ end
32
+ end
33
+
34
+ opt.on("-c", "--context-file FILE") do |file|
35
+ context_file = file
36
+ end
37
+
38
+ opt.on("-h", "--help") do
39
+ puts load_help
40
+ exit(0)
41
+ end
42
+
43
+ opt.on("-v", "--version") do
44
+ puts Ruex::VERSION
45
+ exit(0)
46
+ end
47
+ end
48
+
49
+ begin
50
+ opts.parse!(ARGV)
51
+ rescue OptionParser::InvalidOption => e
52
+ warn "Unknown option: #{e.args.join(" ")}"
53
+ exit(1)
54
+ end
55
+
56
+ # Load context file
57
+ if context_file
58
+ begin
59
+ bindings.merge!(ContextFile.read(context_file))
60
+ rescue => e
61
+ warn e.message
62
+ exit(1)
63
+ end
64
+ end
65
+
66
+ # Load paths
67
+ load_paths.each { |p| $LOAD_PATH.unshift(p) }
68
+
69
+ # load custom tag libraries
70
+ require_files.each do |lib|
71
+ require lib
72
+ mod_name = path_to_module_name(lib)
73
+ Ruex::Core.include(Object.const_get(mod_name))
74
+ end
75
+
76
+ # Execute
77
+ Ruex::CLI.new.run(
78
+ expr: expr,
79
+ file: file_path,
80
+ ctx: bindings
81
+ )
82
+
data/lib/ruex/cli.rb ADDED
@@ -0,0 +1,23 @@
1
+ require 'ruex'
2
+
3
+ module Ruex
4
+ class CLI
5
+ include Ruex
6
+
7
+ def run(expr: nil, file: nil, ctx: {})
8
+ input = if expr
9
+ expr
10
+ elsif file
11
+ File.read(file)
12
+ else
13
+ $stdin.read
14
+ end
15
+
16
+ input = input.to_s.strip
17
+ return if input.empty?
18
+
19
+ puts render(input, ctx: ctx)
20
+ end
21
+ end
22
+ end
23
+
data/lib/ruex/core.rb ADDED
@@ -0,0 +1,88 @@
1
+ module Ruex
2
+ class Core
3
+ require 'yaml'
4
+
5
+ TAGS = YAML.load_file([__dir__, "tags.yml"].join('/'))
6
+
7
+ def initialize
8
+ @output = ''.dup
9
+ end
10
+
11
+ def render_attrs(attrs, boolean_attrs)
12
+ return '' if attrs.empty?
13
+
14
+ parts = []
15
+
16
+ # data attributes
17
+ if attrs.key?(:data)
18
+ data_hash = attrs.delete(:data)
19
+ data_hash.each do |k, v|
20
+ html_key = "data-#{k.to_s.gsub('_', '-')}"
21
+ parts << %(#{html_key}="#{v}")
22
+ end
23
+ end
24
+
25
+ attrs.each do |k, v|
26
+ if boolean_attrs&.include?(k)
27
+ # boolean attributes
28
+ parts << k.to_s if v # use only when the option is enabled
29
+ else
30
+ # normal attributes
31
+ parts << %(#{k}="#{v}")
32
+ end
33
+ end
34
+
35
+ parts.join(" ")
36
+ end
37
+
38
+ def render_tag(tag_def, args, attrs, block)
39
+ name = tag_def['name']
40
+ void = tag_def['void']
41
+ boolean_attrs = (tag_def['boolean'] || []).map(&:to_sym)
42
+
43
+ rendered_attrs = render_attrs(attrs, boolean_attrs)
44
+ attr_str = rendered_attrs.empty? ? '' : %( #{rendered_attrs})
45
+
46
+ if void
47
+ @output << "<#{name}#{attr_str}>".dup
48
+ return
49
+ end
50
+
51
+ @output << "<#{name}#{attr_str}>".dup
52
+
53
+ if block
54
+ block.call
55
+ elsif args.first
56
+ @output << args.first.to_s
57
+ end
58
+
59
+ @output << "</#{name}>".dup
60
+ end
61
+
62
+ TAGS.each do |tag|
63
+ define_method(tag['name']) do |*args, **attrs, &block|
64
+ render_tag(tag, args, attrs, block)
65
+ end
66
+ end
67
+
68
+ # for text node
69
+ def text(str)
70
+ @output << str.to_s
71
+ end
72
+
73
+ alias _ text
74
+
75
+ def render(code, ctx: {})
76
+ @output = "".dup
77
+
78
+ ___binding___ = binding
79
+ ctx.each do |k, v|
80
+ ___binding___.local_variable_set(k, v)
81
+ end
82
+
83
+ eval(code, ___binding___, __FILE__, __LINE__)
84
+ #instance_eval(code, __FILE__, __LINE__)
85
+ @output
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,76 @@
1
+ module Ruex
2
+ module Helper
3
+ module CLI
4
+ require "json"
5
+ require "yaml"
6
+
7
+ def load_help
8
+ help_path = File.expand_path("../../../doc/help", __dir__)
9
+ File.read(help_path)
10
+ end
11
+
12
+ # Convert "foo/bar_baz" → "Foo::BarBaz"
13
+ def path_to_module_name(lib)
14
+ lib
15
+ .sub(/\.rb$/, "")
16
+ .split("/")
17
+ .map { |seg| seg.split("_").map(&:capitalize).join }
18
+ .join("::")
19
+ end
20
+
21
+ class ContextFile
22
+ def self.read(path_or_io)
23
+ data = if path_or_io.is_a?(StringIO)
24
+ stringio_contents(path_or_io)
25
+ else
26
+ context_file_contents(path_or_io)
27
+ end
28
+ deep_symbolize(data)
29
+ end
30
+
31
+ def self.deep_symbolize(obj)
32
+ case obj
33
+ when Hash
34
+ obj.each_with_object({}) do |(k, v), h|
35
+ h[k.to_sym] = deep_symbolize(v)
36
+ end
37
+ when Array
38
+ obj.map { |v| deep_symbolize(v) }
39
+ else
40
+ obj
41
+ end
42
+ end
43
+
44
+ def self.stringio_contents(sio)
45
+ data = Psych.safe_load(sio.read, permitted_classes: [Symbol], aliases: false)
46
+ raise ArgumentError, "-b expects a Hash literal" unless data.is_a?(Hash)
47
+ data
48
+ rescue Psych::DisallowedClass
49
+ raise ArgumentError, "-b expects a Hash literal"
50
+ rescue Psych::SyntaxError
51
+ raise ArgumentError, "Invalid -b hash"
52
+ end
53
+
54
+ def self.context_file_contents(path)
55
+ contents = File.read(path)
56
+ data =
57
+ case File.extname(path)
58
+ when ".yml", ".yaml"
59
+ Psych.safe_load(contents, permitted_classes: [Symbol], aliases: false)
60
+ when ".json"
61
+ JSON.parse(contents)
62
+ else
63
+ raise ArgumentError, "Unknown context file format: #{path}"
64
+ end
65
+
66
+ raise ArgumentError, "Context file must contain a top-level Hash/Map object" unless data.is_a?(Hash)
67
+
68
+ data
69
+ rescue Psych::DisallowedClass
70
+ raise ArgumentError, "disallowed class"
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
76
+
data/lib/ruex/tags.yml ADDED
@@ -0,0 +1,194 @@
1
+ - name: html
2
+ - name: head
3
+ - name: title
4
+ - name: base
5
+ void: true
6
+ - name: link
7
+ void: true
8
+ boolean:
9
+ - disabled
10
+ - name: meta
11
+ void: true
12
+ - name: style
13
+ - name: script
14
+ boolean:
15
+ - async
16
+ - defer
17
+ - nomodule
18
+ - name: noscript
19
+ - name: template
20
+
21
+ # Sections
22
+ - name: body
23
+ - name: section
24
+ - name: nav
25
+ - name: article
26
+ - name: aside
27
+ - name: h1
28
+ - name: h2
29
+ - name: h3
30
+ - name: h4
31
+ - name: h5
32
+ - name: h6
33
+ - name: header
34
+ - name: footer
35
+ - name: address
36
+ - name: main
37
+
38
+ # Grouping content
39
+ - name: p
40
+ - name: hr
41
+ void: true
42
+ - name: pre
43
+ - name: blockquote
44
+ - name: ol
45
+ - name: ul
46
+ - name: li
47
+ - name: dl
48
+ - name: dt
49
+ - name: dd
50
+ - name: figure
51
+ - name: figcaption
52
+ - name: div
53
+
54
+ # Text-level semantics
55
+ - name: a
56
+ - name: em
57
+ - name: strong
58
+ - name: small
59
+ - name: s
60
+ - name: cite
61
+ - name: q
62
+ - name: dfn
63
+ - name: abbr
64
+ - name: data
65
+ - name: time
66
+ - name: code
67
+ - name: var
68
+ - name: samp
69
+ - name: kbd
70
+ - name: sub
71
+ - name: sup
72
+ - name: i
73
+ - name: b
74
+ - name: u
75
+ - name: mark
76
+ - name: ruby
77
+ - name: rt
78
+ - name: rp
79
+ - name: bdi
80
+ - name: bdo
81
+ - name: span
82
+ - name: br
83
+ void: true
84
+ - name: wbr
85
+ void: true
86
+
87
+ # Edits
88
+ - name: ins
89
+ - name: del
90
+
91
+ # Embedded content
92
+ - name: picture
93
+ - name: source
94
+ void: true
95
+ - name: img
96
+ void: true
97
+ - name: iframe
98
+ - name: embed
99
+ void: true
100
+ - name: object
101
+ - name: param
102
+ void: true
103
+ - name: video
104
+ boolean:
105
+ - autoplay
106
+ - controls
107
+ - loop
108
+ - muted
109
+ - name: audio
110
+ boolean:
111
+ - autoplay
112
+ - controls
113
+ - loop
114
+ - muted
115
+ - name: track
116
+ void: true
117
+ - name: map
118
+ - name: area
119
+ void: true
120
+
121
+ # Forms
122
+ - name: form
123
+ boolean:
124
+ - novalidate
125
+ - name: label
126
+ - name: input
127
+ void: true
128
+ boolean:
129
+ - disabled
130
+ - readonly
131
+ - required
132
+ - autofocus
133
+ - multiple
134
+ - hidden
135
+ - formnovalidate
136
+ - checked
137
+ - name: button
138
+ boolean:
139
+ - disabled
140
+ - autofocus
141
+ - formnovalidate
142
+ - name: select
143
+ boolean:
144
+ - disabled
145
+ - multiple
146
+ - required
147
+ - autofocus
148
+ - name: datalist
149
+ - name: optgroup
150
+ boolean:
151
+ - disabled
152
+ - name: option
153
+ boolean:
154
+ - disabled
155
+ - selected
156
+ - name: textarea
157
+ boolean:
158
+ - disabled
159
+ - readonly
160
+ - required
161
+ - autofocus
162
+ - name: output
163
+ - name: progress
164
+ - name: meter
165
+ - name: fieldset
166
+ boolean:
167
+ - disabled
168
+ - name: legend
169
+
170
+ # Interactive elements
171
+ - name: details
172
+ - name: summary
173
+ - name: dialog
174
+ - name: menu
175
+ - name: menuitem
176
+
177
+ # Table content
178
+ - name: table
179
+ - name: caption
180
+ - name: colgroup
181
+ - name: col
182
+ void: true
183
+ - name: thead
184
+ - name: tbody
185
+ - name: tfoot
186
+ - name: tr
187
+ - name: td
188
+ - name: th
189
+
190
+ # Scripting
191
+ - name: canvas
192
+ - name: slot
193
+ - name: portal
194
+
@@ -0,0 +1,4 @@
1
+ module Ruex
2
+ VERSION = File.read(File.expand_path("../../VERSION", __dir__)).strip
3
+ end
4
+
data/lib/ruex.rb ADDED
@@ -0,0 +1,7 @@
1
+ module Ruex
2
+ require 'ruex/core'
3
+
4
+ def render(code, ctx: {})
5
+ Core.new.render(code, ctx: ctx)
6
+ end
7
+ end
metadata ADDED
@@ -0,0 +1,53 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ruex
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Takanobu Maekawa
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies: []
12
+ description: |-
13
+ A library and CLI tool that generates HTML using plain Ruby expressions.
14
+ It is intended for static site or page generation, and is not suitable as a dynamic web page renderer.
15
+ executables:
16
+ - ruex
17
+ extensions: []
18
+ extra_rdoc_files: []
19
+ files:
20
+ - LICENSE
21
+ - README.md
22
+ - VERSION
23
+ - bin/ruex
24
+ - lib/ruex.rb
25
+ - lib/ruex/cli.rb
26
+ - lib/ruex/core.rb
27
+ - lib/ruex/helper/cli.rb
28
+ - lib/ruex/tags.yml
29
+ - lib/ruex/version.rb
30
+ homepage: https://github.com/tmkw/ruex
31
+ licenses:
32
+ - BSD-2-Clause
33
+ metadata:
34
+ homepage_uri: https://github.com/tmkw/ruex
35
+ source_code_uri: https://github.com/tmkw/ruex
36
+ rdoc_options: []
37
+ require_paths:
38
+ - lib
39
+ required_ruby_version: !ruby/object:Gem::Requirement
40
+ requirements:
41
+ - - ">="
42
+ - !ruby/object:Gem::Version
43
+ version: 4.0.0
44
+ required_rubygems_version: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - ">="
47
+ - !ruby/object:Gem::Version
48
+ version: '0'
49
+ requirements: []
50
+ rubygems_version: 4.0.3
51
+ specification_version: 4
52
+ summary: Static HTML generation with plain Ruby expressions
53
+ test_files: []