fiasco-template 0.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
+ SHA1:
3
+ metadata.gz: 11cbe3ece994f98ca8bde7971960d447f6014756
4
+ data.tar.gz: a49d13f4c731a7f29cd7fac9b4da6968a2fe693b
5
+ SHA512:
6
+ metadata.gz: 559985260dd577af330558e26e0b6c9a851598b751342a3c436d3624a92b8424df7cfb4135a6ff7a32b0c7f8994839a0808884a355e425db5c92dc520f0aef60
7
+ data.tar.gz: 9a5d95d6bfaa1914d370fcb06fec9bac3c7c0f9b14dfb3a31647c754bdcaca32b9ed00bf362fbd6b57bf87a1044621b47588151aa015c673814e68efb0866672
@@ -0,0 +1,79 @@
1
+ require 'strscan'
2
+
3
+ module Fiasco::Template
4
+ class Compiler
5
+ OPENERS = /(.*?)(^[ \t]*%|\{%-?|\{\{-?|\{#-?|\z)/m
6
+ DEFAULT_DISPLAY_VALUE = ->(outvar, literal){"#{outvar} << (#{literal}).to_s"}
7
+ DEFAULT_DISPLAY_TEXT = ->(text){text.dump}
8
+
9
+ def initialize(options = {})
10
+ @output_var = options.fetch(:output_var, '@render_output')
11
+ @display_value = options.fetch(:display_value, DEFAULT_DISPLAY_VALUE)
12
+ @display_text = options.fetch(:display_text, DEFAULT_DISPLAY_TEXT)
13
+ end
14
+
15
+ def closer_for(tag)
16
+ case tag
17
+ when /\{%-?/ then /(.*?)(-?%\}|\z)/m
18
+ when /\{\{-?/ then /(.*?)(-?}\}|\z)/m
19
+ when /\{#-?/ then /(.*?)(-?#\}|\z)/m
20
+ when '%' then /(.*?)($)/
21
+ end
22
+ end
23
+
24
+ def scan(body)
25
+ scanner = StringScanner.new(body)
26
+ open_tag = nil
27
+
28
+ until scanner.eos?
29
+ if open_tag
30
+ scanner.scan(closer_for(open_tag))
31
+ inner, close_tag = scanner[1], scanner[2]
32
+
33
+ case open_tag
34
+ when '{{', '{{-' then yield [:display, inner]
35
+ when '{%', '{%-' then yield [:code, inner]
36
+ when '{#', '{#-' then yield [:comment, inner]
37
+ when '%' then yield [:code_line, inner]
38
+ end
39
+
40
+ open_tag = nil
41
+ else
42
+ scanner.scan(OPENERS)
43
+ before, open_tag = scanner[1], scanner[2]
44
+ newlines_count = before.count("\n")
45
+ open_tag.lstrip! # for % which captures preceeding whitespace
46
+
47
+ text = before
48
+ text.lstrip! if close_tag && close_tag[0] == '-'
49
+ text.rstrip! if open_tag[-1] == '-'
50
+ text.chomp! if open_tag == '%'
51
+
52
+ yield [:text, text]
53
+ yield [:newlines, newlines_count]
54
+ end
55
+ end
56
+ end
57
+
58
+ def compile(body)
59
+ src = []
60
+
61
+ scan(body) do |command, data|
62
+ case command
63
+ when :newlines
64
+ src << "\n" * data unless data == 0
65
+ when :text
66
+ src << "#{@output_var} << #{@display_text.(data)}" unless data.empty?
67
+ when :code, :code_line
68
+ src << data
69
+ when :display
70
+ src << @display_value.(@output_var, data)
71
+ when :comment
72
+ # skip
73
+ end
74
+ end
75
+
76
+ src.join(';')
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,171 @@
1
+ require 'set'
2
+ require_relative 'compiler'
3
+
4
+ module Fiasco::Template
5
+ class RenderContext
6
+ class NullOutput; def <<(value); end; end
7
+
8
+ Entry = Struct.new(:body, :filename)
9
+
10
+ def initialize
11
+ @content_blocks = Hash.new {|h,k| h[k] = [] }
12
+ @template_locals = Hash.new {|h,k| h[k] = [] }
13
+ @templates = {}
14
+ @compiled = Set.new
15
+ display_value = lambda do |outvar, literal|
16
+ "__tmp = (#{literal}); #{outvar} << display_value(__tmp)"
17
+ end
18
+ @compiler = Compiler.new(display_value: display_value)
19
+ end
20
+
21
+ def block(key, &b)
22
+ @content_blocks[key.to_s] << b
23
+ end
24
+
25
+ def superblock(*args)
26
+ # for a blocklevel N, we can find the superblock (block with the
27
+ # same name defined on a parent template) in N+1
28
+ @blocklevel += 1
29
+ @content_blocks[@blockname][@blocklevel].call(*args)
30
+ ensure
31
+ @blocklevel -= 1
32
+ end
33
+
34
+ def yield_block(key, *args, &b)
35
+ key = key.to_s
36
+ @content_blocks[key] << b if b
37
+
38
+ if @content_blocks[key].length > 0
39
+ begin
40
+ old_blocklevel, @blocklevel = @blocklevel, -1
41
+ old_blockname, @blockname = @blockname, key
42
+
43
+ # @blocklevel starts at -1, making this call render
44
+ # the block at level 0 (the last one defined in the
45
+ # inheritance chain)
46
+ superblock(*args)
47
+ ensure
48
+ @blocklevel, @blockname = old_blocklevel, old_blockname
49
+ end
50
+ else
51
+ ''
52
+ end
53
+ end
54
+
55
+ def extends(base, *args)
56
+ # Store render output remporarily because we don't want to
57
+ # render any output of a child template until we render the parent
58
+ @render_output, @original_render_output = NullOutput.new, @render_output
59
+ @extends = [base, args]
60
+ end
61
+
62
+ def _compile(name, entry, locals = [])
63
+ src = "params ||= {}; @render_output ||= ''; "
64
+ locals.each {|var| src += "#{var} = params[:#{var}]; "}
65
+ src << "\n"
66
+ src << @compiler.compile(entry.body)
67
+ src << "\n@render_output"
68
+
69
+ meth = <<-EOS
70
+ #coding:UTF-8
71
+ define_singleton_method(:'__view__#{name}') do |params|
72
+ #{src}
73
+ end
74
+ EOS
75
+ eval(meth, binding, entry.filename || "(TEMPLATE:#{name})", -2)
76
+ @compiled << name
77
+ end
78
+
79
+ def _process_locals(name, locals)
80
+ seen_variables = @template_locals[name]
81
+ diff = locals.keys - seen_variables
82
+
83
+ unless diff.empty?
84
+ seen_variables += diff
85
+ @compiled.delete(name)
86
+ end
87
+
88
+ seen_variables
89
+ end
90
+
91
+ def _declare(options)
92
+ contents = options[:path] ? File.read(options[:path]) : options[:contents]
93
+
94
+ if contents.nil?
95
+ raise ArgumentError.new("Need either path or contents")
96
+ end
97
+
98
+ entry = Entry.new(contents, options[:path])
99
+
100
+ yield(entry)
101
+ end
102
+
103
+ def declare(name, options = {})
104
+ name = name.to_s
105
+ _declare(options) {|e| @templates[name] = e}
106
+ end
107
+
108
+ def _render(name, locals = {})
109
+ name = name.to_s
110
+ variables = _process_locals(name, locals)
111
+
112
+ unless @compiled.include?(name)
113
+ entry = @templates.fetch(name) do
114
+ raise ArgumentError, "template `#{name}` not declared"
115
+ end
116
+
117
+ _compile(name, entry, variables)
118
+ end
119
+
120
+ send("__view__#{name}", locals)
121
+
122
+ if @extends
123
+ parent, pargs = @extends
124
+ @extends = nil
125
+ @render_output, @original_render_output = @original_render_output, nil
126
+ _render(parent, *pargs)
127
+ end
128
+
129
+ @render_output
130
+ end
131
+
132
+ def render(name, locals = {})
133
+ _render(name, locals)
134
+ ensure
135
+ @content_blocks.clear
136
+ @render_output = nil
137
+ end
138
+
139
+ alias_method :[], :render
140
+
141
+ def display_value(value)
142
+ str = value.to_s
143
+ value.tainted? ? Rack::Utils.escape_html(str) : str
144
+ end
145
+
146
+ def macro(mname, defaults = {}, &b)
147
+ arguments = b.parameters
148
+ define_singleton_method mname do |named = defaults, &block|
149
+ args = arguments.select{|t| t[0] != :block}.map do |type, name|
150
+ named.fetch(name) do
151
+ defaults.fetch(name) do
152
+ msg = "Macro invocation '#{mname}' is missing a required argument: #{name}"
153
+ raise ArgumentError, msg, caller(10)
154
+ end
155
+ end
156
+ end
157
+ b.call(*args, &block)
158
+ end
159
+ end
160
+
161
+ def load_macros(options)
162
+ @render_output, tmp = NullOutput.new, @render_output
163
+ b = binding
164
+ _declare(options) do |e|
165
+ eval(@compiler.compile(e.body), b, e.filename || 'MACROS')
166
+ end
167
+ ensure
168
+ @render_output = tmp
169
+ end
170
+ end
171
+ end
@@ -0,0 +1,8 @@
1
+ module Fiasco
2
+ module Template
3
+ VERSION = '0.0.1'
4
+ end
5
+ end
6
+
7
+ require_relative 'template/compiler'
8
+ require_relative 'template/render_context'
@@ -0,0 +1,12 @@
1
+ % macro :input, type: 'text', value: '', size: 20 do |name, type, value, size|
2
+ <input type="{{type}}" name="{{name}}" value="{{value}}" size="{{size}}">
3
+ % end
4
+ % macro :label, required: false do |text, required|
5
+ <label>{{text}}{% if required %}<span class=required>*</span>{% end %}</label>
6
+ % end
7
+ % macro :field, type: 'text', required: false, lbl: nil do |type, name, lbl, required|
8
+ <div class=field>
9
+ % label text: lbl || name.gsub(/[-_]/, ' ').capitalize, required: required
10
+ % input name: name, type: type
11
+ </div>
12
+ % end
@@ -0,0 +1,10 @@
1
+ <!doctype html>
2
+ <html>
3
+ <head><title>{% yield_block('title') do %}My Site{% end %}</title></head>
4
+ <body class="{% yield_block('body_classes') %}">
5
+ <div id=wrapper>
6
+ <h1>{% yield_block('title') %}</h1>
7
+ % yield_block('contents')
8
+ </div>
9
+ </body>
10
+ </html>
@@ -0,0 +1,9 @@
1
+ % extends 'base'
2
+
3
+ {% block('title') do %}{% superblock %} -- Homepage{% end %}
4
+
5
+ % block('contents') do
6
+ <div class=main>
7
+ <h2>This is the homepage</h2>
8
+ </div>
9
+ % end
@@ -0,0 +1,5 @@
1
+ % extends 'child'
2
+
3
+ % block 'contents' do
4
+ Grandchild
5
+ % end
@@ -0,0 +1,9 @@
1
+ <form method=post>
2
+ <fieldset>
3
+ % field(name: 'username', value: user_name)
4
+ % field(name: 'password', type: 'password')
5
+ % field(name: 'password_confirm', type: 'password')
6
+ </fieldset>
7
+
8
+ <button>Submit</button>
9
+ </form>
@@ -0,0 +1 @@
1
+ {{ local1 }} {{ local2 }}
@@ -0,0 +1,105 @@
1
+ require 'test/unit'
2
+ require_relative '../lib/fiasco/template'
3
+
4
+ class TestCompiler < Test::Unit::TestCase
5
+ def compile_and_run(body, binding)
6
+ @__result = ''
7
+ s = Fiasco::Template::Compiler.new(output_var: "@__result")
8
+ src = s.compile(body)
9
+ eval src, binding, '(template)', 1
10
+ @__result
11
+ end
12
+
13
+ def test_complete
14
+ body = <<-EOT
15
+ <head><title>{{title}}</title></head>
16
+ <body>
17
+ {% 3.times do |n| %}-{{n}}-{% end %}
18
+ {% 3.times do |n| -%}
19
+ -{{n}}-
20
+ {#- comment #}
21
+ {%- end %}
22
+
23
+ % val = '-' * 10
24
+ {{val}}
25
+ </body>
26
+ EOT
27
+
28
+ expected = <<-EOT
29
+ <head><title>The Title</title></head>
30
+ <body>
31
+ -0--1--2-
32
+ -0--1--2-
33
+
34
+ ----------
35
+ </body>
36
+ EOT
37
+
38
+ title = 'The Title'
39
+ result = compile_and_run(body, binding)
40
+
41
+ assert_equal expected, result
42
+ end
43
+
44
+ def test_expression_substitution
45
+ value = 'value'
46
+ result = compile_and_run('**{{value}} {{ value + value }}**', binding)
47
+
48
+ assert_equal '**value valuevalue**', result
49
+ end
50
+
51
+ def test_code_blocks
52
+ body = '{% value = 10 %}{% 3.times do %}{{ value * 100 }}{% end %}'
53
+ result = compile_and_run(body, binding)
54
+
55
+ assert_equal "100010001000", result
56
+ end
57
+
58
+ def test_code_lines
59
+ body = <<-EOT
60
+ % 3.times do |n|
61
+ Iteration: {{n}}
62
+ % end
63
+ EOT
64
+ result = compile_and_run(body, binding)
65
+
66
+ assert_equal "\nIteration: 0\nIteration: 1\nIteration: 2\n", result
67
+ end
68
+
69
+ def test_whitespace_handling
70
+ value = '|value|'
71
+
72
+ body = " \n {{ value }} \n "
73
+ assert_equal " \n |value| \n ", compile_and_run(body, binding)
74
+
75
+ body = " \n {{- value }} \n "
76
+ assert_equal "|value| \n ", compile_and_run(body, binding)
77
+
78
+ body = " \n {{ value -}} \n "
79
+ assert_equal " \n |value|", compile_and_run(body, binding)
80
+
81
+ body = " \n {{- value -}} \n "
82
+ assert_equal "|value|", compile_and_run(body, binding)
83
+ end
84
+
85
+ def test_comment
86
+ body = "123{# comment #}456"
87
+ assert_equal "123456", compile_and_run(body, binding)
88
+ end
89
+
90
+ def test_error_line_mapping
91
+ body = <<-EOT
92
+ Line 1
93
+ Line {{ 1 + 1 }}
94
+ Line {{- 1 + 2 -}}
95
+ Line {{ invalid }}
96
+ Line 5
97
+ EOT
98
+ begin
99
+ compile_and_run(body, binding)
100
+ rescue StandardError => e
101
+ e.backtrace[0] =~ /\(template\):(\d+):.*/
102
+ assert_equal "Error in line: 4", "Error in line: #{$1}"
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,95 @@
1
+ require 'test/unit'
2
+ require_relative '../lib/fiasco/template'
3
+
4
+ $renderer = Fiasco::Template::RenderContext.new
5
+
6
+ Dir['./test/templates/*.html'].each do |path|
7
+ name = path.gsub(/\.html$/, '').gsub('./test/templates/', '')
8
+ $renderer.declare(name, path: path)
9
+ end
10
+
11
+ $renderer.load_macros(path: './test/macros/fields.html')
12
+
13
+ class TestRender < Test::Unit::TestCase
14
+ def test_simple_render
15
+ expected = <<-EOS
16
+ <!doctype html>
17
+ <html>
18
+ <head><title>My Site</title></head>
19
+ <body class="">
20
+ <div id=wrapper>
21
+ <h1>My Site</h1>
22
+ </div>
23
+ </body>
24
+ </html>
25
+ EOS
26
+
27
+ assert_equal expected, $renderer.render('base')
28
+ end
29
+
30
+ def test_inheritance
31
+ expected = <<-EOS
32
+ <!doctype html>
33
+ <html>
34
+ <head><title>My Site -- Homepage</title></head>
35
+ <body class="">
36
+ <div id=wrapper>
37
+ <h1>My Site -- Homepage</h1>
38
+ <div class=main>
39
+ <h2>This is the homepage</h2>
40
+ </div>
41
+ </div>
42
+ </body>
43
+ </html>
44
+ EOS
45
+
46
+ assert_equal expected, $renderer.render('child')
47
+ end
48
+
49
+ def test_nested_inheritance
50
+ expected = <<-EOS
51
+ <!doctype html>
52
+ <html>
53
+ <head><title>My Site -- Homepage</title></head>
54
+ <body class="">
55
+ <div id=wrapper>
56
+ <h1>My Site -- Homepage</h1>
57
+ Grandchild
58
+ </div>
59
+ </body>
60
+ </html>
61
+ EOS
62
+
63
+ assert_equal expected, $renderer.render('grandchild')
64
+ end
65
+
66
+ def test_locals
67
+ assert_equal "a b\n", $renderer.render('with_locals',
68
+ local1: 'a', local2: 'b')
69
+ end
70
+
71
+ def test_macros
72
+ expected = <<-EOS
73
+ <form method=post>
74
+ <fieldset>
75
+ <div class=field>
76
+ <label>Username</label>
77
+ <input type="text" name="username" value="" size="20">
78
+ </div>
79
+ <div class=field>
80
+ <label>Password</label>
81
+ <input type="password" name="password" value="" size="20">
82
+ </div>
83
+ <div class=field>
84
+ <label>Password confirm</label>
85
+ <input type="password" name="password_confirm" value="" size="20">
86
+ </div>
87
+ </fieldset>
88
+
89
+ <button>Submit</button>
90
+ </form>
91
+ EOS
92
+
93
+ assert_equal expected, $renderer.render('uses_macros', user_name: 'Admin')
94
+ end
95
+ end
metadata ADDED
@@ -0,0 +1,63 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: fiasco-template
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Bruno Deferrari
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-03-17 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: Templating engine inspired by Jinja2.
14
+ email:
15
+ - utizoc@gmail.com
16
+ executables: []
17
+ extensions: []
18
+ extra_rdoc_files: []
19
+ files:
20
+ - lib/fiasco/template/compiler.rb
21
+ - lib/fiasco/template/render_context.rb
22
+ - lib/fiasco/template.rb
23
+ - test/test_compiler.rb
24
+ - test/test_render_context.rb
25
+ - test/macros/fields.html
26
+ - test/templates/base.html
27
+ - test/templates/child.html
28
+ - test/templates/grandchild.html
29
+ - test/templates/uses_macros.html
30
+ - test/templates/with_locals.html
31
+ homepage: http://github.com/tizoc/fiasco-template
32
+ licenses:
33
+ - MIT
34
+ metadata: {}
35
+ post_install_message:
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: '0'
44
+ required_rubygems_version: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - '>='
47
+ - !ruby/object:Gem::Version
48
+ version: '0'
49
+ requirements: []
50
+ rubyforge_project:
51
+ rubygems_version: 2.0.3
52
+ signing_key:
53
+ specification_version: 4
54
+ summary: Templating engine inspired by Jinja2.
55
+ test_files:
56
+ - test/test_compiler.rb
57
+ - test/test_render_context.rb
58
+ - test/macros/fields.html
59
+ - test/templates/base.html
60
+ - test/templates/child.html
61
+ - test/templates/grandchild.html
62
+ - test/templates/uses_macros.html
63
+ - test/templates/with_locals.html