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.
Files changed (249) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +6 -0
  3. data/.rubocop_todo.yml +86 -86
  4. data/CHANGELOG.md +12 -2
  5. data/README.adoc +53 -1
  6. data/docs/.vitepress/config.ts +68 -8
  7. data/docs/.vitepress/theme/style.css +570 -272
  8. data/docs/CONVERSION_GUIDE.adoc +31 -8
  9. data/docs/EXTRACT_TTC_MIGRATION.md +1 -1
  10. data/docs/WOFF_WOFF2_FORMATS.adoc +53 -0
  11. data/docs/api/conversion-options.md +37 -14
  12. data/docs/cli/audit.md +337 -0
  13. data/docs/cli/convert.md +20 -1
  14. data/docs/cli/index.md +31 -0
  15. data/docs/guide/color.md +1 -1
  16. data/docs/guide/conversion/options.md +32 -3
  17. data/docs/guide/conversion/ttf-otf.md +1 -1
  18. data/docs/guide/conversion/type1.md +1 -1
  19. data/docs/guide/conversion/web.md +91 -32
  20. data/docs/guide/conversion.md +6 -5
  21. data/docs/guide/formats/woff.md +35 -11
  22. data/docs/guide/index.md +2 -2
  23. data/docs/guide/migrations/extract-ttc.md +1 -1
  24. data/docs/guide/quick-start.md +4 -4
  25. data/docs/guide/type1.md +4 -4
  26. data/docs/guide/woff.md +19 -17
  27. data/docs/index.md +2 -0
  28. data/docs/lychee.toml +5 -1
  29. data/docs/package.json +1 -1
  30. data/docs/public/robots.txt +4 -0
  31. data/docs/scripts/post-build.mjs +81 -0
  32. data/lib/fontisan/base_collection.rb +23 -9
  33. data/lib/fontisan/binary/structures.rb +0 -2
  34. data/lib/fontisan/binary.rb +11 -0
  35. data/lib/fontisan/cldr/aggregator.rb +33 -0
  36. data/lib/fontisan/cldr/cache_manager.rb +110 -0
  37. data/lib/fontisan/cldr/config.rb +59 -0
  38. data/lib/fontisan/cldr/download_error.rb +9 -0
  39. data/lib/fontisan/cldr/downloader.rb +79 -0
  40. data/lib/fontisan/cldr/error.rb +8 -0
  41. data/lib/fontisan/cldr/index.rb +64 -0
  42. data/lib/fontisan/cldr/index_builder.rb +72 -0
  43. data/lib/fontisan/cldr/unicode_set_parser.rb +189 -0
  44. data/lib/fontisan/cldr/unknown_version_error.rb +9 -0
  45. data/lib/fontisan/cldr/version_resolver.rb +91 -0
  46. data/lib/fontisan/cldr.rb +23 -0
  47. data/lib/fontisan/cli/cldr_cli.rb +85 -0
  48. data/lib/fontisan/cli.rb +31 -2
  49. data/lib/fontisan/collection/builder.rb +0 -4
  50. data/lib/fontisan/collection/dfont_builder.rb +0 -4
  51. data/lib/fontisan/collection/shared_logic.rb +0 -2
  52. data/lib/fontisan/collection/writer.rb +0 -3
  53. data/lib/fontisan/collection.rb +15 -0
  54. data/lib/fontisan/commands/base_command.rb +0 -3
  55. data/lib/fontisan/commands/convert_command.rb +25 -20
  56. data/lib/fontisan/commands/dump_table_command.rb +0 -3
  57. data/lib/fontisan/commands/export_command.rb +0 -4
  58. data/lib/fontisan/commands/features_command.rb +0 -3
  59. data/lib/fontisan/commands/instance_command.rb +0 -5
  60. data/lib/fontisan/commands/ls_command.rb +0 -6
  61. data/lib/fontisan/commands/optical_size_command.rb +0 -3
  62. data/lib/fontisan/commands/pack_command.rb +0 -5
  63. data/lib/fontisan/commands/scripts_command.rb +0 -2
  64. data/lib/fontisan/commands/subset_command.rb +0 -3
  65. data/lib/fontisan/commands/unicode_command.rb +0 -3
  66. data/lib/fontisan/commands/unpack_command.rb +0 -7
  67. data/lib/fontisan/commands/validate_command.rb +0 -8
  68. data/lib/fontisan/commands/variable_command.rb +0 -3
  69. data/lib/fontisan/commands.rb +26 -0
  70. data/lib/fontisan/config/cldr.yml +22 -0
  71. data/lib/fontisan/config/conversion_matrix.yml +38 -0
  72. data/lib/fontisan/constants.rb +19 -0
  73. data/lib/fontisan/conversion_options.rb +30 -19
  74. data/lib/fontisan/converters/cff_table_builder.rb +0 -3
  75. data/lib/fontisan/converters/collection_converter.rb +0 -8
  76. data/lib/fontisan/converters/conversion_strategy.rb +161 -46
  77. data/lib/fontisan/converters/format_converter.rb +143 -32
  78. data/lib/fontisan/converters/glyf_table_builder.rb +0 -2
  79. data/lib/fontisan/converters/outline_converter.rb +0 -19
  80. data/lib/fontisan/converters/outline_extraction.rb +0 -5
  81. data/lib/fontisan/converters/outline_optimizer.rb +0 -5
  82. data/lib/fontisan/converters/svg_generator.rb +0 -4
  83. data/lib/fontisan/converters/table_copier.rb +0 -2
  84. data/lib/fontisan/converters/type1_converter.rb +0 -11
  85. data/lib/fontisan/converters/woff2_encoder.rb +49 -20
  86. data/lib/fontisan/converters/woff_writer.rb +211 -282
  87. data/lib/fontisan/converters.rb +21 -0
  88. data/lib/fontisan/dfont_collection.rb +29 -10
  89. data/lib/fontisan/export/exporter.rb +0 -6
  90. data/lib/fontisan/export/transformers/font_to_ttx.rb +0 -9
  91. data/lib/fontisan/export/transformers/head_transformer.rb +0 -2
  92. data/lib/fontisan/export/transformers/hhea_transformer.rb +0 -2
  93. data/lib/fontisan/export/transformers/maxp_transformer.rb +0 -2
  94. data/lib/fontisan/export/transformers/name_transformer.rb +0 -2
  95. data/lib/fontisan/export/transformers/os2_transformer.rb +0 -2
  96. data/lib/fontisan/export/transformers/post_transformer.rb +0 -2
  97. data/lib/fontisan/export/transformers.rb +17 -0
  98. data/lib/fontisan/export.rb +13 -0
  99. data/lib/fontisan/font_loader.rb +14 -19
  100. data/lib/fontisan/font_writer.rb +0 -1
  101. data/lib/fontisan/formatters.rb +9 -0
  102. data/lib/fontisan/hints/hint_converter.rb +0 -1
  103. data/lib/fontisan/hints/postscript_hint_applier.rb +0 -9
  104. data/lib/fontisan/hints/postscript_hint_extractor.rb +0 -2
  105. data/lib/fontisan/hints/truetype_hint_extractor.rb +0 -2
  106. data/lib/fontisan/hints.rb +19 -0
  107. data/lib/fontisan/metrics_calculator.rb +0 -2
  108. data/lib/fontisan/models/all_scripts_features_info.rb +0 -1
  109. data/lib/fontisan/models/cldr/language_coverage.rb +31 -0
  110. data/lib/fontisan/models/cldr.rb +12 -0
  111. data/lib/fontisan/models/collection_brief_info.rb +0 -1
  112. data/lib/fontisan/models/collection_info.rb +0 -2
  113. data/lib/fontisan/models/collection_list_info.rb +0 -1
  114. data/lib/fontisan/models/collection_validation_report.rb +0 -2
  115. data/lib/fontisan/models/color_glyph.rb +0 -1
  116. data/lib/fontisan/models/font_report.rb +0 -1
  117. data/lib/fontisan/models/ttx/tables.rb +21 -0
  118. data/lib/fontisan/models/ttx/ttfont.rb +0 -8
  119. data/lib/fontisan/models/ttx.rb +14 -0
  120. data/lib/fontisan/models.rb +47 -0
  121. data/lib/fontisan/open_type_collection.rb +6 -5
  122. data/lib/fontisan/open_type_font.rb +8 -2
  123. data/lib/fontisan/open_type_font_extensions.rb +9 -9
  124. data/lib/fontisan/optimizers/pattern_analyzer.rb +0 -1
  125. data/lib/fontisan/optimizers.rb +14 -0
  126. data/lib/fontisan/outline_extractor.rb +0 -2
  127. data/lib/fontisan/parsers/dfont_parser.rb +0 -1
  128. data/lib/fontisan/parsers.rb +10 -0
  129. data/lib/fontisan/pipeline/format_detector.rb +29 -102
  130. data/lib/fontisan/pipeline/output_writer.rb +11 -9
  131. data/lib/fontisan/pipeline/strategies/instance_strategy.rb +0 -4
  132. data/lib/fontisan/pipeline/strategies/named_strategy.rb +0 -4
  133. data/lib/fontisan/pipeline/strategies/preserve_strategy.rb +0 -2
  134. data/lib/fontisan/pipeline/strategies.rb +16 -0
  135. data/lib/fontisan/pipeline/transformation_pipeline.rb +0 -7
  136. data/lib/fontisan/pipeline/variation_resolver.rb +0 -7
  137. data/lib/fontisan/pipeline.rb +14 -0
  138. data/lib/fontisan/sfnt_font.rb +29 -14
  139. data/lib/fontisan/sfnt_table.rb +0 -4
  140. data/lib/fontisan/subset/builder.rb +0 -6
  141. data/lib/fontisan/subset.rb +13 -0
  142. data/lib/fontisan/svg/font_generator.rb +0 -4
  143. data/lib/fontisan/svg/glyph_generator.rb +0 -2
  144. data/lib/fontisan/svg.rb +12 -0
  145. data/lib/fontisan/tables/cbdt.rb +0 -1
  146. data/lib/fontisan/tables/cblc.rb +0 -1
  147. data/lib/fontisan/tables/cff/charset.rb +0 -1
  148. data/lib/fontisan/tables/cff/charstring.rb +0 -1
  149. data/lib/fontisan/tables/cff/charstring_rebuilder.rb +0 -4
  150. data/lib/fontisan/tables/cff/charstrings_index.rb +0 -3
  151. data/lib/fontisan/tables/cff/dict.rb +0 -1
  152. data/lib/fontisan/tables/cff/encoding.rb +0 -1
  153. data/lib/fontisan/tables/cff/header.rb +0 -2
  154. data/lib/fontisan/tables/cff/hint_operation_injector.rb +0 -2
  155. data/lib/fontisan/tables/cff/index.rb +0 -1
  156. data/lib/fontisan/tables/cff/private_dict.rb +0 -2
  157. data/lib/fontisan/tables/cff/private_dict_writer.rb +0 -2
  158. data/lib/fontisan/tables/cff/table_builder.rb +0 -6
  159. data/lib/fontisan/tables/cff/top_dict.rb +0 -2
  160. data/lib/fontisan/tables/cff.rb +23 -15
  161. data/lib/fontisan/tables/cff2/charstring_parser.rb +0 -2
  162. data/lib/fontisan/tables/cff2/table_builder.rb +0 -11
  163. data/lib/fontisan/tables/cff2/table_reader.rb +0 -2
  164. data/lib/fontisan/tables/cff2.rb +13 -14
  165. data/lib/fontisan/tables/cmap.rb +24 -2
  166. data/lib/fontisan/tables/cmap_table.rb +0 -3
  167. data/lib/fontisan/tables/colr.rb +0 -1
  168. data/lib/fontisan/tables/cpal.rb +0 -1
  169. data/lib/fontisan/tables/cvar.rb +0 -2
  170. data/lib/fontisan/tables/fvar.rb +0 -1
  171. data/lib/fontisan/tables/glyf/compound_glyph_resolver.rb +0 -2
  172. data/lib/fontisan/tables/glyf/glyph_builder.rb +0 -3
  173. data/lib/fontisan/tables/glyf.rb +0 -6
  174. data/lib/fontisan/tables/glyf_table.rb +0 -3
  175. data/lib/fontisan/tables/gpos.rb +0 -2
  176. data/lib/fontisan/tables/gsub.rb +0 -2
  177. data/lib/fontisan/tables/gvar.rb +0 -2
  178. data/lib/fontisan/tables/head.rb +0 -2
  179. data/lib/fontisan/tables/head_table.rb +0 -3
  180. data/lib/fontisan/tables/hhea.rb +0 -2
  181. data/lib/fontisan/tables/hhea_table.rb +0 -3
  182. data/lib/fontisan/tables/hmtx.rb +0 -2
  183. data/lib/fontisan/tables/hmtx_table.rb +0 -3
  184. data/lib/fontisan/tables/hvar.rb +0 -3
  185. data/lib/fontisan/tables/loca.rb +0 -2
  186. data/lib/fontisan/tables/loca_table.rb +0 -3
  187. data/lib/fontisan/tables/maxp.rb +0 -2
  188. data/lib/fontisan/tables/maxp_table.rb +0 -3
  189. data/lib/fontisan/tables/mvar.rb +0 -3
  190. data/lib/fontisan/tables/name.rb +0 -2
  191. data/lib/fontisan/tables/name_table.rb +0 -3
  192. data/lib/fontisan/tables/os2_table.rb +0 -3
  193. data/lib/fontisan/tables/post_table.rb +0 -3
  194. data/lib/fontisan/tables/sbix.rb +0 -1
  195. data/lib/fontisan/tables/svg.rb +0 -1
  196. data/lib/fontisan/tables/variation_common.rb +0 -1
  197. data/lib/fontisan/tables/vvar.rb +0 -3
  198. data/lib/fontisan/tables.rb +55 -0
  199. data/lib/fontisan/true_type_collection.rb +6 -14
  200. data/lib/fontisan/true_type_font.rb +8 -2
  201. data/lib/fontisan/true_type_font_extensions.rb +9 -9
  202. data/lib/fontisan/type1/afm_generator.rb +0 -4
  203. data/lib/fontisan/type1/conversion_options.rb +0 -2
  204. data/lib/fontisan/type1/encodings.rb +0 -2
  205. data/lib/fontisan/type1/generator.rb +0 -8
  206. data/lib/fontisan/type1/pfa_generator.rb +0 -3
  207. data/lib/fontisan/type1/pfb_generator.rb +0 -5
  208. data/lib/fontisan/type1/pfm_generator.rb +0 -4
  209. data/lib/fontisan/type1.rb +42 -69
  210. data/lib/fontisan/type1_font.rb +40 -11
  211. data/lib/fontisan/utilities/checksum_calculator.rb +0 -1
  212. data/lib/fontisan/utilities.rb +10 -0
  213. data/lib/fontisan/utils.rb +10 -0
  214. data/lib/fontisan/validation/collection_validator.rb +0 -2
  215. data/lib/fontisan/validation.rb +9 -0
  216. data/lib/fontisan/validators/basic_validator.rb +0 -2
  217. data/lib/fontisan/validators/font_book_validator.rb +0 -2
  218. data/lib/fontisan/validators/opentype_validator.rb +0 -2
  219. data/lib/fontisan/validators/profile_loader.rb +0 -5
  220. data/lib/fontisan/validators/validator.rb +0 -2
  221. data/lib/fontisan/validators/web_font_validator.rb +0 -2
  222. data/lib/fontisan/validators.rb +14 -0
  223. data/lib/fontisan/variable/delta_applicator.rb +0 -4
  224. data/lib/fontisan/variable/instancer.rb +0 -3
  225. data/lib/fontisan/variable/static_font_builder.rb +0 -3
  226. data/lib/fontisan/variable.rb +16 -0
  227. data/lib/fontisan/variation/blend_applier.rb +0 -2
  228. data/lib/fontisan/variation/cache.rb +0 -2
  229. data/lib/fontisan/variation/converter.rb +0 -3
  230. data/lib/fontisan/variation/data_extractor.rb +0 -2
  231. data/lib/fontisan/variation/delta_applier.rb +0 -5
  232. data/lib/fontisan/variation/inspector.rb +0 -1
  233. data/lib/fontisan/variation/instance_generator.rb +0 -6
  234. data/lib/fontisan/variation/instance_writer.rb +0 -5
  235. data/lib/fontisan/variation/metrics_adjuster.rb +0 -4
  236. data/lib/fontisan/variation/optimizer.rb +0 -3
  237. data/lib/fontisan/variation/parallel_generator.rb +0 -3
  238. data/lib/fontisan/variation/subsetter.rb +0 -4
  239. data/lib/fontisan/variation/tuple_variation_header.rb +0 -2
  240. data/lib/fontisan/variation/variable_svg_generator.rb +0 -3
  241. data/lib/fontisan/variation/variation_context.rb +0 -3
  242. data/lib/fontisan/variation/variation_preserver.rb +0 -3
  243. data/lib/fontisan/variation.rb +31 -0
  244. data/lib/fontisan/version.rb +1 -1
  245. data/lib/fontisan/woff2.rb +13 -0
  246. data/lib/fontisan/woff2_font.rb +31 -9
  247. data/lib/fontisan/woff_font.rb +31 -2
  248. data/lib/fontisan.rb +121 -196
  249. 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
 
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "base_record"
4
-
5
3
  module Fontisan
6
4
  module Binary
7
5
  # OpenType Offset Table (Font Header)
@@ -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,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fontisan
4
+ module Cldr
5
+ # Raised by Cldr::Downloader when the upstream HTTP fetch or the zip
6
+ # extraction fails. Caught by AuditCommand to degrade-with-warning.
7
+ class DownloadError < Cldr::Error; end
8
+ end
9
+ 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,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fontisan
4
+ module Cldr
5
+ # Base error class for all CLDR-related failures.
6
+ class Error < StandardError; end
7
+ end
8
+ 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