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
|
# frozen_string_literal: true
# Redmine - project management software
# Copyright (C) 2006-2023 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
module Redmine
class AssetPath
attr_reader :paths, :prefix, :version
def initialize(base_dir, paths, prefix=nil)
@base_dir = base_dir
@paths = paths
@prefix = prefix
@transition = Transition.new(src: Set.new, dest: Set.new)
@version = Rails.application.config.assets.version
end
def update(transition_map:, assets:)
each_file do |file, intermediate_path, logical_path|
@transition.add_src intermediate_path, logical_path
@transition.add_dest intermediate_path, logical_path
asset = if file.extname == '.css'
Redmine::Asset.new(file, logical_path: logical_path, version: version, transition_map: transition_map)
else
Propshaft::Asset.new(file, logical_path: logical_path, version: version)
end
assets[asset.logical_path.to_s] ||= asset
end
@transition.update(transition_map)
nil
end
def each_file
paths.each do |path|
without_dotfiles(all_files_from_tree(path)).each do |file|
relative_path = file.relative_path_from(path).to_s
logical_path = prefix ? File.join(prefix, relative_path) : relative_path
intermediate_path = Pathname.new("/#{prefix}").join(file.relative_path_from(@base_dir))
yield file, intermediate_path, logical_path
end
end
end
private
Transition = Struct.new(:src, :dest, keyword_init: true) do
def add_src(file, logical_path)
src.add path_pair(file, logical_path) if file.extname == '.css'
end
def add_dest(file, logical_path)
return if file.extname == '.js' || file.extname == '.map'
# No parent-child directories are needed in dest.
dirname = file.dirname
if child = dest.find{|d| child_path? dirname, d[0]}
dest.delete child
dest.add path_pair(file, logical_path)
elsif !dest.any?{|d| parent_path? dirname, d[0]}
dest.add path_pair(file, logical_path)
end
end
def path_pair(file, logical_path)
[file.dirname, Pathname.new("/#{logical_path}").dirname]
end
def parent_path?(path, other)
return false if other == path
path.ascend.any?(other)
end
def child_path?(path, other)
return false if path == other
other.ascend.any?(path)
end
def update(transition_map)
product = src.to_a.product(dest.to_a).select{|t| t[0] != t[1]}
maps = product.map do |t|
AssetPathMap.new(src: t[0][0], dest: t[1][0], logical_src: t[0][1], logical_dest: t[1][1])
end
maps.each do |m|
if m.before != m.after
transition_map[m.dirname] ||= {}
transition_map[m.dirname][m.before] = m.after
end
end
end
end
AssetPathMap = Struct.new(:src, :dest, :logical_src, :logical_dest, keyword_init: true) do
def dirname
key = logical_src.to_s.sub('/', '')
key == '' ? '.' : key
end
def before
dest.relative_path_from(src).to_s
end
def after
logical_dest.relative_path_from(logical_src).to_s
end
end
def without_dotfiles(files)
files.reject { |file| file.basename.to_s.starts_with?(".") }
end
def all_files_from_tree(path)
path.children.flat_map { |child| child.directory? ? all_files_from_tree(child) : child }
end
end
class AssetLoadPath < Propshaft::LoadPath
attr_reader :extension_paths, :default_asset_path, :transition_map
def initialize(config)
@extension_paths = config.redmine_extension_paths
@default_asset_path = config.redmine_default_asset_path
super(config.paths, version: config.version)
end
def asset_files
Enumerator.new do |y|
Rails.logger.info all_paths
all_paths.each do |path|
next unless path.exist?
without_dotfiles(all_files_from_tree(path)).each do |file|
y << file
end
end
end
end
def assets_by_path
merge_required = @cached_assets_by_path.nil?
super
if merge_required
@transition_map = {}
default_asset_path.update(assets: @cached_assets_by_path, transition_map: transition_map)
extension_paths.each do |asset_path|
# Support link from extension assets to assets in the application
default_asset_path.each_file do |file, intermediate_path, logical_path|
asset_path.instance_eval { @transition.add_dest intermediate_path, logical_path }
end
asset_path.update(assets: @cached_assets_by_path, transition_map: transition_map)
end
end
@cached_assets_by_path
end
def cache_sweeper
@cache_sweeper ||= begin
exts_to_watch = Mime::EXTENSION_LOOKUP.map(&:first)
files_to_watch = Array(all_paths).to_h { |dir| [dir.to_s, exts_to_watch] }
Rails.application.config.file_watcher.new([], files_to_watch) do
clear_cache
end
end
end
def all_paths
[paths, default_asset_path.paths, extension_paths.map{|path| path.paths}].flatten.compact
end
def clear_cache
@transition_map = nil
super
end
end
class Asset < Propshaft::Asset
def initialize(file, logical_path:, version:, transition_map:)
@transition_map = transition_map
super(file, logical_path: logical_path, version: version)
end
def content
if conversion = @transition_map[logical_path.dirname.to_s]
convert_path super, conversion
else
super
end
end
ASSET_URL_PATTERN = /(url\(\s*["']?([^"'\s)]+)\s*["']?\s*\))/ unless defined? ASSET_URL_PATTERN
def convert_path(input, conversion)
input.gsub(ASSET_URL_PATTERN) do |matched|
conversion.each do |key, val|
matched.sub!(key, val)
end
matched
end
end
end
end
|