File: haml_attribute_builder.rb

package info (click to toggle)
ruby-hamlit 2.15.1-3
  • links: PTS, VCS
  • area: main
  • in suites: bookworm, forky, sid, trixie
  • size: 1,996 kB
  • sloc: ruby: 10,548; ansic: 536; sh: 23; makefile: 8
file content (164 lines) | stat: -rw-r--r-- 5,428 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
# frozen_string_literal: true

module Hamlit
  module HamlAttributeBuilder
    # https://html.spec.whatwg.org/multipage/syntax.html#attributes-2
    INVALID_ATTRIBUTE_NAME_REGEX = /[ \0"'>\/=]/

    class << self
      def build_attributes(is_html, attr_wrapper, escape_attrs, hyphenate_data_attrs, attributes = {})
        # @TODO this is an absolutely ridiculous amount of arguments. At least
        # some of this needs to be moved into an instance method.
        join_char = hyphenate_data_attrs ? '-' : '_'

        attributes.each do |key, value|
          if value.is_a?(Hash)
            data_attributes = attributes.delete(key)
            data_attributes = flatten_data_attributes(data_attributes, '', join_char)
            data_attributes = build_data_keys(data_attributes, hyphenate_data_attrs, key)
            verify_attribute_names!(data_attributes.keys)
            attributes = data_attributes.merge(attributes)
          end
        end

        result = attributes.collect do |attr, value|
          next if value.nil?

          value = filter_and_join(value, ' ') if attr == 'class'
          value = filter_and_join(value, '_') if attr == 'id'

          if value == true
            next " #{attr}" if is_html
            next " #{attr}=#{attr_wrapper}#{attr}#{attr_wrapper}"
          elsif value == false
            next
          end

          value =
            if escape_attrs == :once
              Hamlit::HamlHelpers.escape_once_without_haml_xss(value.to_s)
            elsif escape_attrs
              Hamlit::HamlHelpers.html_escape_without_haml_xss(value.to_s)
            else
              value.to_s
            end
          " #{attr}=#{attr_wrapper}#{value}#{attr_wrapper}"
        end
        result.compact!
        result.sort!
        result.join
      end

      # @return [String, nil]
      def filter_and_join(value, separator)
        return '' if (value.respond_to?(:empty?) && value.empty?)

        if value.is_a?(Array)
          value = value.flatten
          value.map! {|item| item ? item.to_s : nil}
          value.compact!
          value = value.join(separator)
        else
          value = value ? value.to_s : nil
        end
        !value.nil? && !value.empty? && value
      end

      # Merges two attribute hashes.
      # This is the same as `to.merge!(from)`,
      # except that it merges id, class, and data attributes.
      #
      # ids are concatenated with `"_"`,
      # and classes are concatenated with `" "`.
      # data hashes are simply merged.
      #
      # Destructively modifies `to`.
      #
      # @param to [{String => String,Hash}] The attribute hash to merge into
      # @param from [{String => Object}] The attribute hash to merge from
      # @return [{String => String,Hash}] `to`, after being merged
      def merge_attributes!(to, from)
        from.keys.each do |key|
          to[key] = merge_value(key, to[key], from[key])
        end
        to
      end

      # Merge multiple values to one attribute value. No destructive operation.
      #
      # @param key [String]
      # @param values [Array<Object>]
      # @return [String,Hash]
      def merge_values(key, *values)
        values.inject(nil) do |to, from|
          merge_value(key, to, from)
        end
      end

      def verify_attribute_names!(attribute_names)
        attribute_names.each do |attribute_name|
          if attribute_name =~ INVALID_ATTRIBUTE_NAME_REGEX
            raise HamlInvalidAttributeNameError.new("Invalid attribute name '#{attribute_name}' was rendered")
          end
        end
      end

      private

      # Merge a couple of values to one attribute value. No destructive operation.
      #
      # @param to [String,Hash,nil]
      # @param from [Object]
      # @return [String,Hash]
      def merge_value(key, to, from)
        if from.kind_of?(Hash) || to.kind_of?(Hash)
          from = { nil => from } if !from.is_a?(Hash)
          to   = { nil => to }   if !to.is_a?(Hash)
          to.merge(from)
        elsif key == 'id'
          merged_id = filter_and_join(from, '_')
          if to && merged_id
            merged_id = "#{to}_#{merged_id}"
          elsif to || merged_id
            merged_id ||= to
          end
          merged_id
        elsif key == 'class'
          merged_class = filter_and_join(from, ' ')
          if to && merged_class
            merged_class = (to.split(' ') | merged_class.split(' ')).join(' ')
          elsif to || merged_class
            merged_class ||= to
          end
          merged_class
        else
          from
        end
      end

      def build_data_keys(data_hash, hyphenate, attr_name="data")
        Hash[data_hash.map do |name, value|
          if name == nil
            [attr_name, value]
          elsif hyphenate
            ["#{attr_name}-#{name.to_s.tr('_', '-')}", value]
          else
            ["#{attr_name}-#{name}", value]
          end
        end]
      end

      def flatten_data_attributes(data, key, join_char, seen = [])
        return {key => data} unless data.is_a?(Hash)

        return {key => nil} if seen.include? data.object_id
        seen << data.object_id

        data.sort {|x, y| x[0].to_s <=> y[0].to_s}.inject({}) do |hash, (k, v)|
          joined = key == '' ? k : [key, k].join(join_char)
          hash.merge! flatten_data_attributes(v, joined, join_char, seen)
        end
      end
    end
  end
end