fontist 3.0.0 → 3.0.2

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.
Files changed (60) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/discover-fonts.yml +76 -0
  3. data/.github/workflows/rake.yml +103 -8
  4. data/.rubocop_todo.yml +179 -139
  5. data/TODO.audit-docs.md +164 -0
  6. data/TODO.improve-docs.md +114 -0
  7. data/TODO.upgrade-excavate.md +107 -0
  8. data/docs/guide/formulas.md +37 -1
  9. data/docs/guide/how-it-works.md +13 -0
  10. data/docs/guide/platforms/windows.md +67 -0
  11. data/fontist.gemspec +2 -2
  12. data/lib/fontist/cache/store.rb +1 -1
  13. data/lib/fontist/cli.rb +2 -1
  14. data/lib/fontist/errors.rb +24 -3
  15. data/lib/fontist/extract.rb +1 -0
  16. data/lib/fontist/font.rb +2 -2
  17. data/lib/fontist/font_finder.rb +1 -2
  18. data/lib/fontist/font_installer.rb +16 -14
  19. data/lib/fontist/format_matcher.rb +4 -2
  20. data/lib/fontist/format_spec.rb +1 -1
  21. data/lib/fontist/formula.rb +15 -3
  22. data/lib/fontist/formula_picker.rb +5 -3
  23. data/lib/fontist/import/create_formula.rb +5 -0
  24. data/lib/fontist/import/formula_builder.rb +10 -1
  25. data/lib/fontist/import/google/data_sources/github.rb +4 -4
  26. data/lib/fontist/import/google/font_database.rb +8 -8
  27. data/lib/fontist/import/google/formula_builders/formula_builder_v4.rb +1 -1
  28. data/lib/fontist/import/google/formula_builders/formula_builder_v5.rb +9 -3
  29. data/lib/fontist/import/google/metadata_adapter.rb +6 -6
  30. data/lib/fontist/import/google/models/font_family.rb +1 -1
  31. data/lib/fontist/import/import_display.rb +5 -5
  32. data/lib/fontist/import/macos_importer.rb +1 -1
  33. data/lib/fontist/import/upgrade_formulas.rb +1 -3
  34. data/lib/fontist/import/v4_to_v5_migrator.rb +2 -1
  35. data/lib/fontist/import/windows/fod_capabilities.yml +654 -0
  36. data/lib/fontist/import/windows/windows_license.txt +4 -0
  37. data/lib/fontist/import/windows.rb +162 -0
  38. data/lib/fontist/import.rb +3 -1
  39. data/lib/fontist/import_source.rb +1 -0
  40. data/lib/fontist/indexes/directory_snapshot.rb +2 -2
  41. data/lib/fontist/indexes/incremental_scanner.rb +2 -2
  42. data/lib/fontist/indexes.rb +8 -4
  43. data/lib/fontist/macos/catalog/asset.rb +2 -2
  44. data/lib/fontist/macos_import_source.rb +0 -1
  45. data/lib/fontist/repo.rb +1 -1
  46. data/lib/fontist/resource.rb +5 -1
  47. data/lib/fontist/resources/windows_fod_resource.rb +51 -0
  48. data/lib/fontist/resources.rb +1 -0
  49. data/lib/fontist/system_index.rb +5 -5
  50. data/lib/fontist/utils/downloader.rb +8 -3
  51. data/lib/fontist/utils/system.rb +19 -2
  52. data/lib/fontist/validation.rb +1 -1
  53. data/lib/fontist/validator.rb +2 -2
  54. data/lib/fontist/version.rb +1 -1
  55. data/lib/fontist/windows_fod_metadata.rb +83 -0
  56. data/lib/fontist/windows_import_source.rb +54 -0
  57. data/lib/fontist.rb +4 -1
  58. data/script/generate_windows_formulas.rb +24 -0
  59. data/script/validate_windows_fod_ci.rb +175 -0
  60. metadata +17 -6
@@ -90,6 +90,8 @@ module Fontist
90
90
  Resources::GoogleResource
91
91
  elsif @formula.source == "apple_cdn"
92
92
  Resources::AppleCDNResource
93
+ elsif @formula.source == "windows_fod"
94
+ Resources::WindowsFodResource
93
95
  else
94
96
  Resources::ArchiveResource
95
97
  end
@@ -98,16 +100,14 @@ module Fontist
98
100
  end
99
101
 
100
102
  def resource_options
101
- @resource_options ||= begin
102
- if @formula.resources.size == 1 || !@formula.v5?
103
- @formula.resources.first
104
- elsif @format_spec&.has_constraints?
105
- matcher = FormatMatcher.new(@format_spec)
106
- matcher.select_preferred_resource(@formula.resources)
107
- else
108
- find_desktop_resource || @formula.resources.first
109
- end
110
- end
103
+ @resource_options ||= if @formula.resources.size == 1 || !@formula.v5?
104
+ @formula.resources.first
105
+ elsif @format_spec&.has_constraints?
106
+ matcher = FormatMatcher.new(@format_spec)
107
+ matcher.select_preferred_resource(@formula.resources)
108
+ else
109
+ find_desktop_resource || @formula.resources.first
110
+ end
111
111
  end
112
112
 
113
113
  def find_desktop_resource
@@ -135,7 +135,9 @@ module Fontist
135
135
  file_names = styles.map { |s| s.source_font || s.font }
136
136
 
137
137
  if @formula.v5? && resource_options&.source == "google" && file_names.any?
138
- resource_basenames = Array(resource_options.files).map { |f| File.basename(f) }
138
+ resource_basenames = Array(resource_options.files).map do |f|
139
+ File.basename(f)
140
+ end
139
141
  unless file_names.any? { |f| resource_basenames.include?(f) }
140
142
  return resource_basenames
141
143
  end
@@ -169,7 +171,7 @@ module Fontist
169
171
  @subdirectories ||= begin
170
172
  extracts = [@formula.extract].flatten.compact
171
173
  # options is a collection, so we need to flatten it too
172
- options = extracts.flat_map { |e| e.options }.compact
174
+ options = extracts.flat_map(&:options).compact
173
175
  options.filter_map(&:fonts_sub_dir)
174
176
  end
175
177
  end
@@ -296,12 +298,12 @@ module Fontist
296
298
  def temp_path_for(source_path, format)
297
299
  base = File.basename(source_path, ".*")
298
300
  dir = @format_spec&.transcode_path || Dir.mktmpdir
299
- FileUtils.mkdir_p(dir) unless Dir.exist?(dir)
301
+ FileUtils.mkdir_p(dir)
300
302
  File.join(dir, "#{base}.#{format}")
301
303
  end
302
304
 
303
305
  def cleanup_temp_file(path)
304
- File.delete(path) if File.exist?(path)
306
+ FileUtils.rm_f(path)
305
307
  rescue StandardError
306
308
  # Ignore cleanup errors
307
309
  end
@@ -116,7 +116,9 @@ module Fontist
116
116
  end
117
117
 
118
118
  # Check if Fontisan can convert from any available format
119
- convertible = available_formats.find { |f| self.class.can_convert?(f, requested) }
119
+ convertible = available_formats.find do |f|
120
+ self.class.can_convert?(f, requested)
121
+ end
120
122
  if convertible
121
123
  return {
122
124
  strategy: :convert,
@@ -163,7 +165,7 @@ module Fontist
163
165
  def find_variable_resource(resources)
164
166
  return nil unless @spec.prefer_variable
165
167
 
166
- resources.find { |r| r.variable_font? }
168
+ resources.find(&:variable_font?)
167
169
  end
168
170
 
169
171
  def axes_match?(available_axes)
@@ -47,7 +47,7 @@ module Fontist
47
47
  return nil if value.nil?
48
48
  return value if value.is_a?(Array)
49
49
 
50
- value.to_s.split(",").map(&:strip).compact
50
+ value.to_s.split(",").filter_map(&:strip)
51
51
  end
52
52
 
53
53
  # Check if any format constraints are specified
@@ -45,12 +45,14 @@ module Fontist
45
45
  "MacosImportSource",
46
46
  "GoogleImportSource",
47
47
  "SilImportSource",
48
+ "WindowsImportSource",
48
49
  ]
49
50
  attribute :font_version, :string
50
51
 
51
52
  key_value do
52
53
  # Only serialize schema_version if it's v5 (5), not for v4 formulas
53
- map "schema_version", to: :schema_version, render_nil: false, render_default: false
54
+ map "schema_version", to: :schema_version, render_nil: false,
55
+ render_default: false
54
56
  map "name", to: :name
55
57
  map "description", to: :description
56
58
  map "homepage", to: :homepage
@@ -67,6 +69,7 @@ module Fontist
67
69
  files: :files,
68
70
  format: :format,
69
71
  variable_axes: :variable_axes,
72
+ capability_name: :capability_name,
70
73
  }
71
74
  map "digest", to: :digest
72
75
  map "instructions", to: :instructions
@@ -85,6 +88,7 @@ module Fontist
85
88
  "macos" => "Fontist::MacosImportSource",
86
89
  "google" => "Fontist::GoogleImportSource",
87
90
  "sil" => "Fontist::SilImportSource",
91
+ "windows" => "Fontist::WindowsImportSource",
88
92
  },
89
93
  }
90
94
  map "font_version", to: :font_version
@@ -96,9 +100,9 @@ module Fontist
96
100
  end
97
101
 
98
102
  def all
99
- formulas = Dir[Fontist.formulas_path.join("**/*.yml").to_s].map do |path|
103
+ formulas = Dir[Fontist.formulas_path.join("**/*.yml").to_s].filter_map do |path|
100
104
  Formula.from_file(path)
101
- end.compact
105
+ end
102
106
 
103
107
  FormulaCollection.new(formulas)
104
108
  end
@@ -232,10 +236,18 @@ module Fontist
232
236
  import_source.is_a?(SilImportSource)
233
237
  end
234
238
 
239
+ def windows_import?
240
+ import_source.is_a?(WindowsImportSource)
241
+ end
242
+
235
243
  def manual_formula?
236
244
  import_source.nil?
237
245
  end
238
246
 
247
+ def windows_fod?
248
+ source == "windows_fod" && platforms&.any? { |p| p == "windows" || p.start_with?("windows-") }
249
+ end
250
+
239
251
  def compatible_with_current_platform?
240
252
  return true unless macos_import?
241
253
 
@@ -48,14 +48,14 @@ module Fontist
48
48
  def filter_by_format_spec(formulas)
49
49
  matcher = FormatMatcher.new(@format_spec)
50
50
 
51
- formulas.map do |formula|
51
+ formulas.filter_map do |formula|
52
52
  next formula unless formula.v5?
53
53
 
54
54
  matching = matcher.filter_resources(formula.resources)
55
55
  if matching.any?
56
56
  formula.dup.tap { |f| f.resources = matching }
57
57
  end
58
- end.compact
58
+ end
59
59
  end
60
60
 
61
61
  def ensure_fontist_version(formulas)
@@ -99,7 +99,9 @@ module Fontist
99
99
  end
100
100
 
101
101
  def raise_format_not_available_error(formulas)
102
- available = formulas.flat_map { |f| Array(f.resources).map(&:format) }.compact.uniq
102
+ available = formulas.flat_map do |f|
103
+ Array(f.resources).map(&:format)
104
+ end.compact.uniq
103
105
  raise Errors::FormatNotAvailableError.new(
104
106
  @font_name,
105
107
  @format_spec.format || @format_spec.prefer_format,
@@ -47,6 +47,11 @@ module Fontist
47
47
  version: @options[:import_source].version,
48
48
  release_date: @options[:import_source].release_date,
49
49
  )
50
+ when WindowsImportSource
51
+ builder.set_windows_import_source(
52
+ capability_name: @options[:import_source].capability_name,
53
+ min_windows_version: @options[:import_source].min_windows_version,
54
+ )
50
55
  end
51
56
  else
52
57
  # Legacy support for backward compatibility
@@ -85,6 +85,15 @@ family_id:)
85
85
  )
86
86
  end
87
87
 
88
+ # Convenience method to set Windows import source
89
+ def set_windows_import_source(capability_name:, min_windows_version: "10.0")
90
+ @import_source = WindowsImportSource.new(
91
+ type: "windows",
92
+ capability_name: capability_name,
93
+ min_windows_version: min_windows_version,
94
+ )
95
+ end
96
+
88
97
  private
89
98
 
90
99
  def formula_attributes
@@ -203,7 +212,7 @@ family_id:)
203
212
  result[k] = compacted unless compacted.nil?
204
213
  end
205
214
  when Array
206
- value.map { |item| deep_compact(item) }.compact
215
+ value.filter_map { |item| deep_compact(item) }
207
216
  else
208
217
  value
209
218
  end
@@ -69,7 +69,7 @@ module Fontist
69
69
 
70
70
  unless ofl_path.exist? || apache_path.exist? || ufl_path.exist?
71
71
  raise ArgumentError,
72
- "Source path does not contain expected font directories: "\
72
+ "Source path does not contain expected font directories: " \
73
73
  "#{@source_path}"
74
74
  end
75
75
  end
@@ -183,7 +183,7 @@ module Fontist
183
183
  description: font_file.description,
184
184
  }
185
185
  rescue StandardError => e
186
- warn "Warning: Failed to parse font file #{filename}: "\
186
+ warn "Warning: Failed to parse font file #{filename}: " \
187
187
  "#{e.message}"
188
188
  end
189
189
  end
@@ -199,11 +199,11 @@ module Fontist
199
199
  return [] unless metadata.fonts
200
200
 
201
201
  fonts_array = metadata.fonts.is_a?(Array) ? metadata.fonts : [metadata.fonts]
202
- fonts_array.map do |font|
202
+ fonts_array.filter_map do |font|
203
203
  weight = font.respond_to?(:weight) ? font.weight : font["weight"]
204
204
  style = font.respond_to?(:style) ? font.style : font["style"]
205
205
  variant_name(weight, style)
206
- end.compact.uniq
206
+ end.uniq
207
207
  end
208
208
 
209
209
  # Generate variant name from weight and style
@@ -136,7 +136,7 @@ github_data: nil, version: 4, source_path: nil)
136
136
 
137
137
  # Get all unique categories
138
138
  def categories
139
- all_fonts.map(&:category).compact.uniq.sort
139
+ all_fonts.filter_map(&:category).uniq.sort
140
140
  end
141
141
 
142
142
  # Get fonts available in TTF format
@@ -190,7 +190,7 @@ github_data: nil, version: 4, source_path: nil)
190
190
 
191
191
  # Generate formulas for all fonts
192
192
  def to_formulas
193
- all_fonts.map { |f| to_formula(f.family) }.compact
193
+ all_fonts.filter_map { |f| to_formula(f.family) }
194
194
  end
195
195
 
196
196
  # Save formulas to disk
@@ -198,12 +198,12 @@ github_data: nil, version: 4, source_path: nil)
198
198
  families = family_name ? [font_by_name(family_name)] : all_fonts
199
199
  families = families.compact
200
200
 
201
- families.map do |family|
201
+ families.filter_map do |family|
202
202
  formula = to_formula(family.family)
203
203
  next unless formula
204
204
 
205
205
  save_formula(formula, family.family, output_dir)
206
- end.compact
206
+ end
207
207
  end
208
208
 
209
209
  # Find filename for a variant
@@ -322,8 +322,8 @@ github_data: nil, version: 4, source_path: nil)
322
322
  def index_github_data
323
323
  return {} if @github_data_raw.empty?
324
324
 
325
- @github_data_raw.each_with_object({}) do |family, hash|
326
- hash[family.family] = family
325
+ @github_data_raw.to_h do |family|
326
+ [family.family, family]
327
327
  end
328
328
  end
329
329
 
@@ -367,8 +367,8 @@ github_data: nil, version: 4, source_path: nil)
367
367
 
368
368
  # Index fonts by family name
369
369
  def index_by_family(fonts)
370
- fonts.each_with_object({}) do |font, hash|
371
- hash[font.family] = font
370
+ fonts.to_h do |font|
371
+ [font.family, font]
372
372
  end
373
373
  end
374
374
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  require_relative "base_formula_builder"
4
4
  require_relative "../../../utils/downloader"
5
- require_relative "../../font_metadata_extractor" # lib/fontist/import/font_metadata_extractor.rb
5
+ require_relative "../../font_metadata_extractor" # lib/fontist/import/font_metadata_extractor.rb
6
6
 
7
7
  module Fontist
8
8
  module Import
@@ -111,8 +111,11 @@ module Fontist
111
111
  end
112
112
 
113
113
  def detect_actual_format(urls, declared_format)
114
- extensions = urls.map { |url| url.split("/").last[/\.(\w+)$/, 1]&.downcase }.compact.uniq
115
- if extensions.size == 1 && %w[ttf otf woff woff2].include?(extensions.first)
114
+ extensions = urls.filter_map do |url|
115
+ url.split("/").last[/\.(\w+)$/, 1]&.downcase
116
+ end.uniq
117
+ if extensions.size == 1 && %w[ttf otf woff
118
+ woff2].include?(extensions.first)
116
119
  extensions.first
117
120
  else
118
121
  declared_format
@@ -174,7 +177,10 @@ module Fontist
174
177
  # For variable fonts, ALL styles should be marked as variable
175
178
  if family.variable_font?
176
179
  style["variable_font"] = true
177
- style["variable_axes"] = family.axes.map(&:tag) if family.axes&.any?
180
+ if family.axes&.any?
181
+ style["variable_axes"] =
182
+ family.axes.map(&:tag)
183
+ end
178
184
  else
179
185
  style["variable_font"] = false
180
186
  end
@@ -77,7 +77,7 @@ module Fontist
77
77
  font_fields = message.find_fields("fonts")
78
78
  return nil if font_fields.empty?
79
79
 
80
- font_fields.map do |field|
80
+ font_fields.filter_map do |field|
81
81
  next unless field.message_field?
82
82
 
83
83
  # field.value is a Hash for message fields in unibuf
@@ -94,7 +94,7 @@ module Fontist
94
94
  "full_name" => field_value(font_msg, "full_name"),
95
95
  "copyright" => field_value(font_msg, "copyright"),
96
96
  }.compact
97
- end.compact
97
+ end
98
98
  end
99
99
 
100
100
  # Extract axes array
@@ -102,7 +102,7 @@ module Fontist
102
102
  axis_fields = message.find_fields("axes")
103
103
  return nil if axis_fields.empty?
104
104
 
105
- axis_fields.map do |field|
105
+ axis_fields.filter_map do |field|
106
106
  next unless field.message_field?
107
107
 
108
108
  axis_hash = field.value
@@ -115,7 +115,7 @@ module Fontist
115
115
  "max_value" => field_float(axis_msg, "max_value"),
116
116
  "default_value" => field_float(axis_msg, "default_value"),
117
117
  }.compact
118
- end.compact
118
+ end
119
119
  end
120
120
 
121
121
  # Extract source information
@@ -142,7 +142,7 @@ module Fontist
142
142
  file_fields = source_msg.find_fields("files")
143
143
  return nil if file_fields.empty?
144
144
 
145
- file_fields.map do |field|
145
+ file_fields.filter_map do |field|
146
146
  next unless field.message_field?
147
147
 
148
148
  file_hash = field.value
@@ -153,7 +153,7 @@ module Fontist
153
153
  "source_file" => field_value(file_msg, "source_file"),
154
154
  "dest_file" => field_value(file_msg, "dest_file"),
155
155
  }.compact
156
- end.compact
156
+ end
157
157
  end
158
158
 
159
159
  # Extract registry default overrides as hash
@@ -43,7 +43,7 @@ module Fontist
43
43
  files_value = attributes.delete(:files)
44
44
  attributes[:files_data] = files_value
45
45
  end
46
- super(**attributes)
46
+ super
47
47
  end
48
48
 
49
49
  # Get files as a Hash
@@ -113,7 +113,7 @@ module Fontist
113
113
  # @param title [String] Section title (deprecated - use header instead)
114
114
  def self.section(title)
115
115
  Fontist.ui.say("")
116
- Fontist.ui.say("── #{title} " + "─" * (76 - title.length))
116
+ Fontist.ui.say("── #{title} " + ("─" * (76 - title.length)))
117
117
  Fontist.ui.say("")
118
118
  end
119
119
 
@@ -247,7 +247,7 @@ module Fontist
247
247
  return unless results[:errors]&.any?
248
248
 
249
249
  Fontist.ui.say(" #{Paint['⚠',
250
- :yellow]} Note: #{results[:failed]} font#{results[:failed] > 1 ? 's' : ''} failed during import.")
250
+ :yellow]} Note: #{results[:failed]} font#{'s' if results[:failed] > 1} failed during import.")
251
251
  Fontist.ui.say("")
252
252
  end
253
253
 
@@ -266,10 +266,10 @@ module Fontist
266
266
  return if total.zero?
267
267
 
268
268
  if results[:successful] > (total * 0.5)
269
- Fontist.ui.say(Paint[" 🎉 Great success! #{results[:successful]} formula#{results[:successful] > 1 ? 's' : ''} created!",
269
+ Fontist.ui.say(Paint[" 🎉 Great success! #{results[:successful]} formula#{'s' if results[:successful] > 1} created!",
270
270
  :green, :bright])
271
271
  elsif results[:successful].positive?
272
- Fontist.ui.say(Paint[" 👍 Keep going! #{results[:successful]} formula#{results[:successful] > 1 ? 's' : ''} created.",
272
+ Fontist.ui.say(Paint[" 👍 Keep going! #{results[:successful]} formula#{'s' if results[:successful] > 1} created.",
273
273
  :yellow, :bright])
274
274
  end
275
275
 
@@ -277,7 +277,7 @@ module Fontist
277
277
  end
278
278
 
279
279
  def calculate_total(results)
280
- results[:total] || (results[:successful] || 0) + (results[:failed] || 0) + (results[:skipped] || 0)
280
+ results[:total] || ((results[:successful] || 0) + (results[:failed] || 0) + (results[:skipped] || 0))
281
281
  end
282
282
  end
283
283
  end
@@ -111,7 +111,7 @@ force: false, verbose: false, import_cache: nil, schema_version: 4)
111
111
  Fontist.ui.say("#{Paint[progress,
112
112
  :white]} #{Paint["#{percentage}%",
113
113
  :yellow]} | #{Paint[family_name, :cyan,
114
- :bright]} #{Paint["(#{fonts_count} font#{fonts_count > 1 ? 's' : ''})", :black,
114
+ :bright]} #{Paint["(#{fonts_count} font#{'s' if fonts_count > 1})", :black,
115
115
  :bright]}")
116
116
 
117
117
  start_time = Time.now
@@ -438,9 +438,7 @@ module Fontist
438
438
 
439
439
  entry_path = File.join(extract_dir, entry.full_name)
440
440
  FileUtils.mkdir_p(File.dirname(entry_path))
441
- File.open(entry_path, "wb") do |f|
442
- f.write(entry.read)
443
- end
441
+ File.binwrite(entry_path, entry.read)
444
442
  end
445
443
  end
446
444
 
@@ -93,7 +93,8 @@ module Fontist
93
93
  private
94
94
 
95
95
  def load_yaml_without_aliases(path)
96
- data = YAML.safe_load(File.read(path), aliases: true, permitted_classes: [Date])
96
+ data = YAML.safe_load(File.read(path), aliases: true,
97
+ permitted_classes: [Date])
97
98
  JSON.parse(JSON.generate(data))
98
99
  end
99
100