unmagic-color 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,107 +3,277 @@
3
3
  module Unmagic
4
4
  class Color
5
5
  class RGB < Color
6
- # Named colors support for RGB colors.
6
+ # Named colors support for RGB colors with X11 and CSS/W3C databases.
7
7
  #
8
- # Provides access to standard named colors (like "red", "blue", "goldenrod")
9
- # and converts them to RGB color instances.
8
+ # Provides access to named colors from two databases:
9
+ # - X11 database (658 colors) - default
10
+ # - CSS/W3C database (148 colors) - accessible via prefix
10
11
  #
11
- # @example Parse a named color
12
+ # @example Parse a named color (uses X11 by default)
12
13
  # Unmagic::Color::RGB::Named.parse("goldenrod")
13
14
  # #=> RGB instance for #daa520
14
15
  #
16
+ # @example Use CSS/W3C database with prefix
17
+ # Unmagic::Color::RGB::Named.parse("css:gray")
18
+ # #=> RGB instance for #808080 (CSS value)
19
+ # Unmagic::Color::RGB::Named.parse("gray")
20
+ # #=> RGB instance for #bebebe (X11 value)
21
+ #
15
22
  # @example Case-insensitive and whitespace-tolerant
16
23
  # Unmagic::Color::RGB::Named.parse("Golden Rod")
17
24
  # #=> RGB instance for #daa520
18
25
  # Unmagic::Color::RGB::Named.parse("GOLDENROD")
19
26
  # #=> RGB instance for #daa520
20
27
  #
28
+ # Five colors have different values between databases:
29
+ # gray/grey (#bebebe X11 vs #808080 CSS), green (#00ff00 X11 vs #008000 CSS),
30
+ # maroon (#b03060 X11 vs #800000 CSS), purple (#a020f0 X11 vs #800080 CSS)
31
+ #
21
32
  # @example Check if a name is valid
22
33
  # Unmagic::Color::RGB::Named.valid?("goldenrod")
23
34
  # #=> true
24
35
  # Unmagic::Color::RGB::Named.valid?("notacolor")
25
36
  # #=> false
26
37
  class Named
38
+ # Database for loading and accessing color data from files.
39
+ #
40
+ # Handles lazy loading, name normalization, and color lookup.
41
+ # @api private
42
+ class Database
43
+ # @return [String, nil] The name of the database
44
+ # @api private
45
+ attr_reader :name
46
+
47
+ # @return [Array<String>] Alternative names for the database
48
+ # @api private
49
+ attr_reader :aliases
50
+
51
+ # Initialize a new color database.
52
+ #
53
+ # @param path [String] Path to the database file
54
+ # @param name [String, nil] The name of the database (e.g., "x11", "css")
55
+ # @param aliases [Array<String>] Alternative names for the database
56
+ def initialize(path:, name: nil, aliases: [])
57
+ @filepath = path
58
+ @name = name
59
+ @aliases = aliases
60
+ @data = nil
61
+ end
62
+
63
+ # Lookup color by name, returns RGB color or nil.
64
+ #
65
+ # @param color_name [String] The color name to lookup
66
+ # @return [RGB, nil] The RGB color instance or nil if not found
67
+ def [](color_name)
68
+ normalized = normalize_name(color_name)
69
+ int_value = data[normalized]
70
+ int_value ? RGB.build(int_value) : nil
71
+ end
72
+
73
+ # Check if color exists in database.
74
+ #
75
+ # @param color_name [String] The color name to check
76
+ # @return [Boolean] true if color exists
77
+ def valid?(color_name)
78
+ normalized = normalize_name(color_name)
79
+ data.key?(normalized)
80
+ end
81
+
82
+ # Get all color names in database.
83
+ #
84
+ # @return [Array<String>] Array of all color names
85
+ def all
86
+ data.keys
87
+ end
88
+
89
+ # Check if database has been loaded.
90
+ #
91
+ # @return [Boolean] true if data has been loaded from file
92
+ def loaded?
93
+ !@data.nil?
94
+ end
95
+
96
+ # Calculate memory size of loaded database.
97
+ #
98
+ # @return [Integer] Memory size in bytes
99
+ # @api private
100
+ def memsize
101
+ require "objspace"
102
+
103
+ memory = ObjectSpace.memsize_of(data)
104
+
105
+ data.each do |key, value|
106
+ memory += ObjectSpace.memsize_of(key)
107
+ memory += ObjectSpace.memsize_of(value)
108
+ end
109
+
110
+ memory
111
+ end
112
+
113
+ private
114
+
115
+ # Lazy load data from file.
116
+ #
117
+ # @return [Hash] Hash of normalized color names to integer values
118
+ def data
119
+ @data ||= load_data
120
+ end
121
+
122
+ # Normalize color name for lookup.
123
+ # Converts to lowercase and removes all whitespace.
124
+ #
125
+ # @param name [String] The color name to normalize
126
+ # @return [String] The normalized name
127
+ def normalize_name(name)
128
+ name.to_s.downcase.gsub(/\s+/, "")
129
+ end
130
+
131
+ require "json"
132
+
133
+ # Load and parse database file.
134
+ #
135
+ # @return [Hash] Hash of normalized color names to integer values
136
+ def load_data
137
+ unless File.exist?(@filepath)
138
+ raise Error, "Color database file not found: #{@filepath}"
139
+ end
140
+
141
+ # Load and parse JSON file (Ruby's JSON parser handles // comments)
142
+ # Keys are already normalized in the JSON file
143
+ JSON.parse(File.read(@filepath))
144
+ end
145
+ end
146
+
27
147
  # Error raised when a color name is not found
28
148
  class ParseError < Color::Error; end
29
149
 
150
+ # X11 color database (658 colors)
151
+ X11 = Database.new(
152
+ path: File.join(Color::DATA_PATH, "x11.jsonc"),
153
+ name: "x11",
154
+ )
155
+
156
+ # CSS/W3C color database (148 colors)
157
+ CSS = Database.new(
158
+ path: File.join(Color::DATA_PATH, "css.jsonc"),
159
+ name: "css",
160
+ aliases: ["w3c"],
161
+ )
162
+
30
163
  class << self
31
164
  # Parse a named color and return its RGB representation.
32
165
  #
166
+ # Supports database prefixes (css:, w3c:, x11:) to select specific database.
167
+ # Without prefix, uses X11 database by default.
168
+ #
33
169
  # @param name [String] The color name to parse (case-insensitive)
34
170
  # @return [RGB] The RGB color instance
35
171
  # @raise [ParseError] If the color name is not recognized
36
172
  #
37
- # @example
173
+ # @example Parse from X11 database (default)
38
174
  # Unmagic::Color::RGB::Named.parse("goldenrod")
39
175
  # #=> RGB instance for #daa520
176
+ #
177
+ # @example Parse from CSS database
178
+ # Unmagic::Color::RGB::Named.parse("css:gray")
179
+ # #=> RGB instance for #808080
40
180
  def parse(name)
41
- normalized_name = normalize_name(name)
42
- hex_value = data[normalized_name]
181
+ database, color_name = resolve_database(name)
182
+ color = database[color_name]
43
183
 
44
- raise ParseError, "Unknown color name: #{name.inspect}" unless hex_value
184
+ raise ParseError, "Unknown color name in #{database.name} database: #{color_name.inspect}" unless color
45
185
 
46
- Hex.parse(hex_value)
186
+ color
47
187
  end
48
188
 
49
189
  # Check if a color name is valid.
50
190
  #
191
+ # Supports database prefixes to check specific database.
192
+ #
51
193
  # @param name [String] The color name to check
52
194
  # @return [Boolean] true if the name exists
53
195
  #
54
- # @example
196
+ # @example Check in X11 database (default)
55
197
  # Unmagic::Color::RGB::Named.valid?("goldenrod")
56
198
  # #=> true
199
+ #
200
+ # @example Check in CSS database
201
+ # Unmagic::Color::RGB::Named.valid?("css:gray")
202
+ # #=> true
57
203
  def valid?(name)
58
- normalized_name = normalize_name(name)
59
- data.key?(normalized_name)
204
+ database, color_name = resolve_database(name)
205
+ database.valid?(color_name)
60
206
  end
61
207
 
62
- # Get all available color names.
208
+ # Get all available color databases.
63
209
  #
64
- # @return [Array<String>] Array of all color names
210
+ # @return [Array<Database>] Array of database instances
65
211
  #
66
- # @example
67
- # Unmagic::Color::RGB::Named.all.take(5)
68
- # #=> ["black", "silver", "gray", "white", "maroon"]
69
- def all
70
- data.keys
212
+ # @example Get all databases
213
+ # Unmagic::Color::RGB::Named.databases
214
+ # #=> [X11, CSS]
215
+ #
216
+ # @example Get color names from a specific database
217
+ # Unmagic::Color::RGB::Named.databases.first.all.take(5)
218
+ # #=> ["aliceblue", "antiquewhite", ...]
219
+ def databases
220
+ [X11, CSS]
71
221
  end
72
222
 
73
- private
74
-
75
- # Normalize a color name for lookup.
76
- # Converts to lowercase and removes all whitespace.
223
+ # Find a database by name or alias.
77
224
  #
78
- # @param name [String] The color name to normalize
79
- # @return [String] The normalized name
80
- def normalize_name(name)
81
- name.to_s.downcase.gsub(/\s+/, "")
225
+ # @param search [String] Name or alias to search for
226
+ # @return [Database, nil] Matching database or nil
227
+ #
228
+ # @example Find by name
229
+ # Unmagic::Color::RGB::Named.find_by_name("x11")
230
+ # #=> X11 database
231
+ #
232
+ # @example Find by alias
233
+ # Unmagic::Color::RGB::Named.find_by_name("w3c")
234
+ # #=> CSS database
235
+ def find_by_name(search)
236
+ normalized = search.strip.downcase
237
+ all_by_name.fetch(normalized)
238
+ rescue KeyError
239
+ nil
82
240
  end
83
241
 
84
- # Load color data from the rgb.txt file.
85
- # Uses memoization to only load the file once.
242
+ private
243
+
244
+ # Hash mapping all database names and aliases to their instances.
86
245
  #
87
- # @return [Hash] Hash of color names to hex values
88
- def data
89
- @data ||= load_data
246
+ # @return [Hash<String, Database>] Name/alias to database mapping
247
+ def all_by_name
248
+ @all_by_name ||= begin
249
+ hash = {}
250
+ databases.each do |database|
251
+ hash[database.name] = database
252
+ database.aliases.each { |alias_name| hash[alias_name] = database }
253
+ end
254
+ hash
255
+ end
90
256
  end
91
257
 
92
- # Load and parse the rgb.txt file.
258
+ # Resolve database and color name from input.
93
259
  #
94
- # @return [Hash] Hash of color names to hex values
95
- def load_data
96
- data_file = File.join(__dir__, "..", "..", "..", "..", "data", "rgb.txt")
97
- colors = {}
260
+ # Extracts database prefix if present (css:, w3c:, x11:).
261
+ # Returns the appropriate database instance and cleaned color name.
262
+ #
263
+ # @param name [String] The input name (may include prefix)
264
+ # @return [Array<Database, String>] Database instance and color name
265
+ def resolve_database(name)
266
+ if name.include?(":")
267
+ prefix, color_name = name.split(":", 2)
268
+ database = find_by_name(prefix)
98
269
 
99
- File.readlines(data_file).each do |line|
100
- name, hex = line.strip.split("\t")
101
- next if name.nil? || hex.nil?
270
+ # Invalid prefix, treat whole string as color name
271
+ return [X11, name] unless database
102
272
 
103
- colors[name] = hex
273
+ [database, color_name]
274
+ else
275
+ [X11, name]
104
276
  end
105
-
106
- colors
107
277
  end
108
278
  end
109
279
  end