fontist 1.21.4 → 2.0.1

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 (42) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/rake.yml +4 -0
  3. data/Gemfile +11 -0
  4. data/README.adoc +6 -6
  5. data/docs/guide/api-ruby.md +8 -9
  6. data/fontist.gemspec +14 -24
  7. data/formula_filename_index.yml +6210 -0
  8. data/formula_index.yml +2568 -0
  9. data/lib/fontist/cli.rb +14 -14
  10. data/lib/fontist/config.rb +75 -14
  11. data/lib/fontist/errors.rb +2 -0
  12. data/lib/fontist/extract.rb +25 -0
  13. data/lib/fontist/font.rb +6 -8
  14. data/lib/fontist/font_collection.rb +16 -0
  15. data/lib/fontist/font_installer.rb +4 -4
  16. data/lib/fontist/font_model.rb +15 -0
  17. data/lib/fontist/font_path.rb +1 -1
  18. data/lib/fontist/font_style.rb +37 -0
  19. data/lib/fontist/formula.rb +169 -112
  20. data/lib/fontist/import/formula_serializer.rb +4 -4
  21. data/lib/fontist/import/google_import.rb +1 -1
  22. data/lib/fontist/index.rb +47 -8
  23. data/lib/fontist/indexes/default_family_font_index.rb +28 -9
  24. data/lib/fontist/indexes/filename_index.rb +45 -8
  25. data/lib/fontist/indexes/font_index.rb +3 -3
  26. data/lib/fontist/indexes/formula_key_to_path.rb +35 -0
  27. data/lib/fontist/indexes/index_mixin.rb +109 -0
  28. data/lib/fontist/indexes/preferred_family_font_index.rb +28 -9
  29. data/lib/fontist/manifest.rb +144 -2
  30. data/lib/fontist/manifest_request.rb +64 -0
  31. data/lib/fontist/manifest_response.rb +66 -0
  32. data/lib/fontist/repo.rb +1 -1
  33. data/lib/fontist/system_font.rb +7 -25
  34. data/lib/fontist/system_index.rb +137 -126
  35. data/lib/fontist/utils/cache.rb +54 -4
  36. data/lib/fontist/version.rb +1 -1
  37. data/lib/fontist.rb +33 -13
  38. metadata +16 -136
  39. data/lib/fontist/indexes/base_index.rb +0 -92
  40. data/lib/fontist/indexes/index_formula.rb +0 -36
  41. data/lib/fontist/manifest/install.rb +0 -35
  42. data/lib/fontist/manifest/locations.rb +0 -84
@@ -2,157 +2,142 @@ require_relative "font_file"
2
2
  require_relative "collection_file"
3
3
 
4
4
  module Fontist
5
- class SystemIndex
6
- include Utils::Locking
7
-
8
- class DefaultFamily
9
- def family_name(name)
10
- name.family
11
- end
5
+ # {:path=>"/Library/Fonts/Arial Unicode.ttf",
6
+ # :full_name=>"Arial Unicode MS",
7
+ # :family_name=>"Arial Unicode MS",
8
+ # :preferred_family_name=>"Arial",
9
+ # :preferred_subfamily=>"Regular",
10
+ # :subfamily=>"Regular"},
11
+ class SystemIndexFont < Lutaml::Model::Serializable
12
+ attribute :path, :string
13
+ attribute :full_name, :string
14
+ attribute :family_name, :string
15
+ attribute :preferred_family_name, :string
16
+ attribute :preferred_subfamily, :string
17
+ attribute :subfamily, :string
18
+ alias :type :subfamily
19
+
20
+ key_value do
21
+ map "path", to: :path
22
+ map "full_name", to: :full_name
23
+ map "family_name", to: :family_name
24
+ map "type", to: :subfamily
25
+ map "preferred_family_name", to: :preferred_family_name
26
+ map "preferred_subfamily", to: :preferred_subfamily
27
+ end
28
+ end
12
29
 
13
- def type(name)
14
- name.subfamily
15
- end
30
+ class SystemIndexFontCollection < Lutaml::Model::Collection
31
+ instances :fonts, SystemIndexFont
32
+ attr_accessor :path, :paths_loader
16
33
 
17
- def transform_override_keys(dict)
18
- dict
19
- end
34
+ key_value do
35
+ map_instances to: :fonts
20
36
  end
21
37
 
22
- class PreferredFamily
23
- def family_name(name)
24
- name.preferred_family || name.family
25
- end
26
-
27
- def type(name)
28
- name.preferred_subfamily || name.subfamily
29
- end
38
+ def set_path(path)
39
+ @path = path
40
+ end
30
41
 
31
- def transform_override_keys(dict)
32
- mapping = { preferred_family_name: :family_name, preferred_type: :type }
33
- dict.transform_keys! { |k| mapping[k] }
34
- end
42
+ def set_path_loader(paths_loader)
43
+ @paths_loader = paths_loader
35
44
  end
36
45
 
37
- ALLOWED_KEYS = %i[path full_name family_name type].freeze
46
+ def self.from_file(path:, paths_loader:)
47
+ # If the file does not exist, return a new collection
48
+ return new.set_content(path, paths_loader) unless File.exist?(path)
38
49
 
39
- def self.system_index
40
- path = if Fontist.preferred_family?
41
- Fontist.system_preferred_family_index_path
42
- else
43
- Fontist.system_index_path
44
- end
50
+ from_yaml(File.read(path)).set_content(path, paths_loader)
51
+ end
45
52
 
46
- @system_index ||= {}
47
- @system_index[Fontist.preferred_family?] ||= {}
48
- @system_index[Fontist.preferred_family?][path] ||=
49
- new(path, -> { SystemFont.font_paths }, family)
53
+ def set_content(path, paths_loader)
54
+ tap do |content|
55
+ content.set_path(path)
56
+ content.set_path_loader(paths_loader)
57
+ end
50
58
  end
51
59
 
52
- def self.fontist_index
53
- path = if Fontist.preferred_family?
54
- Fontist.fontist_preferred_family_index_path
55
- else
56
- Fontist.fontist_index_path
57
- end
60
+ ALLOWED_KEYS = %i[path full_name family_name type].freeze
58
61
 
59
- @fontist_index ||= {}
60
- @fontist_index[Fontist.preferred_family?] ||= {}
61
- @fontist_index[Fontist.preferred_family?][path] ||=
62
- new(path, -> { SystemFont.fontist_font_paths }, family)
63
- end
62
+ # Check if the content has all required keys
63
+ def check_index
64
+ Fontist.formulas_repo_path_exists!
64
65
 
65
- def self.family
66
- Fontist.preferred_family? ? PreferredFamily.new : DefaultFamily.new
67
- end
66
+ Array(fonts).each do |font|
67
+ missing_keys = ALLOWED_KEYS.reject do |key|
68
+ font.send(key)
69
+ end
68
70
 
69
- def excluded_fonts
70
- @excluded_fonts ||= YAML.load_file(Fontist.excluded_fonts_path)
71
+ raise_font_index_corrupted(font, missing_keys) if missing_keys.any?
72
+ end
71
73
  end
72
74
 
73
- def initialize(index_path, font_paths_fetcher, family)
74
- @index_path = index_path
75
- @font_paths_fetcher = font_paths_fetcher
76
- @family = family
75
+ def to_file(path)
76
+ FileUtils.mkdir_p(File.dirname(path))
77
+ File.write(path, to_yaml)
77
78
  end
78
79
 
79
80
  def find(font, style)
80
- fonts = index.select do |file|
81
- file[:family_name].casecmp?(font) &&
82
- (style.nil? || file[:type].casecmp?(style))
81
+ found_fonts = index.select do |file|
82
+ file.family_name.casecmp?(font) &&
83
+ (style.nil? || file.type.casecmp?(style))
83
84
  end
84
85
 
85
- fonts.empty? ? nil : fonts
86
+ found_fonts.empty? ? nil : found_fonts
86
87
  end
87
88
 
88
- def rebuild
89
- build_index
90
- end
91
-
92
- private
93
-
94
89
  def index
95
- return @index unless index_changed?
90
+ return fonts unless index_changed?
96
91
 
97
- @index = build_index
98
- end
92
+ build
93
+ check_index
99
94
 
100
- def index_changed?
101
- @index.nil? ||
102
- @index.map { |x| x[:path] }.uniq.sort != font_paths.sort
95
+ fonts
103
96
  end
104
97
 
105
- def font_paths
106
- @font_paths_fetcher.call
98
+ def index_changed?
99
+ fonts.nil? || fonts.empty? || font_paths != (@paths_loader&.call || []).sort.uniq
107
100
  end
108
101
 
109
- def build_index
110
- lock(lock_path) do
111
- do_build_index
102
+ def update
103
+ tap do |col|
104
+ col.fonts = detect_paths(@paths_loader&.call || [])
112
105
  end
113
106
  end
114
107
 
115
- def lock_path
116
- Utils::Cache.lock_path(@index_path)
117
- end
118
-
119
- def do_build_index
108
+ def build(forced: false)
120
109
  previous_index = load_index
121
- updated_index = detect_paths(font_paths, previous_index)
122
- updated_index.tap do |index|
123
- save_index(index) if changed?(updated_index, previous_index)
110
+ updated_fonts = update
111
+ if forced || changed?(updated_fonts, previous_index.fonts || [])
112
+ to_file(@path)
124
113
  end
114
+
115
+ self
125
116
  end
126
117
 
127
- def changed?(this, that)
128
- this.map { |x| x[:path] }.uniq.sort != that.map { |x| x[:path] }.uniq.sort
118
+ def rebuild
119
+ build(forced: true)
129
120
  end
130
121
 
122
+ private
123
+
131
124
  def load_index
132
- index = File.exist?(@index_path) ? YAML.load_file(@index_path) : []
133
- check_index(index)
125
+ index = self.class.from_file(path: @path, paths_loader: @paths_loader)
126
+ index.check_index
134
127
  index
135
128
  end
136
129
 
137
- def check_index(index)
138
- index.each do |item|
139
- missing_keys = ALLOWED_KEYS - item.keys
140
- unless missing_keys.empty?
141
- raise(Errors::FontIndexCorrupted, <<~MSG.chomp)
142
- Font index is corrupted.
143
- Item #{item.inspect} misses required attributes: #{missing_keys.join(', ')}.
144
- You can remove the index file (#{@index_path}) and try again.
145
- MSG
146
- end
147
- end
130
+ def font_paths
131
+ fonts.map(&:path).uniq.sort
148
132
  end
149
133
 
150
- def detect_paths(paths, index)
151
- by_path = index.group_by { |x| x[:path] }
152
-
153
- paths.flat_map do |path|
154
- next by_path[path] if by_path[path]
134
+ def changed?(this_fonts, that_fonts)
135
+ this_fonts.map(&:path).uniq.sort != that_fonts.map(&:path).uniq.sort
136
+ end
155
137
 
138
+ def detect_paths(paths)
139
+ # paths are file paths to font files
140
+ paths.sort.uniq.flat_map do |path|
156
141
  detect_fonts(path)
157
142
  end.compact
158
143
  end
@@ -169,6 +154,10 @@ module Fontist
169
154
  excluded_fonts.include?(File.basename(path))
170
155
  end
171
156
 
157
+ def excluded_fonts
158
+ @excluded_fonts ||= YAML.load_file(Fontist.excluded_fonts_path)
159
+ end
160
+
172
161
  def gather_fonts(path)
173
162
  case File.extname(path).gsub(/^\./, "").downcase
174
163
  when "ttf", "otf"
@@ -189,42 +178,64 @@ module Fontist
189
178
  end
190
179
 
191
180
  def detect_file_font(path)
192
- file = FontFile.from_path(path)
181
+ font_file = FontFile.from_path(path)
193
182
 
194
- parse_font(file, path)
183
+ parse_font(font_file, path)
195
184
  end
196
185
 
197
186
  def detect_collection_fonts(path)
198
187
  CollectionFile.from_path(path) do |collection|
199
- collection.map do |file|
200
- parse_font(file, path)
188
+ collection.map do |font_file|
189
+ parse_font(font_file, path)
201
190
  end
202
191
  end
203
192
  end
204
193
 
205
- def parse_font(file, path)
206
- family_name = @family.family_name(file)
207
-
208
- {
194
+ def parse_font(font_file, path)
195
+ SystemIndexFont.new(
209
196
  path: path,
210
- full_name: file.full_name,
211
- family_name: family_name,
212
- type: @family.type(file),
213
- }.merge(override_font_props(path, family_name))
197
+ full_name: font_file.full_name,
198
+ family_name: font_file.family,
199
+ subfamily: font_file.subfamily,
200
+ preferred_family_name: font_file.preferred_family,
201
+ preferred_subfamily_name: font_file.preferred_subfamily,
202
+ )
203
+ end
204
+
205
+ def raise_font_index_corrupted(font, missing_keys)
206
+ raise(Errors::FontIndexCorrupted, <<~MSG.chomp)
207
+ Font index is corrupted.
208
+ Item #{font.inspect} misses required attributes: #{missing_keys.join(', ')}.
209
+ You can remove the index file (#{@path}) and try again.
210
+ MSG
214
211
  end
212
+ end
215
213
 
216
- def override_font_props(path, font_name)
217
- override = Formula.find_by_font_file(path)
218
- &.style_override(font_name)&.to_h || {}
214
+ class SystemIndex
215
+ include Utils::Locking
219
216
 
220
- @family.transform_override_keys(override)
221
- .slice(*ALLOWED_KEYS)
217
+ def self.system_index
218
+ @system_index = SystemIndexFontCollection.from_file(
219
+ path: Fontist.system_index_path,
220
+ paths_loader: -> { SystemFont.font_paths },
221
+ )
222
222
  end
223
223
 
224
- def save_index(index)
225
- dir = File.dirname(@index_path)
226
- FileUtils.mkdir_p(dir)
227
- File.write(@index_path, YAML.dump(index))
224
+ def self.fontist_index
225
+ @fontist_index = SystemIndexFontCollection.from_file(
226
+ path: Fontist.fontist_index_path,
227
+ paths_loader: -> { SystemFont.fontist_font_paths },
228
+ )
228
229
  end
230
+
231
+ # def build_index
232
+ # lock(lock_path) do
233
+ # do_build_index
234
+ # end
235
+ # end
236
+
237
+ # def lock_path
238
+ # Utils::Cache.lock_path(@index_path)
239
+ # end
229
240
  end
230
241
  end
@@ -1,5 +1,55 @@
1
+ require "lutaml/model"
2
+
1
3
  module Fontist
2
4
  module Utils
5
+ class CacheIndexItem < Lutaml::Model::Serializable
6
+ attribute :url, :string
7
+ attribute :name, :string
8
+ end
9
+
10
+ class CacheIndex < Lutaml::Model::Serializable
11
+ attribute :items, CacheIndexItem, collection: true, default: []
12
+
13
+ key_value do
14
+ map to: :items, root_mappings: {
15
+ url: :key,
16
+ name: :value,
17
+ }
18
+ end
19
+
20
+ def self.from_file(path)
21
+ return new unless File.exist?(path)
22
+
23
+ content = File.read(path)
24
+
25
+ return new if content.strip.empty? || content.strip == "---"
26
+
27
+ from_yaml(content) || {}
28
+ end
29
+
30
+ def to_file(path)
31
+ File.write(path, to_yaml)
32
+ end
33
+
34
+ def [](key)
35
+ Array(items).find { |i| i.url == key }&.name
36
+ end
37
+
38
+ def []=(key, value)
39
+ item = Array(items).find { |i| i.url == key }
40
+ if item
41
+ item.name = value
42
+ else
43
+ items << CacheIndexItem.new(url: key, name: value)
44
+ end
45
+ end
46
+
47
+ def delete(key)
48
+ item = Array(items).find { |i| i.url == key }
49
+ items.delete(item) if item
50
+ end
51
+ end
52
+
3
53
  class Cache
4
54
  MAX_FILENAME_SIZE = 255
5
55
 
@@ -34,7 +84,7 @@ module Fontist
34
84
  return unless map[key]
35
85
 
36
86
  value = map.delete(key)
37
- File.write(cache_map_path, YAML.dump(map))
87
+ map.to_file(cache_map_path)
38
88
  value
39
89
  end
40
90
  end
@@ -43,7 +93,7 @@ module Fontist
43
93
  lock(lock_path) do
44
94
  map = load_cache
45
95
  map[key] = value
46
- File.write(cache_map_path, YAML.dump(map))
96
+ map.to_file(cache_map_path)
47
97
  end
48
98
  end
49
99
 
@@ -54,7 +104,7 @@ module Fontist
54
104
  end
55
105
 
56
106
  def load_cache
57
- cache_map_path.exist? ? YAML.load_file(cache_map_path) : {}
107
+ CacheIndex.from_file(cache_map_path)
58
108
  end
59
109
 
60
110
  def downloaded_file(path)
@@ -83,7 +133,7 @@ module Fontist
83
133
  lock(lock_path) do
84
134
  map = load_cache
85
135
  map[key] = path
86
- File.write(cache_map_path, YAML.dump(map))
136
+ map.to_file(cache_map_path)
87
137
  end
88
138
 
89
139
  path
@@ -1,3 +1,3 @@
1
1
  module Fontist
2
- VERSION = "1.21.4".freeze
2
+ VERSION = "2.0.1".freeze
3
3
  end
data/lib/fontist.rb CHANGED
@@ -1,19 +1,9 @@
1
1
  require "down"
2
2
  require "digest"
3
- require "json"
4
- require "yaml"
5
3
  require "singleton"
6
4
 
7
- require "fontist/errors"
8
- require "fontist/version"
9
-
10
- require "fontist/repo"
11
- require "fontist/font"
12
- require "fontist/formula"
13
- require "fontist/system_font"
14
- require "fontist/manifest"
15
- require "fontist/helpers"
16
- require "fontist/config"
5
+ require_relative "fontist/errors"
6
+ require_relative "fontist/version"
17
7
 
18
8
  module Fontist
19
9
  def self.ui
@@ -141,7 +131,7 @@ module Fontist
141
131
  end
142
132
 
143
133
  def self.use_cache?
144
- instance_variable_defined?("@use_cache") ? @use_cache : true
134
+ instance_variable_defined?(:@use_cache) ? @use_cache : true
145
135
  end
146
136
 
147
137
  def self.use_cache=(bool)
@@ -163,4 +153,34 @@ module Fontist
163
153
  def self.google_fonts_key
164
154
  ENV["GOOGLE_FONTS_API_KEY"] || config[:google_fonts_key]
165
155
  end
156
+
157
+ def self.formulas_repo_path_exists!
158
+ return true if Dir.exist?(Fontist.formulas_repo_path)
159
+
160
+ raise Errors::MainRepoNotFoundError.new(
161
+ "Please fetch formulas with `fontist update`.",
162
+ )
163
+ end
166
164
  end
165
+
166
+ require_relative "fontist/repo"
167
+ require_relative "fontist/font"
168
+ require_relative "fontist/formula"
169
+ require_relative "fontist/system_font"
170
+ require_relative "fontist/manifest"
171
+ require_relative "fontist/manifest_response"
172
+ require_relative "fontist/manifest_request"
173
+ require_relative "fontist/helpers"
174
+ require_relative "fontist/config"
175
+ require_relative "fontist/update"
176
+ require_relative "fontist/index"
177
+ require_relative "fontist/indexes/font_index"
178
+ require_relative "fontist/indexes/filename_index"
179
+ require_relative "fontist/cli"
180
+ require_relative "fontist/font_installer"
181
+ require_relative "fontist/fontconfig"
182
+ require_relative "fontist/formula_picker"
183
+ require_relative "fontist/formula_suggestion"
184
+ require_relative "fontist/extract"
185
+ require_relative "fontist/font_style"
186
+ require_relative "fontist/font_collection"