fontisan 0.4.7 → 0.4.9
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/CHANGELOG.md +13 -0
- data/README.adoc +116 -2
- data/docs/.vitepress/config.ts +1 -7
- data/docs/COLLECTION_VALIDATION.adoc +61 -0
- data/docs/STITCHER_GUIDE.adoc +138 -1
- data/docs/cli/convert.md +44 -2
- data/docs/cli/index.md +21 -28
- data/docs/cli/validate-collection.md +95 -0
- data/docs/cli/validate.md +8 -0
- data/docs/index.md +0 -2
- data/lib/fontisan/cli.rb +29 -8
- data/lib/fontisan/collection/reader/stats.rb +23 -0
- data/lib/fontisan/collection/reader.rb +90 -0
- data/lib/fontisan/collection.rb +1 -0
- data/lib/fontisan/commands/convert_command.rb +96 -18
- data/lib/fontisan/commands/multi_format_output.rb +59 -0
- data/lib/fontisan/commands/validate_collection_command.rb +121 -0
- data/lib/fontisan/commands.rb +2 -0
- data/lib/fontisan/error.rb +25 -0
- data/lib/fontisan/models.rb +0 -1
- data/lib/fontisan/stitcher/collection_result.rb +18 -0
- data/lib/fontisan/stitcher/partition_strategy/base.rb +23 -0
- data/lib/fontisan/stitcher/partition_strategy/blueprint.rb +24 -0
- data/lib/fontisan/stitcher/partition_strategy/by_plane.rb +131 -0
- data/lib/fontisan/stitcher/partition_strategy/partition.rb +24 -0
- data/lib/fontisan/stitcher/partition_strategy.rb +22 -0
- data/lib/fontisan/stitcher.rb +44 -10
- data/lib/fontisan/ufo/compile/name.rb +2 -2
- data/lib/fontisan/ufo/info.rb +48 -0
- data/lib/fontisan/unicode/plane.rb +56 -0
- data/lib/fontisan/unicode.rb +17 -0
- data/lib/fontisan/version.rb +1 -1
- data/lib/fontisan.rb +2 -2
- metadata +14 -18
- data/docs/cli/audit.md +0 -337
- data/lib/fontisan/cldr/aggregator.rb +0 -33
- data/lib/fontisan/cldr/cache_manager.rb +0 -110
- data/lib/fontisan/cldr/config.rb +0 -59
- data/lib/fontisan/cldr/download_error.rb +0 -9
- data/lib/fontisan/cldr/downloader.rb +0 -79
- data/lib/fontisan/cldr/error.rb +0 -8
- data/lib/fontisan/cldr/index.rb +0 -64
- data/lib/fontisan/cldr/index_builder.rb +0 -72
- data/lib/fontisan/cldr/unicode_set_parser.rb +0 -189
- data/lib/fontisan/cldr/unknown_version_error.rb +0 -9
- data/lib/fontisan/cldr/version_resolver.rb +0 -91
- data/lib/fontisan/cldr.rb +0 -23
- data/lib/fontisan/cli/cldr_cli.rb +0 -85
- data/lib/fontisan/config/cldr.yml +0 -22
- data/lib/fontisan/models/cldr/language_coverage.rb +0 -31
- data/lib/fontisan/models/cldr.rb +0 -12
data/docs/cli/audit.md
DELETED
|
@@ -1,337 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
title: audit
|
|
3
|
-
---
|
|
4
|
-
|
|
5
|
-
# audit
|
|
6
|
-
|
|
7
|
-
Produce a complete per-face font audit report — identity, style, metrics,
|
|
8
|
-
coverage, licensing, hinting, color capabilities, variable-font detail,
|
|
9
|
-
OpenType layout features, and Unicode/CLDR aggregation.
|
|
10
|
-
|
|
11
|
-
`audit` is the successor to `otfinfo`: it covers everything `otfinfo`
|
|
12
|
-
reports plus a great deal more (coverage, hinting, color, variation,
|
|
13
|
-
layout, language coverage), and supports collections, compare mode, and
|
|
14
|
-
whole-library summaries.
|
|
15
|
-
|
|
16
|
-
## Quick Reference
|
|
17
|
-
|
|
18
|
-
```bash
|
|
19
|
-
# One face
|
|
20
|
-
fontisan audit FONT.ttf
|
|
21
|
-
|
|
22
|
-
# Collection (one report per face)
|
|
23
|
-
fontisan audit COLLECTION.ttc
|
|
24
|
-
|
|
25
|
-
# Whole library
|
|
26
|
-
fontisan audit DIR/ --recursive --summary
|
|
27
|
-
|
|
28
|
-
# Compare two fonts or saved reports
|
|
29
|
-
fontisan audit --compare A.ttf B.ttf
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
## Variants
|
|
33
|
-
|
|
34
|
-
| Variant | What it does | Output |
|
|
35
|
-
|---------|-------------|--------|
|
|
36
|
-
| `audit FONT.ttf` | One AuditReport for the single face | `AuditReport` |
|
|
37
|
-
| `audit COLLECTION.ttc` | One AuditReport per face (in source order) | `Array<AuditReport>` |
|
|
38
|
-
| `audit DIR/ --recursive --summary` | Walk the directory, summarize the library | `LibrarySummary` |
|
|
39
|
-
| `audit --compare A B` | Diff two faces or two saved reports | `AuditDiff` |
|
|
40
|
-
|
|
41
|
-
## Options
|
|
42
|
-
|
|
43
|
-
| Option | Description |
|
|
44
|
-
|--------|-------------|
|
|
45
|
-
| `--format FORMAT` | Output format: `text` (default), `yaml`, `json` |
|
|
46
|
-
| `--output PATH`, `-o` | Write to a directory (collection/library), a file (single/compare), or stdout |
|
|
47
|
-
| `--font-index N` | Audit only face N of a collection (default: all) |
|
|
48
|
-
| `--brief` | Fast inventory — skip metrics/hinting/color/layout/UCD/CLDR |
|
|
49
|
-
| `--all-codepoints` | Include the full per-codepoint list (defaults to a compact range view) |
|
|
50
|
-
| `--ucd-version VER` | Aggregate against this UCD version (`latest` to probe) |
|
|
51
|
-
| `--with-language-coverage` | Compute CLDR language coverage % (auto-downloads CLDR on first use) |
|
|
52
|
-
| `--cldr-version VER` | CLDR version to use (`latest` to probe) |
|
|
53
|
-
| `--compare` | Diff two inputs (requires exactly two paths) |
|
|
54
|
-
| `--recursive` | Library mode: walk into subdirectories |
|
|
55
|
-
| `--summary` | Library mode: produce a `LibrarySummary` over a directory |
|
|
56
|
-
|
|
57
|
-
## Brief Mode
|
|
58
|
-
|
|
59
|
-
`--brief` runs only the cheap name-table extractors (provenance, identity,
|
|
60
|
-
style, licensing, coverage) and skips metrics, hinting, color,
|
|
61
|
-
variation, OpenType layout, UCD block/script aggregation, and CLDR
|
|
62
|
-
language coverage. Useful for taking a fast inventory of large libraries.
|
|
63
|
-
|
|
64
|
-
```bash
|
|
65
|
-
fontisan audit FONT.ttf --brief
|
|
66
|
-
fontisan audit DIR/ --recursive --summary --brief
|
|
67
|
-
```
|
|
68
|
-
|
|
69
|
-
Note: `audit --brief` is distinct from `info --brief`. `info --brief`
|
|
70
|
-
loads only 6 tables; `audit --brief` still loads the full font (Coverage
|
|
71
|
-
reads `cmap`) but selects a cheaper extractor subset.
|
|
72
|
-
|
|
73
|
-
## Single-Face Audit
|
|
74
|
-
|
|
75
|
-
```bash
|
|
76
|
-
# Text formatter (default)
|
|
77
|
-
fontisan audit FONT.ttf
|
|
78
|
-
|
|
79
|
-
# YAML (machine-readable)
|
|
80
|
-
fontisan audit FONT.ttf --format yaml
|
|
81
|
-
|
|
82
|
-
# JSON
|
|
83
|
-
fontisan audit FONT.ttf --format json | jq '.licensing'
|
|
84
|
-
|
|
85
|
-
# Write to disk
|
|
86
|
-
fontisan audit FONT.ttf -o report.yaml
|
|
87
|
-
```
|
|
88
|
-
|
|
89
|
-
Sample text output (truncated):
|
|
90
|
-
|
|
91
|
-
```
|
|
92
|
-
NotoSans-Regular
|
|
93
|
-
================================================================================
|
|
94
|
-
generated_at: 2026-06-24T18:11:39Z fontisan: 0.2.20
|
|
95
|
-
source_sha256: f5f552c8c5edb61fe6efb824baf4d4de47b1a8689ab4925ff43f7bd6a4ebece5
|
|
96
|
-
source_file: /path/to/NotoSans-Regular.ttf
|
|
97
|
-
source_format: ttf layout: single face (1/1)
|
|
98
|
-
|
|
99
|
-
IDENTITY
|
|
100
|
-
Family: Noto Sans
|
|
101
|
-
Subfamily: Regular
|
|
102
|
-
Full name: Noto Sans Regular
|
|
103
|
-
PostScript: NotoSans-Regular
|
|
104
|
-
Version: Version 2.015; ttfautohint (v1.8.4.7-5d5b)
|
|
105
|
-
Revision: 2.0149993896484375
|
|
106
|
-
|
|
107
|
-
STYLE
|
|
108
|
-
Weight class: 400 (Regular)
|
|
109
|
-
Width class: 5 (Medium)
|
|
110
|
-
Bold: no
|
|
111
|
-
Italic: no
|
|
112
|
-
PANOSE: 2 11 5 2 4 5 4 2 2 4
|
|
113
|
-
|
|
114
|
-
COVERAGE
|
|
115
|
-
Codepoints: 3094
|
|
116
|
-
Glyphs: 4515
|
|
117
|
-
cmap subtables: 4, 12
|
|
118
|
-
Ranges (top 10): U+0000-U+0000, U+000D-U+000D, U+0020-U+007E, ...
|
|
119
|
-
Unicode scripts: Latin, ...
|
|
120
|
-
|
|
121
|
-
LICENSING
|
|
122
|
-
License URL: https://scripts.sil.org/OFL
|
|
123
|
-
Embedding: 0x0000 (Editable embedding)
|
|
124
|
-
|
|
125
|
-
METRICS
|
|
126
|
-
Units per em: 1000
|
|
127
|
-
Ascender: 1160
|
|
128
|
-
Descender: -288
|
|
129
|
-
...
|
|
130
|
-
|
|
131
|
-
UNICODE BLOCKS
|
|
132
|
-
Basic Latin 95 / 128 74.2%
|
|
133
|
-
Latin-1 Supplement 96 / 128 75.0%
|
|
134
|
-
...
|
|
135
|
-
|
|
136
|
-
VARIABLE FONT
|
|
137
|
-
Axes: wght (100–900, default 400)
|
|
138
|
-
Named instances: 9
|
|
139
|
-
|
|
140
|
-
OPENTYPE LAYOUT
|
|
141
|
-
Scripts: latn, cyrl, grek, ...
|
|
142
|
-
Features: c2sc, calt, case, dlig, dnom, frac, kern, liga, ...
|
|
143
|
-
|
|
144
|
-
LANGUAGE COVERAGE
|
|
145
|
-
en: 99.2%
|
|
146
|
-
fr: 97.4%
|
|
147
|
-
...
|
|
148
|
-
```
|
|
149
|
-
|
|
150
|
-
## Collections
|
|
151
|
-
|
|
152
|
-
For TTC/OTC/dfont, one report is produced per face in source order.
|
|
153
|
-
|
|
154
|
-
```bash
|
|
155
|
-
# Audit every face
|
|
156
|
-
fontisan audit COLLECTION.ttc
|
|
157
|
-
|
|
158
|
-
# Audit only face 2
|
|
159
|
-
fontisan audit COLLECTION.ttc --font-index 2
|
|
160
|
-
|
|
161
|
-
# Write one file per face into a directory
|
|
162
|
-
fontisan audit COLLECTION.ttc -o reports/
|
|
163
|
-
|
|
164
|
-
# Resulting files use the postscript name with a 2-digit index prefix:
|
|
165
|
-
# 00-NotoSans-Regular.yaml
|
|
166
|
-
# 01-NotoSans-Bold.yaml
|
|
167
|
-
# 02-NotoSerif-Italic.yaml
|
|
168
|
-
```
|
|
169
|
-
|
|
170
|
-
## Compare Mode
|
|
171
|
-
|
|
172
|
-
`--compare` diffs two inputs. Each input is one of:
|
|
173
|
-
|
|
174
|
-
- A previously saved audit report (`.yaml` / `.yml` / `.json`)
|
|
175
|
-
- A font file (audited on-the-fly)
|
|
176
|
-
|
|
177
|
-
Mixed inputs are allowed — useful for tracking a font's evolution
|
|
178
|
-
against a checked-in baseline.
|
|
179
|
-
|
|
180
|
-
```bash
|
|
181
|
-
# Two live fonts
|
|
182
|
-
fontisan audit --compare a.ttf b.ttf
|
|
183
|
-
|
|
184
|
-
# Saved baseline vs. live
|
|
185
|
-
fontisan audit --compare baseline.yaml new.ttf
|
|
186
|
-
|
|
187
|
-
# Two saved reports
|
|
188
|
-
fontisan audit --compare v1.yaml v2.yaml -o diff.yaml
|
|
189
|
-
```
|
|
190
|
-
|
|
191
|
-
The output is an `AuditDiff` containing:
|
|
192
|
-
|
|
193
|
-
- `field_changes` — scalar field-level changes (e.g. weight_class 400 → 700)
|
|
194
|
-
- `codepoints` — added/removed/unchanged codepoint counts
|
|
195
|
-
- `added_features` / `removed_features`
|
|
196
|
-
- `added_scripts` / `removed_scripts`
|
|
197
|
-
- `added_blocks` / `removed_blocks`
|
|
198
|
-
- `added_languages` / `removed_languages`
|
|
199
|
-
|
|
200
|
-
## Library Mode
|
|
201
|
-
|
|
202
|
-
Point `audit` at a directory with `--recursive` and/or `--summary` to
|
|
203
|
-
scan a whole library of fonts.
|
|
204
|
-
|
|
205
|
-
```bash
|
|
206
|
-
# Flat directory
|
|
207
|
-
fontisan audit lib/ --summary
|
|
208
|
-
|
|
209
|
-
# Walk subdirectories
|
|
210
|
-
fontisan audit lib/ --recursive --summary
|
|
211
|
-
|
|
212
|
-
# YAML for downstream processing
|
|
213
|
-
fontisan audit lib/ --recursive --summary --format yaml -o library.yaml
|
|
214
|
-
```
|
|
215
|
-
|
|
216
|
-
The output is a `LibrarySummary` containing:
|
|
217
|
-
|
|
218
|
-
- `root_path`, `total_files`, `total_faces`, `scanned_extensions`
|
|
219
|
-
- `aggregate_metrics` — total codepoints/glyphs/bytes summed across faces
|
|
220
|
-
- `script_coverage` — per-script face counts and lists
|
|
221
|
-
- `duplicate_groups` — files grouped by `source_sha256` (size > 1)
|
|
222
|
-
- `license_distribution` — face counts per `license_url`
|
|
223
|
-
- `per_face_reports` — the full per-face reports used to aggregate
|
|
224
|
-
|
|
225
|
-
Files that fail to load (corrupt, unsupported) are listed on stderr as
|
|
226
|
-
`skipped <path>` and excluded from the summary.
|
|
227
|
-
|
|
228
|
-
## UCD Aggregation
|
|
229
|
-
|
|
230
|
-
By default, audit aggregates codepoints against the configured-default
|
|
231
|
-
UCD version, producing:
|
|
232
|
-
|
|
233
|
-
- `blocks` — per-Unicode-block coverage rows (name, range, total, covered, fill_ratio, complete)
|
|
234
|
-
- `unicode_scripts` — distinct scripts present in the font
|
|
235
|
-
|
|
236
|
-
Override with `--ucd-version`:
|
|
237
|
-
|
|
238
|
-
```bash
|
|
239
|
-
# Use a specific UCD version
|
|
240
|
-
fontisan audit FONT.ttf --ucd-version 16.0.0
|
|
241
|
-
|
|
242
|
-
# Probe and use the latest
|
|
243
|
-
fontisan audit FONT.ttf --ucd-version latest
|
|
244
|
-
```
|
|
245
|
-
|
|
246
|
-
Manage the local UCD cache with `fontisan ucd`:
|
|
247
|
-
|
|
248
|
-
```bash
|
|
249
|
-
fontisan ucd status
|
|
250
|
-
fontisan ucd list
|
|
251
|
-
fontisan ucd download 17.0.0
|
|
252
|
-
```
|
|
253
|
-
|
|
254
|
-
## CLDR Language Coverage
|
|
255
|
-
|
|
256
|
-
`--with-language-coverage` computes per-language coverage % using CLDR
|
|
257
|
-
exemplar sets. The first invocation downloads the CLDR data
|
|
258
|
-
(~MBs); subsequent invocations use the cache.
|
|
259
|
-
|
|
260
|
-
```bash
|
|
261
|
-
fontisan audit FONT.ttf --with-language-coverage
|
|
262
|
-
|
|
263
|
-
# Use a specific CLDR version
|
|
264
|
-
fontisan audit FONT.ttf --with-language-coverage --cldr-version 45
|
|
265
|
-
```
|
|
266
|
-
|
|
267
|
-
Manage the CLDR cache with `fontisan cldr`:
|
|
268
|
-
|
|
269
|
-
```bash
|
|
270
|
-
fontisan cldr status
|
|
271
|
-
fontisan cldr list
|
|
272
|
-
fontisan cldr download 45
|
|
273
|
-
```
|
|
274
|
-
|
|
275
|
-
## Ruby API
|
|
276
|
-
|
|
277
|
-
### Single face
|
|
278
|
-
|
|
279
|
-
```ruby
|
|
280
|
-
require "fontisan"
|
|
281
|
-
|
|
282
|
-
# Returns an AuditReport for a single font, or an Array<AuditReport>
|
|
283
|
-
# for a collection (one per face).
|
|
284
|
-
report = Fontisan::Commands::AuditCommand.new("font.ttf",
|
|
285
|
-
ucd_version: "17.0.0").run
|
|
286
|
-
|
|
287
|
-
puts report.family_name
|
|
288
|
-
puts report.total_codepoints
|
|
289
|
-
puts report.licensing.license_url
|
|
290
|
-
puts report.blocks.first.fill_ratio
|
|
291
|
-
```
|
|
292
|
-
|
|
293
|
-
### Compare
|
|
294
|
-
|
|
295
|
-
```ruby
|
|
296
|
-
diff = Fontisan::Commands::AuditCompareCommand.new(
|
|
297
|
-
"baseline.yaml", "new.ttf", ucd_version: "17.0.0"
|
|
298
|
-
).run
|
|
299
|
-
|
|
300
|
-
diff.field_changes.each { |c| puts "#{c.field}: #{c.left} → #{c.right}" }
|
|
301
|
-
puts "added codepoints: #{diff.codepoints.added_count}"
|
|
302
|
-
```
|
|
303
|
-
|
|
304
|
-
### Library summary
|
|
305
|
-
|
|
306
|
-
```ruby
|
|
307
|
-
cmd = Fontisan::Commands::AuditLibraryCommand.new(
|
|
308
|
-
"lib/", recursive: true, options: { ucd_version: "17.0.0" }
|
|
309
|
-
)
|
|
310
|
-
summary = cmd.run
|
|
311
|
-
cmd.skipped.each { |path| warn "skipped #{path}" }
|
|
312
|
-
|
|
313
|
-
summary.script_coverage.each do |row|
|
|
314
|
-
puts "#{row.script}: #{row.face_count} faces"
|
|
315
|
-
end
|
|
316
|
-
summary.duplicate_groups.each do |group|
|
|
317
|
-
puts "duplicate #{group.source_sha256[0,8]}: #{group.files.join(', ')}"
|
|
318
|
-
end
|
|
319
|
-
```
|
|
320
|
-
|
|
321
|
-
### Brief mode
|
|
322
|
-
|
|
323
|
-
```ruby
|
|
324
|
-
# Use the audit_brief: key (NOT brief:, which would trigger metadata-only
|
|
325
|
-
# font loading via BaseCommand).
|
|
326
|
-
report = Fontisan::Commands::AuditCommand.new(
|
|
327
|
-
"font.ttf", audit_brief: true, ucd_version: "17.0.0"
|
|
328
|
-
).run
|
|
329
|
-
```
|
|
330
|
-
|
|
331
|
-
## Related Commands
|
|
332
|
-
|
|
333
|
-
- [info](/cli/info) — Lighter-weight font metadata (replaces `otfinfo -i`)
|
|
334
|
-
- [tables](/cli/tables) — Raw OpenType table listing
|
|
335
|
-
- [features](/cli/features) — Just GSUB/GPOS features
|
|
336
|
-
- [scripts](/cli/scripts) — Just supported scripts
|
|
337
|
-
- [validate](/cli/validate) — Pass/fail quality checks (audit is descriptive; validate is prescriptive)
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Fontisan
|
|
4
|
-
module Cldr
|
|
5
|
-
# Produces audit-ready per-language coverage from a codepoint list
|
|
6
|
-
# and a Cldr::Index of per-language exemplar sets.
|
|
7
|
-
#
|
|
8
|
-
# Pure: no I/O, no side effects.
|
|
9
|
-
module Aggregator
|
|
10
|
-
module_function
|
|
11
|
-
|
|
12
|
-
# @param codepoints [Enumerable<Integer>] font's codepoints
|
|
13
|
-
# @param languages_index [Cldr::Index]
|
|
14
|
-
# @return [Array<Models::Cldr::LanguageCoverage>] sorted by
|
|
15
|
-
# descending coverage_ratio, then by language name
|
|
16
|
-
def aggregate(codepoints, languages_index)
|
|
17
|
-
font_set = Set.new(codepoints)
|
|
18
|
-
|
|
19
|
-
languages_index.entries.map do |lang, required_set|
|
|
20
|
-
covered = (font_set & required_set).size
|
|
21
|
-
total = required_set.size
|
|
22
|
-
Models::Cldr::LanguageCoverage.new(
|
|
23
|
-
language: lang,
|
|
24
|
-
covered: covered,
|
|
25
|
-
total: total,
|
|
26
|
-
coverage_ratio: total.zero? ? 0.0 : covered.fdiv(total).round(4),
|
|
27
|
-
fully_supported: total.positive? && covered == total,
|
|
28
|
-
)
|
|
29
|
-
end.sort_by { |lc| [lc.coverage_ratio * -1, lc.language] }
|
|
30
|
-
end
|
|
31
|
-
end
|
|
32
|
-
end
|
|
33
|
-
end
|
|
@@ -1,110 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "pathname"
|
|
4
|
-
|
|
5
|
-
module Fontisan
|
|
6
|
-
module Cldr
|
|
7
|
-
# Manages the on-disk CLDR cache layout.
|
|
8
|
-
#
|
|
9
|
-
# Cache root resolution honors `XDG_CONFIG_HOME` per the XDG Base
|
|
10
|
-
# Directory Specification. Falls back to `~/.config` on Unix.
|
|
11
|
-
#
|
|
12
|
-
# Layout:
|
|
13
|
-
#
|
|
14
|
-
# <root>/
|
|
15
|
-
# <version>/
|
|
16
|
-
# json/ # extracted CLDR JSON archive
|
|
17
|
-
# cldr-json/
|
|
18
|
-
# cldr-characters-full/
|
|
19
|
-
# main/<lang>/characters.json
|
|
20
|
-
# index/
|
|
21
|
-
# languages.yml # built index of per-language codepoint sets
|
|
22
|
-
#
|
|
23
|
-
# No network access — all methods are pure filesystem operations.
|
|
24
|
-
module CacheManager
|
|
25
|
-
LANGUAGES_INDEX_FILENAME = "languages.yml"
|
|
26
|
-
private_constant :LANGUAGES_INDEX_FILENAME
|
|
27
|
-
|
|
28
|
-
class << self
|
|
29
|
-
# Root path of the CLDR cache.
|
|
30
|
-
# @return [Pathname]
|
|
31
|
-
def root
|
|
32
|
-
base = xdg_config_home || File.join(Dir.home, ".config")
|
|
33
|
-
Pathname.new(base).join("fontisan", "cldr")
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
# Per-version directory.
|
|
37
|
-
# @param version [String] e.g. "46.0.0"
|
|
38
|
-
# @return [Pathname]
|
|
39
|
-
def version_dir(version)
|
|
40
|
-
root.join(version)
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
# Directory where the raw CLDR JSON archive is extracted.
|
|
44
|
-
# @param version [String]
|
|
45
|
-
# @return [Pathname]
|
|
46
|
-
def json_dir(version)
|
|
47
|
-
version_dir(version).join("json")
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
# Directory containing the per-language characters.json files
|
|
51
|
-
# inside the extracted archive.
|
|
52
|
-
# @param version [String]
|
|
53
|
-
# @return [Pathname]
|
|
54
|
-
def characters_main_dir(version)
|
|
55
|
-
json_dir(version).join("cldr-json", "cldr-characters-full", "main")
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
# Directory holding the derived language index for a version.
|
|
59
|
-
# @param version [String]
|
|
60
|
-
# @return [Pathname]
|
|
61
|
-
def index_dir(version)
|
|
62
|
-
version_dir(version).join("index")
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
def languages_index_path(version)
|
|
66
|
-
index_dir(version).join(LANGUAGES_INDEX_FILENAME)
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
# True if the extracted JSON archive is present for this version.
|
|
70
|
-
# @param version [String]
|
|
71
|
-
# @return [Boolean]
|
|
72
|
-
def cached?(version)
|
|
73
|
-
characters_main_dir(version).exist?
|
|
74
|
-
end
|
|
75
|
-
|
|
76
|
-
# All versions currently in the cache (sorted ascending).
|
|
77
|
-
# @return [Array<String>]
|
|
78
|
-
def cached_versions
|
|
79
|
-
return [] unless root.exist?
|
|
80
|
-
|
|
81
|
-
root.children.select(&:directory?).map { |p| p.basename.to_s }.sort
|
|
82
|
-
end
|
|
83
|
-
|
|
84
|
-
# Create the version directory and json/index subdirs.
|
|
85
|
-
# Idempotent.
|
|
86
|
-
# @param version [String]
|
|
87
|
-
def ensure_version_dir!(version)
|
|
88
|
-
json_dir(version).mkpath
|
|
89
|
-
index_dir(version).mkpath
|
|
90
|
-
end
|
|
91
|
-
|
|
92
|
-
# Remove a version from the cache. No-op if absent.
|
|
93
|
-
# @param version [String]
|
|
94
|
-
def remove_version(version)
|
|
95
|
-
dir = version_dir(version)
|
|
96
|
-
dir.rmtree if dir.exist?
|
|
97
|
-
end
|
|
98
|
-
|
|
99
|
-
private
|
|
100
|
-
|
|
101
|
-
def xdg_config_home
|
|
102
|
-
env = ENV["XDG_CONFIG_HOME"]
|
|
103
|
-
return nil if env.nil? || env.empty?
|
|
104
|
-
|
|
105
|
-
env
|
|
106
|
-
end
|
|
107
|
-
end
|
|
108
|
-
end
|
|
109
|
-
end
|
|
110
|
-
end
|
data/lib/fontisan/cldr/config.rb
DELETED
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "yaml"
|
|
4
|
-
|
|
5
|
-
module Fontisan
|
|
6
|
-
module Cldr
|
|
7
|
-
# Single source of truth for CLDR version selection.
|
|
8
|
-
#
|
|
9
|
-
# Wraps `lib/fontisan/config/cldr.yml`. Loads the YAML once at first
|
|
10
|
-
# access and memoizes. All other Cldr::* classes resolve versions,
|
|
11
|
-
# URLs, and known-version validation through this module.
|
|
12
|
-
module Config
|
|
13
|
-
CONFIG_PATH = File.expand_path("../config/cldr.yml", __dir__)
|
|
14
|
-
private_constant :CONFIG_PATH
|
|
15
|
-
|
|
16
|
-
class << self
|
|
17
|
-
# The CLDR version Fontisan uses by default for auto-download and
|
|
18
|
-
# `fontisan cldr download` (no args). String like "46.0.0".
|
|
19
|
-
def default_version
|
|
20
|
-
data[:default_version]
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
# Array of CLDR version strings this Fontisan release recognizes.
|
|
24
|
-
# Used by VersionResolver to reject unknown versions early.
|
|
25
|
-
def known_versions
|
|
26
|
-
data[:known_versions]
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
# Base URL for fetching CLDR JSON artifacts.
|
|
30
|
-
def base_url
|
|
31
|
-
data[:base_url]
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
# Listing URL for `--latest` probing.
|
|
35
|
-
def listing_url
|
|
36
|
-
data[:listing_url]
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
# Full URL to the CLDR JSON full archive for a version.
|
|
40
|
-
# @param version [String] e.g. "46.0.0"
|
|
41
|
-
# @return [String]
|
|
42
|
-
def archive_url_for(version)
|
|
43
|
-
"#{base_url}/#{version}/cldr-#{version}-json-full.zip"
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
# True if the version appears in `known_versions`.
|
|
47
|
-
def known?(version)
|
|
48
|
-
known_versions.include?(version)
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
private
|
|
52
|
-
|
|
53
|
-
def data
|
|
54
|
-
@data ||= YAML.load_file(CONFIG_PATH).transform_keys(&:to_sym)
|
|
55
|
-
end
|
|
56
|
-
end
|
|
57
|
-
end
|
|
58
|
-
end
|
|
59
|
-
end
|
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "net/http"
|
|
4
|
-
require "uri"
|
|
5
|
-
require "tempfile"
|
|
6
|
-
require "zip"
|
|
7
|
-
|
|
8
|
-
module Fontisan
|
|
9
|
-
module Cldr
|
|
10
|
-
# Fetches CLDR JSON archives from unicode-org/cldr-json GitHub releases
|
|
11
|
-
# and unpacks them into the cache.
|
|
12
|
-
#
|
|
13
|
-
# Single entry point: `Downloader.download(version, force:)`.
|
|
14
|
-
# Idempotent unless `force: true`. Returns the path to the extracted
|
|
15
|
-
# `main/` characters directory.
|
|
16
|
-
module Downloader
|
|
17
|
-
class << self
|
|
18
|
-
# Download and unpack CLDR JSON for `version`.
|
|
19
|
-
#
|
|
20
|
-
# @param version [String] e.g. "46.0.0"
|
|
21
|
-
# @param force [Boolean] if false and cache already has the
|
|
22
|
-
# extracted files, return without re-fetching.
|
|
23
|
-
# @return [Pathname] path to the extracted main/ characters dir
|
|
24
|
-
# @raise [DownloadError] on HTTP failure or zip extraction failure
|
|
25
|
-
def download(version, force: false)
|
|
26
|
-
target = CacheManager.characters_main_dir(version)
|
|
27
|
-
return target if target.exist? && !force
|
|
28
|
-
|
|
29
|
-
CacheManager.ensure_version_dir!(version)
|
|
30
|
-
zip_data = fetch_zip(version)
|
|
31
|
-
extract_archive(zip_data, CacheManager.json_dir(version))
|
|
32
|
-
target
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
private
|
|
36
|
-
|
|
37
|
-
def fetch_zip(version)
|
|
38
|
-
uri = URI(Config.archive_url_for(version))
|
|
39
|
-
response = Net::HTTP.get_response(uri)
|
|
40
|
-
unless response.is_a?(Net::HTTPSuccess)
|
|
41
|
-
raise DownloadError,
|
|
42
|
-
"GET #{uri} returned HTTP #{response.code}: #{response.message}"
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
body = response.body
|
|
46
|
-
if body.nil? || body.empty?
|
|
47
|
-
raise DownloadError, "GET #{uri} returned an empty body"
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
body
|
|
51
|
-
rescue StandardError => e
|
|
52
|
-
raise e if e.is_a?(DownloadError)
|
|
53
|
-
|
|
54
|
-
raise DownloadError, "Failed to fetch #{uri}: #{e.message}"
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
def extract_archive(zip_data, target_dir)
|
|
58
|
-
Tempfile.create(["fontisan-cldr", ".zip"]) do |tmp|
|
|
59
|
-
tmp.binmode
|
|
60
|
-
tmp.write(zip_data)
|
|
61
|
-
tmp.flush
|
|
62
|
-
tmp.rewind
|
|
63
|
-
|
|
64
|
-
target_dir.mkpath
|
|
65
|
-
Zip::File.open(tmp.path) do |zip|
|
|
66
|
-
zip.each do |entry|
|
|
67
|
-
next unless entry.file?
|
|
68
|
-
|
|
69
|
-
out = target_dir.join(entry.name)
|
|
70
|
-
out.dirname.mkpath
|
|
71
|
-
entry.extract(out.to_s) { true } # overwrite
|
|
72
|
-
end
|
|
73
|
-
end
|
|
74
|
-
end
|
|
75
|
-
end
|
|
76
|
-
end
|
|
77
|
-
end
|
|
78
|
-
end
|
|
79
|
-
end
|
data/lib/fontisan/cldr/error.rb
DELETED
data/lib/fontisan/cldr/index.rb
DELETED
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Fontisan
|
|
4
|
-
module Cldr
|
|
5
|
-
# In-memory per-language codepoint lookup.
|
|
6
|
-
#
|
|
7
|
-
# Loads a YAML index of `{language: [codepoint, ...]}`. Each language's
|
|
8
|
-
# codepoints are stored as a Set<Integer> for O(1) intersection checks.
|
|
9
|
-
#
|
|
10
|
-
# Used by {Cldr::Aggregator} to compute per-language coverage %.
|
|
11
|
-
class Index
|
|
12
|
-
include Enumerable
|
|
13
|
-
|
|
14
|
-
# @param entries [Hash{String=>Set<Integer>, Array<Integer>}]
|
|
15
|
-
def initialize(entries = {})
|
|
16
|
-
@entries = entries.transform_values do |cps|
|
|
17
|
-
cps.is_a?(Set) ? cps : Set.new(cps)
|
|
18
|
-
end
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
# @return [Hash{String=>Set<Integer>}]
|
|
22
|
-
attr_reader :entries
|
|
23
|
-
|
|
24
|
-
def each(&)
|
|
25
|
-
@entries.each(&)
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
def size
|
|
29
|
-
@entries.size
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
def languages
|
|
33
|
-
@entries.keys.sort
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
# @param language [String]
|
|
37
|
-
# @return [Set<Integer>, nil]
|
|
38
|
-
def lookup(language)
|
|
39
|
-
@entries[language]
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
def include?(language)
|
|
43
|
-
@entries.key?(language)
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
# Serialize to a YAML file.
|
|
47
|
-
# @param path [String, Pathname]
|
|
48
|
-
# @return [void]
|
|
49
|
-
def save(path)
|
|
50
|
-
File.open(path, "w") do |file|
|
|
51
|
-
YAML.dump(@entries.transform_values(&:sort), file)
|
|
52
|
-
end
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
# Load from a YAML file previously written by #save.
|
|
56
|
-
# @param path [String, Pathname]
|
|
57
|
-
# @return [Index]
|
|
58
|
-
def self.load(path)
|
|
59
|
-
hash = YAML.load_file(path)
|
|
60
|
-
new(hash)
|
|
61
|
-
end
|
|
62
|
-
end
|
|
63
|
-
end
|
|
64
|
-
end
|