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
@@ -0,0 +1,162 @@
1
+ require "yaml"
2
+ require "fileutils"
3
+
4
+ module Fontist
5
+ module Import
6
+ class Windows
7
+ HOMEPAGE = "https://learn.microsoft.com/en-us/typography/fonts/windows_11_font_list".freeze
8
+
9
+ def initialize(formulas_dir: nil)
10
+ @custom_formulas_dir = formulas_dir
11
+ end
12
+
13
+ def call
14
+ capabilities = WindowsFodMetadata.all_capabilities
15
+
16
+ Fontist.ui.say(
17
+ "Generating #{capabilities.size} Windows FOD formula files...",
18
+ )
19
+
20
+ capabilities.each do |cap_name|
21
+ generate_formula(cap_name)
22
+ end
23
+
24
+ Fontist.ui.say("Done. #{capabilities.size} formulas generated.")
25
+ end
26
+
27
+ private
28
+
29
+ def generate_formula(cap_name)
30
+ description = WindowsFodMetadata.description_for_capability(cap_name)
31
+ fonts_data = WindowsFodMetadata.fonts_for_capability(cap_name)
32
+ formula = build_formula(cap_name, description, fonts_data)
33
+ write_formula(description, formula, fonts_data.size)
34
+ end
35
+
36
+ def write_formula(description, formula, font_count)
37
+ path = formula_path(description)
38
+ FileUtils.mkdir_p(File.dirname(path))
39
+ File.write(path, YAML.dump(stringify_keys(formula)))
40
+ Fontist.ui.say(
41
+ " Created: #{File.basename(path)} (#{font_count} fonts)",
42
+ )
43
+ end
44
+
45
+ def build_formula(cap_name, description, fonts_data)
46
+ base_attrs(description).merge(
47
+ resources: build_resources(cap_name, description, fonts_data),
48
+ fonts: build_fonts(fonts_data),
49
+ **import_source_attrs(cap_name),
50
+ )
51
+ end
52
+
53
+ def base_attrs(description)
54
+ {
55
+ schema_version: 5,
56
+ name: description,
57
+ description: "#{description} for Windows",
58
+ homepage: HOMEPAGE,
59
+ platforms: ["windows"],
60
+ open_license: license_text,
61
+ }
62
+ end
63
+
64
+ def import_source_attrs(cap_name)
65
+ {
66
+ import_source: {
67
+ type: "windows",
68
+ capability_name: cap_name,
69
+ min_windows_version: "10.0",
70
+ },
71
+ }
72
+ end
73
+
74
+ def build_resources(cap_name, description, fonts_data)
75
+ all_fonts = collect_font_filenames(fonts_data)
76
+ formats = all_fonts.map { |f| detect_format(f) }.uniq
77
+
78
+ resource = {
79
+ source: "windows_fod",
80
+ capability_name: cap_name,
81
+ }
82
+ resource[:format] = formats.first if formats.size == 1
83
+
84
+ { normalize_key(description) => resource }
85
+ end
86
+
87
+ def collect_font_filenames(fonts_data)
88
+ fonts_data.flat_map do |_, data|
89
+ data["styles"].map { |s| s["font"] }
90
+ end
91
+ end
92
+
93
+ def build_fonts(fonts_data)
94
+ fonts_data.map do |family_name, data|
95
+ {
96
+ name: family_name,
97
+ styles: build_styles(family_name, data["styles"]),
98
+ }
99
+ end
100
+ end
101
+
102
+ def build_styles(family_name, styles)
103
+ styles.map do |style|
104
+ fmt = detect_format(style["font"])
105
+ {
106
+ family_name: family_name,
107
+ type: style["type"],
108
+ font: style["font"],
109
+ formats: [fmt],
110
+ variable_font: false,
111
+ }
112
+ end
113
+ end
114
+
115
+ def detect_format(filename)
116
+ File.extname(filename).downcase.delete(".").then do |ext|
117
+ %w[ttf ttc otf otc].include?(ext) ? ext : "ttf"
118
+ end
119
+ end
120
+
121
+ def formula_path(description)
122
+ filename = "#{normalize_key(description)}.yml"
123
+ formula_dir.join(filename)
124
+ end
125
+
126
+ def formula_dir
127
+ @formula_dir ||= if @custom_formulas_dir
128
+ Pathname.new(@custom_formulas_dir).tap do |path|
129
+ FileUtils.mkdir_p(path)
130
+ end
131
+ else
132
+ Fontist.formulas_path.join("windows").tap do |path|
133
+ FileUtils.mkdir_p(path)
134
+ end
135
+ end
136
+ end
137
+
138
+ def normalize_key(name)
139
+ name.downcase.gsub(/[^a-z0-9]+/, "_").gsub(/^_|_$/, "")
140
+ end
141
+
142
+ def license_text
143
+ @license_text ||= File.read(
144
+ File.expand_path("windows/windows_license.txt", __dir__),
145
+ )
146
+ end
147
+
148
+ def stringify_keys(obj)
149
+ case obj
150
+ when Hash
151
+ obj.each_with_object({}) do |(k, v), result|
152
+ result[k.to_s] = stringify_keys(v)
153
+ end
154
+ when Array
155
+ obj.map { |item| stringify_keys(item) }
156
+ else
157
+ obj
158
+ end
159
+ end
160
+ end
161
+ end
162
+ end
@@ -4,7 +4,8 @@ module Fontist
4
4
  autoload :CreateFormula, "#{__dir__}/import/create_formula"
5
5
  autoload :Files, "#{__dir__}/import/files"
6
6
  autoload :FontMetadataExtractor, "#{__dir__}/import/font_metadata_extractor"
7
- autoload :FontParsingErrorCollector, "#{__dir__}/import/font_parsing_error_collector"
7
+ autoload :FontParsingErrorCollector,
8
+ "#{__dir__}/import/font_parsing_error_collector"
8
9
  autoload :FontStyle, "#{__dir__}/import/font_style"
9
10
  autoload :FormulaBuilder, "#{__dir__}/import/formula_builder"
10
11
  autoload :FormulaSerializer, "#{__dir__}/import/formula_serializer"
@@ -22,6 +23,7 @@ module Fontist
22
23
  autoload :TemplateHelper, "#{__dir__}/import/template_helper"
23
24
  autoload :TextHelper, "#{__dir__}/import/text_helper"
24
25
  autoload :UpgradeFormulas, "#{__dir__}/import/upgrade_formulas"
26
+ autoload :Windows, "#{__dir__}/import/windows"
25
27
 
26
28
  class << self
27
29
  def name_to_filename(name)
@@ -16,6 +16,7 @@ module Fontist
16
16
  "macos" => "Fontist::MacosImportSource",
17
17
  "google" => "Fontist::GoogleImportSource",
18
18
  "sil" => "Fontist::SilImportSource",
19
+ "windows" => "Fontist::WindowsImportSource",
19
20
  }
20
21
  end
21
22
 
@@ -57,8 +57,8 @@ module Fontist
57
57
  def initialize(directory_path, files, scanned_at)
58
58
  @directory_path = directory_path
59
59
  @files = files.freeze # Immutable
60
- @files_by_filename = files.each_with_object({}) do |f, h|
61
- h[f[:filename]] = f
60
+ @files_by_filename = files.to_h do |f|
61
+ [f[:filename], f]
62
62
  end.freeze
63
63
  @scanned_at = scanned_at
64
64
  end
@@ -53,14 +53,14 @@ module Fontist
53
53
  # cache: Optional hash of path => cached_version
54
54
  # Returns: Array of font metadata hashes
55
55
  def self.scan_batch(paths, cache: {})
56
- paths.map do |path|
56
+ paths.filter_map do |path|
57
57
  cached = cache[path]
58
58
  if cached
59
59
  scan_with_cache(path, cached)
60
60
  else
61
61
  scan_font_file(path)
62
62
  end
63
- end.compact
63
+ end
64
64
  end
65
65
 
66
66
  # Compute SHA256 signature of first 1KB for quick change detection
@@ -1,17 +1,21 @@
1
1
  module Fontist
2
2
  module Indexes
3
- autoload :BaseFontCollectionIndex, "#{__dir__}/indexes/base_font_collection_index"
4
- autoload :DefaultFamilyFontIndex, "#{__dir__}/indexes/default_family_font_index"
3
+ autoload :BaseFontCollectionIndex,
4
+ "#{__dir__}/indexes/base_font_collection_index"
5
+ autoload :DefaultFamilyFontIndex,
6
+ "#{__dir__}/indexes/default_family_font_index"
5
7
  autoload :DirectoryChange, "#{__dir__}/indexes/directory_change"
6
8
  autoload :DirectorySnapshot, "#{__dir__}/indexes/directory_snapshot"
7
9
  autoload :FilenameIndex, "#{__dir__}/indexes/filename_index"
8
10
  autoload :FontIndex, "#{__dir__}/indexes/font_index"
9
11
  autoload :FontistIndex, "#{__dir__}/indexes/fontist_index"
10
12
  autoload :FormulaKeyToPath, "#{__dir__}/indexes/formula_key_to_path"
11
- autoload :IncrementalIndexUpdater, "#{__dir__}/indexes/incremental_index_updater"
13
+ autoload :IncrementalIndexUpdater,
14
+ "#{__dir__}/indexes/incremental_index_updater"
12
15
  autoload :IncrementalScanner, "#{__dir__}/indexes/incremental_scanner"
13
16
  autoload :IndexMixin, "#{__dir__}/indexes/index_mixin"
14
- autoload :PreferredFamilyFontIndex, "#{__dir__}/indexes/preferred_family_font_index"
17
+ autoload :PreferredFamilyFontIndex,
18
+ "#{__dir__}/indexes/preferred_family_font_index"
15
19
  autoload :SystemIndex, "#{__dir__}/indexes/system_index"
16
20
  autoload :UserIndex, "#{__dir__}/indexes/user_index"
17
21
  end
@@ -29,11 +29,11 @@ module Fontist
29
29
  end
30
30
 
31
31
  def postscript_names
32
- fonts.map(&:postscript_name).compact
32
+ fonts.filter_map(&:postscript_name)
33
33
  end
34
34
 
35
35
  def font_families
36
- fonts.map(&:font_family_name).compact.uniq
36
+ fonts.filter_map(&:font_family_name).uniq
37
37
  end
38
38
 
39
39
  def primary_family_name
@@ -1,4 +1,3 @@
1
-
2
1
  module Fontist
3
2
  # Import source for macOS supplementary fonts
4
3
  #
data/lib/fontist/repo.rb CHANGED
@@ -194,7 +194,7 @@ module Fontist
194
194
 
195
195
  # Normalize git@ style to https style for comparison
196
196
  # git@github.com:user/repo -> github.com/user/repo
197
- normalized.sub(/:/, "/")
197
+ normalized.sub(":", "/")
198
198
  end
199
199
 
200
200
  def ensure_private_formulas_path_exists
@@ -15,6 +15,9 @@ module Fontist
15
15
  attribute :format, :string # ttf, otf, woff2, ttc, otc
16
16
  attribute :variable_axes, :string, collection: true # [wght], [ital,wght], etc.
17
17
 
18
+ # Windows FOD support
19
+ attribute :capability_name, :string
20
+
18
21
  key_value do
19
22
  map "name", to: :name
20
23
  map "source", to: :source
@@ -25,10 +28,11 @@ module Fontist
25
28
  map "files", to: :files
26
29
  map "format", to: :format
27
30
  map "variable_axes", to: :variable_axes
31
+ map "capability_name", to: :capability_name
28
32
  end
29
33
 
30
34
  def empty?
31
- Array(urls).empty? && Array(files).empty?
35
+ Array(urls).empty? && Array(files).empty? && capability_name.nil?
32
36
  end
33
37
 
34
38
  def variable_font?
@@ -0,0 +1,51 @@
1
+ module Fontist
2
+ module Resources
3
+ class WindowsFodResource
4
+ def initialize(resource, options = {})
5
+ @resource = resource
6
+ @options = options
7
+ end
8
+
9
+ def files(source_names, &block)
10
+ install_capability!
11
+
12
+ source_names.each do |filename|
13
+ path = File.join(system_fonts_dir, filename)
14
+ yield path if File.exist?(path)
15
+ end
16
+ end
17
+
18
+ private
19
+
20
+ def install_capability!
21
+ cap_name = @resource.capability_name
22
+ return if capability_installed?(cap_name)
23
+
24
+ Fontist.ui.say("Installing Windows font capability: #{cap_name}")
25
+ result = Utils::System.run_powershell(
26
+ "Add-WindowsCapability -Online -Name '#{ps_escape(cap_name)}'",
27
+ )
28
+ unless result.success?
29
+ raise Errors::WindowsFodInstallError.new(cap_name, result.stderr)
30
+ end
31
+ end
32
+
33
+ def capability_installed?(name)
34
+ result = Utils::System.run_powershell(
35
+ "(Get-WindowsCapability -Online -Name '#{ps_escape(name)}').State",
36
+ )
37
+ result.stdout.strip == "Installed"
38
+ end
39
+
40
+ # Escape single quotes for PowerShell single-quoted strings
41
+ def ps_escape(str)
42
+ str.gsub("'", "''")
43
+ end
44
+
45
+ def system_fonts_dir
46
+ windir = ENV["windir"] || ENV["SystemRoot"] || "C:/Windows"
47
+ File.join(windir, "Fonts")
48
+ end
49
+ end
50
+ end
51
+ end
@@ -3,5 +3,6 @@ module Fontist
3
3
  autoload :AppleCDNResource, "#{__dir__}/resources/apple_cdn_resource"
4
4
  autoload :ArchiveResource, "#{__dir__}/resources/archive_resource"
5
5
  autoload :GoogleResource, "#{__dir__}/resources/google_resource"
6
+ autoload :WindowsFodResource, "#{__dir__}/resources/windows_fod_resource"
6
7
  end
7
8
  end
@@ -426,7 +426,7 @@ module Fontist
426
426
 
427
427
  def scan_directory_mtimes
428
428
  dirs = extract_font_directories
429
- dirs.map { |dir| [dir, directory_mtime(dir)] }.to_h
429
+ dirs.to_h { |dir| [dir, directory_mtime(dir)] }
430
430
  end
431
431
 
432
432
  def directory_mtime(dir)
@@ -444,10 +444,10 @@ module Fontist
444
444
  templates = SystemFont.system_config["system"][os]["paths"]
445
445
 
446
446
  # Extract directory part before wildcards
447
- base_dirs = templates.map do |pattern|
447
+ base_dirs = templates.filter_map do |pattern|
448
448
  # Remove glob patterns to get base directory
449
449
  pattern.split("/*").first
450
- end.compact.uniq
450
+ end.uniq
451
451
 
452
452
  # Add fontist fonts directory
453
453
  base_dirs << Fontist.fonts_path.to_s
@@ -458,10 +458,10 @@ module Fontist
458
458
  def parse_directory_mtimes
459
459
  return {} unless directory_mtimes
460
460
 
461
- Hash[directory_mtimes.map do |entry|
461
+ directory_mtimes.to_h do |entry|
462
462
  dir, mtime = entry.split(":", 2)
463
463
  [dir, mtime.to_i]
464
- end]
464
+ end
465
465
  end
466
466
 
467
467
  def save_metadata
@@ -1,6 +1,10 @@
1
1
  module Fontist
2
2
  module Utils
3
3
  class Downloader
4
+ DEFAULT_USER_AGENT = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) " \
5
+ "AppleWebKit/537.36 (KHTML, like Gecko) " \
6
+ "Chrome/131.0.0.0 Safari/537.36".freeze
7
+
4
8
  class << self
5
9
  def download(*args)
6
10
  new(*args).download
@@ -135,10 +139,11 @@ module Fontist
135
139
 
136
140
  def headers
137
141
  obj = Helpers.url_object(@file)
138
- obj.respond_to?(:headers) &&
142
+ formula_headers = (obj.respond_to?(:headers) &&
139
143
  obj.headers &&
140
- obj.headers.to_h.map { |k, v| [k.to_s, v] }.to_h || # rubocop:disable Style/HashTransformKeys, Metrics/LineLength
141
- {}
144
+ obj.headers.to_h.to_h { |k, v| [k.to_s, v] }) || {} # rubocop:disable Style/HashTransformKeys, Metrics/LineLength
145
+
146
+ { "User-Agent" => DEFAULT_USER_AGENT }.merge(formula_headers)
142
147
  end
143
148
 
144
149
  def extract_raw_url
@@ -3,7 +3,7 @@ module Fontist
3
3
  module System
4
4
  # Platform override from environment (ONLY platform tags supported)
5
5
  def self.platform_override
6
- ENV["FONTIST_PLATFORM_OVERRIDE"]
6
+ ENV.fetch("FONTIST_PLATFORM_OVERRIDE", nil)
7
7
  end
8
8
 
9
9
  def self.platform_override?
@@ -154,7 +154,7 @@ module Fontist
154
154
 
155
155
  # Convert to comparable integer: major * 10000 + minor * 100 + patch
156
156
  # This allows: 10.11.6 = 101106, 26.0.0 = 260000
157
- major * 10000 + minor * 100 + patch
157
+ (major * 10000) + (minor * 100) + patch
158
158
  end
159
159
 
160
160
  def self.version_in_range?(min_version, max_version)
@@ -179,6 +179,23 @@ module Fontist
179
179
  true
180
180
  end
181
181
 
182
+ PowerShellResult = Struct.new(:stdout, :stderr, :success,
183
+ keyword_init: true) do
184
+ alias_method :success?, :success
185
+ end
186
+
187
+ def self.run_powershell(command)
188
+ require "open3"
189
+ stdout, stderr, status = Open3.capture3(
190
+ "powershell.exe", "-NoProfile", "-NonInteractive", "-Command", command
191
+ )
192
+ PowerShellResult.new(stdout: stdout, stderr: stderr,
193
+ success: status.success?)
194
+ rescue Errno::ENOENT
195
+ PowerShellResult.new(stdout: "", stderr: "powershell.exe not found",
196
+ success: false)
197
+ end
198
+
182
199
  def self.catalog_version_for_macos
183
200
  # Check for platform override first
184
201
  if platform_override?
@@ -58,7 +58,7 @@ module Fontist
58
58
  self.valid_fonts = results.count(&:valid)
59
59
  self.invalid_fonts = total_fonts - valid_fonts
60
60
 
61
- times = results.map(&:time_taken).compact
61
+ times = results.filter_map(&:time_taken)
62
62
  self.total_time = times.sum
63
63
  self.avg_time_per_font = times.empty? ? 0.0 : (total_time / times.size)
64
64
  self.min_time = times.min || 0.0
@@ -172,9 +172,9 @@ module Fontist
172
172
 
173
173
  # Validate fonts sequentially with cache lookup.
174
174
  def validate_sequential(font_paths, cache_lookup:, verbose:)
175
- font_paths.map do |path|
175
+ font_paths.filter_map do |path|
176
176
  validate_with_cache_lookup(path, cache_lookup)
177
- end.compact
177
+ end
178
178
  end
179
179
 
180
180
  # Validate single font, using cache if available and file unchanged.
@@ -1,3 +1,3 @@
1
1
  module Fontist
2
- VERSION = "3.0.0".freeze
2
+ VERSION = "3.0.2".freeze
3
3
  end
@@ -0,0 +1,83 @@
1
+ require "yaml"
2
+
3
+ module Fontist
4
+ # Metadata for Windows Features on Demand (FOD) font capabilities
5
+ #
6
+ # Provides lookup methods to map between FOD capability names and
7
+ # their associated font families/filenames.
8
+ class WindowsFodMetadata
9
+ DATA_PATH = File.expand_path("import/windows/fod_capabilities.yml", __dir__)
10
+
11
+ class << self
12
+ # Reverse lookup: font family name -> capability name
13
+ #
14
+ # @param font_name [String] The font family name (e.g., "Meiryo")
15
+ # @return [String, nil] The capability name or nil if not found
16
+ def capability_for_font(font_name)
17
+ reverse_map[font_name.downcase]
18
+ end
19
+
20
+ # Get all font families for a capability
21
+ #
22
+ # @param cap_name [String] The capability name
23
+ # @return [Hash, nil] Hash of font family names to their metadata
24
+ def fonts_for_capability(cap_name)
25
+ cap = metadata.dig("capabilities", cap_name)
26
+ cap&.fetch("fonts", nil)
27
+ end
28
+
29
+ # Get the description for a capability
30
+ #
31
+ # @param cap_name [String] The capability name
32
+ # @return [String, nil] The description
33
+ def description_for_capability(cap_name)
34
+ metadata.dig("capabilities", cap_name, "description")
35
+ end
36
+
37
+ # Flat list of all FOD font family names
38
+ #
39
+ # @return [Array<String>] All font family names
40
+ def all_font_names
41
+ metadata["capabilities"].flat_map do |_cap, data|
42
+ data["fonts"].keys
43
+ end
44
+ end
45
+
46
+ # List of all capability names
47
+ #
48
+ # @return [Array<String>] All capability names
49
+ def all_capabilities
50
+ metadata["capabilities"].keys
51
+ end
52
+
53
+ # Raw parsed YAML metadata
54
+ #
55
+ # @return [Hash] The parsed YAML data
56
+ def metadata
57
+ @metadata ||= YAML.safe_load(File.read(DATA_PATH))
58
+ end
59
+
60
+ # Reset cached metadata (for testing)
61
+ # @api private
62
+ def reset_cache
63
+ @metadata = nil
64
+ @reverse_map = nil
65
+ end
66
+
67
+ private
68
+
69
+ # Build reverse lookup map: lowercase font name -> capability name
70
+ def reverse_map
71
+ @reverse_map ||= begin
72
+ map = {}
73
+ metadata["capabilities"].each do |cap_name, data|
74
+ data["fonts"].each_key do |font_name|
75
+ map[font_name.downcase] = cap_name
76
+ end
77
+ end
78
+ map
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,54 @@
1
+ module Fontist
2
+ # Import source for Windows Features on Demand (FOD) supplementary fonts
3
+ #
4
+ # Tracks the capability name and minimum Windows version for fonts
5
+ # installed via Add-WindowsCapability.
6
+ class WindowsImportSource < ImportSource
7
+ attribute :capability_name, :string
8
+ attribute :min_windows_version, :string
9
+
10
+ key_value do
11
+ map "capability_name", to: :capability_name
12
+ map "min_windows_version", to: :min_windows_version
13
+ end
14
+
15
+ # Returns the capability name for differentiation
16
+ #
17
+ # @return [String, nil] Capability name or nil
18
+ def differentiation_key
19
+ capability_name
20
+ end
21
+
22
+ # Checks if this import source is older than the provided new source
23
+ #
24
+ # @param new_source [WindowsImportSource] The new source to compare against
25
+ # @return [Boolean] true if this source is outdated
26
+ def outdated?(new_source)
27
+ return false unless new_source.is_a?(WindowsImportSource)
28
+
29
+ # Windows FOD capabilities don't have versioned updates in the same way;
30
+ # they are either present or not. Always return false.
31
+ false
32
+ end
33
+
34
+ # Returns a human-readable string representation
35
+ #
36
+ # @return [String] String representation for debugging/logging
37
+ def to_s
38
+ "Windows FOD (capability: #{capability_name}, " \
39
+ "min_version: #{min_windows_version})"
40
+ end
41
+
42
+ # Equality check based on capability name
43
+ #
44
+ # @param other [Object] The object to compare
45
+ # @return [Boolean] true if objects are equal
46
+ def ==(other)
47
+ return false unless other.is_a?(WindowsImportSource)
48
+
49
+ capability_name == other.capability_name
50
+ end
51
+
52
+ alias eql? ==
53
+ end
54
+ end
data/lib/fontist.rb CHANGED
@@ -52,7 +52,10 @@ module Fontist
52
52
  autoload :GoogleImportSource, "#{__dir__}/fontist/google_import_source"
53
53
  autoload :MacosImportSource, "#{__dir__}/fontist/macos_import_source"
54
54
  autoload :SilImportSource, "#{__dir__}/fontist/sil_import_source"
55
- autoload :MacosFrameworkMetadata, "#{__dir__}/fontist/macos_framework_metadata"
55
+ autoload :MacosFrameworkMetadata,
56
+ "#{__dir__}/fontist/macos_framework_metadata"
57
+ autoload :WindowsImportSource, "#{__dir__}/fontist/windows_import_source"
58
+ autoload :WindowsFodMetadata, "#{__dir__}/fontist/windows_fod_metadata"
56
59
 
57
60
  # Manifest classes
58
61
  autoload :Manifest, "#{__dir__}/fontist/manifest"
@@ -0,0 +1,24 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Generate Windows FOD (Features on Demand) formula files
5
+ #
6
+ # Usage:
7
+ # bundle exec ruby script/generate_windows_formulas.rb [output_dir]
8
+ #
9
+ # Generates formula YAML files for all 25 Windows FOD font capabilities.
10
+ # Output goes to Formulas/windows/ by default, or to the specified directory.
11
+
12
+ require "bundler/setup"
13
+ require "fontist"
14
+ require_relative "../lib/fontist/import/windows"
15
+
16
+ output_dir = ARGV[0]
17
+
18
+ importer = if output_dir
19
+ Fontist::Import::Windows.new(formulas_dir: output_dir)
20
+ else
21
+ Fontist::Import::Windows.new
22
+ end
23
+
24
+ importer.call