fontisan 0.2.17 → 0.2.23
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/.rubocop.yml +6 -0
- data/.rubocop_todo.yml +86 -86
- data/CHANGELOG.md +12 -2
- data/README.adoc +53 -1
- data/docs/.vitepress/config.ts +68 -8
- data/docs/.vitepress/theme/style.css +570 -272
- data/docs/CONVERSION_GUIDE.adoc +31 -8
- data/docs/EXTRACT_TTC_MIGRATION.md +1 -1
- data/docs/WOFF_WOFF2_FORMATS.adoc +53 -0
- data/docs/api/conversion-options.md +37 -14
- data/docs/cli/audit.md +337 -0
- data/docs/cli/convert.md +20 -1
- data/docs/cli/index.md +31 -0
- data/docs/guide/color.md +1 -1
- data/docs/guide/conversion/options.md +32 -3
- data/docs/guide/conversion/ttf-otf.md +1 -1
- data/docs/guide/conversion/type1.md +1 -1
- data/docs/guide/conversion/web.md +91 -32
- data/docs/guide/conversion.md +6 -5
- data/docs/guide/formats/woff.md +35 -11
- data/docs/guide/index.md +2 -2
- data/docs/guide/migrations/extract-ttc.md +1 -1
- data/docs/guide/quick-start.md +4 -4
- data/docs/guide/type1.md +4 -4
- data/docs/guide/woff.md +19 -17
- data/docs/index.md +2 -0
- data/docs/lychee.toml +5 -1
- data/docs/package.json +1 -1
- data/docs/public/robots.txt +4 -0
- data/docs/scripts/post-build.mjs +81 -0
- data/lib/fontisan/base_collection.rb +23 -9
- data/lib/fontisan/binary/structures.rb +0 -2
- data/lib/fontisan/binary.rb +11 -0
- data/lib/fontisan/cldr/aggregator.rb +33 -0
- data/lib/fontisan/cldr/cache_manager.rb +110 -0
- data/lib/fontisan/cldr/config.rb +59 -0
- data/lib/fontisan/cldr/download_error.rb +9 -0
- data/lib/fontisan/cldr/downloader.rb +79 -0
- data/lib/fontisan/cldr/error.rb +8 -0
- data/lib/fontisan/cldr/index.rb +64 -0
- data/lib/fontisan/cldr/index_builder.rb +72 -0
- data/lib/fontisan/cldr/unicode_set_parser.rb +189 -0
- data/lib/fontisan/cldr/unknown_version_error.rb +9 -0
- data/lib/fontisan/cldr/version_resolver.rb +91 -0
- data/lib/fontisan/cldr.rb +23 -0
- data/lib/fontisan/cli/cldr_cli.rb +85 -0
- data/lib/fontisan/cli.rb +31 -2
- data/lib/fontisan/collection/builder.rb +0 -4
- data/lib/fontisan/collection/dfont_builder.rb +0 -4
- data/lib/fontisan/collection/shared_logic.rb +0 -2
- data/lib/fontisan/collection/writer.rb +0 -3
- data/lib/fontisan/collection.rb +15 -0
- data/lib/fontisan/commands/base_command.rb +0 -3
- data/lib/fontisan/commands/convert_command.rb +25 -20
- data/lib/fontisan/commands/dump_table_command.rb +0 -3
- data/lib/fontisan/commands/export_command.rb +0 -4
- data/lib/fontisan/commands/features_command.rb +0 -3
- data/lib/fontisan/commands/instance_command.rb +0 -5
- data/lib/fontisan/commands/ls_command.rb +0 -6
- data/lib/fontisan/commands/optical_size_command.rb +0 -3
- data/lib/fontisan/commands/pack_command.rb +0 -5
- data/lib/fontisan/commands/scripts_command.rb +0 -2
- data/lib/fontisan/commands/subset_command.rb +0 -3
- data/lib/fontisan/commands/unicode_command.rb +0 -3
- data/lib/fontisan/commands/unpack_command.rb +0 -7
- data/lib/fontisan/commands/validate_command.rb +0 -8
- data/lib/fontisan/commands/variable_command.rb +0 -3
- data/lib/fontisan/commands.rb +26 -0
- data/lib/fontisan/config/cldr.yml +22 -0
- data/lib/fontisan/config/conversion_matrix.yml +38 -0
- data/lib/fontisan/constants.rb +19 -0
- data/lib/fontisan/conversion_options.rb +30 -19
- data/lib/fontisan/converters/cff_table_builder.rb +0 -3
- data/lib/fontisan/converters/collection_converter.rb +0 -8
- data/lib/fontisan/converters/conversion_strategy.rb +161 -46
- data/lib/fontisan/converters/format_converter.rb +143 -32
- data/lib/fontisan/converters/glyf_table_builder.rb +0 -2
- data/lib/fontisan/converters/outline_converter.rb +0 -19
- data/lib/fontisan/converters/outline_extraction.rb +0 -5
- data/lib/fontisan/converters/outline_optimizer.rb +0 -5
- data/lib/fontisan/converters/svg_generator.rb +0 -4
- data/lib/fontisan/converters/table_copier.rb +0 -2
- data/lib/fontisan/converters/type1_converter.rb +0 -11
- data/lib/fontisan/converters/woff2_encoder.rb +49 -20
- data/lib/fontisan/converters/woff_writer.rb +211 -282
- data/lib/fontisan/converters.rb +21 -0
- data/lib/fontisan/dfont_collection.rb +29 -10
- data/lib/fontisan/export/exporter.rb +0 -6
- data/lib/fontisan/export/transformers/font_to_ttx.rb +0 -9
- data/lib/fontisan/export/transformers/head_transformer.rb +0 -2
- data/lib/fontisan/export/transformers/hhea_transformer.rb +0 -2
- data/lib/fontisan/export/transformers/maxp_transformer.rb +0 -2
- data/lib/fontisan/export/transformers/name_transformer.rb +0 -2
- data/lib/fontisan/export/transformers/os2_transformer.rb +0 -2
- data/lib/fontisan/export/transformers/post_transformer.rb +0 -2
- data/lib/fontisan/export/transformers.rb +17 -0
- data/lib/fontisan/export.rb +13 -0
- data/lib/fontisan/font_loader.rb +14 -19
- data/lib/fontisan/font_writer.rb +0 -1
- data/lib/fontisan/formatters.rb +9 -0
- data/lib/fontisan/hints/hint_converter.rb +0 -1
- data/lib/fontisan/hints/postscript_hint_applier.rb +0 -9
- data/lib/fontisan/hints/postscript_hint_extractor.rb +0 -2
- data/lib/fontisan/hints/truetype_hint_extractor.rb +0 -2
- data/lib/fontisan/hints.rb +19 -0
- data/lib/fontisan/metrics_calculator.rb +0 -2
- data/lib/fontisan/models/all_scripts_features_info.rb +0 -1
- data/lib/fontisan/models/cldr/language_coverage.rb +31 -0
- data/lib/fontisan/models/cldr.rb +12 -0
- data/lib/fontisan/models/collection_brief_info.rb +0 -1
- data/lib/fontisan/models/collection_info.rb +0 -2
- data/lib/fontisan/models/collection_list_info.rb +0 -1
- data/lib/fontisan/models/collection_validation_report.rb +0 -2
- data/lib/fontisan/models/color_glyph.rb +0 -1
- data/lib/fontisan/models/font_report.rb +0 -1
- data/lib/fontisan/models/ttx/tables.rb +21 -0
- data/lib/fontisan/models/ttx/ttfont.rb +0 -8
- data/lib/fontisan/models/ttx.rb +14 -0
- data/lib/fontisan/models.rb +47 -0
- data/lib/fontisan/open_type_collection.rb +6 -5
- data/lib/fontisan/open_type_font.rb +8 -2
- data/lib/fontisan/open_type_font_extensions.rb +9 -9
- data/lib/fontisan/optimizers/pattern_analyzer.rb +0 -1
- data/lib/fontisan/optimizers.rb +14 -0
- data/lib/fontisan/outline_extractor.rb +0 -2
- data/lib/fontisan/parsers/dfont_parser.rb +0 -1
- data/lib/fontisan/parsers.rb +10 -0
- data/lib/fontisan/pipeline/format_detector.rb +29 -102
- data/lib/fontisan/pipeline/output_writer.rb +11 -9
- data/lib/fontisan/pipeline/strategies/instance_strategy.rb +0 -4
- data/lib/fontisan/pipeline/strategies/named_strategy.rb +0 -4
- data/lib/fontisan/pipeline/strategies/preserve_strategy.rb +0 -2
- data/lib/fontisan/pipeline/strategies.rb +16 -0
- data/lib/fontisan/pipeline/transformation_pipeline.rb +0 -7
- data/lib/fontisan/pipeline/variation_resolver.rb +0 -7
- data/lib/fontisan/pipeline.rb +14 -0
- data/lib/fontisan/sfnt_font.rb +29 -14
- data/lib/fontisan/sfnt_table.rb +0 -4
- data/lib/fontisan/subset/builder.rb +0 -6
- data/lib/fontisan/subset.rb +13 -0
- data/lib/fontisan/svg/font_generator.rb +0 -4
- data/lib/fontisan/svg/glyph_generator.rb +0 -2
- data/lib/fontisan/svg.rb +12 -0
- data/lib/fontisan/tables/cbdt.rb +0 -1
- data/lib/fontisan/tables/cblc.rb +0 -1
- data/lib/fontisan/tables/cff/charset.rb +0 -1
- data/lib/fontisan/tables/cff/charstring.rb +0 -1
- data/lib/fontisan/tables/cff/charstring_rebuilder.rb +0 -4
- data/lib/fontisan/tables/cff/charstrings_index.rb +0 -3
- data/lib/fontisan/tables/cff/dict.rb +0 -1
- data/lib/fontisan/tables/cff/encoding.rb +0 -1
- data/lib/fontisan/tables/cff/header.rb +0 -2
- data/lib/fontisan/tables/cff/hint_operation_injector.rb +0 -2
- data/lib/fontisan/tables/cff/index.rb +0 -1
- data/lib/fontisan/tables/cff/private_dict.rb +0 -2
- data/lib/fontisan/tables/cff/private_dict_writer.rb +0 -2
- data/lib/fontisan/tables/cff/table_builder.rb +0 -6
- data/lib/fontisan/tables/cff/top_dict.rb +0 -2
- data/lib/fontisan/tables/cff.rb +23 -15
- data/lib/fontisan/tables/cff2/charstring_parser.rb +0 -2
- data/lib/fontisan/tables/cff2/table_builder.rb +0 -11
- data/lib/fontisan/tables/cff2/table_reader.rb +0 -2
- data/lib/fontisan/tables/cff2.rb +13 -14
- data/lib/fontisan/tables/cmap.rb +24 -2
- data/lib/fontisan/tables/cmap_table.rb +0 -3
- data/lib/fontisan/tables/colr.rb +0 -1
- data/lib/fontisan/tables/cpal.rb +0 -1
- data/lib/fontisan/tables/cvar.rb +0 -2
- data/lib/fontisan/tables/fvar.rb +0 -1
- data/lib/fontisan/tables/glyf/compound_glyph_resolver.rb +0 -2
- data/lib/fontisan/tables/glyf/glyph_builder.rb +0 -3
- data/lib/fontisan/tables/glyf.rb +0 -6
- data/lib/fontisan/tables/glyf_table.rb +0 -3
- data/lib/fontisan/tables/gpos.rb +0 -2
- data/lib/fontisan/tables/gsub.rb +0 -2
- data/lib/fontisan/tables/gvar.rb +0 -2
- data/lib/fontisan/tables/head.rb +0 -2
- data/lib/fontisan/tables/head_table.rb +0 -3
- data/lib/fontisan/tables/hhea.rb +0 -2
- data/lib/fontisan/tables/hhea_table.rb +0 -3
- data/lib/fontisan/tables/hmtx.rb +0 -2
- data/lib/fontisan/tables/hmtx_table.rb +0 -3
- data/lib/fontisan/tables/hvar.rb +0 -3
- data/lib/fontisan/tables/loca.rb +0 -2
- data/lib/fontisan/tables/loca_table.rb +0 -3
- data/lib/fontisan/tables/maxp.rb +0 -2
- data/lib/fontisan/tables/maxp_table.rb +0 -3
- data/lib/fontisan/tables/mvar.rb +0 -3
- data/lib/fontisan/tables/name.rb +0 -2
- data/lib/fontisan/tables/name_table.rb +0 -3
- data/lib/fontisan/tables/os2_table.rb +0 -3
- data/lib/fontisan/tables/post_table.rb +0 -3
- data/lib/fontisan/tables/sbix.rb +0 -1
- data/lib/fontisan/tables/svg.rb +0 -1
- data/lib/fontisan/tables/variation_common.rb +0 -1
- data/lib/fontisan/tables/vvar.rb +0 -3
- data/lib/fontisan/tables.rb +55 -0
- data/lib/fontisan/true_type_collection.rb +6 -14
- data/lib/fontisan/true_type_font.rb +8 -2
- data/lib/fontisan/true_type_font_extensions.rb +9 -9
- data/lib/fontisan/type1/afm_generator.rb +0 -4
- data/lib/fontisan/type1/conversion_options.rb +0 -2
- data/lib/fontisan/type1/encodings.rb +0 -2
- data/lib/fontisan/type1/generator.rb +0 -8
- data/lib/fontisan/type1/pfa_generator.rb +0 -3
- data/lib/fontisan/type1/pfb_generator.rb +0 -5
- data/lib/fontisan/type1/pfm_generator.rb +0 -4
- data/lib/fontisan/type1.rb +42 -69
- data/lib/fontisan/type1_font.rb +40 -11
- data/lib/fontisan/utilities/checksum_calculator.rb +0 -1
- data/lib/fontisan/utilities.rb +10 -0
- data/lib/fontisan/utils.rb +10 -0
- data/lib/fontisan/validation/collection_validator.rb +0 -2
- data/lib/fontisan/validation.rb +9 -0
- data/lib/fontisan/validators/basic_validator.rb +0 -2
- data/lib/fontisan/validators/font_book_validator.rb +0 -2
- data/lib/fontisan/validators/opentype_validator.rb +0 -2
- data/lib/fontisan/validators/profile_loader.rb +0 -5
- data/lib/fontisan/validators/validator.rb +0 -2
- data/lib/fontisan/validators/web_font_validator.rb +0 -2
- data/lib/fontisan/validators.rb +14 -0
- data/lib/fontisan/variable/delta_applicator.rb +0 -4
- data/lib/fontisan/variable/instancer.rb +0 -3
- data/lib/fontisan/variable/static_font_builder.rb +0 -3
- data/lib/fontisan/variable.rb +16 -0
- data/lib/fontisan/variation/blend_applier.rb +0 -2
- data/lib/fontisan/variation/cache.rb +0 -2
- data/lib/fontisan/variation/converter.rb +0 -3
- data/lib/fontisan/variation/data_extractor.rb +0 -2
- data/lib/fontisan/variation/delta_applier.rb +0 -5
- data/lib/fontisan/variation/inspector.rb +0 -1
- data/lib/fontisan/variation/instance_generator.rb +0 -6
- data/lib/fontisan/variation/instance_writer.rb +0 -5
- data/lib/fontisan/variation/metrics_adjuster.rb +0 -4
- data/lib/fontisan/variation/optimizer.rb +0 -3
- data/lib/fontisan/variation/parallel_generator.rb +0 -3
- data/lib/fontisan/variation/subsetter.rb +0 -4
- data/lib/fontisan/variation/tuple_variation_header.rb +0 -2
- data/lib/fontisan/variation/variable_svg_generator.rb +0 -3
- data/lib/fontisan/variation/variation_context.rb +0 -3
- data/lib/fontisan/variation/variation_preserver.rb +0 -3
- data/lib/fontisan/variation.rb +31 -0
- data/lib/fontisan/version.rb +1 -1
- data/lib/fontisan/woff2.rb +13 -0
- data/lib/fontisan/woff2_font.rb +31 -9
- data/lib/fontisan/woff_font.rb +31 -2
- data/lib/fontisan.rb +121 -196
- metadata +46 -2
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "bindata"
|
|
4
|
-
require_relative "constants"
|
|
5
|
-
require_relative "collection/shared_logic"
|
|
6
4
|
|
|
7
5
|
module Fontisan
|
|
8
6
|
# Abstract base class for font collections (TTC/OTC)
|
|
@@ -118,6 +116,29 @@ module Fontisan
|
|
|
118
116
|
num_fonts
|
|
119
117
|
end
|
|
120
118
|
|
|
119
|
+
# Whether this object represents a font collection rather than a single
|
|
120
|
+
# font. Each font class is the authority on this question.
|
|
121
|
+
#
|
|
122
|
+
# @return [Boolean]
|
|
123
|
+
def collection? = true
|
|
124
|
+
|
|
125
|
+
# Variation profile. Collections are containers; per-font variation is
|
|
126
|
+
# not exposed at the collection level.
|
|
127
|
+
#
|
|
128
|
+
# @return [Symbol] :static
|
|
129
|
+
def variation_type = :static
|
|
130
|
+
|
|
131
|
+
# Outline representation. Collections may contain mixed-flavor fonts;
|
|
132
|
+
# per-font outline type requires loading an individual font.
|
|
133
|
+
#
|
|
134
|
+
# @return [Symbol] :unknown
|
|
135
|
+
def outline_type = :unknown
|
|
136
|
+
|
|
137
|
+
# Collections have no single SFNT table directory.
|
|
138
|
+
#
|
|
139
|
+
# @return [Array<String>] empty
|
|
140
|
+
def table_names = []
|
|
141
|
+
|
|
121
142
|
# Validate format correctness
|
|
122
143
|
#
|
|
123
144
|
# @return [Boolean] true if the format is valid, false otherwise
|
|
@@ -156,10 +177,6 @@ module Fontisan
|
|
|
156
177
|
# list.fonts.each { |f| puts "#{f.index}: #{f.family_name}" }
|
|
157
178
|
# end
|
|
158
179
|
def list_fonts(io)
|
|
159
|
-
require_relative "models/collection_list_info"
|
|
160
|
-
require_relative "models/collection_font_summary"
|
|
161
|
-
require_relative "tables/name"
|
|
162
|
-
|
|
163
180
|
font_class = self.class.font_class
|
|
164
181
|
|
|
165
182
|
fonts = font_offsets.map.with_index do |offset, index|
|
|
@@ -222,9 +239,6 @@ module Fontisan
|
|
|
222
239
|
# puts "Version: #{info.version_string}"
|
|
223
240
|
# end
|
|
224
241
|
def collection_info(io, path)
|
|
225
|
-
require_relative "models/collection_info"
|
|
226
|
-
require_relative "models/table_sharing_info"
|
|
227
|
-
|
|
228
242
|
# Calculate table sharing statistics
|
|
229
243
|
table_sharing = calculate_table_sharing(io)
|
|
230
244
|
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Autoload hub for the Fontisan::Binary namespace.
|
|
4
|
+
|
|
5
|
+
module Fontisan
|
|
6
|
+
module Binary
|
|
7
|
+
autoload :BaseRecord, "fontisan/binary/base_record"
|
|
8
|
+
autoload :OffsetTable, "fontisan/binary/structures"
|
|
9
|
+
autoload :TableDirectoryEntry, "fontisan/binary/structures"
|
|
10
|
+
end
|
|
11
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
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
|
|
@@ -0,0 +1,110 @@
|
|
|
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
|
|
@@ -0,0 +1,59 @@
|
|
|
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
|
|
@@ -0,0 +1,79 @@
|
|
|
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
|
|
@@ -0,0 +1,64 @@
|
|
|
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
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Fontisan
|
|
6
|
+
module Cldr
|
|
7
|
+
# Builds the per-language codepoint index from a cached CLDR JSON
|
|
8
|
+
# archive and persists it as a YAML file for future Index loads.
|
|
9
|
+
#
|
|
10
|
+
# Walks every `main/<lang>/characters.json` file under the cached
|
|
11
|
+
# archive, extracts `exemplarCharacters` (plus auxiliary and index
|
|
12
|
+
# sets when present), parses each via {UnicodeSetParser}, and unions
|
|
13
|
+
# the result into a single codepoint set per language.
|
|
14
|
+
module IndexBuilder
|
|
15
|
+
class << self
|
|
16
|
+
# Build + persist the languages index for a cached version.
|
|
17
|
+
# @param version [String]
|
|
18
|
+
# @return [Index]
|
|
19
|
+
def build(version)
|
|
20
|
+
entries = collect_from_cache(version)
|
|
21
|
+
CacheManager.index_dir(version).mkpath
|
|
22
|
+
index = Index.new(entries)
|
|
23
|
+
index.save(CacheManager.languages_index_path(version))
|
|
24
|
+
index
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Pure: build an Index from a hash of `language => exemplar_string`.
|
|
28
|
+
# @param exemplars_by_lang [Hash{String=>String}]
|
|
29
|
+
# @return [Index]
|
|
30
|
+
def build_from_exemplars(exemplars_by_lang)
|
|
31
|
+
entries = exemplars_by_lang.transform_values do |set_str|
|
|
32
|
+
set_str.nil? ? Set.new : Set.new(UnicodeSetParser.call(set_str))
|
|
33
|
+
end
|
|
34
|
+
Index.new(entries)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def collect_from_cache(version)
|
|
40
|
+
main_dir = CacheManager.characters_main_dir(version)
|
|
41
|
+
return {} unless main_dir.exist?
|
|
42
|
+
|
|
43
|
+
main_dir.children.select(&:directory?).each_with_object({}) do |lang_dir, hash|
|
|
44
|
+
file = lang_dir.join("characters.json")
|
|
45
|
+
next unless file.exist?
|
|
46
|
+
|
|
47
|
+
lang = lang_dir.basename.to_s
|
|
48
|
+
hash[lang] = parse_language_file(file)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def parse_language_file(file)
|
|
53
|
+
data = JSON.parse(file.read)
|
|
54
|
+
lang_key = data.dig("main", "locale") ||
|
|
55
|
+
data["main"]&.keys&.first
|
|
56
|
+
return Set.new unless lang_key
|
|
57
|
+
|
|
58
|
+
chars_node = data.dig("main", lang_key, "characters") || {}
|
|
59
|
+
sets = %w[exemplarCharacters auxiliary exemplarCharactersIndex
|
|
60
|
+
exemplarCharactersPunctuation].filter_map do |field|
|
|
61
|
+
chars_node[field]
|
|
62
|
+
end
|
|
63
|
+
sets.inject(Set.new) do |acc, set_str|
|
|
64
|
+
acc | Set.new(UnicodeSetParser.call(set_str))
|
|
65
|
+
rescue UnicodeSetParser::ParseError
|
|
66
|
+
acc
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|