fontist 2.1.5 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.gitignore +5 -0
- data/.rubocop_todo.yml +476 -11
- data/README.adoc +1 -1
- data/TODO.fontist-v5.md +196 -0
- data/fontist.gemspec +1 -1
- data/lib/fontist/cache/store.rb +17 -7
- data/lib/fontist/cli.rb +116 -0
- data/lib/fontist/errors.rb +61 -0
- data/lib/fontist/extract.rb +13 -0
- data/lib/fontist/font.rb +9 -1
- data/lib/fontist/font_collection.rb +1 -0
- data/lib/fontist/font_finder.rb +154 -0
- data/lib/fontist/font_installer.rb +172 -15
- data/lib/fontist/font_model.rb +1 -0
- data/lib/fontist/font_path.rb +4 -4
- data/lib/fontist/font_style.rb +17 -0
- data/lib/fontist/format_matcher.rb +176 -0
- data/lib/fontist/format_spec.rb +80 -0
- data/lib/fontist/formula.rb +126 -215
- data/lib/fontist/formula_picker.rb +39 -4
- data/lib/fontist/import/create_formula.rb +80 -3
- data/lib/fontist/import/formula_builder.rb +5 -1
- data/lib/fontist/import/google/font_database.rb +15 -153
- data/lib/fontist/import/google/formula_builder.rb +26 -0
- data/lib/fontist/import/google/formula_builders/base_formula_builder.rb +93 -0
- data/lib/fontist/import/google/formula_builders/formula_builder_v4.rb +155 -0
- data/lib/fontist/import/google/formula_builders/formula_builder_v5.rb +193 -0
- data/lib/fontist/import/google_fonts_importer.rb +17 -5
- data/lib/fontist/import/{macos.rb → macos_importer.rb} +4 -2
- data/lib/fontist/import/recursive_extraction.rb +2 -0
- data/lib/fontist/import/{sil_import.rb → sil_importer.rb} +3 -1
- data/lib/fontist/import/upgrade_formulas.rb +1 -1
- data/lib/fontist/import/v4_to_v5_migrator.rb +263 -0
- data/lib/fontist/import_cli.rb +20 -2
- data/lib/fontist/indexes/index_mixin.rb +8 -4
- data/lib/fontist/manifest.rb +27 -1
- data/lib/fontist/path_scanning.rb +1 -1
- data/lib/fontist/resource.rb +54 -0
- data/lib/fontist/resource_collection.rb +18 -0
- data/lib/fontist/system_index.rb +18 -9
- data/lib/fontist/utils/downloader.rb +0 -2
- data/lib/fontist/utils/github_client.rb +5 -2
- data/lib/fontist/utils/github_url.rb +4 -3
- data/lib/fontist/utils.rb +1 -1
- data/lib/fontist/version.rb +1 -1
- data/lib/fontist.rb +3 -2
- metadata +19 -8
|
@@ -4,17 +4,18 @@ require "paint"
|
|
|
4
4
|
|
|
5
5
|
module Fontist
|
|
6
6
|
module Import
|
|
7
|
-
class
|
|
7
|
+
class MacosImporter
|
|
8
8
|
HOMEPAGE = "https://support.apple.com/en-om/HT211240#document".freeze
|
|
9
9
|
|
|
10
10
|
def initialize(catalog_path, formulas_dir: nil, font_name: nil,
|
|
11
|
-
force: false, verbose: false, import_cache: nil)
|
|
11
|
+
force: false, verbose: false, import_cache: nil, schema_version: 4)
|
|
12
12
|
@catalog_path = catalog_path
|
|
13
13
|
@custom_formulas_dir = formulas_dir
|
|
14
14
|
@font_name_filter = font_name
|
|
15
15
|
@force = force
|
|
16
16
|
@verbose = verbose
|
|
17
17
|
@import_cache = import_cache
|
|
18
|
+
@schema_version = schema_version
|
|
18
19
|
@success_count = 0
|
|
19
20
|
@failure_count = 0
|
|
20
21
|
@skipped_count = 0
|
|
@@ -129,6 +130,7 @@ force: false, verbose: false, import_cache: nil)
|
|
|
129
130
|
verbose: @verbose,
|
|
130
131
|
import_cache: @import_cache,
|
|
131
132
|
name: family_name,
|
|
133
|
+
schema_version: @schema_version,
|
|
132
134
|
).call
|
|
133
135
|
|
|
134
136
|
elapsed = Time.now - start_time
|
|
@@ -3,7 +3,7 @@ require "paint"
|
|
|
3
3
|
|
|
4
4
|
module Fontist
|
|
5
5
|
module Import
|
|
6
|
-
class
|
|
6
|
+
class SilImporter
|
|
7
7
|
ROOT = "https://software.sil.org/fonts/".freeze
|
|
8
8
|
|
|
9
9
|
def initialize(options = {})
|
|
@@ -12,6 +12,7 @@ module Fontist
|
|
|
12
12
|
@verbose = options[:verbose]
|
|
13
13
|
@import_cache = options[:import_cache]
|
|
14
14
|
@force = options[:force]
|
|
15
|
+
@schema_version = options[:schema_version] || 4
|
|
15
16
|
@success_count = 0
|
|
16
17
|
@failure_count = 0
|
|
17
18
|
@skipped_count = 0
|
|
@@ -301,6 +302,7 @@ module Fontist
|
|
|
301
302
|
options[:import_source] = import_source if import_source
|
|
302
303
|
options[:import_cache] = @import_cache if @import_cache
|
|
303
304
|
options[:keep_existing] = !@force
|
|
305
|
+
options[:schema_version] = @schema_version
|
|
304
306
|
# All SIL fonts use the SIL Open Font License
|
|
305
307
|
options[:open_license] = "OFL-1.1"
|
|
306
308
|
|
|
@@ -19,7 +19,7 @@ module Fontist
|
|
|
19
19
|
# - Detects variable font axes from filenames
|
|
20
20
|
class UpgradeFormulas
|
|
21
21
|
ARCHIVE_EXTENSIONS = %w[zip tar gz tgz bz2 7z rar].freeze
|
|
22
|
-
FONT_EXTENSIONS = %w[ttf otf woff2 ttc otc].freeze
|
|
22
|
+
FONT_EXTENSIONS = %w[ttf otf woff woff2 ttc otc].freeze
|
|
23
23
|
|
|
24
24
|
def initialize(formulas_path, options = {})
|
|
25
25
|
@formulas_path = formulas_path
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
require "date"
|
|
2
|
+
require "fileutils"
|
|
3
|
+
require "json"
|
|
4
|
+
require "yaml"
|
|
5
|
+
|
|
6
|
+
module Fontist
|
|
7
|
+
module Import
|
|
8
|
+
# Migrate v4 formulas to v5 schema
|
|
9
|
+
#
|
|
10
|
+
# This script converts existing v4 formula files to v5 format by:
|
|
11
|
+
# 1. Adding schema_version: 5
|
|
12
|
+
# 2. Detecting format from file extensions in resources
|
|
13
|
+
# 3. Detecting variable fonts from filename patterns
|
|
14
|
+
#
|
|
15
|
+
# Usage:
|
|
16
|
+
# Fontist::Import::V4ToV5Migrator.new(input_path, output_path).migrate_all
|
|
17
|
+
#
|
|
18
|
+
class V4ToV5Migrator
|
|
19
|
+
FONT_EXTENSIONS = %w[ttf otf woff woff2 ttc otc dfont].freeze
|
|
20
|
+
ARCHIVE_EXTENSIONS = %w[zip tar gz tgz bz2 7z rar exe cab].freeze
|
|
21
|
+
|
|
22
|
+
def initialize(input_path, output_path = nil, options = {})
|
|
23
|
+
@input_path = input_path
|
|
24
|
+
@output_path = output_path || input_path
|
|
25
|
+
@verbose = options[:verbose]
|
|
26
|
+
@dry_run = options[:dry_run]
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Migrate all formulas in the input path
|
|
30
|
+
#
|
|
31
|
+
# @return [Hash] results with counts of migrated, skipped, failed
|
|
32
|
+
def migrate_all
|
|
33
|
+
results = { migrated: 0, skipped: 0, failed: 0, errors: [] }
|
|
34
|
+
|
|
35
|
+
files = formula_files
|
|
36
|
+
log "Found #{files.size} formula file(s) to process"
|
|
37
|
+
|
|
38
|
+
files.each do |path|
|
|
39
|
+
result = migrate_file(path)
|
|
40
|
+
case result
|
|
41
|
+
when :migrated
|
|
42
|
+
results[:migrated] += 1
|
|
43
|
+
when :skipped
|
|
44
|
+
results[:skipped] += 1
|
|
45
|
+
end
|
|
46
|
+
rescue StandardError => e
|
|
47
|
+
results[:failed] += 1
|
|
48
|
+
results[:errors] << { formula: path, error: e.message }
|
|
49
|
+
log "✗ Failed #{File.basename(path)}: #{e.message}"
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
log_summary(results)
|
|
53
|
+
results
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Migrate a single formula file
|
|
57
|
+
#
|
|
58
|
+
# @param path [String] path to formula file
|
|
59
|
+
# @return [Symbol] :migrated or :skipped
|
|
60
|
+
def migrate_file(path)
|
|
61
|
+
formula_data = load_yaml_without_aliases(path)
|
|
62
|
+
already_v5 = formula_data["schema_version"] == 5
|
|
63
|
+
|
|
64
|
+
# Add schema_version: 5
|
|
65
|
+
formula_data = add_schema_version(formula_data) unless already_v5
|
|
66
|
+
|
|
67
|
+
# Check if the raw file contains YAML aliases that need resolving
|
|
68
|
+
changed = file_has_yaml_aliases?(path)
|
|
69
|
+
|
|
70
|
+
changed |= upgrade_resources(formula_data) if formula_data["resources"]
|
|
71
|
+
|
|
72
|
+
# Nothing to do if already v5 and no fixes needed
|
|
73
|
+
if already_v5 && !changed
|
|
74
|
+
log " Already v5: #{File.basename(path)}"
|
|
75
|
+
return :skipped
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Calculate output path
|
|
79
|
+
output_file = output_path_for(path)
|
|
80
|
+
|
|
81
|
+
# Save if not dry run
|
|
82
|
+
if @dry_run
|
|
83
|
+
log " Would save: #{output_file}"
|
|
84
|
+
else
|
|
85
|
+
FileUtils.mkdir_p(File.dirname(output_file))
|
|
86
|
+
File.write(output_file, YAML.dump(formula_data))
|
|
87
|
+
log " Saved: #{output_file}"
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
:migrated
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
private
|
|
94
|
+
|
|
95
|
+
def load_yaml_without_aliases(path)
|
|
96
|
+
data = YAML.safe_load(File.read(path), aliases: true, permitted_classes: [Date])
|
|
97
|
+
JSON.parse(JSON.generate(data))
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def file_has_yaml_aliases?(path)
|
|
101
|
+
File.read(path).match?(/^\s*- \*\d+/)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def formula_files
|
|
105
|
+
if File.file?(@input_path)
|
|
106
|
+
[@input_path]
|
|
107
|
+
elsif File.directory?(@input_path)
|
|
108
|
+
Dir.glob(File.join(@input_path, "**/*.yml")).sort
|
|
109
|
+
else
|
|
110
|
+
[]
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def output_path_for(input_file)
|
|
115
|
+
return input_file if @output_path.nil? || @input_path == @output_path
|
|
116
|
+
|
|
117
|
+
# If input is a file and output is a directory, use the same filename
|
|
118
|
+
if File.file?(@input_path) && File.directory?(@output_path)
|
|
119
|
+
return File.join(@output_path, File.basename(input_file))
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Calculate relative path and map to output
|
|
123
|
+
relative = input_file.sub(@input_path, "")
|
|
124
|
+
File.join(@output_path, relative).sub(%r{/+}, "/")
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def add_schema_version(formula_data)
|
|
128
|
+
# Insert schema_version at the beginning for clean YAML output
|
|
129
|
+
{ "schema_version" => 5 }.merge(formula_data)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def upgrade_resources(formula_data)
|
|
133
|
+
changed = false
|
|
134
|
+
|
|
135
|
+
formula_data["resources"].each do |resource_name, resource_data|
|
|
136
|
+
next unless resource_data.is_a?(Hash)
|
|
137
|
+
|
|
138
|
+
# Skip archives - they don't have format
|
|
139
|
+
next if archive_resource?(resource_name, resource_data)
|
|
140
|
+
|
|
141
|
+
# Add format if missing
|
|
142
|
+
unless resource_data["format"]
|
|
143
|
+
format = detect_format(resource_name, resource_data)
|
|
144
|
+
if format
|
|
145
|
+
resource_data["format"] = format
|
|
146
|
+
changed = true
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Add variable_axes if missing and detected
|
|
151
|
+
unless resource_data["variable_axes"]
|
|
152
|
+
axes = detect_variable_axes(resource_name, resource_data)
|
|
153
|
+
if axes&.any?
|
|
154
|
+
resource_data["variable_axes"] = axes
|
|
155
|
+
changed = true
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
changed
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def archive_resource?(resource_name, resource_data)
|
|
164
|
+
return true if archive_extension?(resource_name)
|
|
165
|
+
|
|
166
|
+
urls = Array(resource_data["urls"] || resource_data["files"])
|
|
167
|
+
urls.any? { |url| archive_extension?(url) }
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def archive_extension?(path)
|
|
171
|
+
path =~ /\.(#{ARCHIVE_EXTENSIONS.join('|')})(?:\?|$)/i
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def detect_format(resource_name, resource_data)
|
|
175
|
+
# Try from resource name
|
|
176
|
+
format = format_from_name(resource_name)
|
|
177
|
+
return format if format
|
|
178
|
+
|
|
179
|
+
# Try from URLs
|
|
180
|
+
urls = Array(resource_data["urls"] || resource_data["files"])
|
|
181
|
+
urls.each do |url|
|
|
182
|
+
format = format_from_url(url)
|
|
183
|
+
return format if format
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# Try from files array
|
|
187
|
+
files = Array(resource_data["files"])
|
|
188
|
+
files.each do |file|
|
|
189
|
+
format = format_from_name(file)
|
|
190
|
+
return format if format
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
nil
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def format_from_name(name)
|
|
197
|
+
if name =~ /\.(\w+)(?:\?|$)/
|
|
198
|
+
ext = Regexp.last_match(1).downcase
|
|
199
|
+
return ext if FONT_EXTENSIONS.include?(ext)
|
|
200
|
+
end
|
|
201
|
+
nil
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def format_from_url(url)
|
|
205
|
+
filename = url.split("/").last.split("?").first
|
|
206
|
+
format_from_name(filename)
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def detect_variable_axes(resource_name, resource_data)
|
|
210
|
+
# Try from resource name
|
|
211
|
+
axes = axes_from_name(resource_name)
|
|
212
|
+
return axes if axes.any?
|
|
213
|
+
|
|
214
|
+
# Try from URLs
|
|
215
|
+
urls = Array(resource_data["urls"] || resource_data["files"])
|
|
216
|
+
urls.each do |url|
|
|
217
|
+
axes = axes_from_name(url)
|
|
218
|
+
return axes if axes.any?
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
# Try from files array
|
|
222
|
+
files = Array(resource_data["files"])
|
|
223
|
+
files.each do |file|
|
|
224
|
+
axes = axes_from_name(file)
|
|
225
|
+
return axes if axes.any?
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
[]
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def axes_from_name(name)
|
|
232
|
+
if name =~ /\[([^\]]+)\]/
|
|
233
|
+
Regexp.last_match(1).split(",").map(&:strip)
|
|
234
|
+
else
|
|
235
|
+
[]
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
def log(message)
|
|
240
|
+
puts message if @verbose
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def log_summary(results)
|
|
244
|
+
return unless @verbose
|
|
245
|
+
|
|
246
|
+
puts "\n#{'=' * 60}"
|
|
247
|
+
puts "Migration Summary"
|
|
248
|
+
puts "=" * 60
|
|
249
|
+
puts " Migrated: #{results[:migrated]}"
|
|
250
|
+
puts " Skipped: #{results[:skipped]}"
|
|
251
|
+
puts " Failed: #{results[:failed]}"
|
|
252
|
+
|
|
253
|
+
if results[:errors].any?
|
|
254
|
+
puts "\nErrors:"
|
|
255
|
+
results[:errors].each do |error|
|
|
256
|
+
puts " - #{error[:formula]}: #{error[:error]}"
|
|
257
|
+
end
|
|
258
|
+
end
|
|
259
|
+
puts "=" * 60
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
end
|
|
263
|
+
end
|
data/lib/fontist/import_cli.rb
CHANGED
|
@@ -21,6 +21,10 @@ module Fontist
|
|
|
21
21
|
option :import_cache,
|
|
22
22
|
type: :string,
|
|
23
23
|
desc: "Directory for import cache (default: ~/.fontist/import_cache)"
|
|
24
|
+
option :schema_version,
|
|
25
|
+
type: :numeric,
|
|
26
|
+
default: 4,
|
|
27
|
+
desc: "Formula schema version (4 or 5). v5 supports multi-format fonts."
|
|
24
28
|
|
|
25
29
|
def google
|
|
26
30
|
handle_class_options(options)
|
|
@@ -35,6 +39,7 @@ module Fontist
|
|
|
35
39
|
force: options[:force],
|
|
36
40
|
verbose: options[:verbose],
|
|
37
41
|
import_cache: options[:import_cache],
|
|
42
|
+
schema_version: options[:schema_version],
|
|
38
43
|
)
|
|
39
44
|
|
|
40
45
|
result = importer.import
|
|
@@ -78,9 +83,14 @@ module Fontist
|
|
|
78
83
|
option :import_cache,
|
|
79
84
|
type: :string,
|
|
80
85
|
desc: "Directory for import cache (default: ~/.fontist/import_cache)"
|
|
86
|
+
option :schema_version,
|
|
87
|
+
type: :numeric,
|
|
88
|
+
default: 4,
|
|
89
|
+
desc: "Formula schema version (4 or 5). v5 supports multi-format fonts."
|
|
81
90
|
|
|
82
91
|
def macos
|
|
83
92
|
handle_class_options(options)
|
|
93
|
+
require_relative "import/macos_importer"
|
|
84
94
|
|
|
85
95
|
# Handle deprecated formulas_dir option
|
|
86
96
|
output_dir = if options[:formulas_dir] && !options[:output_path]
|
|
@@ -95,13 +105,14 @@ module Fontist
|
|
|
95
105
|
verbose = options[:verbose]
|
|
96
106
|
font_name = options[:font_name]
|
|
97
107
|
|
|
98
|
-
Import::
|
|
108
|
+
Import::MacosImporter.new(
|
|
99
109
|
plist_path,
|
|
100
110
|
formulas_dir: output_dir,
|
|
101
111
|
font_name: font_name,
|
|
102
112
|
force: force,
|
|
103
113
|
verbose: verbose,
|
|
104
114
|
import_cache: options[:import_cache],
|
|
115
|
+
schema_version: options[:schema_version],
|
|
105
116
|
).call
|
|
106
117
|
|
|
107
118
|
CLI::STATUS_SUCCESS
|
|
@@ -127,16 +138,23 @@ module Fontist
|
|
|
127
138
|
option :import_cache,
|
|
128
139
|
type: :string,
|
|
129
140
|
desc: "Directory for import cache (default: ~/.fontist/import_cache)"
|
|
141
|
+
option :schema_version,
|
|
142
|
+
type: :numeric,
|
|
143
|
+
default: 4,
|
|
144
|
+
desc: "Formula schema version (4 or 5). v5 supports multi-format fonts."
|
|
130
145
|
|
|
131
146
|
def sil
|
|
132
147
|
handle_class_options(options)
|
|
133
148
|
|
|
134
|
-
|
|
149
|
+
require "fontist/import/sil_importer"
|
|
150
|
+
|
|
151
|
+
importer = Fontist::Import::SilImporter.new(
|
|
135
152
|
output_path: options[:output_path],
|
|
136
153
|
font_name: options[:font_name],
|
|
137
154
|
force: options[:force],
|
|
138
155
|
verbose: options[:verbose],
|
|
139
156
|
import_cache: options[:import_cache],
|
|
157
|
+
schema_version: options[:schema_version],
|
|
140
158
|
)
|
|
141
159
|
|
|
142
160
|
result = importer.call
|
|
@@ -49,9 +49,9 @@ module Fontist
|
|
|
49
49
|
|
|
50
50
|
file_content = File.read(file_path).strip
|
|
51
51
|
|
|
52
|
-
if file_content.empty?
|
|
53
|
-
|
|
54
|
-
|
|
52
|
+
if file_content.empty? || file_content == "---"
|
|
53
|
+
# Return empty collection for empty index files
|
|
54
|
+
return new
|
|
55
55
|
end
|
|
56
56
|
|
|
57
57
|
from_yaml(file_content)
|
|
@@ -105,7 +105,11 @@ module Fontist
|
|
|
105
105
|
end
|
|
106
106
|
|
|
107
107
|
def add_formula(formula)
|
|
108
|
-
|
|
108
|
+
# Accept FormulaV4, FormulaV5, or any object that responds to all_fonts
|
|
109
|
+
unless formula.respond_to?(:all_fonts) && formula.respond_to?(:path)
|
|
110
|
+
raise ArgumentError,
|
|
111
|
+
"Expected formula-like object, got #{formula.class}"
|
|
112
|
+
end
|
|
109
113
|
|
|
110
114
|
formula.all_fonts.each do |font|
|
|
111
115
|
font.styles.each do |style|
|
data/lib/fontist/manifest.rb
CHANGED
|
@@ -1,10 +1,35 @@
|
|
|
1
1
|
require "lutaml/model"
|
|
2
|
+
require_relative "format_spec"
|
|
2
3
|
|
|
3
4
|
module Fontist
|
|
4
5
|
class ManifestFont < Lutaml::Model::Serializable
|
|
5
6
|
attribute :name, :string
|
|
6
7
|
attribute :styles, :string, collection: true
|
|
7
8
|
|
|
9
|
+
# Format specification (if not available, will transcode using Fontisan)
|
|
10
|
+
attribute :format, :string
|
|
11
|
+
attribute :variable_axes, :string, collection: true
|
|
12
|
+
attribute :prefer_variable, :boolean, default: false
|
|
13
|
+
|
|
14
|
+
# Transcoding options
|
|
15
|
+
attribute :transcode_path, :string
|
|
16
|
+
attribute :keep_original, :boolean, default: true
|
|
17
|
+
|
|
18
|
+
# Collection options
|
|
19
|
+
attribute :collection_index, :integer
|
|
20
|
+
|
|
21
|
+
# Build FormatSpec from attributes
|
|
22
|
+
def format_spec
|
|
23
|
+
FormatSpec.new(
|
|
24
|
+
format: format,
|
|
25
|
+
variable_axes: variable_axes,
|
|
26
|
+
prefer_variable: prefer_variable,
|
|
27
|
+
transcode_path: transcode_path,
|
|
28
|
+
keep_original: keep_original,
|
|
29
|
+
collection_index: collection_index,
|
|
30
|
+
)
|
|
31
|
+
end
|
|
32
|
+
|
|
8
33
|
def style_paths(locations: false)
|
|
9
34
|
ary = Array(styles)
|
|
10
35
|
(ary.empty? ? [nil] : ary).flat_map do |style|
|
|
@@ -29,7 +54,7 @@ module Fontist
|
|
|
29
54
|
end
|
|
30
55
|
|
|
31
56
|
def install(confirmation: "no", hide_licenses: false, no_progress: false,
|
|
32
|
-
location: nil)
|
|
57
|
+
location: nil)
|
|
33
58
|
validate_location_parameter!(location)
|
|
34
59
|
validate_platform_compatibility!
|
|
35
60
|
|
|
@@ -40,6 +65,7 @@ location: nil)
|
|
|
40
65
|
hide_licenses: hide_licenses,
|
|
41
66
|
no_progress: no_progress,
|
|
42
67
|
location: location,
|
|
68
|
+
format_spec: format_spec,
|
|
43
69
|
)
|
|
44
70
|
rescue Fontist::Errors::PlatformMismatchError => e
|
|
45
71
|
# Re-raise with clear context for manifest users
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
require "lutaml/model"
|
|
2
|
+
|
|
3
|
+
module Fontist
|
|
4
|
+
# Resource - v5 resource with format metadata for multi-format support
|
|
5
|
+
class Resource < Lutaml::Model::Serializable
|
|
6
|
+
attribute :name, :string
|
|
7
|
+
attribute :source, :string
|
|
8
|
+
attribute :urls, :string, collection: true
|
|
9
|
+
attribute :sha256, :string, collection: true
|
|
10
|
+
attribute :file_size, :integer
|
|
11
|
+
attribute :family, :string
|
|
12
|
+
attribute :files, :string, collection: true
|
|
13
|
+
|
|
14
|
+
# v5 format metadata
|
|
15
|
+
attribute :format, :string # ttf, otf, woff2, ttc, otc
|
|
16
|
+
attribute :variable_axes, :string, collection: true # [wght], [ital,wght], etc.
|
|
17
|
+
|
|
18
|
+
key_value do
|
|
19
|
+
map "name", to: :name
|
|
20
|
+
map "source", to: :source
|
|
21
|
+
map "urls", to: :urls
|
|
22
|
+
map "sha256", to: :sha256
|
|
23
|
+
map "file_size", to: :file_size
|
|
24
|
+
map "family", to: :family
|
|
25
|
+
map "files", to: :files
|
|
26
|
+
map "format", to: :format
|
|
27
|
+
map "variable_axes", to: :variable_axes
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def empty?
|
|
31
|
+
Array(urls).empty? && Array(files).empty?
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def variable_font?
|
|
35
|
+
variable_axes && !variable_axes.empty?
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def static_font?
|
|
39
|
+
!variable_font?
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def axes_tags
|
|
43
|
+
Array(variable_axes).map(&:to_s)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def has_axis?(tag)
|
|
47
|
+
axes_tags.include?(tag.to_s)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def collection_file?
|
|
51
|
+
%w[ttc otc].include?(format&.to_s)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
require "lutaml/model"
|
|
2
|
+
require_relative "resource"
|
|
3
|
+
|
|
4
|
+
module Fontist
|
|
5
|
+
# Resource Collection
|
|
6
|
+
class ResourceCollection < Lutaml::Model::Collection
|
|
7
|
+
instances :resources, Resource
|
|
8
|
+
|
|
9
|
+
key_value do
|
|
10
|
+
map_key to_instance: :name
|
|
11
|
+
map_instances to: :resources
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def empty?
|
|
15
|
+
resources.nil? || Array(resources).all?(&:empty?)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
data/lib/fontist/system_index.rb
CHANGED
|
@@ -117,6 +117,11 @@ module Fontist
|
|
|
117
117
|
attribute :file_size, :integer
|
|
118
118
|
attribute :file_mtime, :integer
|
|
119
119
|
|
|
120
|
+
# Format tracking (v5 schema support)
|
|
121
|
+
attribute :format, :string
|
|
122
|
+
attribute :variable_font, :boolean, default: false
|
|
123
|
+
attribute :variable_axes, :string, collection: true
|
|
124
|
+
|
|
120
125
|
alias :type :subfamily
|
|
121
126
|
|
|
122
127
|
key_value do
|
|
@@ -128,6 +133,9 @@ module Fontist
|
|
|
128
133
|
map "preferred_subfamily_name", to: :preferred_subfamily_name
|
|
129
134
|
map "file_size", to: :file_size
|
|
130
135
|
map "file_mtime", to: :file_mtime
|
|
136
|
+
map "format", to: :format
|
|
137
|
+
map "variable_font", to: :variable_font
|
|
138
|
+
map "variable_axes", to: :variable_axes
|
|
131
139
|
end
|
|
132
140
|
end
|
|
133
141
|
|
|
@@ -195,7 +203,7 @@ module Fontist
|
|
|
195
203
|
File.write(path, to_yaml)
|
|
196
204
|
end
|
|
197
205
|
|
|
198
|
-
def find(font, style)
|
|
206
|
+
def find(font, style, format_spec: nil)
|
|
199
207
|
current_fonts = index
|
|
200
208
|
|
|
201
209
|
return nil if current_fonts.nil? || current_fonts.empty?
|
|
@@ -210,6 +218,13 @@ module Fontist
|
|
|
210
218
|
end
|
|
211
219
|
end
|
|
212
220
|
|
|
221
|
+
# Apply format filtering if specified
|
|
222
|
+
if format_spec&.has_constraints? && found_fonts
|
|
223
|
+
require_relative "format_matcher"
|
|
224
|
+
matcher = FormatMatcher.new(format_spec)
|
|
225
|
+
found_fonts = matcher.filter_indexed_fonts(found_fonts)
|
|
226
|
+
end
|
|
227
|
+
|
|
213
228
|
found_fonts.empty? ? nil : found_fonts
|
|
214
229
|
end
|
|
215
230
|
|
|
@@ -295,12 +310,6 @@ module Fontist
|
|
|
295
310
|
self
|
|
296
311
|
end
|
|
297
312
|
|
|
298
|
-
def update
|
|
299
|
-
tap do |col|
|
|
300
|
-
col.fonts = detect_paths(@paths_loader&.call || [])
|
|
301
|
-
end
|
|
302
|
-
end
|
|
303
|
-
|
|
304
313
|
def update(verbose: false, stats: nil)
|
|
305
314
|
tap do |col|
|
|
306
315
|
col.fonts = detect_paths(@paths_loader&.call || [], verbose: verbose,
|
|
@@ -637,9 +646,9 @@ spinner_index = nil)
|
|
|
637
646
|
|
|
638
647
|
def gather_fonts(path)
|
|
639
648
|
case File.extname(path).gsub(/^\./, "").downcase
|
|
640
|
-
when "ttf", "otf"
|
|
649
|
+
when "ttf", "otf", "woff", "woff2"
|
|
641
650
|
detect_file_font(path)
|
|
642
|
-
when "ttc"
|
|
651
|
+
when "ttc", "otc"
|
|
643
652
|
detect_collection_fonts(path)
|
|
644
653
|
else
|
|
645
654
|
print_recognition_error(Errors::UnknownFontTypeError.new(path), path)
|
|
@@ -31,11 +31,14 @@ module Fontist
|
|
|
31
31
|
end
|
|
32
32
|
|
|
33
33
|
def fetch_release(client, parsed_url)
|
|
34
|
-
client.release_for_tag("#{parsed_url.owner}/#{parsed_url.repo}",
|
|
34
|
+
client.release_for_tag("#{parsed_url.owner}/#{parsed_url.repo}",
|
|
35
|
+
parsed_url.tag)
|
|
35
36
|
end
|
|
36
37
|
|
|
37
38
|
def find_asset_url(release, asset_name)
|
|
38
|
-
release.assets.find
|
|
39
|
+
release.assets.find do |asset|
|
|
40
|
+
asset.name == asset_name
|
|
41
|
+
end&.browser_download_url
|
|
39
42
|
end
|
|
40
43
|
end
|
|
41
44
|
end
|
|
@@ -2,7 +2,7 @@ module Fontist
|
|
|
2
2
|
module Utils
|
|
3
3
|
class GitHubUrl
|
|
4
4
|
GITHUB_RELEASE_PATTERN =
|
|
5
|
-
%r{^https?://github\.com/(?<owner>[^/]+)/(?<repo>[^/]+)/releases/download/(?<tag>[^/]+)/(?<asset>.+)$}
|
|
5
|
+
%r{^https?://github\.com/(?<owner>[^/]+)/(?<repo>[^/]+)/releases/download/(?<tag>[^/]+)/(?<asset>.+)$}.freeze
|
|
6
6
|
|
|
7
7
|
class << self
|
|
8
8
|
def match?(url)
|
|
@@ -19,7 +19,7 @@ module Fontist
|
|
|
19
19
|
repo: match[:repo],
|
|
20
20
|
tag: match[:tag],
|
|
21
21
|
asset: match[:asset],
|
|
22
|
-
original_url: url_string
|
|
22
|
+
original_url: url_string,
|
|
23
23
|
)
|
|
24
24
|
else
|
|
25
25
|
ParsedUrl.from_non_github_url(url_string)
|
|
@@ -39,7 +39,8 @@ module Fontist
|
|
|
39
39
|
end
|
|
40
40
|
|
|
41
41
|
def self.from_non_github_url(original_url)
|
|
42
|
-
new(owner: nil, repo: nil, tag: nil, asset: nil,
|
|
42
|
+
new(owner: nil, repo: nil, tag: nil, asset: nil,
|
|
43
|
+
original_url: original_url)
|
|
43
44
|
end
|
|
44
45
|
|
|
45
46
|
def matched?
|