Class: LocoMotion::Icons::Scanner

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

Overview

Scans application source for static icon references so loco_motion:icons:sync can vendor only the icons actually used — the icon analogue of Tailwind scanning your templates for class names.

It is intentionally a plain regex scan (no Ruby evaluation), so it is fully deterministic: the same source always yields the same set. It recognizes the loco_icon helper and the universal icon: / left_icon: / right_icon: / middle_icon: options, whether the name is a string ("home") or a symbol (:home). The library and variant come from the token itself — [library:]name[/variant] (see Reference) — so a reference is self-contained and the scan never has to guess from other arguments that may be on another line.

Like Tailwind, it cannot see dynamically-built names (loco_icon("bars-#{n}"), icon: some_var) — those belong in Configuration#icon_safelist.

Documentation text is not scanned: the body of a HAML filter block (:markdown prose, :plain code samples inside doc_code, and friends) is rendered as text, never executed as Ruby, so a backticked icon: "home/solid" in a guide is not a usage. The exceptions are the code filters (:ruby / :erb) and #{...} interpolation inside any filter — both of those do execute and are scanned normally.

Constant Summary collapse

HELPER =

The qualified token in a loco_icon("foo") call.

%r{\bloco_icon\s*\(?\s*(["'])([a-z0-9][a-z0-9:/-]*)\1}
KWARG =

A token passed via icon:/left_icon:/right_icon:/middle_icon: (also covers loco_icon(icon: "foo")). Longest keys first so icon does not win inside left_icon.

%r{\b(?:left_icon|right_icon|middle_icon|icon)\s*:\s*(["'])([a-z0-9][a-z0-9:/-]*)\1}
HELPER_SYMBOL =

The symbol forms — loco_icon(:home) and icon: :home. A bare Ruby symbol can't contain -, /, or :, so these only ever name a single simple icon (tokens / hyphenated names must be strings).

/\bloco_icon\s*\(?\s*:([a-z0-9][a-z0-9_]*)\b/
KWARG_SYMBOL =
/\b(?:left_icon|right_icon|middle_icon|icon)\s*:\s*:([a-z0-9][a-z0-9_]*)\b/
TEXT_FILTER =

A HAML filter header (:markdown, :plain, ...) whose indented block is text, not code. :ruby and :erb blocks hold executable code, so they are excluded here and scanned like ordinary lines.

/\A(\s*):(?!ruby\b|erb\b)\w+\s*\z/
INTERPOLATION =

An interpolated segment inside filter text — the one part of a filter block that executes as Ruby.

/\#\{([^}]*)\}/

Instance Method Summary collapse

Constructor Details

#initialize(paths:, root:, default_library:) ⇒ Scanner

Returns a new instance of Scanner.

Parameters:

  • paths (Array<String>)

    Glob patterns to scan (relative to root).

  • root (String)

    The base directory the globs resolve against.

  • default_library (String, Symbol)

    Library for references that do not name one explicitly.



62
63
64
65
66
# File 'lib/loco_motion/icons/scanner.rb', line 62

def initialize(paths:, root:, default_library:)
  @paths = Array(paths)
  @root = root.to_s
  @default_library = default_library.to_s
end

Instance Method Details

#referencesArray<Hash>

Returns Unique references, each { library:, variant:, name: } (variant is nil when unspecified, meaning "the library's default"), sorted for stable output.

Returns:

  • (Array<Hash>)

    Unique references, each { library:, variant:, name: } (variant is nil when unspecified, meaning "the library's default"), sorted for stable output.



73
74
75
76
77
78
79
80
81
# File 'lib/loco_motion/icons/scanner.rb', line 73

def references
  refs = {}

  files.each do |file|
    each_reference(file) { |ref| refs[ref] ||= ref }
  end

  refs.values.sort_by { |r| [r[:library], r[:variant].to_s, r[:name]] }
end