Module: SvgSprite

Defined in:
lib/svg_sprite.rb

Constant Summary collapse

FA_ICON_MAP =
{ "far fa-" => "far-", "fab fa-" => "fab-", "fas fa-" => "", "fa-" => "" }
CORE_SVG_SPRITES =
Dir.glob("#{Rails.root}/vendor/assets/svg-icons/**/*.svg")
THEME_SPRITE_VAR_NAME =
"icons-sprite"
MAX_THEME_SPRITE_SIZE =
1024.kilobytes

Class Method Summary collapse

Class Method Details

.all_icons(theme_id = nil) ⇒ Object



351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
# File 'lib/svg_sprite.rb', line 351

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

.badge_iconsObject



469
470
471
# File 'lib/svg_sprite.rb', line 469

def self.badge_icons
  get_set_cache("badge_icons") { Badge.pluck(:icon).uniq }
end

.bundle(theme_id = nil) ⇒ Object



388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
# File 'lib/svg_sprite.rb', line 388

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

.cacheObject



515
516
517
# File 'lib/svg_sprite.rb', line 515

def self.cache
  @cache ||= DistributedCache.new("svg_sprite")
end

.clear_plugin_svg_sprite_cache!Object

Just used in tests



280
281
282
# File 'lib/svg_sprite.rb', line 280

def self.clear_plugin_svg_sprite_cache!
  @plugin_svgs = nil
end

.core_svgsObject



272
273
274
275
276
277
# File 'lib/svg_sprite.rb', line 272

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

.custom_icons(theme_id) ⇒ Object



500
501
502
503
# File 'lib/svg_sprite.rb', line 500

def self.custom_icons(theme_id)
  # Automatically register icons in sprites added via themes or plugins
  custom_svgs(theme_id).keys
end

.custom_svgs(theme_id) ⇒ Object



347
348
349
# File 'lib/svg_sprite.rb', line 347

def self.custom_svgs(theme_id)
  plugin_svgs.merge(theme_svgs(theme_id))
end

.expire_cacheObject



378
379
380
# File 'lib/svg_sprite.rb', line 378

def self.expire_cache
  cache&.clear
end

.get_set_cache(key, &block) ⇒ Object



511
512
513
# File 'lib/svg_sprite.rb', line 511

def self.get_set_cache(key, &block)
  cache.defer_get_set(key, &block)
end

.group_iconsObject



473
474
475
# File 'lib/svg_sprite.rb', line 473

def self.group_icons
  get_set_cache("group_icons") { Group.pluck(:flair_icon).uniq }
end

.icon_picker_search(keyword, only_available = false) ⇒ Object



413
414
415
416
417
418
419
420
# File 'lib/svg_sprite.rb', line 413

def self.icon_picker_search(keyword, only_available = false)
  icons = all_icons(SiteSetting.default_theme_id) if only_available

  symbols = svgs_for(SiteSetting.default_theme_id)
  symbols.slice!(*icons) if only_available
  symbols.reject! { |icon_id, sym| !icon_id.include?(keyword) } unless keyword.empty?
  symbols.sort_by(&:first).map { |icon_id, symbol| { id: icon_id, symbol: symbol } }
end

.path(theme_id = nil) ⇒ Object



374
375
376
# File 'lib/svg_sprite.rb', line 374

def self.path(theme_id = nil)
  "/svg-sprite/#{Discourse.current_hostname}/svg-#{theme_id}-#{version(theme_id)}.js"
end

.plugin_iconsObject



465
466
467
# File 'lib/svg_sprite.rb', line 465

def self.plugin_icons
  DiscoursePluginRegistry.svg_icons
end

.plugin_svgsObject



284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
# File 'lib/svg_sprite.rb', line 284

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

.preloadObject



249
250
251
252
253
# File 'lib/svg_sprite.rb', line 249

def self.preload
  settings_icons
  group_icons
  badge_icons
end

.prepare_symbol(symbol, svg_filename = nil) ⇒ Object



439
440
441
442
443
444
445
446
447
448
449
450
# File 'lib/svg_sprite.rb', line 439

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

.process(icon_name) ⇒ Object



505
506
507
508
509
# File 'lib/svg_sprite.rb', line 505

def self.process(icon_name)
  icon_name = icon_name.strip
  FA_ICON_MAP.each { |k, v| icon_name = icon_name.sub(k, v) }
  icon_name
end

.raw_svg(name) ⇒ Object

For use in no_ember .html.erb layouts



423
424
425
426
427
428
429
430
431
432
433
# File 'lib/svg_sprite.rb', line 423

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

.search(searched_icon) ⇒ Object



407
408
409
410
411
# File 'lib/svg_sprite.rb', line 407

def self.search(searched_icon)
  searched_icon = process(searched_icon.dup)

  svgs_for(SiteSetting.default_theme_id)[searched_icon] || false
end

.settings_iconsObject



452
453
454
455
456
457
458
459
460
461
462
463
# File 'lib/svg_sprite.rb', line 452

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

.svgs_for(theme_id) ⇒ Object



382
383
384
385
386
# File 'lib/svg_sprite.rb', line 382

def self.svgs_for(theme_id)
  svgs = core_svgs
  svgs = svgs.merge(custom_svgs(theme_id)) if theme_id.present?
  svgs
end

.symbols_for(svg_filename, sprite, strict:) ⇒ Object



255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
# File 'lib/svg_sprite.rb', line 255

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

.theme_icons(theme_id) ⇒ Object



477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
# File 'lib/svg_sprite.rb', line 477

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

.theme_sprite_variable_nameObject



435
436
437
# File 'lib/svg_sprite.rb', line 435

def self.theme_sprite_variable_name
  THEME_SPRITE_VAR_NAME
end

.theme_svgs(theme_id) ⇒ Object



301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
# File 'lib/svg_sprite.rb', line 301

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

.version(theme_id = nil) ⇒ Object



368
369
370
371
372
# File 'lib/svg_sprite.rb', line 368

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