forked from instructure/canvas-lms
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathbrandable_css.rb
318 lines (277 loc) · 14.5 KB
/
brandable_css.rb
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
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
# frozen_string_literal: true
#
# Copyright (C) 2015 - present Instructure, Inc.
#
# This file is part of Canvas.
#
# Canvas is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the Free
# Software Foundation, version 3 of the License.
#
# Canvas 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 Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along
# with this program. If not, see <http://www.gnu.org/licenses/>.
module BrandableCSS
APP_ROOT = (defined?(Rails) && Rails.root) || Pathname.pwd
CONFIG = YAML.load_file(APP_ROOT.join("config/brandable_css.yml")).freeze
BRANDABLE_VARIABLES = JSON.parse(File.read(APP_ROOT.join(CONFIG["paths"]["brandable_variables_json"]))).freeze
MIGRATION_NAME = "RegenerateBrandFilesBasedOnNewDefaults"
VARIABLE_HUMAN_NAMES = {
"ic-brand-primary" => -> { I18n.t("Primary Brand Color") },
"ic-brand-font-color-dark" => -> { I18n.t("Main Text Color") },
"ic-link-color" => -> { I18n.t("Link Color") },
"ic-brand-button--primary-bgd" => -> { I18n.t("Primary Button") },
"ic-brand-button--primary-text" => -> { I18n.t("Primary Button Text") },
"ic-brand-button--secondary-bgd" => -> { I18n.t("Secondary Button") },
"ic-brand-button--secondary-text" => -> { I18n.t("Secondary Button Text") },
"ic-brand-global-nav-bgd" => -> { I18n.t("Nav Background") },
"ic-brand-global-nav-ic-icon-svg-fill" => -> { I18n.t("Nav Icon") },
"ic-brand-global-nav-ic-icon-svg-fill--active" => -> { I18n.t("Nav Icon Active") },
"ic-brand-global-nav-menu-item__text-color" => -> { I18n.t("Nav Text") },
"ic-brand-global-nav-menu-item__text-color--active" => -> { I18n.t("Nav Text Active") },
"ic-brand-global-nav-avatar-border" => -> { I18n.t("Nav Avatar Border") },
"ic-brand-global-nav-menu-item__badge-bgd" => -> { I18n.t("Nav Badge") },
"ic-brand-global-nav-menu-item__badge-bgd--active" => -> { I18n.t("Nav Badge Active") },
"ic-brand-global-nav-menu-item__badge-text" => -> { I18n.t("Nav Badge Text") },
"ic-brand-global-nav-menu-item__badge-text--active" => -> { I18n.t("Nav Badge Text Active") },
"ic-brand-global-nav-logo-bgd" => -> { I18n.t("Nav Logo Background") },
"ic-brand-header-image" => -> { I18n.t("Nav Logo") },
"ic-brand-mobile-global-nav-logo" => -> { I18n.t("Responsive Global Nav Logo") },
"ic-brand-watermark" => -> { I18n.t("Watermark") },
"ic-brand-watermark-opacity" => -> { I18n.t("Watermark Opacity") },
"ic-brand-favicon" => -> { I18n.t("Favicon") },
"ic-brand-apple-touch-icon" => -> { I18n.t("Mobile Homescreen Icon") },
"ic-brand-msapplication-tile-color" => -> { I18n.t("Windows Tile Color") },
"ic-brand-msapplication-tile-square" => -> { I18n.t("Windows Tile: Square") },
"ic-brand-msapplication-tile-wide" => -> { I18n.t("Windows Tile: Wide") },
"ic-brand-right-sidebar-logo" => -> { I18n.t("Right Sidebar Logo") },
"ic-brand-Login-body-bgd-color" => -> { I18n.t("Background Color") },
"ic-brand-Login-body-bgd-image" => -> { I18n.t("Background Image") },
"ic-brand-Login-body-bgd-shadow-color" => -> { I18n.t("Body Shadow") },
"ic-brand-Login-logo" => -> { I18n.t("Login Logo") },
"ic-brand-Login-Content-bgd-color" => -> { I18n.t("Top Box Background") },
"ic-brand-Login-Content-border-color" => -> { I18n.t("Top Box Border") },
"ic-brand-Login-Content-inner-bgd" => -> { I18n.t("Inner Box Background") },
"ic-brand-Login-Content-inner-border" => -> { I18n.t("Inner Box Border") },
"ic-brand-Login-Content-inner-body-bgd" => -> { I18n.t("Form Background") },
"ic-brand-Login-Content-inner-body-border" => -> { I18n.t("Form Border") },
"ic-brand-Login-Content-label-text-color" => -> { I18n.t("Login Label") },
"ic-brand-Login-Content-password-text-color" => -> { I18n.t("Login Link Color") },
"ic-brand-Login-footer-link-color" => -> { I18n.t("Login Footer Link") },
"ic-brand-Login-footer-link-color-hover" => -> { I18n.t("Login Footer Link Hover") },
"ic-brand-Login-instructure-logo" => -> { I18n.t("Login Instructure Logo") }
}.freeze
GROUP_NAMES = {
"global_branding" => -> { I18n.t("Global Branding") },
"global_navigation" => -> { I18n.t("Global Navigation") },
"watermarks" => -> { I18n.t("Watermarks & Other Images") },
"login" => -> { I18n.t("Login Screen") }
}.freeze
HELPER_TEXTS = {
"ic-brand-header-image" => -> { I18n.t("Accepted formats: svg, png, jpg, gif") },
"ic-brand-mobile-global-nav-logo" => -> { I18n.t("Appears at the top of the global navigation tray that opens on mobile sized screens. display height: 48px. Accepted formats: svg, png, jpg, gif") },
"ic-brand-watermark" => -> { I18n.t("This image appears as a background watermark to your page. Accepted formats: png, svg, gif, jpeg") },
"ic-brand-watermark-opacity" => -> { I18n.t("Specify the transparency of the watermark background image.") },
"ic-brand-favicon" => -> { I18n.t("You can use a single 16x16, 32x32, 48x48 ico file.") },
"ic-brand-apple-touch-icon" => -> { I18n.t("The shortcut icon for iOS/Android devices. 180x180 png") },
"ic-brand-msapplication-tile-square" => -> { I18n.t("558x558 png, jpg, gif (1.8x the standard tile size, so it can be scaled up or down as needed)") },
"ic-brand-msapplication-tile-wide" => -> { I18n.t("558x270 png, jpg, gif") },
"ic-brand-right-sidebar-logo" => -> { I18n.t("A full-size logo that appears in the right sidebar on the Canvas dashboard. Ideal size is 360 x 140 pixels. Accepted formats: svg, png, jpeg, gif") },
"ic-brand-Login-body-bgd-shadow-color" => -> { I18n.t("accepted formats: hex, rgba, rgb, hsl") }
}.freeze
class << self
def variables_map
@variables_map ||= BRANDABLE_VARIABLES.each_with_object({}) do |variable_group, memo|
variable_group["variables"].each { |variable| memo[variable["variable_name"]] = variable }
end.freeze
end
def variables_map_with_image_urls
@variables_map_with_image_urls ||= variables_map.transform_values do |config|
if config["type"] == "image"
config.merge("default" => ActionController::Base.helpers.image_url(config["default"]))
else
config
end
end.freeze
end
def migration_version
# ActiveRecord usually uses integer timestamps to generate migration versions but any integer
# will work, so we just use the result of stripping out the alphabetic characters from the md5
@migration_version ||= default_variables_md5.gsub(/[a-z]/, "").to_i
end
def default_variables_md5
@default_variables_md5 ||= begin
things_that_go_into_defaults_md5 = variables_map.transform_values do |value|
case value["type"]
when "image"
# to make consistent md5s whether the cdn is enabled or not, don't include hostname in defaults
ActionController::Base.helpers.image_path(value["default"], host: "")
else
value["default"]
end
end
Digest::MD5.hexdigest(things_that_go_into_defaults_md5.to_json).freeze
end
end
def handle_urls(value, config, css_urls)
return value unless config["type"] == "image" && css_urls
"url('#{value}')" if value.present?
end
# gets the *effective* value for a brandable variable
def brand_variable_value(variable_name, active_brand_config = nil, config_map = variables_map, css_urls = false)
config = config_map[variable_name]
explicit_value = active_brand_config && active_brand_config.get_value(variable_name).presence
return handle_urls(explicit_value, config, css_urls) if explicit_value
default = config["default"]
if default&.starts_with?("$")
if css_urls
return "var(--#{default[1..]})"
else
return brand_variable_value(default[1..], active_brand_config, config_map, css_urls)
end
end
# while in our sass, we want `url(/images/foo.png)`,
# the Rails Asset Helpers expect us to not have the '/images/', eg: <%= image_tag('foo.png') %>
default = default.sub(%r{^/images/}, "") if config["type"] == "image"
handle_urls(default, config, css_urls)
end
def computed_variables(active_brand_config = nil)
[
["ic-brand-primary", "darken", 5],
["ic-brand-primary", "darken", 10],
["ic-brand-primary", "darken", 15],
["ic-brand-primary", "lighten", 5],
["ic-brand-primary", "lighten", 10],
["ic-brand-primary", "lighten", 15],
["ic-brand-button--primary-bgd", "darken", 5],
["ic-brand-button--primary-bgd", "darken", 15],
["ic-brand-button--secondary-bgd", "darken", 5],
["ic-brand-button--secondary-bgd", "darken", 15],
["ic-brand-font-color-dark", "lighten", 15],
["ic-brand-font-color-dark", "lighten", 28],
["ic-link-color", "darken", 10],
["ic-link-color", "lighten", 10],
].each_with_object({}) do |(variable_name, darken_or_lighten, percent), memo|
color = brand_variable_value(variable_name, active_brand_config, variables_map_with_image_urls)
computed_color = CanvasColor::Color.new(color).send(darken_or_lighten, percent / 100.0)
memo["#{variable_name}-#{darken_or_lighten}ed-#{percent}"] = computed_color.to_s
end
end
def all_brand_variable_values(active_brand_config = nil, css_urls = false)
variables_map.each_with_object(computed_variables(active_brand_config)) do |(key, _), memo|
memo[key] = brand_variable_value(key, active_brand_config, variables_map_with_image_urls, css_urls)
end
end
def all_brand_variable_values_as_json(active_brand_config = nil)
all_brand_variable_values(active_brand_config).to_json
end
def all_brand_variable_values_as_js(active_brand_config = nil)
"CANVAS_ACTIVE_BRAND_VARIABLES = #{all_brand_variable_values_as_json(active_brand_config)};"
end
def all_brand_variable_values_as_css(active_brand_config = nil)
":root {
#{all_brand_variable_values(active_brand_config, true).map { |k, v| "--#{k}: #{v};" }.join("\n")}
}"
end
def public_brandable_css_folder
Pathname.new("public/dist/brandable_css")
end
def default_brand_folder
public_brandable_css_folder.join("default")
end
def default_brand_file(type, high_contrast = false)
default_brand_folder.join("variables#{high_contrast ? "-high_contrast" : ""}-#{default_variables_md5}.#{type}")
end
def high_contrast_overrides
Class.new do
def get_value(variable_name)
{ "ic-brand-primary" => "#0770A3", "ic-link-color" => "#0073A7" }[variable_name]
end
end.new
end
def default(type, high_contrast = false)
bc = high_contrast ? high_contrast_overrides : nil
send("all_brand_variable_values_as_#{type}", bc)
end
def save_default!(type, high_contrast = false)
default_brand_folder.mkpath
default_brand_file(type, high_contrast).write(default(type, high_contrast))
move_default_to_s3_if_enabled!(type, high_contrast)
end
def save_default_files!
[true, false].each do |high_contrast|
%w[js css json].each { |type| save_default!(type, high_contrast) }
end
end
def move_default_to_s3_if_enabled!(type, high_contrast = false)
return unless defined?(Canvas) && Canvas::Cdn.enabled?
s3_uploader.upload_file(public_default_path(type, high_contrast))
begin
File.delete(default_brand_file(type, high_contrast))
rescue Errno::ENOENT
# continue if something else deleted it in another process
end
end
def s3_uploader
@s3_uploaderer ||= Canvas::Cdn::S3Uploader.new
end
def public_default_path(type, high_contrast = false)
"dist/brandable_css/default/variables#{high_contrast ? "-high_contrast" : ""}-#{default_variables_md5}.#{type}"
end
def variants
@variants ||= CONFIG["variants"].keys.freeze
end
def brandable_variants
@brandable_variants ||= CONFIG["variants"].select { |_, v| v["brandable"] }.map { |k, _| k }.freeze
end
def combined_checksums
if defined?(ActionController) && ActionController::Base.perform_caching && defined?(@combined_checksums)
return @combined_checksums
end
file = APP_ROOT.join(CONFIG["paths"]["bundles_with_deps"])
if file.exist?
@combined_checksums = JSON.parse(file.read).transform_values do |v|
v.symbolize_keys.slice(:combinedChecksum, :includesNoVariables)
end.freeze
elsif defined?(Rails) && Rails.env.production?
raise "#{file.expand_path} does not exist. You need to run `yarn run build:css` before you can serve css."
else
# for dev/test there might be cases where you don't want it to raise an exception
# if you haven't ran `brandable_css` and the manifest file doesn't exist yet.
# eg: you want to test a controller action and you don't care that it links
# to a css file that hasn't been created yet.
default_value = { combinedChecksum: "Error: unknown css checksum. you need to run brandable_css" }.freeze
@combined_checksums = Hash.new(default_value).freeze
end
end
# javascript needs to know the checksums of the available variants for each
# handlebars template so that it loads the corresponding stylesheet when the
# template is rendered at runtime
def handlebars_index_json
if defined?(ActionController) && ActionController::Base.perform_caching && defined?(@handlebars_index_json)
return @handlebars_index_json
end
file = APP_ROOT.join(CONFIG.dig("indices", "handlebars", "path"))
unless file.exist?
raise "#{file.expand_path} does not exist. You need to run `yarn run build:css` before you can serve css."
end
@handlebars_index_json = file.read
end
# bundle path should be something like "bundles/speedgrader" or "plugins/analytics/something"
def cache_for(bundle_path, variant)
key = ["#{bundle_path}.scss", variant].join(CONFIG["manifest_key_seperator"])
fingerprint = combined_checksums[key]
raise "Fingerprint not found. #{bundle_path} #{variant}" unless fingerprint
fingerprint
end
def all_fingerprints_for(bundle_path)
variants.index_with do |variant|
cache_for(bundle_path, variant)
end
end
end
end