fontist 3.0.28 → 3.0.30
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/README.adoc +148 -0
- data/fontist.gemspec +4 -3
- data/lib/fontist/cache/store.rb +13 -6
- data/lib/fontist/cli.rb +106 -6
- data/lib/fontist/css/google_family.rb +50 -0
- data/lib/fontist/css/local.rb +157 -0
- data/lib/fontist/css/styles.rb +79 -0
- data/lib/fontist/css.rb +110 -0
- data/lib/fontist/errors.rb +3 -0
- data/lib/fontist/font_file.rb +2 -1
- data/lib/fontist/font_finder.rb +15 -2
- data/lib/fontist/font_installer.rb +97 -94
- data/lib/fontist/font_operations/convert.rb +55 -0
- data/lib/fontist/font_operations/extract.rb +53 -0
- data/lib/fontist/font_operations/instance.rb +92 -0
- data/lib/fontist/font_operations/pack.rb +49 -0
- data/lib/fontist/font_operations/subset.rb +68 -0
- data/lib/fontist/font_operations.rb +25 -0
- data/lib/fontist/format_spec.rb +35 -0
- data/lib/fontist/formula.rb +33 -5
- data/lib/fontist/formula_picker.rb +27 -8
- data/lib/fontist/from_file_cache.rb +4 -0
- data/lib/fontist/helpers.rb +1 -1
- data/lib/fontist/import/convert_formulas.rb +1 -1
- data/lib/fontist/import/google/data_sources/base.rb +3 -3
- data/lib/fontist/import/google/data_sources/github.rb +28 -0
- data/lib/fontist/import/google/formula_builders/formula_builder_v5.rb +30 -57
- data/lib/fontist/import/google/models/font_family.rb +7 -4
- data/lib/fontist/import/google/variable_font_source.rb +105 -0
- data/lib/fontist/import/google.rb +1 -0
- data/lib/fontist/import/helpers/hash_helper.rb +1 -1
- data/lib/fontist/import/sil_importer.rb +18 -5
- data/lib/fontist/import/v4_to_v5_migrator.rb +1 -1
- data/lib/fontist/import/windows.rb +13 -2
- data/lib/fontist/import_cli.rb +19 -0
- data/lib/fontist/index_cli.rb +1 -1
- data/lib/fontist/indexes/index_mixin.rb +4 -4
- data/lib/fontist/json.rb +76 -0
- data/lib/fontist/manifest.rb +63 -0
- data/lib/fontist/resource.rb +6 -1
- data/lib/fontist/validate_cli.rb +1 -1
- data/lib/fontist/version.rb +1 -1
- data/lib/fontist/yaml_backend.rb +18 -5
- data/lib/fontist.rb +3 -0
- metadata +17 -19
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d858c57f16d09454000112d1047f120b1a752a76331e3788f65cb441fe7a668c
|
|
4
|
+
data.tar.gz: 60c5f0e33198e6b7b8ad24db123280a5fff7c4eaf8b56581d3ab8c9108114b44
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 581ebfd99f20a966cea6574294b4a3de879cdcea6051f916645e84ac9bd5e9ee16d4d0bfcbbd82f0291b6d6ae3e72dcf1c5524d02f6e1d67d3fbcfd880b03d73
|
|
7
|
+
data.tar.gz: 7188a89be063579e76ffd351c5ddd0c7296836a4c6162c1974ca54f79ca5cddd6134a71b3d48d04455fbc1ac8debbe1e6f2922d414a16aea2e85c83a62742fc2
|
data/README.adoc
CHANGED
|
@@ -311,11 +311,92 @@ Supported options:
|
|
|
311
311
|
`-S, [--size-limit=N]`:: Specify upper limit for file size of a formula to be installed
|
|
312
312
|
(default is 300 MB)
|
|
313
313
|
`-u, [--update-fontconfig]`:: Update Fontconfig
|
|
314
|
+
`--collection-index=N`, `--format=FORMAT`, `--fallback-to-original`,
|
|
315
|
+
`--subset-unicodes=LIST`, `--subset-text=TEXT`:: See <<Font operations>>
|
|
314
316
|
|
|
315
317
|
|
|
316
318
|
NOTE: The `install` command is similar to the `Font.install` library call.
|
|
317
319
|
|
|
318
320
|
|
|
321
|
+
=== Font operations
|
|
322
|
+
|
|
323
|
+
Fontist can transform fonts with the https://github.com/fontist/fontisan[Fontisan]
|
|
324
|
+
gem while installing, and bundle fonts into collections. Without any of these
|
|
325
|
+
options, `install` copies fonts unchanged.
|
|
326
|
+
|
|
327
|
+
==== Install one member of a TTC/OTC collection
|
|
328
|
+
|
|
329
|
+
[source,sh]
|
|
330
|
+
----
|
|
331
|
+
fontist install "Noto Sans Oriya" --collection-index 1
|
|
332
|
+
----
|
|
333
|
+
|
|
334
|
+
Installs member 1 (zero-based) as a standalone font. An index outside the
|
|
335
|
+
collection fails with `Collection index N out of range. Valid range: 0-M`.
|
|
336
|
+
|
|
337
|
+
==== Convert formats
|
|
338
|
+
|
|
339
|
+
[source,sh]
|
|
340
|
+
----
|
|
341
|
+
fontist install "Lato" --format woff2
|
|
342
|
+
----
|
|
343
|
+
|
|
344
|
+
Targets: `ttf`, `otf`, `woff`, `woff2`. If Fontisan cannot convert the font,
|
|
345
|
+
the install fails with an error. Pass `--fallback-to-original` to install the
|
|
346
|
+
original format instead.
|
|
347
|
+
|
|
348
|
+
==== Subset
|
|
349
|
+
|
|
350
|
+
[source,sh]
|
|
351
|
+
----
|
|
352
|
+
fontist install "Lato" --subset-unicodes "U+0020-007E,U+00E9"
|
|
353
|
+
fontist install "Lato" --subset-text "Hello"
|
|
354
|
+
----
|
|
355
|
+
|
|
356
|
+
Keeps only the glyphs for those characters. Options can be combined with
|
|
357
|
+
`--format`.
|
|
358
|
+
|
|
359
|
+
==== Bundle a collection
|
|
360
|
+
|
|
361
|
+
[source,sh]
|
|
362
|
+
----
|
|
363
|
+
fontist pack family.ttc Regular.ttf Bold.ttf
|
|
364
|
+
----
|
|
365
|
+
|
|
366
|
+
==== Ruby API
|
|
367
|
+
|
|
368
|
+
[source,ruby]
|
|
369
|
+
----
|
|
370
|
+
Fontist::FontOperations::Extract.call("family.ttc", index: 1, output_dir: "out")
|
|
371
|
+
Fontist::FontOperations::Pack.call("family.ttc", ["a.ttf", "b.ttf"])
|
|
372
|
+
Fontist::FontOperations::Convert.call("a.ttf", "woff2", "a.woff2")
|
|
373
|
+
Fontist::FontOperations::Subset.call("a.ttf", "sub.ttf", unicodes: "U+0041-005A")
|
|
374
|
+
|
|
375
|
+
Fontist::Font.install("Lato", format: "woff2", subset_text: "Hello")
|
|
376
|
+
----
|
|
377
|
+
|
|
378
|
+
==== Variable-font instancing
|
|
379
|
+
|
|
380
|
+
`--instance` installs a static instance of a variable font at the given axis
|
|
381
|
+
values. Tags and ranges are checked against the font's `fvar` table; an
|
|
382
|
+
unknown axis, an out-of-range value or a non-variable font is an error.
|
|
383
|
+
|
|
384
|
+
[source,sh]
|
|
385
|
+
----
|
|
386
|
+
fontist install "Mona Sans" --instance wght=300
|
|
387
|
+
fontist install "Mona Sans" --instance wght=300,wdth=90
|
|
388
|
+
----
|
|
389
|
+
|
|
390
|
+
[source,ruby]
|
|
391
|
+
----
|
|
392
|
+
Fontist::FontOperations::Instance.call("Font[wght].ttf", "light.ttf",
|
|
393
|
+
coordinates: { "wght" => 300 })
|
|
394
|
+
Fontist::Font.install("Mona Sans", instance: "wght=300")
|
|
395
|
+
----
|
|
396
|
+
|
|
397
|
+
NOTE: Instancing needs a Fontisan release that includes static instancing
|
|
398
|
+
(fontisan commit `b06fab3` or later; not in 0.4.46).
|
|
399
|
+
|
|
319
400
|
=== Install locations
|
|
320
401
|
|
|
321
402
|
==== General
|
|
@@ -1083,6 +1164,73 @@ Fontist::Fontconfig.remove(force: true) # do not fail if no config exists
|
|
|
1083
1164
|
----
|
|
1084
1165
|
|
|
1085
1166
|
|
|
1167
|
+
== Web fonts
|
|
1168
|
+
|
|
1169
|
+
=== Google Fonts CSS URL: `fontist css`
|
|
1170
|
+
|
|
1171
|
+
Prints the Google Fonts CSS2 URL for a Google family. It reads the formula
|
|
1172
|
+
only: no network access and no font installation.
|
|
1173
|
+
|
|
1174
|
+
[source,sh]
|
|
1175
|
+
----
|
|
1176
|
+
$ fontist css "Source Sans 3" --styles 300,300i,600,600i
|
|
1177
|
+
https://fonts.googleapis.com/css2?family=Source+Sans+3:ital,wght@0,300;0,600;1,300;1,600&display=swap
|
|
1178
|
+
|
|
1179
|
+
$ fontist css "Space Mono" --styles 400,700 --format link
|
|
1180
|
+
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Space+Mono:wght@400;700&display=swap">
|
|
1181
|
+
----
|
|
1182
|
+
|
|
1183
|
+
Options:
|
|
1184
|
+
|
|
1185
|
+
`--styles`:: Comma-separated weights from 100 to 900; add `i` for italic
|
|
1186
|
+
(`300,300i,600`). Weights are checked against the styles the formula lists
|
|
1187
|
+
(for example Thin, ExtraLight, Light, Regular, Medium, SemiBold, Bold,
|
|
1188
|
+
ExtraBold, Black). Without `--styles` the URL asks for the provider default.
|
|
1189
|
+
`--display`:: `auto`, `block`, `swap` (default), `fallback` or `optional`.
|
|
1190
|
+
`--format`:: `url` (default) or `link` (an HTML `<link>` tag with `&` escaped
|
|
1191
|
+
as `&`).
|
|
1192
|
+
|
|
1193
|
+
Only the URL (or tag) goes to stdout. Errors go to stderr with a nonzero
|
|
1194
|
+
exit status, for an unknown family, a family that is not served by Google
|
|
1195
|
+
Fonts, an unlisted style or an invalid `--display`.
|
|
1196
|
+
|
|
1197
|
+
The family name sent to Google is the formula's Google resource `family`; it
|
|
1198
|
+
is never derived from the formula name. So `Source Sans Pro` is not renamed to
|
|
1199
|
+
`Source Sans 3`: use the name the formula lists.
|
|
1200
|
+
|
|
1201
|
+
From Ruby:
|
|
1202
|
+
|
|
1203
|
+
[source,ruby]
|
|
1204
|
+
----
|
|
1205
|
+
Fontist::Css.url("Source Sans 3", styles: %w[300 300i 600 600i])
|
|
1206
|
+
Fontist::Css.link("Space Mono", styles: "400,700", display: "swap")
|
|
1207
|
+
----
|
|
1208
|
+
|
|
1209
|
+
=== Self-hosted CSS: `fontist css --local`
|
|
1210
|
+
|
|
1211
|
+
Builds a CSS bundle from fonts already installed by fontist: one file per
|
|
1212
|
+
style is copied into `--output-dir`, and `fonts.css` gets one `@font-face` per
|
|
1213
|
+
style (`font-family`, `src` with `format()`, `font-weight`, `font-style`).
|
|
1214
|
+
|
|
1215
|
+
[source,sh]
|
|
1216
|
+
----
|
|
1217
|
+
$ fontist install "Space Mono"
|
|
1218
|
+
$ fontist css "Space Mono" --local --output-dir public/fonts \
|
|
1219
|
+
--url-prefix /fonts --styles 400,700
|
|
1220
|
+
public/fonts/fonts.css
|
|
1221
|
+
----
|
|
1222
|
+
|
|
1223
|
+
`--url-prefix`:: Prefix for the font URLs in `fonts.css` (default: relative
|
|
1224
|
+
file names).
|
|
1225
|
+
`--font-format`:: `woff2`, `woff`, `ttf` or `otf`. Selects an installed file of
|
|
1226
|
+
that format; the file must already be installed (for example with
|
|
1227
|
+
`fontist install "Space Mono" --format woff2`). There is no silent fallback
|
|
1228
|
+
to another format.
|
|
1229
|
+
|
|
1230
|
+
Limits: only fonts installed in the fontist fonts directory are used, and no
|
|
1231
|
+
Google unicode-range subset files are produced. If the font is not installed
|
|
1232
|
+
the command tells you to run `fontist install`.
|
|
1233
|
+
|
|
1086
1234
|
== Platform-specific features
|
|
1087
1235
|
|
|
1088
1236
|
=== macOS supplementary fonts
|
data/fontist.gemspec
CHANGED
|
@@ -8,8 +8,10 @@ Gem::Specification.new do |spec|
|
|
|
8
8
|
spec.authors = ["Ribose Inc."]
|
|
9
9
|
spec.email = ["open.source@ribose.com"]
|
|
10
10
|
|
|
11
|
+
# rubocop:disable Layout/LineLength
|
|
11
12
|
spec.summary = "Install openly-licensed fonts on Windows, Linux and Mac!"
|
|
12
13
|
spec.description = "Install openly-licensed fonts on Windows, Linux and Mac!"
|
|
14
|
+
# rubocop:enable Layout/LineLength
|
|
13
15
|
spec.homepage = "https://github.com/fontist/fontist"
|
|
14
16
|
spec.license = "BSD-2-Clause"
|
|
15
17
|
|
|
@@ -31,14 +33,13 @@ Gem::Specification.new do |spec|
|
|
|
31
33
|
|
|
32
34
|
spec.add_dependency "down", "~> 5.0"
|
|
33
35
|
spec.add_dependency "excavate", "~> 1.0", ">= 1.0.3"
|
|
34
|
-
spec.add_dependency "fontisan", "~> 0.4", ">= 0.4.
|
|
36
|
+
spec.add_dependency "fontisan", "~> 0.4", ">= 0.4.47"
|
|
35
37
|
spec.add_dependency "fuzzy_match", "~> 2.1"
|
|
36
38
|
spec.add_dependency "git", "~> 6.0"
|
|
37
|
-
spec.add_dependency "json", "~> 2.0"
|
|
38
39
|
spec.add_dependency "lutaml-model", "~> 0.8.0"
|
|
39
40
|
spec.add_dependency "marcel", "~> 1.0"
|
|
40
41
|
spec.add_dependency "nokogiri", "~> 1.0"
|
|
41
|
-
spec.add_dependency "octokit", "~>
|
|
42
|
+
spec.add_dependency "octokit", "~> 10.0"
|
|
42
43
|
spec.add_dependency "paint", "~> 2.3"
|
|
43
44
|
spec.add_dependency "parallel", "~> 1.24"
|
|
44
45
|
spec.add_dependency "plist", "~> 3.0"
|
data/lib/fontist/cache/store.rb
CHANGED
|
@@ -40,7 +40,7 @@ module Fontist
|
|
|
40
40
|
ensure_cache_dir
|
|
41
41
|
entry = CacheEntry.new(value, ttl)
|
|
42
42
|
write_entry(key, entry)
|
|
43
|
-
rescue TypeError, JSON::
|
|
43
|
+
rescue TypeError, Fontist::JSON::DumpError
|
|
44
44
|
# Values outside the JSON type system (Time, Date, arbitrary
|
|
45
45
|
# objects) skip the write and read back as a miss
|
|
46
46
|
false
|
|
@@ -113,9 +113,9 @@ module Fontist
|
|
|
113
113
|
def read_entry(key)
|
|
114
114
|
return nil unless File.exist?(cache_path(key))
|
|
115
115
|
|
|
116
|
-
payload = JSON.parse(File.read(cache_path(key)))
|
|
116
|
+
payload = Fontist::JSON.parse(File.read(cache_path(key)))
|
|
117
117
|
CacheEntry.new(payload["value"], expires_at: payload["expires_at"])
|
|
118
|
-
rescue JSON::
|
|
118
|
+
rescue Fontist::JSON::ParseError, TypeError, EOFError
|
|
119
119
|
# Corrupted file - delete it and return nil. This can happen
|
|
120
120
|
# on Windows when a file is read while being written, or when
|
|
121
121
|
# cache files from previous runs are corrupted
|
|
@@ -137,9 +137,16 @@ module Fontist
|
|
|
137
137
|
end
|
|
138
138
|
|
|
139
139
|
def entry_payload(entry)
|
|
140
|
-
JSON.dump("version" => FORMAT_VERSION,
|
|
141
|
-
|
|
142
|
-
|
|
140
|
+
Fontist::JSON.dump("version" => FORMAT_VERSION,
|
|
141
|
+
"expires_at" => entry.expires_at,
|
|
142
|
+
"value" => entry.value)
|
|
143
|
+
rescue Fontist::JSON::DumpError
|
|
144
|
+
# Values outside the JSON type system (Time, Date, arbitrary
|
|
145
|
+
# objects) degrade to their string form, keeping the cache a
|
|
146
|
+
# best-effort store instead of a source of crashes
|
|
147
|
+
Fontist::JSON.dump("version" => FORMAT_VERSION,
|
|
148
|
+
"expires_at" => entry.expires_at,
|
|
149
|
+
"value" => entry.value.to_s)
|
|
143
150
|
end
|
|
144
151
|
end
|
|
145
152
|
end
|
data/lib/fontist/cli.rb
CHANGED
|
@@ -29,6 +29,10 @@ module Fontist
|
|
|
29
29
|
ERROR_TO_STATUS = {
|
|
30
30
|
Fontist::Errors::UnsupportedFontError => [STATUS_NON_SUPPORTED_FONT_ERROR],
|
|
31
31
|
Fontist::Errors::MissingFontError => [STATUS_MISSING_FONT_ERROR],
|
|
32
|
+
Fontist::FontOperations::Error => [STATUS_UNKNOWN_ERROR],
|
|
33
|
+
Fontist::Errors::CollectionIndexError => [STATUS_UNKNOWN_ERROR],
|
|
34
|
+
Fontist::Errors::UnsupportedTranscodeError => [STATUS_UNKNOWN_ERROR],
|
|
35
|
+
Fontist::Errors::SourceNotFoundError => [STATUS_UNKNOWN_ERROR],
|
|
32
36
|
Fontist::Errors::SizeLimitError => [
|
|
33
37
|
STATUS_SIZE_LIMIT_ERROR,
|
|
34
38
|
:append,
|
|
@@ -119,8 +123,12 @@ module Fontist
|
|
|
119
123
|
# Format selection options
|
|
120
124
|
option :format,
|
|
121
125
|
type: :string,
|
|
122
|
-
desc: "Font format to install (ttf, otf, woff, woff2
|
|
123
|
-
"If format not available, will
|
|
126
|
+
desc: "Font format to install (ttf, otf, woff, woff2). " \
|
|
127
|
+
"If format not available, will convert via Fontisan " \
|
|
128
|
+
"(fails with an error unless --fallback-to-original)."
|
|
129
|
+
option :fallback_to_original,
|
|
130
|
+
type: :boolean,
|
|
131
|
+
desc: "If conversion fails, install the original format instead"
|
|
124
132
|
option :variable_axes,
|
|
125
133
|
type: :string,
|
|
126
134
|
desc: "Variable axes to match (comma-separated, e.g., 'wght,wdth')"
|
|
@@ -140,7 +148,20 @@ module Fontist
|
|
|
140
148
|
# Collection options
|
|
141
149
|
option :collection_index,
|
|
142
150
|
type: :numeric,
|
|
143
|
-
desc: "
|
|
151
|
+
desc: "Install only this member of a TTC/OTC collection " \
|
|
152
|
+
"as a standalone font (0-indexed)"
|
|
153
|
+
# Subsetting options
|
|
154
|
+
option :subset_unicodes,
|
|
155
|
+
type: :string,
|
|
156
|
+
desc: "Keep only these codepoints (e.g., 'U+0020-007E,U+00E9')"
|
|
157
|
+
option :subset_text,
|
|
158
|
+
type: :string,
|
|
159
|
+
desc: "Keep only the glyphs for these characters"
|
|
160
|
+
# Variable-font instancing
|
|
161
|
+
option :instance,
|
|
162
|
+
type: :string,
|
|
163
|
+
desc: "Install a static instance of a variable font " \
|
|
164
|
+
"(e.g., 'wght=300,wdth=90')"
|
|
144
165
|
def install(*fonts)
|
|
145
166
|
handle_class_options(options)
|
|
146
167
|
|
|
@@ -205,6 +226,18 @@ module Fontist
|
|
|
205
226
|
handle_error(e)
|
|
206
227
|
end
|
|
207
228
|
|
|
229
|
+
desc "pack OUTPUT FONT_FILE...",
|
|
230
|
+
"Bundle two or more font files into a TTC/OTC collection"
|
|
231
|
+
def pack(output, *files)
|
|
232
|
+
handle_class_options(options)
|
|
233
|
+
|
|
234
|
+
Fontist::FontOperations::Pack.call(output, files)
|
|
235
|
+
Fontist.ui.success("Collection written to #{output}")
|
|
236
|
+
STATUS_SUCCESS
|
|
237
|
+
rescue Fontist::Errors::GeneralError => e
|
|
238
|
+
handle_error(e)
|
|
239
|
+
end
|
|
240
|
+
|
|
208
241
|
desc "uninstall/remove FONT", "Uninstall font by font or formula"
|
|
209
242
|
def uninstall(font)
|
|
210
243
|
handle_class_options(options)
|
|
@@ -270,14 +303,16 @@ module Fontist
|
|
|
270
303
|
finder.by_axes(axes)
|
|
271
304
|
elsif options[:category]
|
|
272
305
|
finder.by_category(options[:category])
|
|
306
|
+
elsif options[:format]
|
|
307
|
+
finder.by_format(options[:format])
|
|
273
308
|
else
|
|
274
|
-
error("Please specify --axes, --variable,
|
|
275
|
-
STATUS_UNKNOWN_ERROR)
|
|
309
|
+
error("Please specify --axes, --variable, --category " \
|
|
310
|
+
"or --format", STATUS_UNKNOWN_ERROR)
|
|
276
311
|
return
|
|
277
312
|
end
|
|
278
313
|
|
|
279
314
|
if options[:json]
|
|
280
|
-
Fontist.ui.say(JSON.
|
|
315
|
+
Fontist.ui.say(Fontist::JSON.pretty(results.map(&:to_h)))
|
|
281
316
|
else
|
|
282
317
|
print_find_results(results)
|
|
283
318
|
end
|
|
@@ -287,6 +322,39 @@ module Fontist
|
|
|
287
322
|
handle_error(e)
|
|
288
323
|
end
|
|
289
324
|
|
|
325
|
+
desc "css FAMILY", "Print a Google Fonts CSS URL, or build local CSS"
|
|
326
|
+
long_desc <<-LONGDESC
|
|
327
|
+
Prints the Google Fonts CSS2 URL for FAMILY (no network, no install).
|
|
328
|
+
With --local, copies installed font files into --output-dir and writes
|
|
329
|
+
fonts.css there.
|
|
330
|
+
LONGDESC
|
|
331
|
+
option :styles, type: :string,
|
|
332
|
+
desc: "Comma-separated weights, 'i' for italic " \
|
|
333
|
+
"(e.g., 300,300i,600,600i)"
|
|
334
|
+
option :display, type: :string, default: "swap",
|
|
335
|
+
desc: "font-display: auto, block, swap, fallback, optional"
|
|
336
|
+
option :format, type: :string, default: "url",
|
|
337
|
+
desc: "Output: url or link (HTML <link> tag)"
|
|
338
|
+
option :local, type: :boolean, desc: "Build CSS from installed fonts"
|
|
339
|
+
option :output_dir, type: :string, desc: "Directory for --local output"
|
|
340
|
+
option :url_prefix, type: :string,
|
|
341
|
+
desc: "URL prefix of font files in --local fonts.css"
|
|
342
|
+
option :font_format, type: :string,
|
|
343
|
+
desc: "Font file format for --local (woff2, woff, " \
|
|
344
|
+
"ttf, otf)"
|
|
345
|
+
def css(family)
|
|
346
|
+
handle_class_options(options)
|
|
347
|
+
return css_local(family) if options[:local]
|
|
348
|
+
|
|
349
|
+
css_google(family)
|
|
350
|
+
rescue Fontist::Css::UnknownFamilyError
|
|
351
|
+
css_error($!, STATUS_NON_SUPPORTED_FONT_ERROR)
|
|
352
|
+
rescue Fontist::Errors::MissingFontError
|
|
353
|
+
css_error($!, STATUS_MISSING_FONT_ERROR)
|
|
354
|
+
rescue Fontist::Css::Error
|
|
355
|
+
css_error($!, STATUS_UNKNOWN_ERROR)
|
|
356
|
+
end
|
|
357
|
+
|
|
290
358
|
desc "update", "Update formulas"
|
|
291
359
|
def update
|
|
292
360
|
handle_class_options(options)
|
|
@@ -414,6 +482,38 @@ module Fontist
|
|
|
414
482
|
STATUS_SUCCESS
|
|
415
483
|
end
|
|
416
484
|
|
|
485
|
+
def css_google(family)
|
|
486
|
+
kind = options[:format]
|
|
487
|
+
unless %w[url link].include?(kind)
|
|
488
|
+
raise Fontist::Css::InvalidOptionError,
|
|
489
|
+
"Invalid format '#{kind}'. Use url or link."
|
|
490
|
+
end
|
|
491
|
+
|
|
492
|
+
$stdout.puts Fontist::Css.public_send(
|
|
493
|
+
kind, family, styles: options[:styles], display: options[:display]
|
|
494
|
+
)
|
|
495
|
+
success
|
|
496
|
+
end
|
|
497
|
+
|
|
498
|
+
def css_local(family)
|
|
499
|
+
unless options[:output_dir]
|
|
500
|
+
raise Fontist::Css::InvalidOptionError, "--local requires --output-dir."
|
|
501
|
+
end
|
|
502
|
+
|
|
503
|
+
path = Fontist::Css.local(family, output_dir: options[:output_dir],
|
|
504
|
+
url_prefix: options[:url_prefix],
|
|
505
|
+
styles: options[:styles],
|
|
506
|
+
font_format: options[:font_format])
|
|
507
|
+
$stdout.puts path
|
|
508
|
+
success
|
|
509
|
+
end
|
|
510
|
+
|
|
511
|
+
# Errors go to stderr so stdout carries only the result.
|
|
512
|
+
def css_error(exception, status)
|
|
513
|
+
warn exception.message
|
|
514
|
+
status
|
|
515
|
+
end
|
|
516
|
+
|
|
417
517
|
def handle_error(exception)
|
|
418
518
|
status, mode, message = ERROR_TO_STATUS[exception.class]
|
|
419
519
|
raise exception unless status
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
module Fontist
|
|
2
|
+
module Css
|
|
3
|
+
# Finds the Google formula for a family name and its canonical
|
|
4
|
+
# Google Fonts family (Resource#family; never derived from a slug).
|
|
5
|
+
module GoogleFamily
|
|
6
|
+
class << self
|
|
7
|
+
# @return [Array(Formula, String)] formula and canonical family
|
|
8
|
+
def resolve(family)
|
|
9
|
+
name = family.to_s.strip
|
|
10
|
+
matches = google_matches(candidates(name), name)
|
|
11
|
+
raise NotGoogleFamilyError, name if matches.empty?
|
|
12
|
+
|
|
13
|
+
keys = matches.map { |formula, _| formula.key }
|
|
14
|
+
raise AmbiguousFamilyError.new(name, keys) if keys.uniq.size > 1
|
|
15
|
+
|
|
16
|
+
matches.first
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def candidates(name)
|
|
22
|
+
formulas = name.empty? ? [] : Formula.find_many(name)
|
|
23
|
+
raise UnknownFamilyError, name if formulas.empty?
|
|
24
|
+
|
|
25
|
+
formulas
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def google_matches(formulas, name)
|
|
29
|
+
formulas.filter_map do |formula|
|
|
30
|
+
canonical = canonical_family(formula, name)
|
|
31
|
+
[formula, canonical] if canonical
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def canonical_family(formula, name)
|
|
36
|
+
wanted = normalize(name)
|
|
37
|
+
Array(formula.resources).each do |resource|
|
|
38
|
+
next unless resource.source == "google"
|
|
39
|
+
return resource.family if normalize(resource.family) == wanted
|
|
40
|
+
end
|
|
41
|
+
nil
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def normalize(text)
|
|
45
|
+
text.to_s.downcase.gsub(/[\s_-]+/, " ")
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
module Fontist
|
|
2
|
+
module Css
|
|
3
|
+
# Builds a self-hosted CSS bundle from fonts already installed by
|
|
4
|
+
# fontist: one copied file per style plus fonts.css with one @font-face
|
|
5
|
+
# per style. No downloads, installs or format conversion happen here.
|
|
6
|
+
class Local
|
|
7
|
+
CSS_FORMATS = {
|
|
8
|
+
"woff2" => "woff2",
|
|
9
|
+
"woff" => "woff",
|
|
10
|
+
"ttf" => "truetype",
|
|
11
|
+
"otf" => "opentype",
|
|
12
|
+
}.freeze
|
|
13
|
+
|
|
14
|
+
def initialize(family, output_dir:, url_prefix: nil, styles: nil,
|
|
15
|
+
font_format: nil)
|
|
16
|
+
@family = family.to_s.strip
|
|
17
|
+
@output_dir = output_dir.to_s
|
|
18
|
+
@url_prefix = url_prefix.to_s
|
|
19
|
+
@styles = styles
|
|
20
|
+
@font_format = font_format&.to_s&.downcase
|
|
21
|
+
validate_font_format!
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# @return [String] path of the written fonts.css
|
|
25
|
+
def call
|
|
26
|
+
raise UnknownFamilyError, @family if @family.empty?
|
|
27
|
+
|
|
28
|
+
entries = installed_entries
|
|
29
|
+
FileUtils.mkdir_p(@output_dir)
|
|
30
|
+
entries.each { |e| write_font(e) }
|
|
31
|
+
css_path = File.join(@output_dir, "fonts.css")
|
|
32
|
+
File.write(css_path, entries.map { |e| font_face(e) }.join("\n"))
|
|
33
|
+
css_path
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def validate_font_format!
|
|
39
|
+
return if @font_format.nil? || CSS_FORMATS.key?(@font_format)
|
|
40
|
+
|
|
41
|
+
raise InvalidOptionError,
|
|
42
|
+
"Unsupported font format '#{@font_format}'. " \
|
|
43
|
+
"Use one of: #{CSS_FORMATS.keys.join(', ')}."
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def installed_entries
|
|
47
|
+
formulas = Formula.find_many(@family)
|
|
48
|
+
raise UnknownFamilyError, @family if formulas.empty?
|
|
49
|
+
|
|
50
|
+
faces = formulas.filter_map { |formula| selected_faces(formula) }
|
|
51
|
+
raise UnknownFamilyError, @family if faces.empty?
|
|
52
|
+
|
|
53
|
+
faces.each do |candidate|
|
|
54
|
+
entries = locate(candidate)
|
|
55
|
+
return entries unless entries.empty?
|
|
56
|
+
end
|
|
57
|
+
raise Errors::MissingFontError, @family
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Faces of this family in the formula, narrowed to --styles.
|
|
61
|
+
def selected_faces(formula)
|
|
62
|
+
faces = Styles.faces(formula).select { |f| family?(f.style) }
|
|
63
|
+
return if faces.empty?
|
|
64
|
+
|
|
65
|
+
requested = Styles.parse(@styles)
|
|
66
|
+
return faces if requested.empty?
|
|
67
|
+
|
|
68
|
+
Styles.verify(@family, faces, requested)
|
|
69
|
+
faces.select { |f| requested.include?(f.tuple) }
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def family?(style)
|
|
73
|
+
name = style.preferred_family_name || style.family_name
|
|
74
|
+
name.to_s.casecmp?(@family)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def locate(faces)
|
|
78
|
+
files = installed_files
|
|
79
|
+
faces.filter_map { |face| entry_for(face, files) }
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# The installed file in the requested format, or the installed original
|
|
83
|
+
# to convert into it.
|
|
84
|
+
def entry_for(face, files)
|
|
85
|
+
path = files[wanted_name(face.style.font)]
|
|
86
|
+
return { path: path, face: face, ext: extension(path) } if path
|
|
87
|
+
return unless @font_format
|
|
88
|
+
|
|
89
|
+
original = files[File.basename(face.style.font.to_s).downcase]
|
|
90
|
+
return unless original
|
|
91
|
+
|
|
92
|
+
{ path: original, face: face, ext: @font_format, convert: true }
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def extension(path)
|
|
96
|
+
File.extname(path).delete(".")
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def write_font(entry)
|
|
100
|
+
if entry[:convert]
|
|
101
|
+
FontOperations::Convert.call(entry[:path], @font_format,
|
|
102
|
+
target(entry))
|
|
103
|
+
else
|
|
104
|
+
FileUtils.cp(entry[:path], target(entry))
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# A file name keyed by lowercase basename, so extension swaps match.
|
|
109
|
+
def wanted_name(font)
|
|
110
|
+
stem = File.basename(font.to_s, ".*")
|
|
111
|
+
ext = @font_format || File.extname(font.to_s).delete(".")
|
|
112
|
+
"#{stem}.#{ext}".downcase
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def installed_files
|
|
116
|
+
patterns = Utils.font_file_patterns(Fontist.fonts_path.join("**").to_s)
|
|
117
|
+
patterns.flat_map { |p| Dir.glob(p) }.sort
|
|
118
|
+
.to_h { |path| [File.basename(path).downcase, path] }
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def target(entry)
|
|
122
|
+
name = "#{File.basename(entry[:path], '.*')}.#{entry[:ext]}"
|
|
123
|
+
File.join(@output_dir, name)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def font_face(entry)
|
|
127
|
+
name = File.basename(target(entry))
|
|
128
|
+
face = entry[:face]
|
|
129
|
+
<<~CSS
|
|
130
|
+
@font-face {
|
|
131
|
+
font-family: "#{escape(@family)}";
|
|
132
|
+
src: url("#{escape(url_for(name))}") format("#{css_format(entry)}");
|
|
133
|
+
font-weight: #{face.weight};
|
|
134
|
+
font-style: #{face.italic ? 'italic' : 'normal'};
|
|
135
|
+
}
|
|
136
|
+
CSS
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def url_for(name)
|
|
140
|
+
encoded = CGI.escape(name).gsub("+", "%20")
|
|
141
|
+
prefix = @url_prefix.chomp("/")
|
|
142
|
+
prefix.empty? ? encoded : "#{prefix}/#{encoded}"
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def css_format(entry)
|
|
146
|
+
CSS_FORMATS.fetch(entry[:ext].downcase) do
|
|
147
|
+
raise InvalidOptionError,
|
|
148
|
+
"Cannot describe font file format '#{entry[:ext]}' in CSS."
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def escape(text)
|
|
153
|
+
text.to_s.gsub(/[\\"]/) { |c| "\\#{c}" }.gsub(/\r?\n/, "\\A ")
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|