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.
- checksums.yaml +4 -4
- data/.github/workflows/discover-fonts.yml +76 -0
- data/.github/workflows/rake.yml +103 -8
- data/.rubocop_todo.yml +179 -139
- 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/fontist.gemspec +2 -2
- data/lib/fontist/cache/store.rb +1 -1
- data/lib/fontist/cli.rb +2 -1
- data/lib/fontist/errors.rb +24 -3
- data/lib/fontist/extract.rb +1 -0
- data/lib/fontist/font.rb +2 -2
- data/lib/fontist/font_finder.rb +1 -2
- data/lib/fontist/font_installer.rb +16 -14
- data/lib/fontist/format_matcher.rb +4 -2
- data/lib/fontist/format_spec.rb +1 -1
- data/lib/fontist/formula.rb +15 -3
- data/lib/fontist/formula_picker.rb +5 -3
- data/lib/fontist/import/create_formula.rb +5 -0
- data/lib/fontist/import/formula_builder.rb +10 -1
- data/lib/fontist/import/google/data_sources/github.rb +4 -4
- data/lib/fontist/import/google/font_database.rb +8 -8
- data/lib/fontist/import/google/formula_builders/formula_builder_v4.rb +1 -1
- data/lib/fontist/import/google/formula_builders/formula_builder_v5.rb +9 -3
- data/lib/fontist/import/google/metadata_adapter.rb +6 -6
- data/lib/fontist/import/google/models/font_family.rb +1 -1
- data/lib/fontist/import/import_display.rb +5 -5
- data/lib/fontist/import/macos_importer.rb +1 -1
- data/lib/fontist/import/upgrade_formulas.rb +1 -3
- data/lib/fontist/import/v4_to_v5_migrator.rb +2 -1
- 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 +3 -1
- data/lib/fontist/import_source.rb +1 -0
- data/lib/fontist/indexes/directory_snapshot.rb +2 -2
- data/lib/fontist/indexes/incremental_scanner.rb +2 -2
- data/lib/fontist/indexes.rb +8 -4
- data/lib/fontist/macos/catalog/asset.rb +2 -2
- data/lib/fontist/macos_import_source.rb +0 -1
- data/lib/fontist/repo.rb +1 -1
- 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/system_index.rb +5 -5
- data/lib/fontist/utils/downloader.rb +8 -3
- data/lib/fontist/utils/system.rb +19 -2
- data/lib/fontist/validation.rb +1 -1
- data/lib/fontist/validator.rb +2 -2
- 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 +4 -1
- data/script/generate_windows_formulas.rb +24 -0
- data/script/validate_windows_fod_ci.rb +175 -0
- 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
|
data/lib/fontist/import.rb
CHANGED
|
@@ -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,
|
|
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)
|
|
@@ -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.
|
|
61
|
-
|
|
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.
|
|
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
|
|
63
|
+
end
|
|
64
64
|
end
|
|
65
65
|
|
|
66
66
|
# Compute SHA256 signature of first 1KB for quick change detection
|
data/lib/fontist/indexes.rb
CHANGED
|
@@ -1,17 +1,21 @@
|
|
|
1
1
|
module Fontist
|
|
2
2
|
module Indexes
|
|
3
|
-
autoload :BaseFontCollectionIndex,
|
|
4
|
-
|
|
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,
|
|
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,
|
|
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.
|
|
32
|
+
fonts.filter_map(&:postscript_name)
|
|
33
33
|
end
|
|
34
34
|
|
|
35
35
|
def font_families
|
|
36
|
-
fonts.
|
|
36
|
+
fonts.filter_map(&:font_family_name).uniq
|
|
37
37
|
end
|
|
38
38
|
|
|
39
39
|
def primary_family_name
|
data/lib/fontist/repo.rb
CHANGED
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
|
data/lib/fontist/system_index.rb
CHANGED
|
@@ -426,7 +426,7 @@ module Fontist
|
|
|
426
426
|
|
|
427
427
|
def scan_directory_mtimes
|
|
428
428
|
dirs = extract_font_directories
|
|
429
|
-
dirs.
|
|
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.
|
|
447
|
+
base_dirs = templates.filter_map do |pattern|
|
|
448
448
|
# Remove glob patterns to get base directory
|
|
449
449
|
pattern.split("/*").first
|
|
450
|
-
end.
|
|
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
|
-
|
|
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.
|
|
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
|
@@ -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
|
|
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?
|
data/lib/fontist/validation.rb
CHANGED
|
@@ -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.
|
|
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
|
data/lib/fontist/validator.rb
CHANGED
|
@@ -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.
|
|
175
|
+
font_paths.filter_map do |path|
|
|
176
176
|
validate_with_cache_lookup(path, cache_lookup)
|
|
177
|
-
end
|
|
177
|
+
end
|
|
178
178
|
end
|
|
179
179
|
|
|
180
180
|
# Validate single font, using cache if available and file unchanged.
|
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
|
@@ -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,
|
|
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
|