ezml 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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