File: utils.rb

package info (click to toggle)
ruby-sprockets 3.7.0-1%2Bdeb9u1
  • links: PTS, VCS
  • area: main
  • in suites: stretch
  • size: 544 kB
  • sloc: ruby: 4,163; makefile: 2
file content (231 lines) | stat: -rw-r--r-- 6,023 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
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
require 'set'

module Sprockets
  # Internal: Utils, we didn't know where else to put it! Functions may
  # eventually be shuffled into more specific drawers.
  module Utils
    extend self

    # Internal: Check if object can safely be .dup'd.
    #
    # Similar to ActiveSupport #duplicable? check.
    #
    # obj - Any Object
    #
    # Returns false if .dup would raise a TypeError, otherwise true.
    def duplicable?(obj)
      case obj
      when NilClass, FalseClass, TrueClass, Symbol, Numeric
        false
      else
        true
      end
    end

    # Internal: Duplicate and store key/value on new frozen hash.
    #
    # Seperated for recursive calls, always use hash_reassoc(hash, *keys).
    #
    # hash - Hash
    # key  - Object key
    #
    # Returns Hash.
    def hash_reassoc1(hash, key)
      hash = hash.dup if hash.frozen?
      old_value = hash[key]
      old_value = old_value.dup if duplicable?(old_value)
      new_value = yield old_value
      new_value.freeze if duplicable?(new_value)
      hash.store(key, new_value)
      hash.freeze
    end

    # Internal: Duplicate and store key/value on new frozen hash.
    #
    # Similar to Hash#store for nested frozen hashes.
    #
    # hash  - Hash
    # key   - Object keys. Use multiple keys for nested hashes.
    # block - Receives current value at key.
    #
    # Examples
    #
    #     config = {paths: ["/bin", "/sbin"]}.freeze
    #     new_config = hash_reassoc(config, :paths) do |paths|
    #       paths << "/usr/local/bin"
    #     end
    #
    # Returns duplicated frozen Hash.
    def hash_reassoc(hash, *keys, &block)
      if keys.size == 1
        hash_reassoc1(hash, keys[0], &block)
      else
        hash_reassoc1(hash, keys[0]) do |value|
          hash_reassoc(value, *keys[1..-1], &block)
        end
      end
    end

    # Internal: Check if string has a trailing semicolon.
    #
    # str - String
    #
    # Returns true or false.
    def string_end_with_semicolon?(str)
      i = str.size - 1
      while i >= 0
        c = str[i].ord
        i -= 1

        # Need to compare against the ordinals because the string can be UTF_8 or UTF_32LE encoded
        # 0x0A == "\n"
        # 0x20 == " "
        # 0x09 == "\t"
        # 0x3B == ";"
        unless c == 0x0A || c == 0x20 || c == 0x09
          return c === 0x3B
        end
      end

      true
    end

    # Internal: Accumulate asset source to buffer and append a trailing
    # semicolon if necessary.
    #
    # buf    - String buffer to append to
    # source - String source to append
    #
    # Returns buf String.
    def concat_javascript_sources(buf, source)
      if source.bytesize > 0
        buf << source

        # If the source contains non-ASCII characters, indexing on it becomes O(N).
        # This will lead to O(N^2) performance in string_end_with_semicolon?, so we should use 32 bit encoding to make sure indexing stays O(1)
        source = source.encode(Encoding::UTF_32LE) unless source.ascii_only?

        if !string_end_with_semicolon?(source)
          buf << ";\n"
        elsif source[source.size - 1].ord != 0x0A
          buf << "\n"
        end
      end

      buf
    end

    # Internal: Prepends a leading "." to an extension if its missing.
    #
    #     normalize_extension("js")
    #     # => ".js"
    #
    #     normalize_extension(".css")
    #     # => ".css"
    #
    def normalize_extension(extension)
      extension = extension.to_s
      if extension[/^\./]
        extension
      else
        ".#{extension}"
      end
    end

    # Internal: Feature detect if UnboundMethods can #bind to any Object or
    # just Objects that share the same super class.
    # Basically if RUBY_VERSION >= 2.
    UNBOUND_METHODS_BIND_TO_ANY_OBJECT = begin
      foo = Module.new { def bar; end }
      foo.instance_method(:bar).bind(Object.new)
      true
    rescue TypeError
      false
    end

    # Internal: Inject into target module for the duration of the block.
    #
    # mod - Module
    #
    # Returns result of block.
    def module_include(base, mod)
      old_methods = {}

      mod.instance_methods.each do |sym|
        old_methods[sym] = base.instance_method(sym) if base.method_defined?(sym)
      end

      unless UNBOUND_METHODS_BIND_TO_ANY_OBJECT
        base.send(:include, mod) unless base < mod
      end

      mod.instance_methods.each do |sym|
        method = mod.instance_method(sym)
        base.send(:define_method, sym, method)
      end

      yield
    ensure
      mod.instance_methods.each do |sym|
        base.send(:undef_method, sym) if base.method_defined?(sym)
      end
      old_methods.each do |sym, method|
        base.send(:define_method, sym, method)
      end
    end

    # Internal: Post-order Depth-First search algorithm.
    #
    # Used for resolving asset dependencies.
    #
    # initial - Initial Array of nodes to traverse.
    # block   -
    #   node  - Current node to get children of
    #
    # Returns a Set of nodes.
    def dfs(initial)
      nodes, seen = Set.new, Set.new
      stack = Array(initial).reverse

      while node = stack.pop
        if seen.include?(node)
          nodes.add(node)
        else
          seen.add(node)
          stack.push(node)
          stack.concat(Array(yield node).reverse)
        end
      end

      nodes
    end

    # Internal: Post-order Depth-First search algorithm that gathers all paths
    # along the way.
    #
    # TODO: Rename function.
    #
    # path   - Initial Array node path
    # block  -
    #   node - Current node to get children of
    #
    # Returns an Array of node Arrays.
    def dfs_paths(path)
      paths = []
      stack, seen = [path], Set.new

      while path = stack.pop
        if !seen.include?(path.last)
          seen.add(path.last)
          paths << path if path.size > 1

          Array(yield path.last).reverse_each do |node|
            stack.push(path + [node])
          end
        end
      end

      paths
    end
  end
end