discourse/lib/stylesheet/manager/builder.rb
Osama Sayegh c2fcd55a80
FEATURE: Serve RTL versions of admin and plugins CSS bundles for RTL locales (#21876)
Prior to this commit, we didn't have RTL versions of our admin and plugins CSS bundles and we always served LTR versions of those bundles even when users used an RTL locale, causing admin and plugins UI elements to never look as good as when an LTR locale was used. Example of UI issues prior to this commit were: missing margins, borders on the wrong side and buttons too close to each other etc.

This commit creates an RTL version for the admin CSS bundle as well as RTL bundles for all the installed plugins and serves those RTL bundles to users/sites who use RTL locales.
2023-06-01 05:27:11 +03:00

300 lines
8.1 KiB
Ruby

# frozen_string_literal: true
class Stylesheet::Manager::Builder
attr_reader :theme
def initialize(target: :desktop, theme: nil, color_scheme: nil, manager:)
@target = target
@theme = theme
@color_scheme = color_scheme
@manager = manager
end
def compile(opts = {})
if !opts[:force]
if File.exist?(stylesheet_fullpath)
if !StylesheetCache.where(target: qualified_target, digest: digest).exists?
begin
source_map =
begin
File.read(source_map_fullpath)
rescue Errno::ENOENT
end
StylesheetCache.add(
qualified_target,
digest,
File.read(stylesheet_fullpath),
source_map,
)
rescue => e
Rails.logger.warn "Completely unexpected error adding contents of '#{stylesheet_fullpath}' to cache #{e}"
end
end
return true
end
end
rtl = @target.to_s.end_with?("_rtl")
css, source_map =
with_load_paths do |load_paths|
Stylesheet::Compiler.compile_asset(
@target.to_s.gsub(/_rtl\z/, "").to_sym,
rtl: rtl,
theme_id: theme&.id,
theme_variables: theme&.scss_variables.to_s,
source_map_file: source_map_url_relative_from_stylesheet,
color_scheme_id: @color_scheme&.id,
load_paths: load_paths,
)
rescue SassC::SyntaxError, SassC::NotRenderedError => e
if Stylesheet::Importer::THEME_TARGETS.include?(@target.to_s)
# no special errors for theme, handled in theme editor
["", nil]
elsif @target.to_s == Stylesheet::Manager::COLOR_SCHEME_STYLESHEET
# log error but do not crash for errors in color definitions SCSS
Rails.logger.error "SCSS compilation error: #{e.message}"
["", nil]
else
raise Discourse::ScssError, e.message
end
end
FileUtils.mkdir_p(cache_fullpath)
File.open(stylesheet_fullpath, "w") { |f| f.puts css }
File.open(source_map_fullpath, "w") { |f| f.puts source_map } if source_map.present?
begin
StylesheetCache.add(qualified_target, digest, css, source_map)
rescue => e
Rails.logger.warn "Completely unexpected error adding item to cache #{e}"
end
css
end
def current_hostname
Discourse.current_hostname
end
def cache_fullpath
Stylesheet::Manager.cache_fullpath
end
def stylesheet_fullpath
"#{cache_fullpath}/#{stylesheet_filename}"
end
def source_map_fullpath
"#{cache_fullpath}/#{source_map_filename}"
end
def source_map_filename
"#{stylesheet_filename}.map"
end
def source_map_url_relative_from_stylesheet
"#{source_map_filename}?__ws=#{current_hostname}"
end
def stylesheet_fullpath_no_digest
"#{cache_fullpath}/#{stylesheet_filename_no_digest}"
end
def stylesheet_absolute_url
"#{GlobalSetting.cdn_url}#{stylesheet_relpath}?__ws=#{current_hostname}"
end
def root_path
"#{GlobalSetting.relative_url_root}/"
end
def stylesheet_relpath
"#{root_path}stylesheets/#{stylesheet_filename}"
end
def stylesheet_relpath_no_digest
"#{root_path}stylesheets/#{stylesheet_filename_no_digest}"
end
def qualified_target
if is_theme?
"#{@target}_#{theme&.id}"
elsif @color_scheme
"#{@target}_#{scheme_slug}_#{@color_scheme&.id.to_s}_#{@theme&.id}"
else
scheme_string = theme&.color_scheme ? "_#{theme.color_scheme.id}" : ""
"#{@target}#{scheme_string}"
end
end
def stylesheet_filename(with_digest = true)
digest_string = "_#{self.digest}" if with_digest
"#{qualified_target}#{digest_string}.css"
end
def stylesheet_filename_no_digest
stylesheet_filename(_with_digest = false)
end
def is_theme?
!!(@target.to_s =~ Stylesheet::Manager::THEME_REGEX)
end
def is_color_scheme?
!!(@target.to_s == Stylesheet::Manager::COLOR_SCHEME_STYLESHEET)
end
def scheme_slug
Slug.for(ActiveSupport::Inflector.transliterate(@color_scheme.name), "scheme")
end
# digest encodes the things that trigger a recompile
def digest
@digest ||=
begin
if is_theme?
theme_digest
elsif is_color_scheme?
color_scheme_digest
else
default_digest
end
end
end
def with_load_paths
if theme
theme.with_scss_load_paths { |p| yield p }
else
yield nil
end
end
def scss_digest
if %i[mobile_theme desktop_theme].include?(@target)
resolve_baked_field(@target.to_s.sub("_theme", ""), :scss)
elsif @target == :embedded_theme
resolve_baked_field(:common, :embedded_scss)
else
raise "attempting to look up theme digest for invalid field"
end
end
def theme_digest
Digest::SHA1.hexdigest(
scss_digest.to_s + color_scheme_digest.to_s + settings_digest + uploads_digest +
current_hostname,
)
end
# this protects us from situations where new versions of a plugin removed a file
# old instances may still be serving CSS and not aware of the change
# so we could end up poisoning the cache with a bad file that can not be removed
def plugins_digest
assets = []
DiscoursePluginRegistry.stylesheets.each { |_, paths| assets += paths.to_a }
DiscoursePluginRegistry.mobile_stylesheets.each { |_, paths| assets += paths.to_a }
DiscoursePluginRegistry.desktop_stylesheets.each { |_, paths| assets += paths.to_a }
Digest::SHA1.hexdigest(assets.sort.join)
end
def settings_digest
themes =
if !theme
[]
elsif Theme.is_parent_theme?(theme.id)
@manager.load_themes(@manager.theme_ids)
else
[@manager.get_theme(theme.id)]
end
fields =
themes.each_with_object([]) do |theme, array|
array.concat(theme.yaml_theme_fields.map(&:updated_at))
end
settings =
themes.each_with_object([]) do |theme, array|
array.concat(theme.theme_settings.map(&:updated_at))
end
timestamps = fields.concat(settings).map!(&:to_f).sort!.join(",")
Digest::SHA1.hexdigest(timestamps)
end
def uploads_digest
sha1s = []
(theme&.upload_fields || []).map { |upload_field| sha1s << upload_field.upload&.sha1 }
Digest::SHA1.hexdigest(sha1s.compact.sort!.join("\n"))
end
def default_digest
Digest::SHA1.hexdigest "default-#{Stylesheet::Manager.fs_asset_cachebuster}-#{plugins_digest}-#{current_hostname}"
end
def color_scheme_digest
cs = @color_scheme || theme&.color_scheme
categories_updated =
Stylesheet::Manager
.cache
.defer_get_set("categories_updated") do
Category.where("uploaded_background_id IS NOT NULL").pluck(:updated_at).map(&:to_i).sum
end
fonts = "#{SiteSetting.base_font}-#{SiteSetting.heading_font}"
digest_string = "#{current_hostname}-"
if cs || categories_updated > 0
theme_color_defs = resolve_baked_field(:common, :color_definitions)
digest_string +=
"#{RailsMultisite::ConnectionManagement.current_db}-#{cs&.id}-#{cs&.version}-#{theme_color_defs}-#{Stylesheet::Manager.fs_asset_cachebuster}-#{categories_updated}-#{fonts}"
else
digest_string += "defaults-#{Stylesheet::Manager.fs_asset_cachebuster}-#{fonts}"
if cdn_url = GlobalSetting.cdn_url
digest_string += "-#{cdn_url}"
end
end
Digest::SHA1.hexdigest digest_string
end
def resolve_baked_field(target, name)
theme_ids =
if !theme
[]
elsif Theme.is_parent_theme?(theme.id)
@manager.theme_ids
else
[theme.id]
end
theme_ids = [theme_ids.first] if name != :color_definitions
baked_fields = []
targets = [Theme.targets[target.to_sym], Theme.targets[:common]]
@manager
.load_themes(theme_ids)
.each do |theme|
theme.builder_theme_fields.each do |theme_field|
if theme_field.name == name.to_s && targets.include?(theme_field.target_id)
baked_fields << theme_field
end
end
end
baked_fields
.map do |f|
f.ensure_baked!
f.value_baked || f.value
end
.join("\n")
end
end