fontist 3.0.1 → 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.
- checksums.yaml +4 -4
- data/.github/workflows/discover-fonts.yml +76 -0
- data/.github/workflows/rake.yml +103 -8
- data/TODO.audit-docs.md +164 -0
- data/TODO.improve-docs.md +114 -0
- data/TODO.upgrade-excavate.md +107 -0
- data/docs/guide/formulas.md +37 -1
- data/docs/guide/how-it-works.md +13 -0
- data/docs/guide/platforms/windows.md +67 -0
- data/lib/fontist/errors.rb +21 -0
- data/lib/fontist/font_installer.rb +2 -0
- data/lib/fontist/formula.rb +11 -0
- data/lib/fontist/import/create_formula.rb +5 -0
- data/lib/fontist/import/formula_builder.rb +9 -0
- data/lib/fontist/import/windows/fod_capabilities.yml +654 -0
- data/lib/fontist/import/windows/windows_license.txt +4 -0
- data/lib/fontist/import/windows.rb +162 -0
- data/lib/fontist/import.rb +1 -0
- data/lib/fontist/import_source.rb +1 -0
- data/lib/fontist/resource.rb +5 -1
- data/lib/fontist/resources/windows_fod_resource.rb +51 -0
- data/lib/fontist/resources.rb +1 -0
- data/lib/fontist/utils/downloader.rb +8 -3
- data/lib/fontist/utils/system.rb +17 -0
- data/lib/fontist/version.rb +1 -1
- data/lib/fontist/windows_fod_metadata.rb +83 -0
- data/lib/fontist/windows_import_source.rb +54 -0
- data/lib/fontist.rb +2 -0
- data/script/generate_windows_formulas.rb +24 -0
- data/script/validate_windows_fod_ci.rb +175 -0
- metadata +13 -2
|
@@ -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
|
data/lib/fontist/import.rb
CHANGED
|
@@ -23,6 +23,7 @@ module Fontist
|
|
|
23
23
|
autoload :TemplateHelper, "#{__dir__}/import/template_helper"
|
|
24
24
|
autoload :TextHelper, "#{__dir__}/import/text_helper"
|
|
25
25
|
autoload :UpgradeFormulas, "#{__dir__}/import/upgrade_formulas"
|
|
26
|
+
autoload :Windows, "#{__dir__}/import/windows"
|
|
26
27
|
|
|
27
28
|
class << self
|
|
28
29
|
def name_to_filename(name)
|
data/lib/fontist/resource.rb
CHANGED
|
@@ -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
|
data/lib/fontist/resources.rb
CHANGED
|
@@ -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
|
|
@@ -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.to_h { |k, v| [k.to_s, v] }) || # 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
|
data/lib/fontist/utils/system.rb
CHANGED
|
@@ -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?
|
data/lib/fontist/version.rb
CHANGED
|
@@ -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
|
@@ -54,6 +54,8 @@ module Fontist
|
|
|
54
54
|
autoload :SilImportSource, "#{__dir__}/fontist/sil_import_source"
|
|
55
55
|
autoload :MacosFrameworkMetadata,
|
|
56
56
|
"#{__dir__}/fontist/macos_framework_metadata"
|
|
57
|
+
autoload :WindowsImportSource, "#{__dir__}/fontist/windows_import_source"
|
|
58
|
+
autoload :WindowsFodMetadata, "#{__dir__}/fontist/windows_fod_metadata"
|
|
57
59
|
|
|
58
60
|
# Manifest classes
|
|
59
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
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Validate Windows FOD capabilities against live Windows system
|
|
5
|
+
#
|
|
6
|
+
# Usage:
|
|
7
|
+
# bundle exec ruby script/validate_windows_fod_ci.rb
|
|
8
|
+
#
|
|
9
|
+
# This script:
|
|
10
|
+
# 1. Loads fod_capabilities.yml
|
|
11
|
+
# 2. Queries Windows for real FOD capabilities via PowerShell
|
|
12
|
+
# 3. Compares our YAML capability names against what Windows reports
|
|
13
|
+
# 4. For installed capabilities, checks that expected font files exist
|
|
14
|
+
# 5. Exits 0 on success, 1 on critical mismatches
|
|
15
|
+
|
|
16
|
+
require "yaml"
|
|
17
|
+
require "set"
|
|
18
|
+
|
|
19
|
+
FONTS_DIR = 'C:\Windows\Fonts'
|
|
20
|
+
YAML_PATH = File.expand_path(
|
|
21
|
+
"../lib/fontist/import/windows/fod_capabilities.yml",
|
|
22
|
+
__dir__,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
def main
|
|
26
|
+
unless Gem.win_platform?
|
|
27
|
+
puts "SKIP: Not running on Windows"
|
|
28
|
+
exit 0
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
puts "=== Windows FOD Capability Validation ==="
|
|
32
|
+
puts ""
|
|
33
|
+
|
|
34
|
+
yaml_caps = load_yaml_capabilities
|
|
35
|
+
live_caps = query_live_capabilities
|
|
36
|
+
|
|
37
|
+
puts "YAML capabilities: #{yaml_caps.size}"
|
|
38
|
+
puts "Live capabilities: #{live_caps.size}"
|
|
39
|
+
puts ""
|
|
40
|
+
|
|
41
|
+
matched, missing_from_yaml, extra_in_yaml = compare(yaml_caps, live_caps)
|
|
42
|
+
|
|
43
|
+
print_results(matched, missing_from_yaml, extra_in_yaml)
|
|
44
|
+
|
|
45
|
+
errors = check_installed_fonts(yaml_caps, live_caps)
|
|
46
|
+
|
|
47
|
+
puts ""
|
|
48
|
+
summarize(matched, missing_from_yaml, extra_in_yaml, errors)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def load_yaml_capabilities
|
|
52
|
+
data = YAML.safe_load(File.read(YAML_PATH))
|
|
53
|
+
data.fetch("capabilities", {})
|
|
54
|
+
rescue StandardError => e
|
|
55
|
+
puts "ERROR: Failed to load YAML: #{e.message}"
|
|
56
|
+
exit 1
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def query_live_capabilities
|
|
60
|
+
output = `powershell -NoProfile -Command "Get-WindowsCapability -Online -Name 'Language.Fonts.*' | Select-Object Name, State | ConvertTo-Csv -NoTypeInformation"`
|
|
61
|
+
|
|
62
|
+
unless $?.success?
|
|
63
|
+
puts "ERROR: PowerShell command failed (exit #{$?.exitstatus})"
|
|
64
|
+
exit 1
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
caps = {}
|
|
68
|
+
output.each_line do |line|
|
|
69
|
+
line = line.strip.delete('"')
|
|
70
|
+
next if line.empty? || line.start_with?("Name")
|
|
71
|
+
|
|
72
|
+
name, state = line.split(",", 2)
|
|
73
|
+
caps[name] = state if name && state
|
|
74
|
+
end
|
|
75
|
+
caps
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def compare(yaml_caps, live_caps)
|
|
79
|
+
yaml_names = yaml_caps.keys.to_set
|
|
80
|
+
live_names = live_caps.keys.to_set
|
|
81
|
+
|
|
82
|
+
matched = yaml_names & live_names
|
|
83
|
+
missing_from_yaml = live_names - yaml_names
|
|
84
|
+
extra_in_yaml = yaml_names - live_names
|
|
85
|
+
|
|
86
|
+
[matched, missing_from_yaml, extra_in_yaml]
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def print_results(matched, missing_from_yaml, extra_in_yaml)
|
|
90
|
+
puts "=== Matched (#{matched.size}) ==="
|
|
91
|
+
matched.sort.each { |name| puts " OK: #{name}" }
|
|
92
|
+
|
|
93
|
+
if missing_from_yaml.any?
|
|
94
|
+
puts ""
|
|
95
|
+
puts "=== Missing from our YAML (#{missing_from_yaml.size}) ==="
|
|
96
|
+
missing_from_yaml.sort.each { |name| puts " MISSING: #{name}" }
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
if extra_in_yaml.any?
|
|
100
|
+
puts ""
|
|
101
|
+
puts "=== Extra in our YAML (#{extra_in_yaml.size}) ==="
|
|
102
|
+
extra_in_yaml.sort.each { |name| puts " EXTRA: #{name}" }
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def check_installed_fonts(yaml_caps, live_caps)
|
|
107
|
+
installed = live_caps.select { |_, state| state == "Installed" }
|
|
108
|
+
return [] if installed.empty?
|
|
109
|
+
|
|
110
|
+
puts ""
|
|
111
|
+
puts "=== Font File Checks for Installed Capabilities ==="
|
|
112
|
+
|
|
113
|
+
errors = []
|
|
114
|
+
|
|
115
|
+
installed.each_key do |cap_name|
|
|
116
|
+
cap_data = yaml_caps[cap_name]
|
|
117
|
+
next unless cap_data
|
|
118
|
+
|
|
119
|
+
fonts = cap_data.fetch("fonts", {})
|
|
120
|
+
fonts.each do |family_name, data|
|
|
121
|
+
files = data.fetch("files", [])
|
|
122
|
+
files.each do |filename|
|
|
123
|
+
path = File.join(FONTS_DIR, filename)
|
|
124
|
+
if File.exist?(path)
|
|
125
|
+
puts " OK: #{filename} (#{family_name})"
|
|
126
|
+
else
|
|
127
|
+
puts " MISSING: #{filename} (#{family_name}) — expected at #{path}"
|
|
128
|
+
errors << "#{cap_name}: #{filename} not found"
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
errors
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def summarize(matched, missing_from_yaml, extra_in_yaml, font_errors)
|
|
138
|
+
puts "========================================="
|
|
139
|
+
puts " Validation Summary"
|
|
140
|
+
puts "========================================="
|
|
141
|
+
puts " Capability matches: #{matched.size}"
|
|
142
|
+
puts " Missing from our YAML: #{missing_from_yaml.size}"
|
|
143
|
+
puts " Extra in our YAML: #{extra_in_yaml.size}"
|
|
144
|
+
puts " Font file errors: #{font_errors.size}"
|
|
145
|
+
puts ""
|
|
146
|
+
|
|
147
|
+
if missing_from_yaml.any?
|
|
148
|
+
puts "WARNING: #{missing_from_yaml.size} capabilities exist on Windows but not in our YAML."
|
|
149
|
+
puts " Consider adding them to fod_capabilities.yml"
|
|
150
|
+
puts ""
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
if extra_in_yaml.any?
|
|
154
|
+
puts "WARNING: #{extra_in_yaml.size} capabilities in our YAML not found on this Windows version."
|
|
155
|
+
puts " These may be version-specific or renamed."
|
|
156
|
+
puts ""
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
if font_errors.any?
|
|
160
|
+
puts "WARNING: #{font_errors.size} font files missing for installed capabilities."
|
|
161
|
+
font_errors.each { |e| puts " - #{e}" }
|
|
162
|
+
puts ""
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# Exit 1 only if we have zero matches (indicates a fundamental problem)
|
|
166
|
+
if matched.empty? && (missing_from_yaml.any? || extra_in_yaml.any?)
|
|
167
|
+
puts "CRITICAL: No capability names matched — our YAML may be completely wrong."
|
|
168
|
+
exit 1
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
puts "Validation passed."
|
|
172
|
+
exit 0
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
main
|