fontist 2.1.6 → 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 +184 -120
- 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 +2 -2
- metadata +19 -8
data/TODO.fontist-v5.md
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
# Fontist v5 Schema Implementation Plan
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
This document outlines the implementation plan for Formula Schema v5 with
|
|
6
|
+
multi-format font support. The architecture uses **v5-only classes** -
|
|
7
|
+
migration from v4 to v5 is handled by a separate migration script.
|
|
8
|
+
|
|
9
|
+
## Architecture
|
|
10
|
+
|
|
11
|
+
### Core Principle: v5 Only
|
|
12
|
+
|
|
13
|
+
All formula classes now use v5 schema with:
|
|
14
|
+
- `schema_version: 5` (default)
|
|
15
|
+
- Format metadata on resources (`format`, `variable_axes`)
|
|
16
|
+
- Format metadata on styles (`formats`, `variable_font`, `variable_axes`)
|
|
17
|
+
|
|
18
|
+
### Class Hierarchy
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
Formula (v5 only)
|
|
22
|
+
├── schema_version: 5
|
|
23
|
+
├── resources: ResourceCollection
|
|
24
|
+
│ └── Resource
|
|
25
|
+
│ ├── format: string (ttf, otf, woff, woff2, ttc, otc, dfont)
|
|
26
|
+
│ └── variable_axes: array (e.g., ["wght", "wdth"])
|
|
27
|
+
├── fonts: FontModel[]
|
|
28
|
+
│ └── styles: FontStyle[]
|
|
29
|
+
│ ├── formats: array
|
|
30
|
+
│ ├── variable_font: boolean
|
|
31
|
+
│ └── variable_axes: array
|
|
32
|
+
└── font_collections: FontCollection[]
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Implementation Status
|
|
36
|
+
|
|
37
|
+
### Phase 1: Core Models ✅ DONE
|
|
38
|
+
|
|
39
|
+
- [x] FormatSpec model (`lib/fontist/format_spec.rb`)
|
|
40
|
+
- [x] FormatMatcher service (`lib/fontist/format_matcher.rb`)
|
|
41
|
+
- [x] FontFinder service (`lib/fontist/font_finder.rb`)
|
|
42
|
+
- [x] v5-only Formula class (`lib/fontist/formula.rb`)
|
|
43
|
+
- [x] v5-only Resource class (`lib/fontist/resource.rb`)
|
|
44
|
+
- [x] v5-only FontStyle class (`lib/fontist/font_style.rb`)
|
|
45
|
+
- [x] ResourceCollection class (`lib/fontist/resource_collection.rb`)
|
|
46
|
+
- [x] All 65 tests passing
|
|
47
|
+
|
|
48
|
+
### Phase 2: Import System Fixes ✅ DONE
|
|
49
|
+
|
|
50
|
+
#### Importer Format Metadata Status
|
|
51
|
+
|
|
52
|
+
| Importer | format in resources | variable_axes | Status |
|
|
53
|
+
|----------|-------------------|---------------|--------|
|
|
54
|
+
| Google V5 | ✅ Dynamic | ✅ Yes | DONE |
|
|
55
|
+
| Google V4 | ✅ Hardcoded "ttf" | ⚠️ Skipped | N/A |
|
|
56
|
+
| macOS | ✅ Via CreateFormula | ✅ Via CreateFormula | DONE |
|
|
57
|
+
| SIL | ✅ Via CreateFormula | ✅ Via CreateFormula | DONE |
|
|
58
|
+
|
|
59
|
+
#### Tasks
|
|
60
|
+
|
|
61
|
+
- [x] Fix `CreateFormula` class to detect and add format metadata
|
|
62
|
+
- [x] macOS importer uses CreateFormula (now has format detection)
|
|
63
|
+
- [x] SIL importer uses CreateFormula (now has format detection)
|
|
64
|
+
- [x] Verify Google V5 importer works correctly
|
|
65
|
+
|
|
66
|
+
### Phase 3: Migration Script ✅ DONE
|
|
67
|
+
|
|
68
|
+
Create `lib/fontist/import/v4_to_v5_migrator.rb`: ✅ Created
|
|
69
|
+
|
|
70
|
+
Migration features:
|
|
71
|
+
- [x] Reads v4 YAML
|
|
72
|
+
- [x] Adds schema_version: 5
|
|
73
|
+
- [x] Detects format from file extensions
|
|
74
|
+
- [x] Detects variable fonts from filename patterns
|
|
75
|
+
- [x] Writes v5 YAML
|
|
76
|
+
- [x] CLI command: `fontist migrate-formulas INPUT [OUTPUT]`
|
|
77
|
+
|
|
78
|
+
### Phase 4: Run Imports ✅ DONE (via migration)
|
|
79
|
+
|
|
80
|
+
All existing formulas migrated to v5 schema. Fresh imports can be run if needed:
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
# Google Fonts (if fresh import needed)
|
|
84
|
+
fontist import google --schema-version=5 --output-path=./Formulas/google --force
|
|
85
|
+
|
|
86
|
+
# macOS Fonts (if fresh import needed)
|
|
87
|
+
fontist import macos --schema-version=5 --output-path=./Formulas/macos --force
|
|
88
|
+
|
|
89
|
+
# SIL Fonts (if fresh import needed)
|
|
90
|
+
fontist import sil --schema-version=5 --output-path=./Formulas/sil --force
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Phase 5: Migrate Existing Formulas ✅ DONE
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
# Run migration on all existing v4 formulas
|
|
97
|
+
fontist migrate-formulas ../formulas/Formulas ../formulas/Formulas --verbose
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Results:
|
|
101
|
+
- Migrated: 3206 formulas
|
|
102
|
+
- Skipped (already v5): 474 formulas
|
|
103
|
+
- Failed: 1 (malformed YAML backup file)
|
|
104
|
+
- Total time: ~5 seconds
|
|
105
|
+
|
|
106
|
+
## Critical Files
|
|
107
|
+
|
|
108
|
+
### Core Classes (v5 only)
|
|
109
|
+
|
|
110
|
+
| File | Description | Status |
|
|
111
|
+
|------|-------------|--------|
|
|
112
|
+
| `lib/fontist/formula.rb` | v5 formula with schema_version | ✅ Done |
|
|
113
|
+
| `lib/fontist/resource.rb` | Resource with format/variable_axes | ✅ Done |
|
|
114
|
+
| `lib/fontist/resource_collection.rb` | Resource collection | ✅ Done |
|
|
115
|
+
| `lib/fontist/font_style.rb` | FontStyle with format metadata | ✅ Done |
|
|
116
|
+
| `lib/fontist/font_model.rb` | FontModel using FontStyle | ✅ Done |
|
|
117
|
+
| `lib/fontist/font_collection.rb` | FontCollection using FontModel | ✅ Done |
|
|
118
|
+
|
|
119
|
+
### Services
|
|
120
|
+
|
|
121
|
+
| File | Description | Status |
|
|
122
|
+
|------|-------------|--------|
|
|
123
|
+
| `lib/fontist/format_spec.rb` | FormatSpec model | ✅ Done |
|
|
124
|
+
| `lib/fontist/format_matcher.rb` | Format matching service | ✅ Done |
|
|
125
|
+
| `lib/fontist/font_finder.rb` | Font discovery by capabilities | ✅ Done |
|
|
126
|
+
|
|
127
|
+
### Import System
|
|
128
|
+
|
|
129
|
+
| File | Description | Status |
|
|
130
|
+
|------|-------------|--------|
|
|
131
|
+
| `lib/fontist/import/create_formula.rb` | Generic formula creator | ✅ Fixed |
|
|
132
|
+
| `lib/fontist/import/formula_builder.rb` | Base formula builder | ✅ Works |
|
|
133
|
+
| `lib/fontist/import/google/formula_builder_v5.rb` | Google V5 builder | ✅ Done |
|
|
134
|
+
| `lib/fontist/import/macos_importer.rb` | macOS importer | ✅ Works |
|
|
135
|
+
| `lib/fontist/import/sil_importer.rb` | SIL importer | ✅ Works |
|
|
136
|
+
| `lib/fontist/import/v4_to_v5_migrator.rb` | Migration script | ✅ Created |
|
|
137
|
+
|
|
138
|
+
## Next Steps
|
|
139
|
+
|
|
140
|
+
1. ~~**Fix CreateFormula class**~~ - ✅ Done
|
|
141
|
+
2. ~~**Fix macOS importer**~~ - ✅ Works via CreateFormula
|
|
142
|
+
3. ~~**Fix SIL importer**~~ - ✅ Works via CreateFormula
|
|
143
|
+
4. ~~**Create V4→V5 migrator**~~ - ✅ Created
|
|
144
|
+
5. ~~**Migrate formulas**~~ - ✅ 3206 migrated, 474 skipped
|
|
145
|
+
6. ~~**Commit formulas**~~ - ✅ Committed to v5 branch (3cbe032)
|
|
146
|
+
7. ~~**Fix V5 builder for variable fonts**~~ - ✅ Done (8e0e568)
|
|
147
|
+
8. **Run fresh Google import** - Import with v5 schema to get WOFF2/variable fonts
|
|
148
|
+
9. **Verify** - Test installation with v5 formulas
|
|
149
|
+
|
|
150
|
+
## v5 Formula Example (Roboto Flex)
|
|
151
|
+
|
|
152
|
+
```yaml
|
|
153
|
+
schema_version: 5
|
|
154
|
+
resources:
|
|
155
|
+
woff2_variable:
|
|
156
|
+
format: woff2
|
|
157
|
+
variable_axes: [GRAD, XOPQ, XTRA, YOPQ, YTAS, YTDE, YTFI, YTLC, YTUC, opsz, slnt, wdth, wght]
|
|
158
|
+
ttf_variable:
|
|
159
|
+
format: ttf
|
|
160
|
+
variable_axes: [GRAD, XOPQ, XTRA, YOPQ, YTAS, YTDE, YTFI, YTLC, YTUC, opsz, slnt, wdth, wght]
|
|
161
|
+
fonts:
|
|
162
|
+
- styles:
|
|
163
|
+
- variable_font: true
|
|
164
|
+
variable_axes: [GRAD, XOPQ, XTRA, YOPQ, YTAS, YTDE, YTFI, YTLC, YTUC, opsz, slnt, wdth, wght]
|
|
165
|
+
formats: [ttf, woff2]
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
## Breaking Changes
|
|
169
|
+
|
|
170
|
+
- v4 formulas are no longer supported directly
|
|
171
|
+
- Migration or re-import required for all formulas
|
|
172
|
+
- All formulas must have `schema_version: 5`
|
|
173
|
+
- Resources must have `format` field in v5
|
|
174
|
+
|
|
175
|
+
## Format Detection Logic
|
|
176
|
+
|
|
177
|
+
### File Extension → Format
|
|
178
|
+
|
|
179
|
+
| Extension | Format |
|
|
180
|
+
|-----------|--------|
|
|
181
|
+
| .ttf | ttf |
|
|
182
|
+
| .otf | otf |
|
|
183
|
+
| .woff | woff |
|
|
184
|
+
| .woff2 | woff2 |
|
|
185
|
+
| .ttc | ttc |
|
|
186
|
+
| .otc | otc |
|
|
187
|
+
| .dfont | dfont |
|
|
188
|
+
|
|
189
|
+
### Filename Pattern → Variable Axes
|
|
190
|
+
|
|
191
|
+
| Pattern | Axes |
|
|
192
|
+
|---------|------|
|
|
193
|
+
| `Font[wght].ttf` | ["wght"] |
|
|
194
|
+
| `Font[wght,wdth].ttf` | ["wght", "wdth"] |
|
|
195
|
+
| `Font-Variable.ttf` | Detect from font file |
|
|
196
|
+
| `Font-VF.ttf` | Detect from font file |
|
data/fontist.gemspec
CHANGED
|
@@ -30,7 +30,7 @@ Gem::Specification.new do |spec|
|
|
|
30
30
|
spec.executables = ["fontist"]
|
|
31
31
|
|
|
32
32
|
spec.add_dependency "down", "~> 5.0"
|
|
33
|
-
spec.add_dependency "excavate", "~> 0
|
|
33
|
+
spec.add_dependency "excavate", "~> 1.0", ">= 1.0.3"
|
|
34
34
|
spec.add_dependency "fontisan", "~> 0.2", ">= 0.2.11"
|
|
35
35
|
spec.add_dependency "fuzzy_match", "~> 2.1"
|
|
36
36
|
spec.add_dependency "git", "> 1.0"
|
data/lib/fontist/cache/store.rb
CHANGED
|
@@ -79,12 +79,17 @@ module Fontist
|
|
|
79
79
|
return nil unless File.exist?(cache_path(key))
|
|
80
80
|
|
|
81
81
|
begin
|
|
82
|
-
Marshal
|
|
83
|
-
|
|
82
|
+
# Use binary read mode for Marshal data - critical on Windows
|
|
83
|
+
Marshal.load(File.binread(cache_path(key)))
|
|
84
|
+
rescue ArgumentError, TypeError, EOFError
|
|
84
85
|
# Cache file is corrupted - delete it and return nil
|
|
85
86
|
# This can happen on Windows when file is read while being written,
|
|
86
87
|
# or when cache files from previous runs are corrupted
|
|
87
|
-
|
|
88
|
+
begin
|
|
89
|
+
File.delete(cache_path(key))
|
|
90
|
+
rescue StandardError
|
|
91
|
+
nil
|
|
92
|
+
end
|
|
88
93
|
nil
|
|
89
94
|
end
|
|
90
95
|
end
|
|
@@ -92,15 +97,20 @@ module Fontist
|
|
|
92
97
|
def write_entry(key, entry)
|
|
93
98
|
# Use temp file + atomic rename to prevent race conditions
|
|
94
99
|
# This ensures readers never see partial writes, even on Windows
|
|
95
|
-
temp_path = cache_path(key)
|
|
100
|
+
temp_path = "#{cache_path(key)}.tmp"
|
|
96
101
|
|
|
97
|
-
|
|
102
|
+
# Use binary write mode for Marshal data - critical on Windows
|
|
103
|
+
File.binwrite(temp_path, Marshal.dump(entry))
|
|
98
104
|
# Atomic rename (overwrites target atomically)
|
|
99
105
|
# File.rename is atomic on all platforms for same filesystem
|
|
100
106
|
File.rename(temp_path, cache_path(key))
|
|
101
|
-
rescue => e
|
|
107
|
+
rescue StandardError => e
|
|
102
108
|
# Clean up temp file if rename fails
|
|
103
|
-
|
|
109
|
+
begin
|
|
110
|
+
File.delete(temp_path)
|
|
111
|
+
rescue StandardError
|
|
112
|
+
nil
|
|
113
|
+
end
|
|
104
114
|
raise e
|
|
105
115
|
end
|
|
106
116
|
|
data/lib/fontist/cli.rb
CHANGED
|
@@ -116,6 +116,31 @@ module Fontist
|
|
|
116
116
|
type: :string, aliases: :l,
|
|
117
117
|
enum: ["fontist", "user", "system"],
|
|
118
118
|
desc: "Install location: fontist (default), user, system"
|
|
119
|
+
# Format selection options
|
|
120
|
+
option :format,
|
|
121
|
+
type: :string,
|
|
122
|
+
desc: "Font format to install (ttf, otf, woff, woff2, ttc, otc). " \
|
|
123
|
+
"If format not available, will transcode from available formats."
|
|
124
|
+
option :variable_axes,
|
|
125
|
+
type: :string,
|
|
126
|
+
desc: "Variable axes to match (comma-separated, e.g., 'wght,wdth')"
|
|
127
|
+
option :prefer_variable,
|
|
128
|
+
type: :boolean,
|
|
129
|
+
desc: "Prefer variable fonts over static fonts"
|
|
130
|
+
option :prefer_format,
|
|
131
|
+
type: :string,
|
|
132
|
+
desc: "Preferred format when multiple available"
|
|
133
|
+
option :transcode_path,
|
|
134
|
+
type: :string,
|
|
135
|
+
desc: "Directory to save transcoded fonts (default: same as install location)"
|
|
136
|
+
option :keep_original,
|
|
137
|
+
type: :boolean,
|
|
138
|
+
default: true,
|
|
139
|
+
desc: "Keep original font after transcoding"
|
|
140
|
+
# Collection options
|
|
141
|
+
option :collection_index,
|
|
142
|
+
type: :numeric,
|
|
143
|
+
desc: "Extract specific font from TTC/OTC collection (0-indexed)"
|
|
119
144
|
def install(*fonts)
|
|
120
145
|
handle_class_options(options)
|
|
121
146
|
|
|
@@ -216,6 +241,52 @@ module Fontist
|
|
|
216
241
|
handle_error(e)
|
|
217
242
|
end
|
|
218
243
|
|
|
244
|
+
desc "find", "Find fonts by capabilities"
|
|
245
|
+
option :axes, type: :string,
|
|
246
|
+
desc: "Variable axes to match (comma-separated, e.g., 'wght,wdth')"
|
|
247
|
+
option :variable, type: :boolean,
|
|
248
|
+
desc: "Find all variable fonts"
|
|
249
|
+
option :category, type: :string,
|
|
250
|
+
desc: "Filter by category (sans-serif, serif, monospace, display)"
|
|
251
|
+
option :format, type: :string,
|
|
252
|
+
desc: "Filter by format (ttf, otf, woff2)"
|
|
253
|
+
option :json, type: :boolean,
|
|
254
|
+
desc: "Output as JSON"
|
|
255
|
+
def find
|
|
256
|
+
handle_class_options(options)
|
|
257
|
+
|
|
258
|
+
require_relative "font_finder"
|
|
259
|
+
require_relative "format_spec"
|
|
260
|
+
|
|
261
|
+
finder = FontFinder.new(
|
|
262
|
+
format_spec: FormatSpec.new(format: options[:format]),
|
|
263
|
+
category: options[:category],
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
results = if options[:variable]
|
|
267
|
+
finder.variable_fonts
|
|
268
|
+
elsif options[:axes]
|
|
269
|
+
axes = options[:axes].split(",").map(&:strip)
|
|
270
|
+
finder.by_axes(axes)
|
|
271
|
+
elsif options[:category]
|
|
272
|
+
finder.by_category(options[:category])
|
|
273
|
+
else
|
|
274
|
+
error("Please specify --axes, --variable, or --category",
|
|
275
|
+
STATUS_UNKNOWN_ERROR)
|
|
276
|
+
return
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
if options[:json]
|
|
280
|
+
Fontist.ui.say(JSON.pretty_generate(results.map(&:to_h)))
|
|
281
|
+
else
|
|
282
|
+
print_find_results(results)
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
success
|
|
286
|
+
rescue Fontist::Errors::GeneralError => e
|
|
287
|
+
handle_error(e)
|
|
288
|
+
end
|
|
289
|
+
|
|
219
290
|
desc "update", "Update formulas"
|
|
220
291
|
def update
|
|
221
292
|
handle_class_options(options)
|
|
@@ -227,6 +298,34 @@ module Fontist
|
|
|
227
298
|
STATUS_REPO_COULD_NOT_BE_UPDATED
|
|
228
299
|
end
|
|
229
300
|
|
|
301
|
+
desc "migrate-formulas INPUT [OUTPUT]",
|
|
302
|
+
"Migrate v4 formulas to v5 schema"
|
|
303
|
+
option :verbose, type: :boolean, desc: "Show detailed progress"
|
|
304
|
+
option :dry_run, type: :boolean,
|
|
305
|
+
desc: "Show what would be done without making changes"
|
|
306
|
+
def migrate_formulas(input, output = nil)
|
|
307
|
+
handle_class_options(options)
|
|
308
|
+
|
|
309
|
+
require_relative "import/v4_to_v5_migrator"
|
|
310
|
+
|
|
311
|
+
migrator = Fontist::Import::V4ToV5Migrator.new(input, output,
|
|
312
|
+
verbose: options[:verbose],
|
|
313
|
+
dry_run: options[:dry_run])
|
|
314
|
+
|
|
315
|
+
results = migrator.migrate_all
|
|
316
|
+
|
|
317
|
+
if results[:failed].positive?
|
|
318
|
+
Fontist.ui.error("Migration completed with #{results[:failed]} error(s)")
|
|
319
|
+
STATUS_UNKNOWN_ERROR
|
|
320
|
+
else
|
|
321
|
+
Fontist.ui.success("Migrated #{results[:migrated]} formula(s), " \
|
|
322
|
+
"skipped #{results[:skipped]} already v5 formula(s)")
|
|
323
|
+
success
|
|
324
|
+
end
|
|
325
|
+
rescue Fontist::Errors::GeneralError => e
|
|
326
|
+
handle_error(e)
|
|
327
|
+
end
|
|
328
|
+
|
|
230
329
|
desc "manifest SUBCOMMAND ...ARGS", "Manage font manifests"
|
|
231
330
|
subcommand "manifest", Fontist::ManifestCLI
|
|
232
331
|
|
|
@@ -239,6 +338,7 @@ module Fontist
|
|
|
239
338
|
"Uses `fnmatch` patterns."
|
|
240
339
|
option :name_prefix, desc: "Prefix to add to all font family names, " \
|
|
241
340
|
"e.g. 'Wine ' for compatibility fonts"
|
|
341
|
+
option :schema_version, type: :numeric, default: 5, desc: "Formula schema version (default: 5)"
|
|
242
342
|
def create_formula(url)
|
|
243
343
|
handle_class_options(options)
|
|
244
344
|
name = Fontist::Import::CreateFormula.new(url, options).call
|
|
@@ -371,5 +471,21 @@ module Fontist
|
|
|
371
471
|
end
|
|
372
472
|
end
|
|
373
473
|
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength
|
|
474
|
+
|
|
475
|
+
def print_find_results(results)
|
|
476
|
+
if results.empty?
|
|
477
|
+
Fontist.ui.say("No fonts found matching criteria")
|
|
478
|
+
return
|
|
479
|
+
end
|
|
480
|
+
|
|
481
|
+
Fontist.ui.say("Found #{results.count} fonts:\n")
|
|
482
|
+
|
|
483
|
+
results.each do |match|
|
|
484
|
+
Fontist.ui.say(" #{match.name}")
|
|
485
|
+
Fontist.ui.say(" Axes: #{match.axes.join(', ')}") if match.axes&.any?
|
|
486
|
+
Fontist.ui.say(" Format: #{match.format}") if match.format
|
|
487
|
+
Fontist.ui.say(" Category: #{match.category}") if match.category
|
|
488
|
+
end
|
|
489
|
+
end
|
|
374
490
|
end
|
|
375
491
|
end
|
data/lib/fontist/errors.rb
CHANGED
|
@@ -53,6 +53,13 @@ module Fontist
|
|
|
53
53
|
end
|
|
54
54
|
end
|
|
55
55
|
|
|
56
|
+
class UnsupportedSchemaVersionError < GeneralError
|
|
57
|
+
def initialize(version)
|
|
58
|
+
super("Unsupported formula schema version: #{version}. " \
|
|
59
|
+
"Supported versions: 4, 5")
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
56
63
|
class MainRepoNotFoundError < FormulaIndexNotFoundError; end
|
|
57
64
|
|
|
58
65
|
class InvalidConfigAttributeError < GeneralError; end
|
|
@@ -197,5 +204,59 @@ module Fontist
|
|
|
197
204
|
end.join("\n")
|
|
198
205
|
end
|
|
199
206
|
end
|
|
207
|
+
|
|
208
|
+
# Format not available for font
|
|
209
|
+
class FormatNotAvailableError < GeneralError
|
|
210
|
+
def initialize(font_name, requested_format, available_formats)
|
|
211
|
+
super("Format '#{requested_format}' not available for font " \
|
|
212
|
+
"'#{font_name}'. Available formats: #{available_formats.join(', ')}")
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
# Variable axes not supported
|
|
217
|
+
class VariableAxesNotSupportedError < GeneralError
|
|
218
|
+
def initialize(font_name, requested_axes)
|
|
219
|
+
super("Variable axes #{requested_axes.join(', ')} not supported by " \
|
|
220
|
+
"font '#{font_name}'")
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
# Transcoding errors
|
|
225
|
+
class UnsupportedTranscodeError < GeneralError
|
|
226
|
+
def initialize(source_format, target_format)
|
|
227
|
+
super("Cannot transcode from '#{source_format}' to '#{target_format}'. " \
|
|
228
|
+
"Supported: TTF/OTF to WOFF/WOFF2")
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
class TranscodeToolNotFoundError < GeneralError
|
|
233
|
+
def initialize(tool_name)
|
|
234
|
+
super("Transcode tool '#{tool_name}' not found. " \
|
|
235
|
+
"Please install the required package.")
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
class SourceNotFoundError < GeneralError
|
|
240
|
+
def initialize(path)
|
|
241
|
+
super("Source font file not found: #{path}")
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
class CollectionIndexError < GeneralError
|
|
246
|
+
def initialize(index, max_index)
|
|
247
|
+
super("Collection index #{index} out of range. " \
|
|
248
|
+
"Valid range: 0-#{max_index}")
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
# License-related transcoding error
|
|
253
|
+
class TranscodeLicenseNotAcceptedError < GeneralError
|
|
254
|
+
def initialize(font_name)
|
|
255
|
+
super("Font '#{font_name}' requires license agreement. " \
|
|
256
|
+
"Transcoding is a form of modification that may not be permitted " \
|
|
257
|
+
"by all licenses. Please accept the license with " \
|
|
258
|
+
"--accept-all-licenses to proceed with transcoding.")
|
|
259
|
+
end
|
|
260
|
+
end
|
|
200
261
|
end
|
|
201
262
|
end
|
data/lib/fontist/extract.rb
CHANGED
|
@@ -21,5 +21,18 @@ module Fontist
|
|
|
21
21
|
map "file", to: :file
|
|
22
22
|
map "options", to: :options
|
|
23
23
|
end
|
|
24
|
+
|
|
25
|
+
def empty?
|
|
26
|
+
format.nil? && file.nil? && options_empty?
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def options_empty?
|
|
32
|
+
return true if options.nil?
|
|
33
|
+
return options.empty? if options.respond_to?(:empty?)
|
|
34
|
+
return options.file.nil? && options.fonts_sub_dir.nil? if options.is_a?(ExtractOptions)
|
|
35
|
+
false
|
|
36
|
+
end
|
|
24
37
|
end
|
|
25
38
|
end
|
data/lib/fontist/font.rb
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
require_relative "format_spec"
|
|
2
|
+
|
|
1
3
|
module Fontist
|
|
2
4
|
class Font
|
|
3
5
|
def initialize(options = {})
|
|
@@ -14,6 +16,9 @@ module Fontist
|
|
|
14
16
|
@update_fontconfig = options[:update_fontconfig]
|
|
15
17
|
@install_location = options[:location] || options[:install_location]
|
|
16
18
|
|
|
19
|
+
# Accept FormatSpec or create from options
|
|
20
|
+
@format_spec = options[:format_spec] || FormatSpec.from_options(options)
|
|
21
|
+
|
|
17
22
|
validate_location_parameter!
|
|
18
23
|
check_or_create_fontist_path!
|
|
19
24
|
end
|
|
@@ -179,6 +184,8 @@ module Fontist
|
|
|
179
184
|
options = {
|
|
180
185
|
no_progress: @no_progress,
|
|
181
186
|
location: @install_location,
|
|
187
|
+
format_spec: @format_spec,
|
|
188
|
+
confirmation: @confirmation,
|
|
182
189
|
}
|
|
183
190
|
|
|
184
191
|
if @by_formula
|
|
@@ -194,7 +201,8 @@ module Fontist
|
|
|
194
201
|
size_limit: @size_limit,
|
|
195
202
|
version: @version,
|
|
196
203
|
smallest: @smallest,
|
|
197
|
-
newest: @newest
|
|
204
|
+
newest: @newest,
|
|
205
|
+
format_spec: @format_spec)
|
|
198
206
|
.call(downloadable_formulas)
|
|
199
207
|
end
|
|
200
208
|
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
require_relative "format_spec"
|
|
2
|
+
require_relative "format_matcher"
|
|
3
|
+
|
|
4
|
+
module Fontist
|
|
5
|
+
# Find fonts by their capabilities (axes, formats, etc.)
|
|
6
|
+
#
|
|
7
|
+
# Supports:
|
|
8
|
+
# - Find fonts with specific variable axes
|
|
9
|
+
# - Find fonts with any variable support
|
|
10
|
+
# - Find fonts by category (sans-serif, monospace, etc.)
|
|
11
|
+
#
|
|
12
|
+
# Examples:
|
|
13
|
+
# FontFinder.by_axes(["wght", "wdth"]) # Fonts with both axes
|
|
14
|
+
# FontFinder.variable_fonts # All variable fonts
|
|
15
|
+
# FontFinder.by_category("monospace") # Monospace fonts
|
|
16
|
+
#
|
|
17
|
+
class FontFinder
|
|
18
|
+
def initialize(format_spec: nil, category: nil)
|
|
19
|
+
@format_spec = format_spec
|
|
20
|
+
@category = category
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Find fonts that support ALL specified axes
|
|
24
|
+
def by_axes(axes)
|
|
25
|
+
raise ArgumentError, "axes must be an array" unless axes.is_a?(Array)
|
|
26
|
+
|
|
27
|
+
matching_formulas.flat_map do |formula|
|
|
28
|
+
next [] unless formula.v5?
|
|
29
|
+
|
|
30
|
+
resources = each_resource(formula)
|
|
31
|
+
resources = apply_format_filter(resources)
|
|
32
|
+
resources.select do |resource|
|
|
33
|
+
resource.variable_font? && axes_supported?(resource, axes)
|
|
34
|
+
end.map do |resource|
|
|
35
|
+
build_font_match(formula, resource.name, resource)
|
|
36
|
+
end
|
|
37
|
+
end.flatten
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Find all variable fonts
|
|
41
|
+
def variable_fonts
|
|
42
|
+
matching_formulas.flat_map do |formula|
|
|
43
|
+
next [] unless formula.v5?
|
|
44
|
+
|
|
45
|
+
resources = each_resource(formula)
|
|
46
|
+
resources = apply_format_filter(resources)
|
|
47
|
+
resources.select(&:variable_font?).map do |resource|
|
|
48
|
+
build_font_match(formula, resource.name, resource)
|
|
49
|
+
end
|
|
50
|
+
end.flatten
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Find fonts by category
|
|
54
|
+
def by_category(category)
|
|
55
|
+
matching_formulas.select do |formula|
|
|
56
|
+
extract_category(formula) == category
|
|
57
|
+
end.map do |formula|
|
|
58
|
+
resource_names = extract_resource_names(formula)
|
|
59
|
+
FontMatch.new(
|
|
60
|
+
name: formula.name,
|
|
61
|
+
resources: resource_names,
|
|
62
|
+
category: category,
|
|
63
|
+
)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
private
|
|
68
|
+
|
|
69
|
+
def each_resource(formula)
|
|
70
|
+
return [] unless formula.resources
|
|
71
|
+
|
|
72
|
+
Array(formula.resources)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def extract_resource_names(formula)
|
|
76
|
+
return [] unless formula.resources
|
|
77
|
+
|
|
78
|
+
Array(formula.resources).map(&:name).compact
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def build_font_match(formula, name, resource)
|
|
82
|
+
FontMatch.new(
|
|
83
|
+
name: formula.name,
|
|
84
|
+
resource: name,
|
|
85
|
+
axes: resource.axes_tags,
|
|
86
|
+
format: resource.format,
|
|
87
|
+
category: extract_category(formula),
|
|
88
|
+
)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def matching_formulas
|
|
92
|
+
@matching_formulas ||= Formula.all.select do |formula|
|
|
93
|
+
next false if @category && extract_category(formula) != @category
|
|
94
|
+
|
|
95
|
+
true
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def axes_supported?(resource, required_axes)
|
|
100
|
+
available = resource.axes_tags
|
|
101
|
+
required_axes.all? { |axis| available.include?(axis.to_s) }
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def extract_category(formula)
|
|
105
|
+
# Extract from formula metadata if available
|
|
106
|
+
# Note: Formula does not have a category attribute currently
|
|
107
|
+
# Category is detected from name heuristics
|
|
108
|
+
detect_category_from_name(formula.name)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def detect_category_from_name(name)
|
|
112
|
+
# Heuristics for common patterns
|
|
113
|
+
return "monospace" if name.match?(/mono/i)
|
|
114
|
+
return "sans-serif" if name.match?(/sans[-\s]?serif/i)
|
|
115
|
+
return "serif" if name.match?(/serif/i)
|
|
116
|
+
|
|
117
|
+
"sans-serif"
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def apply_format_filter(resources)
|
|
121
|
+
return resources unless @format_spec&.has_constraints?
|
|
122
|
+
|
|
123
|
+
matcher = FormatMatcher.new(@format_spec)
|
|
124
|
+
matcher.filter_resources(resources)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Result object for font matches
|
|
130
|
+
class FontMatch
|
|
131
|
+
attr_reader :name, :resource, :axes, :format, :category, :resources
|
|
132
|
+
|
|
133
|
+
def initialize(name:, resource: nil, axes: [], format: nil,
|
|
134
|
+
category: nil, resources: nil)
|
|
135
|
+
@name = name
|
|
136
|
+
@resource = resource
|
|
137
|
+
@axes = axes
|
|
138
|
+
@format = format
|
|
139
|
+
@category = category
|
|
140
|
+
@resources = resources
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def to_h
|
|
144
|
+
{
|
|
145
|
+
name: name,
|
|
146
|
+
resource: resource,
|
|
147
|
+
resources: resources,
|
|
148
|
+
axes: axes,
|
|
149
|
+
format: format,
|
|
150
|
+
category: category,
|
|
151
|
+
}.compact
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|