ezml 0.1.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/.DS_Store +0 -0
- data/.gitignore +11 -0
- data/.rspec +3 -0
- data/.travis.yml +5 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +149 -0
- data/LICENSE.txt +21 -0
- data/README.md +140 -0
- data/Rakefile +6 -0
- data/bin/ezml +9 -0
- data/examples/helloworld.ezml +44 -0
- data/examples/helloworld.html +61 -0
- data/examples/template.ezml +69 -0
- data/examples/template.html +102 -0
- data/ezml.gemspec +40 -0
- data/lib/ezml.rb +8 -0
- data/lib/ezml/attribute_builder.rb +141 -0
- data/lib/ezml/attribute_compiler.rb +172 -0
- data/lib/ezml/attribute_parser.rb +133 -0
- data/lib/ezml/buffer.rb +166 -0
- data/lib/ezml/compiler.rb +322 -0
- data/lib/ezml/engine.rb +107 -0
- data/lib/ezml/error.rb +59 -0
- data/lib/ezml/escapable.rb +46 -0
- data/lib/ezml/exec.rb +348 -0
- data/lib/ezml/filters.rb +249 -0
- data/lib/ezml/generator.rb +38 -0
- data/lib/ezml/helpers.rb +299 -0
- data/lib/ezml/options.rb +142 -0
- data/lib/ezml/parser.rb +826 -0
- data/lib/ezml/template_engine.rb +106 -0
- data/lib/ezml/temple_line_counter.rb +26 -0
- data/lib/ezml/util.rb +156 -0
- data/lib/ezml/version.rb +3 -0
- metadata +199 -0
@@ -0,0 +1,172 @@
|
|
1
|
+
require_relative 'attribute_parser'
|
2
|
+
|
3
|
+
module EZML
|
4
|
+
|
5
|
+
class AttributeCompiler
|
6
|
+
class AttributeValue < Struct.new(:type, :key, :value)
|
7
|
+
# @return [String] A Ruby literal of value.
|
8
|
+
def to_literal
|
9
|
+
case type
|
10
|
+
when :static
|
11
|
+
EZML::Util.inspect_obj(value)
|
12
|
+
when :dynamic
|
13
|
+
value
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.runtime_build(attributes, object_ref, dynamic_attributes)
|
19
|
+
"_ezmlout.attributes(#{EZML::Util.inspect_obj(attributes)}, #{object_ref},#{dynamic_attributes.to_literal})"
|
20
|
+
end
|
21
|
+
|
22
|
+
def initialize(options)
|
23
|
+
@is_html = [:html4, :html5].include?(options[:format])
|
24
|
+
@attr_wrapper = options[:attr_wrapper]
|
25
|
+
@escape_attrs = options[:escape_attrs]
|
26
|
+
@hyphenate_data_attrs = options[:hyphenate_data_attrs]
|
27
|
+
end
|
28
|
+
|
29
|
+
def compile(attributes, object_ref, dynamic_attributes)
|
30
|
+
if object_ref != :nil || !AttributeParser.available?
|
31
|
+
return [:dynamic, AttributeCompiler.runtime_build(attributes, object_ref, dynamic_attributes)]
|
32
|
+
end
|
33
|
+
|
34
|
+
parsed_hashes = [dynamic_attributes.new, dynamic_attributes.old].compact.map do |attribute_hash|
|
35
|
+
unless (hash = AttributeParser.parse(attribute_hash))
|
36
|
+
return [:dynamic, AttributeCompiler.runtime_build(attributes, object_ref, dynamic_attributes)]
|
37
|
+
end
|
38
|
+
hash
|
39
|
+
end
|
40
|
+
attribute_values = build_attribute_values(attributes, parsed_hashes)
|
41
|
+
AttributeBuilder.verify_attribute_names!(attribute_values.map(&:key))
|
42
|
+
|
43
|
+
[:multi, *group_values_for_sort(attribute_values).map { |value_group|
|
44
|
+
compile_attribute_values(value_group)
|
45
|
+
}]
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def group_values_for_sort(values)
|
51
|
+
sorted_values = values.sort_by(&:key)
|
52
|
+
[].tap do |value_groups|
|
53
|
+
until sorted_values.empty?
|
54
|
+
key = sorted_values.first.key
|
55
|
+
value_group, sorted_values = sorted_values.partition { |v| v.key.start_with?(key) }
|
56
|
+
value_groups << value_group
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def build_attribute_values(attributes, parsed_hashes)
|
62
|
+
[].tap do |attribute_values|
|
63
|
+
attributes.each do |key, static_value|
|
64
|
+
attribute_values << AttributeValue.new(:static, key, static_value)
|
65
|
+
end
|
66
|
+
parsed_hashes.each do |parsed_hash|
|
67
|
+
parsed_hash.each do |key, dynamic_value|
|
68
|
+
attribute_values << AttributeValue.new(:dynamic, key, dynamic_value)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def compile_attribute_values(values)
|
75
|
+
if values.map(&:key).uniq.size == 1
|
76
|
+
compile_attribute(values.first.key, values)
|
77
|
+
else
|
78
|
+
runtime_build(values)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def runtime_build(values)
|
83
|
+
hash_content = values.group_by(&:key).map do |key, values_for_key|
|
84
|
+
"#{frozen_string(key)} => #{merged_value(key, values_for_key)}"
|
85
|
+
end.join(', ')
|
86
|
+
[:dynamic, "_ezmlout.attributes({ #{hash_content} }, nil)"]
|
87
|
+
end
|
88
|
+
|
89
|
+
def static_build(values)
|
90
|
+
hash_content = values.group_by(&:key).map do |key, values_for_key|
|
91
|
+
"#{frozen_string(key)} => #{merged_value(key, values_for_key)}"
|
92
|
+
end.join(', ')
|
93
|
+
|
94
|
+
arguments = [@is_html, @attr_wrapper, @escape_attrs, @hyphenate_data_attrs]
|
95
|
+
code = "::EZML::AttributeBuilder.build_attributes"\
|
96
|
+
"(#{arguments.map { |a| EZML::Util.inspect_obj(a) }.join(', ')}, { #{hash_content} })"
|
97
|
+
[:static, eval(code).to_s]
|
98
|
+
end
|
99
|
+
|
100
|
+
def merged_value(key, values)
|
101
|
+
if values.size == 1
|
102
|
+
values.first.to_literal
|
103
|
+
else
|
104
|
+
"::EZML::AttributeBuilder.merge_values(#{frozen_string(key)}, #{values.map(&:to_literal).join(', ')})"
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
def frozen_string(str)
|
109
|
+
"#{EZML::Util.inspect_obj(str)}.freeze"
|
110
|
+
end
|
111
|
+
|
112
|
+
def compile_attribute(key, values)
|
113
|
+
if values.all? { |v| Temple::StaticAnalyzer.static?(v.to_literal) }
|
114
|
+
return static_build(values)
|
115
|
+
end
|
116
|
+
|
117
|
+
case key
|
118
|
+
when 'id', 'class'
|
119
|
+
compile_id_or_class_attribute(key, values)
|
120
|
+
else
|
121
|
+
compile_common_attribute(key, values)
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
def compile_id_or_class_attribute(id_or_class, values)
|
126
|
+
var = unique_name
|
127
|
+
[:multi,
|
128
|
+
[:code, "#{var} = (#{merged_value(id_or_class, values)})"],
|
129
|
+
[:case, var,
|
130
|
+
['Hash, Array', runtime_build([AttributeValue.new(:dynamic, id_or_class, var)])],
|
131
|
+
['false, nil', [:multi]],
|
132
|
+
[:else, [:multi,
|
133
|
+
[:static, " #{id_or_class}=#{@attr_wrapper}"],
|
134
|
+
[:escape, @escape_attrs, [:dynamic, var]],
|
135
|
+
[:static, @attr_wrapper]],
|
136
|
+
]
|
137
|
+
],
|
138
|
+
]
|
139
|
+
end
|
140
|
+
|
141
|
+
def compile_common_attribute(key, values)
|
142
|
+
var = unique_name
|
143
|
+
[:multi,
|
144
|
+
[:code, "#{var} = (#{merged_value(key, values)})"],
|
145
|
+
[:case, var,
|
146
|
+
['Hash', runtime_build([AttributeValue.new(:dynamic, key, var)])],
|
147
|
+
['true', true_value(key)],
|
148
|
+
['false, nil', [:multi]],
|
149
|
+
[:else, [:multi,
|
150
|
+
[:static, " #{key}=#{@attr_wrapper}"],
|
151
|
+
[:escape, @escape_attrs, [:dynamic, var]],
|
152
|
+
[:static, @attr_wrapper]],
|
153
|
+
]
|
154
|
+
],
|
155
|
+
]
|
156
|
+
end
|
157
|
+
|
158
|
+
def true_value(key)
|
159
|
+
if @is_html
|
160
|
+
[:static, " #{key}"]
|
161
|
+
else
|
162
|
+
[:static, " #{key}=#{@attr_wrapper}#{key}#{@attr_wrapper}"]
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
def unique_name
|
167
|
+
@unique_name ||= 0
|
168
|
+
"_ezml_attribute_compiler#{@unique_name += 1}"
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
end
|
@@ -0,0 +1,133 @@
|
|
1
|
+
begin
|
2
|
+
require 'ripper'
|
3
|
+
rescue LoadError
|
4
|
+
end
|
5
|
+
|
6
|
+
module EZML
|
7
|
+
|
8
|
+
module AttributeParser
|
9
|
+
class UnexpectedTokenError < StandardError; end
|
10
|
+
class UnexpectedKeyError < StandardError; end
|
11
|
+
|
12
|
+
TYPE = 1
|
13
|
+
TEXT = 2
|
14
|
+
|
15
|
+
IGNORED_TYPES = %i[on_sp on_ignored_nl]
|
16
|
+
|
17
|
+
class << self
|
18
|
+
def available?
|
19
|
+
defined?(Ripper) && Temple::StaticAnalyzer.available?
|
20
|
+
end
|
21
|
+
|
22
|
+
def parse(exp)
|
23
|
+
return nil unless hash_literal?(exp)
|
24
|
+
|
25
|
+
hash = {}
|
26
|
+
each_attribute(exp) do |key, value|
|
27
|
+
hash[key] = value
|
28
|
+
end
|
29
|
+
hash
|
30
|
+
rescue UnexpectedTokenError, UnexpectedKeyError
|
31
|
+
nil
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def hash_literal?(exp)
|
37
|
+
return false if Temple::StaticAnalyzer.syntax_error?(exp)
|
38
|
+
sym, body = Ripper.sexp(exp)
|
39
|
+
sym == :program && body.is_a?(Array) && body.size == 1 && body[0] && body[0][0] == :hash
|
40
|
+
end
|
41
|
+
|
42
|
+
def shift_key!(tokens)
|
43
|
+
while !tokens.empty? && IGNORED_TYPES.include?(tokens.first[TYPE])
|
44
|
+
tokens.shift # ignore spaces
|
45
|
+
end
|
46
|
+
|
47
|
+
_, type, first_text = tokens.shift
|
48
|
+
case type
|
49
|
+
when :on_label # `key:`
|
50
|
+
first_text.tr(':', '')
|
51
|
+
when :on_symbeg # `:key =>`, `:'key' =>` or `:"key" =>`
|
52
|
+
key = tokens.shift[TEXT]
|
53
|
+
if first_text != ':' # `:'key'` or `:"key"`
|
54
|
+
expect_string_end!(tokens.shift)
|
55
|
+
end
|
56
|
+
shift_hash_rocket!(tokens)
|
57
|
+
key
|
58
|
+
when :on_tstring_beg # `"key":`, `'key':` or `"key" =>`
|
59
|
+
key = tokens.shift[TEXT]
|
60
|
+
next_token = tokens.shift
|
61
|
+
if next_token[TYPE] != :on_label_end # on_label_end is `":` or `':`, so `"key" =>`
|
62
|
+
expect_string_end!(next_token)
|
63
|
+
shift_hash_rocket!(tokens)
|
64
|
+
end
|
65
|
+
key
|
66
|
+
else
|
67
|
+
raise UnexpectedKeyError.new("unexpected token is given!: #{first_text} (#{type})")
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def expect_string_end!(token)
|
72
|
+
if token[TYPE] != :on_tstring_end
|
73
|
+
raise UnexpectedTokenError
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def shift_hash_rocket!(tokens)
|
78
|
+
until tokens.empty?
|
79
|
+
_, type, str = tokens.shift
|
80
|
+
break if type == :on_op && str == '=>'
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def each_attribute(hash_literal, &block)
|
85
|
+
all_tokens = Ripper.lex(hash_literal.strip)
|
86
|
+
all_tokens = all_tokens[1...-1] || [] # strip tokens for brackets
|
87
|
+
|
88
|
+
each_balanced_tokens(all_tokens) do |tokens|
|
89
|
+
key = shift_key!(tokens)
|
90
|
+
value = tokens.map {|t| t[2] }.join.strip
|
91
|
+
block.call(key, value)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def each_balanced_tokens(tokens, &block)
|
96
|
+
attr_tokens = []
|
97
|
+
open_tokens = Hash.new { |h, k| h[k] = 0 }
|
98
|
+
|
99
|
+
tokens.each do |token|
|
100
|
+
case token[TYPE]
|
101
|
+
when :on_comma
|
102
|
+
if open_tokens.values.all?(&:zero?)
|
103
|
+
block.call(attr_tokens)
|
104
|
+
attr_tokens = []
|
105
|
+
next
|
106
|
+
end
|
107
|
+
when :on_lbracket
|
108
|
+
open_tokens[:array] += 1
|
109
|
+
when :on_rbracket
|
110
|
+
open_tokens[:array] -= 1
|
111
|
+
when :on_lbrace
|
112
|
+
open_tokens[:block] += 1
|
113
|
+
when :on_rbrace
|
114
|
+
open_tokens[:block] -= 1
|
115
|
+
when :on_lparen
|
116
|
+
open_tokens[:paren] += 1
|
117
|
+
when :on_rparen
|
118
|
+
open_tokens[:paren] -= 1
|
119
|
+
when :on_embexpr_beg
|
120
|
+
open_tokens[:embexpr] += 1
|
121
|
+
when :on_embexpr_end
|
122
|
+
open_tokens[:embexpr] -= 1
|
123
|
+
when *IGNORED_TYPES
|
124
|
+
next if attr_tokens.empty?
|
125
|
+
end
|
126
|
+
|
127
|
+
attr_tokens << token
|
128
|
+
end
|
129
|
+
block.call(attr_tokens) unless attr_tokens.empty?
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
data/lib/ezml/buffer.rb
ADDED
@@ -0,0 +1,166 @@
|
|
1
|
+
module EZML
|
2
|
+
|
3
|
+
class Buffer
|
4
|
+
include EZML::Helpers
|
5
|
+
include EZML::Util
|
6
|
+
|
7
|
+
attr_accessor :buffer
|
8
|
+
attr_accessor :options
|
9
|
+
attr_accessor :upper
|
10
|
+
attr_accessor :capture_position
|
11
|
+
attr_writer :active
|
12
|
+
|
13
|
+
def xhtml?
|
14
|
+
not html?
|
15
|
+
end
|
16
|
+
|
17
|
+
def html?
|
18
|
+
html4? or html5?
|
19
|
+
end
|
20
|
+
|
21
|
+
def html4?
|
22
|
+
@options[:format] == :html4
|
23
|
+
end
|
24
|
+
|
25
|
+
def html5?
|
26
|
+
@options[:format] == :html5
|
27
|
+
end
|
28
|
+
|
29
|
+
def toplevel?
|
30
|
+
upper.nil?
|
31
|
+
end
|
32
|
+
|
33
|
+
def active?
|
34
|
+
@active
|
35
|
+
end
|
36
|
+
|
37
|
+
def tabulation
|
38
|
+
@real_tabs + @tabulation
|
39
|
+
end
|
40
|
+
|
41
|
+
def tabulation=(val)
|
42
|
+
val = val - @real_tabs
|
43
|
+
@tabulation = val > -1 ? val : 0
|
44
|
+
end
|
45
|
+
|
46
|
+
def initialize(upper = nil, options = {})
|
47
|
+
@active = true
|
48
|
+
@upper = upper
|
49
|
+
@options = Options.buffer_defaults
|
50
|
+
@options = @options.merge(options) unless options.empty?
|
51
|
+
@buffer = new_encoded_string
|
52
|
+
@tabulation = 0
|
53
|
+
|
54
|
+
# The number of tabs that Engine thinks we should have
|
55
|
+
# @real_tabs + @tabulation is the number of tabs actually output
|
56
|
+
@real_tabs = 0
|
57
|
+
end
|
58
|
+
|
59
|
+
def push_text(text, tab_change, dont_tab_up)
|
60
|
+
if @tabulation > 0
|
61
|
+
# Have to push every line in by the extra user set tabulation.
|
62
|
+
# Don't push lines with just whitespace, though,
|
63
|
+
# because that screws up precompiled indentation.
|
64
|
+
text.gsub!(/^(?!\s+$)/m, tabs)
|
65
|
+
text.sub!(tabs, '') if dont_tab_up
|
66
|
+
end
|
67
|
+
|
68
|
+
@real_tabs += tab_change
|
69
|
+
@buffer << text
|
70
|
+
end
|
71
|
+
|
72
|
+
def adjust_tabs(tab_change)
|
73
|
+
@real_tabs += tab_change
|
74
|
+
end
|
75
|
+
|
76
|
+
def attributes(class_id, obj_ref, *attributes_hashes)
|
77
|
+
attributes = class_id
|
78
|
+
attributes_hashes.each do |old|
|
79
|
+
result = {}
|
80
|
+
old.each { |k, v| result[k.to_s] = v }
|
81
|
+
AttributeBuilder.merge_attributes!(attributes, result)
|
82
|
+
end
|
83
|
+
AttributeBuilder.merge_attributes!(attributes, parse_object_ref(obj_ref)) if obj_ref
|
84
|
+
AttributeBuilder.build_attributes(
|
85
|
+
html?, @options[:attr_wrapper], @options[:escape_attrs], @options[:hyphenate_data_attrs], attributes)
|
86
|
+
end
|
87
|
+
|
88
|
+
def rstrip!
|
89
|
+
if capture_position.nil?
|
90
|
+
buffer.rstrip!
|
91
|
+
return
|
92
|
+
end
|
93
|
+
|
94
|
+
buffer << buffer.slice!(capture_position..-1).rstrip
|
95
|
+
end
|
96
|
+
|
97
|
+
def fix_textareas!(input)
|
98
|
+
return input unless input.include?('<textarea'.freeze)
|
99
|
+
|
100
|
+
pattern = /<(textarea)([^>]*)>(\n|
)(.*?)<\/textarea>/im
|
101
|
+
input.gsub!(pattern) do |s|
|
102
|
+
match = pattern.match(s)
|
103
|
+
content = match[4]
|
104
|
+
if match[3] == '
'
|
105
|
+
content.sub!(/\A /, ' ')
|
106
|
+
else
|
107
|
+
content.sub!(/\A[ ]*/, '')
|
108
|
+
end
|
109
|
+
"<#{match[1]}#{match[2]}>\n#{content}</#{match[1]}>"
|
110
|
+
end
|
111
|
+
input
|
112
|
+
end
|
113
|
+
|
114
|
+
private
|
115
|
+
|
116
|
+
def new_encoded_string
|
117
|
+
"".encode(options[:encoding])
|
118
|
+
end
|
119
|
+
|
120
|
+
@@tab_cache = {}
|
121
|
+
# Gets `count` tabs. Mostly for internal use.
|
122
|
+
def tabs(count = 0)
|
123
|
+
tabs = [count + @tabulation, 0].max
|
124
|
+
@@tab_cache[tabs] ||= ' ' * tabs
|
125
|
+
end
|
126
|
+
|
127
|
+
def parse_object_ref(ref)
|
128
|
+
prefix = ref[1]
|
129
|
+
ref = ref[0]
|
130
|
+
# Let's make sure the value isn't nil. If it is, return the default Hash.
|
131
|
+
return {} if ref.nil?
|
132
|
+
class_name =
|
133
|
+
if ref.respond_to?(:ezml_object_ref)
|
134
|
+
ref.ezml_object_ref
|
135
|
+
else
|
136
|
+
underscore(ref.class)
|
137
|
+
end
|
138
|
+
ref_id =
|
139
|
+
if ref.respond_to?(:to_key)
|
140
|
+
key = ref.to_key
|
141
|
+
key.join('_') unless key.nil?
|
142
|
+
else
|
143
|
+
ref.id
|
144
|
+
end
|
145
|
+
id = "#{class_name}_#{ref_id || 'new'}"
|
146
|
+
if prefix
|
147
|
+
class_name = "#{ prefix }_#{ class_name}"
|
148
|
+
id = "#{ prefix }_#{ id }"
|
149
|
+
end
|
150
|
+
|
151
|
+
{ 'id'.freeze => id, 'class'.freeze => class_name }
|
152
|
+
end
|
153
|
+
|
154
|
+
def underscore(camel_cased_word)
|
155
|
+
word = camel_cased_word.to_s.dup
|
156
|
+
word.gsub!(/::/, '_')
|
157
|
+
word.gsub!(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
|
158
|
+
word.gsub!(/([a-z\d])([A-Z])/, '\1_\2')
|
159
|
+
word.tr!('-', '_')
|
160
|
+
word.downcase!
|
161
|
+
word
|
162
|
+
end
|
163
|
+
|
164
|
+
end
|
165
|
+
|
166
|
+
end
|