mirror of
https://github.com/discourse/discourse.git
synced 2025-02-24 01:31:54 +08:00

This makes the backtrace for a deprecated icon name error clearer in terms of pointing out which plugin the icon is being registered from. The error will only be raised in the test environment. This PR also extracts out to a separate module the hashmaps & logic for converting FA4 era icons to discourse FA6 compatible icons. This isolates that logic for reuse in DiscoursePluginRegistry - otherwise pulling in the whole SvgSprite resulted in errors.
534 lines
12 KiB
Ruby
534 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
|
|
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
|