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.
@@ -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
@@ -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|&#x000A;)(.*?)<\/textarea>/im
101
+ input.gsub!(pattern) do |s|
102
+ match = pattern.match(s)
103
+ content = match[4]
104
+ if match[3] == '&#x000A;'
105
+ content.sub!(/\A /, '&#x0020;')
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