Class: Guardrails::Icons

Inherits:
Object
  • Object
show all
Defined in:
lib/guardrails/icons.rb

Defined Under Namespace

Classes: Violation

Constant Summary collapse

DEFAULT_SOURCE =
"app/assets/images/icons"
DEFAULT_SPRITE_OUTPUT =
"app/assets/images/icons/sprite.svg"
DEFAULT_VIEWBOX =
"0 0 24 24"
VIEW_PATTERNS =
[
  "app/views/**/*.html.erb",
  "app/components/**/*.html.erb"
].freeze
SVG_OPEN_TAG =
/<svg\b([^>]*)>/m
SVG_INNER =
/<svg\b[^>]*>([\s\S]*?)<\/svg>/m
SVG_BLOCK_PATTERN =
/<svg\b[^>]*>[\s\S]*?<\/svg>/m
VIEWBOX_ATTR =
/\bviewBox\s*=\s*["']([^"']+)["']/i
ERB_BLOCK_PATTERN =
/<%[\s\S]*?%>/
USAGE_PATTERNS =

Patterns that indicate an icon is in use. We’re conservative on this side — false negatives (saying “alive” when actually dead) just leave dead icons in source; false positives (saying “dead” when actually used) cause the user to delete files they need.

Each pattern allows an optional directory prefix before the basename (e.g. ‘image_tag “icons/check.svg”` for files under `app/assets/images/icons/`) and captures only the bare name so it matches against icons collected from disk.

[
  # Sprite reference: <use href="#icon-foo">
  /#icon-([\w-]+)/,
  # Rails image_tag "foo.svg" / image_tag "icons/foo.svg"
  /\bimage_tag\s*\(?\s*["'](?:[^"']*\/)?([\w-]+)\.(?:svg|png|gif|jpe?g|webp)["']/,
  # Rails asset_path / asset_url / image_path / image_url with optional path
  /\b(?:asset_path|asset_url|image_path|image_url)\s*\(?\s*["'](?:[^"']*\/)?([\w-]+)\.(?:svg|png|gif|jpe?g|webp)["']/,
  # CSS url() references in stylesheets and inline style attributes
  /url\s*\(\s*["']?(?:[^"')]*\/)?([\w-]+)\.(?:svg|png|gif|jpe?g|webp)/i
].freeze
USAGE_SCAN_PATTERNS =
[
  "app/views/**/*.html.erb",
  "app/components/**/*.html.erb",
  "app/components/**/*.rb",
  "app/assets/stylesheets/**/*.{css,scss,sass}",
  "app/javascript/**/*.{js,ts,jsx,tsx}"
].freeze

Instance Method Summary collapse

Constructor Details

#initialize(root:, output: $stdout, source: nil, sprite_output: nil) ⇒ Icons

Returns a new instance of Icons.



53
54
55
56
57
58
59
60
# File 'lib/guardrails/icons.rb', line 53

def initialize(root:, output: $stdout, source: nil, sprite_output: nil)
  @root = Pathname(root)
  @output = output
  config = load_config

  @source = resolve_path(source || config.dig("guardrails", "icons", "source") || DEFAULT_SOURCE)
  @sprite_output = resolve_path(sprite_output || config.dig("guardrails", "icons", "sprite_output") || DEFAULT_SPRITE_OUTPUT)
end

Instance Method Details

#audit_inline_svgsObject



71
72
73
# File 'lib/guardrails/icons.rb', line 71

def audit_inline_svgs
  view_files.flat_map { |file| scan_view_for_inline_svgs(file) }
end

#generate_spriteObject



84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
# File 'lib/guardrails/icons.rb', line 84

def generate_sprite
  svgs = collect_svgs
  if svgs.empty?
    @output.puts "No SVGs found in #{relative(@source)}"
    return nil
  end

  symbols = svgs.filter_map { |file| build_symbol(file) }
  sprite = wrap_sprite(symbols)

  @sprite_output.dirname.mkpath
  File.write(@sprite_output, sprite, encoding: Encoding::UTF_8)
  @output.puts "Wrote sprite with #{symbols.length} icons to #{relative(@sprite_output)}"
  @sprite_output
end

#report_dead_iconsObject



75
76
77
78
79
80
81
82
# File 'lib/guardrails/icons.rb', line 75

def report_dead_icons
  icon_names = collect_icon_names
  used_names = collect_used_icon_names
  {
    dead: (icon_names - used_names).sort,
    unknown: (used_names - icon_names).sort
  }
end

#runObject



62
63
64
65
66
67
68
69
# File 'lib/guardrails/icons.rb', line 62

def run
  generate_sprite
  violations = audit_inline_svgs
  report_inline_svgs(violations)
  dead_report = report_dead_icons
  print_dead_report(dead_report)
  { inline_svgs: violations, dead_icons: dead_report[:dead], unknown_refs: dead_report[:unknown] }
end