Files
discourse/lib/svg_sprite.rb
Osama Sayegh b02e87b8c4 DEV: Introduce ColorPaletteEditor component (#31328)
As part of the theme/color palette overhaul project, we're redesigning
the UI for the editing color palettes. This commit introduces a new
`ColorPaletteEditor` component that encapsulates all the logic and
interface for editing color palettes in the redesigned admin interface.
This component isn't used anywhere at this moment, but it will be once
we start introducing the rest of the redesigned interface.
2025-02-25 07:58:04 +03:00

535 lines
12 KiB
Ruby

# frozen_string_literal: true
require_relative "deprecated_icon_handler"
module SvgSprite
SVG_ICONS =
Set.new(
%w[
a
address-book
align-left
anchor
angle-down
angle-right
angle-up
angles-down
angles-left
angles-right
angles-up
arrow-down
arrow-left
arrow-right
arrow-rotate-left
arrow-rotate-right
arrow-up
arrows-rotate
asterisk
at
backward
backward-fast
backward-step
ban
bars
bars-staggered
bed
bell
bell-slash
bold
book
book-open-reader
bookmark
bookmark-delete
box-archive
briefcase
bullseye
calendar-days
caret-down
caret-left
caret-right
caret-up
certificate
chart-bar
chart-pie
check
chevron-down
chevron-left
chevron-right
chevron-up
circle
circle-check
circle-chevron-down
circle-exclamation
circle-half-stroke
circle-info
circle-minus
circle-plus
circle-question
circle-xmark
clock
clock-rotate-left
cloud-arrow-up
code
comment
compress
copy
crosshairs
cube
desktop
diagram-project
discourse-amazon
discourse-bell-exclamation
discourse-bell-one
discourse-bell-slash
discourse-bookmark-clock
discourse-chevron-collapse
discourse-chevron-expand
discourse-compress
discourse-dnd
discourse-emojis
discourse-expand
discourse-other-tab
discourse-sidebar
discourse-sparkles
discourse-threads
download
earth-americas
ellipsis
ellipsis-vertical
envelope
eye
fab-android
fab-apple
fab-chrome
fab-discord
fab-discourse
fab-facebook
fab-facebook-square
fab-github
fab-instagram
fab-linkedin-in
fab-linux
fab-markdown
fab-threads
fab-threads-square
fab-twitter
fab-twitter-square
fab-x-twitter
fab-wikipedia-w
fab-windows
far-bell
far-bell-slash
far-calendar-plus
far-chart-bar
far-circle
far-circle-dot
far-clipboard
far-clock
far-comment
far-comments
far-copyright
far-envelope
far-eye
far-eye-slash
far-face-frown
far-face-meh
far-face-smile
far-file-lines
far-heart
far-image
far-moon
far-pen-to-square
far-rectangle-list
far-square
far-square-check
far-star
far-sun
far-thumbs-down
far-thumbs-up
far-trash-can
file
file-lines
filter
flag
flask
folder
folder-open
forward
forward-fast
forward-step
gavel
gear
gift
globe
grip-lines
hand-point-right
handshake-angle
hashtag
heart
hourglass-start
house
id-card
image
images
inbox
italic
key
keyboard
language
layer-group
left-right
link
link-slash
list
list-check
list-ol
list-ul
location-dot
lock
magnifying-glass
magnifying-glass-minus
magnifying-glass-plus
microphone-slash
minus
mobile-screen-button
moon
paintbrush
palette
paper-plane
pause
pencil
play
plug
plus
power-off
puzzle-piece
question
quote-left
quote-right
reply
right-from-bracket
right-left
right-to-bracket
robot
rocket
rotate
scroll
share
shield-halved
shuffle
signal
sliders
spinner
square-check
square-envelope
square-full
square-plus
star
sun
table
table-cells
table-columns
tag
tags
temperature-three-quarters
thumbs-down
thumbs-up
thumbtack
tippy-rounded-arrow
toggle-off
toggle-on
trash-can
triangle-exclamation
truck-medical
unlock
unlock-keyhole
up-down
up-right-from-square
upload
user
user-gear
user-group
user-pen
user-plus
user-secret
user-shield
user-xmark
users
wand-magic
wrench
xmark
],
)
CORE_SVG_SPRITES = Dir.glob("#{Rails.root}/vendor/assets/svg-icons/**/*.svg")
THEME_SPRITE_VAR_NAME = "icons-sprite"
MAX_THEME_SPRITE_SIZE = 1024.kilobytes
def self.preload
settings_icons
group_icons
badge_icons
end
def self.symbols_for(svg_filename, sprite, strict:)
if strict
Nokogiri.XML(sprite) { |config| config.options = Nokogiri::XML::ParseOptions::NOBLANKS }
else
Nokogiri.XML(sprite)
end.css("symbol")
.filter_map do |sym|
icon_id = prepare_symbol(sym, svg_filename)
if icon_id.present?
sym.attributes["id"].value = icon_id
sym.css("title").each(&:remove)
[icon_id, sym.to_xml]
end
end
.to_h
end
def self.core_svgs
@core_svgs ||=
CORE_SVG_SPRITES.reduce({}) do |symbols, path|
symbols.merge!(symbols_for(File.basename(path, ".svg"), File.read(path), strict: true))
end
end
# Just used in tests
def self.clear_plugin_svg_sprite_cache!
@plugin_svgs = nil
end
def self.plugin_svgs
@plugin_svgs ||=
begin
plugin_paths = []
Discourse
.plugins
.map { |plugin| File.dirname(plugin.path) }
.each { |path| plugin_paths << "#{path}/svg-icons/*.svg" }
custom_sprite_paths = Dir.glob(plugin_paths)
custom_sprite_paths.reduce({}) do |symbols, path|
symbols.merge!(symbols_for(File.basename(path, ".svg"), File.read(path), strict: true))
end
end
end
def self.theme_svgs(theme_id)
if theme_id.present?
cache
.defer_get_set_bulk(
Theme.transform_ids(theme_id),
lambda { |theme_id| "theme_svg_sprites_#{theme_id}" },
) do |theme_ids|
theme_field_uploads =
ThemeField.where(
type_id: ThemeField.types[:theme_upload_var],
name: THEME_SPRITE_VAR_NAME,
theme_id: theme_ids,
).pluck(:upload_id)
theme_sprites =
ThemeSvgSprite.where(theme_id: theme_ids).pluck(:theme_id, :upload_id, :sprite)
missing_sprites = (theme_field_uploads - theme_sprites.map(&:second))
if missing_sprites.present?
Rails.logger.warn(
"Missing ThemeSvgSprites for theme #{theme_id}, uploads #{missing_sprites.join(", ")}",
)
end
theme_sprites
.map do |(theme_id, upload_id, sprite)|
begin
[theme_id, symbols_for("theme_#{theme_id}_#{upload_id}.svg", sprite, strict: false)]
rescue => e
Rails.logger.warn(
"Bad XML in custom sprite in theme with ID=#{theme_id}. Error info: #{e.inspect}",
)
end
end
.compact
.to_h
.values_at(*theme_ids)
end
.values
.compact
.reduce({}) { |a, b| a.merge!(b) }
else
{}
end
end
def self.custom_svgs(theme_id)
plugin_svgs.merge(theme_svgs(theme_id))
end
def self.all_icons(theme_id = nil)
get_set_cache("icons_#{Theme.transform_ids(theme_id).join(",")}") do
Set
.new()
.merge(settings_icons)
.merge(plugin_icons)
.merge(badge_icons)
.merge(group_icons)
.merge(theme_icons(theme_id))
.merge(custom_icons(theme_id))
.delete_if { |i| i.blank? || i.include?("/") }
.map! { |i| process(i.dup) }
.merge(SVG_ICONS)
.sort
end
end
def self.version(theme_id = nil)
get_set_cache("version_#{Theme.transform_ids(theme_id).join(",")}") do
Digest::SHA1.hexdigest(bundle(theme_id))
end
end
def self.path(theme_id = nil)
"/svg-sprite/#{Discourse.current_hostname}/svg-#{theme_id}-#{version(theme_id)}.js"
end
def self.expire_cache
cache&.clear
end
def self.svgs_for(theme_id)
svgs = core_svgs
svgs = svgs.merge(custom_svgs(theme_id)) if theme_id.present?
svgs
end
def self.bundle(theme_id = nil)
icons = all_icons(theme_id)
svg_subset =
"" \
"<!--
Discourse SVG subset of Font Awesome Free by @fontawesome - https://fontawesome.com
License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
-->
<svg xmlns='http://www.w3.org/2000/svg' style='display: none;'>
" \
"".dup
svg_subset << core_svgs.slice(*icons).values.join
svg_subset << custom_svgs(theme_id).values.join
svg_subset << "</svg>"
end
def self.search(searched_icon)
searched_icon = process(searched_icon.dup)
svgs_for(SiteSetting.default_theme_id)[searched_icon] || false
end
def self.icon_picker_search(keyword, only_available = false)
symbols = svgs_for(SiteSetting.default_theme_id)
symbols.slice!(*all_icons(SiteSetting.default_theme_id)) if only_available
symbols.reject! { |icon_id, _sym| !icon_id.include?(keyword) } if keyword.present?
symbols.sort_by(&:first).map { |id, symbol| { id:, symbol: } }
end
# For use in no_ember .html.erb layouts
def self.raw_svg(name)
get_set_cache("raw_svg_#{name}") do
symbol = search(name)
break "" unless symbol
symbol = Nokogiri.XML(symbol).children.first
symbol.name = "svg"
<<~HTML
<svg class="fa d-icon svg-icon svg-node" aria-hidden="true">#{symbol}</svg>
HTML
end.html_safe
end
def self.theme_sprite_variable_name
THEME_SPRITE_VAR_NAME
end
def self.prepare_symbol(symbol, svg_filename = nil)
icon_id = symbol.attr("id")
case svg_filename
when "regular"
icon_id = icon_id.prepend("far-")
when "brands"
icon_id = icon_id.prepend("fab-")
end
icon_id
end
def self.settings_icons
get_set_cache("settings_icons") do
# includes svg_icon_subset and any settings containing _icon (incl. plugin settings)
site_setting_icons = []
SiteSetting.settings_hash.select do |key, value|
site_setting_icons |= value.split("|") if key.to_s.include?("_icon") && String === value
end
site_setting_icons
end
end
def self.plugin_icons
DiscoursePluginRegistry.svg_icons
end
def self.badge_icons
get_set_cache("badge_icons") { Badge.pluck(:icon).uniq }
end
def self.group_icons
get_set_cache("group_icons") { Group.pluck(:flair_icon).uniq }
end
def self.theme_icons(theme_id)
return [] if theme_id.blank?
theme_icon_settings = []
theme_ids = Theme.transform_ids(theme_id)
# Need to load full records for default values
Theme
.where(id: theme_ids)
.each do |theme|
_settings =
theme.cached_settings.each do |key, value|
if key.to_s.include?("_icon") && String === value
theme_icon_settings |= value.split("|")
end
end
end
theme_icon_settings |= ThemeModifierHelper.new(theme_ids: theme_ids).svg_icons
theme_icon_settings
end
def self.custom_icons(theme_id)
# Automatically register icons in sprites added via themes or plugins
custom_svgs(theme_id).keys
end
def self.process(icon_name)
DeprecatedIconHandler.convert_icon(icon_name.strip)
end
def self.get_set_cache(key, &block)
cache.defer_get_set(key, &block)
end
def self.cache
@cache ||= DistributedCache.new("svg_sprite")
end
end