fontist 1.13.0 → 1.14.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -4,6 +4,18 @@ module Fontist
4
4
 
5
5
  class BinaryCallError < GeneralError; end
6
6
 
7
+ class FontconfigNotFoundError < GeneralError
8
+ def initialize
9
+ super("Could not find fontconfig.")
10
+ end
11
+ end
12
+
13
+ class FontconfigFileNotFoundError < GeneralError
14
+ def initialize
15
+ super("Fontist file could not be found in fontconfig configuration.")
16
+ end
17
+ end
18
+
7
19
  class FontIndexCorrupted < GeneralError; end
8
20
 
9
21
  class FontNotFoundError < GeneralError; end
@@ -12,6 +24,16 @@ module Fontist
12
24
  # it depends on this exception to automatically download formulas
13
25
  class FormulaIndexNotFoundError < GeneralError; end
14
26
 
27
+ class FormulaNotFoundError < GeneralError
28
+ def initialize(formula)
29
+ super(<<~MSG.chomp)
30
+ Formula '#{formula}' not found locally nor available in the Fontist formula repository.
31
+ Perhaps it is available at the latest Fontist formula repository.
32
+ You can update the formula repository using the command `fontist update` and try again.
33
+ MSG
34
+ end
35
+ end
36
+
15
37
  class MainRepoNotFoundError < FormulaIndexNotFoundError; end
16
38
 
17
39
  class InvalidResourceError < GeneralError; end
data/lib/fontist/font.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  require "fontist/font_installer"
2
2
  require "fontist/font_path"
3
3
  require "fontist/formula_picker"
4
+ require "fontist/fontconfig"
4
5
 
5
6
  module Fontist
6
7
  class Font
@@ -14,6 +15,8 @@ module Fontist
14
15
  @smallest = options[:smallest]
15
16
  @newest = options[:newest]
16
17
  @size_limit = options[:size_limit]
18
+ @by_formula = options[:formula]
19
+ @update_fontconfig = options[:update_fontconfig]
17
20
 
18
21
  check_or_create_fontist_path!
19
22
  end
@@ -48,6 +51,8 @@ module Fontist
48
51
  end
49
52
 
50
53
  def install
54
+ return install_formula if @by_formula
55
+
51
56
  (find_system_font unless @force) || download_font || manual_font ||
52
57
  raise_non_supported_font
53
58
  end
@@ -103,6 +108,22 @@ module Fontist
103
108
  end
104
109
  end
105
110
 
111
+ def install_formula
112
+ download_formula || raise_formula_not_found
113
+ end
114
+
115
+ def download_formula
116
+ formula = Formula.find_by_key(@name)
117
+ return unless formula
118
+ return unless formula.downloadable?
119
+
120
+ request_formula_installation(formula)
121
+ end
122
+
123
+ def raise_formula_not_found
124
+ raise Errors::FormulaNotFoundError.new(@name)
125
+ end
126
+
106
127
  def font_installer(formula)
107
128
  FontInstaller.new(formula, no_progress: @no_progress)
108
129
  end
@@ -147,14 +168,22 @@ module Fontist
147
168
  def download_font
148
169
  return if sufficient_formulas.empty?
149
170
 
150
- sufficient_formulas.flat_map do |formula|
151
- confirmation = check_and_confirm_required_license(formula)
152
- paths = font_installer(formula).install(confirmation: confirmation)
171
+ paths = sufficient_formulas.flat_map do |formula|
172
+ request_formula_installation(formula)
173
+ end
153
174
 
154
- Fontist.ui.say("Fonts installed at:")
155
- paths.each do |path|
156
- Fontist.ui.say("- #{path}")
157
- end
175
+ update_fontconfig
176
+
177
+ paths
178
+ end
179
+
180
+ def request_formula_installation(formula)
181
+ confirmation = check_and_confirm_required_license(formula)
182
+ paths = font_installer(formula).install(confirmation: confirmation)
183
+
184
+ Fontist.ui.say("Fonts installed at:")
185
+ paths.each do |path|
186
+ Fontist.ui.say("- #{path}")
158
187
  end
159
188
  end
160
189
 
@@ -197,6 +226,12 @@ module Fontist
197
226
  MSG
198
227
  end
199
228
 
229
+ def update_fontconfig
230
+ return unless @update_fontconfig
231
+
232
+ Fontconfig.update
233
+ end
234
+
200
235
  def manual_font
201
236
  return if manual_formulas.empty?
202
237
 
@@ -0,0 +1,87 @@
1
+ module Fontist
2
+ class Fontconfig
3
+ def self.update
4
+ new.update
5
+ end
6
+
7
+ def self.remove(options = {})
8
+ new(options).remove
9
+ end
10
+
11
+ def initialize(options = {})
12
+ @options = options
13
+ end
14
+
15
+ def update
16
+ ensure_fontconfig_installed
17
+ create_config
18
+ regenerate_fontconfig_cache
19
+ end
20
+
21
+ def remove
22
+ return handle_file_not_found unless config_exists?
23
+
24
+ regenerate_fontconfig_cache if fontconfig_installed?
25
+ remove_config
26
+ end
27
+
28
+ private
29
+
30
+ def ensure_fontconfig_installed
31
+ raise Errors::FontconfigNotFoundError unless fontconfig_installed?
32
+ end
33
+
34
+ def fontconfig_installed?
35
+ Utils::System.fontconfig_installed?
36
+ end
37
+
38
+ def create_config
39
+ return if File.exist?(config_path)
40
+
41
+ FileUtils.mkdir_p(File.dirname(config_path))
42
+ File.write(config_path, config_content)
43
+ end
44
+
45
+ def config_path
46
+ File.join(xdg_config_home, "fontconfig", "conf.d", "10-fontist.conf")
47
+ end
48
+
49
+ def xdg_config_home
50
+ ENV["XDG_CONFIG_HOME"] || File.join(Dir.home, ".config")
51
+ end
52
+
53
+ def config_content
54
+ <<~CONTENT
55
+ <?xml version='1.0'?>
56
+ <!DOCTYPE fontconfig SYSTEM 'fonts.dtd'>
57
+ <fontconfig>
58
+ <dir>#{Fontist.fonts_path}</dir>
59
+ </fontconfig>
60
+ CONTENT
61
+ end
62
+
63
+ def regenerate_fontconfig_cache
64
+ Helpers.run("fc-cache -f")
65
+ end
66
+
67
+ def ensure_file_exists
68
+ return if @options[:force]
69
+
70
+ raise Errors::FontconfigFileNotFoundError unless File.exist?(config_path)
71
+ end
72
+
73
+ def config_exists?
74
+ File.exist?(config_path)
75
+ end
76
+
77
+ def handle_file_not_found
78
+ return if @options[:force]
79
+
80
+ raise Errors::FontconfigFileNotFoundError
81
+ end
82
+
83
+ def remove_config
84
+ FileUtils.rm(config_path)
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,29 @@
1
+ module Fontist
2
+ class FontconfigCLI < Thor
3
+ include CLI::ClassOptions
4
+
5
+ desc "update", "Update fontconfig configuration to use fontist fonts"
6
+ def update
7
+ handle_class_options(options)
8
+ Fontconfig.update
9
+ Fontist.ui.success("Fontconfig file has been successfully updated.")
10
+ CLI::STATUS_SUCCESS
11
+ rescue Errors::FontconfigNotFoundError => e
12
+ Fontist.ui.error(e.message)
13
+ CLI::STATUS_FONTCONFIG_NOT_FOUND
14
+ end
15
+
16
+ desc "remove", "Remove fontist file in fontconfig configuration"
17
+ option :force, type: :boolean, aliases: :f,
18
+ desc: "Proceed even if does not exist"
19
+ def remove
20
+ handle_class_options(options)
21
+ Fontconfig.remove(options)
22
+ Fontist.ui.success("Fontconfig file has been successfully removed.")
23
+ CLI::STATUS_SUCCESS
24
+ rescue Errors::FontconfigFileNotFoundError => e
25
+ Fontist.ui.error(e.message)
26
+ CLI::STATUS_FONTCONFIG_FILE_NOT_FOUND
27
+ end
28
+ end
29
+ end
@@ -45,6 +45,13 @@ module Fontist
45
45
  end.flatten
46
46
  end
47
47
 
48
+ def self.find_by_key(key)
49
+ path = Fontist.formulas_path.join("#{key}.yml")
50
+ return unless File.exist?(path)
51
+
52
+ new_from_file(path)
53
+ end
54
+
48
55
  def self.new_from_file(path)
49
56
  data = YAML.load_file(path)
50
57
  new(data, path)
@@ -72,7 +79,7 @@ module Fontist
72
79
  end
73
80
 
74
81
  def key
75
- @data["key"] || default_key
82
+ key_from_path
76
83
  end
77
84
 
78
85
  def description
@@ -137,7 +144,7 @@ module Fontist
137
144
 
138
145
  private
139
146
 
140
- def default_key
147
+ def key_from_path
141
148
  escaped = Regexp.escape(Fontist.formulas_path.to_s + "/")
142
149
  @path.sub(Regexp.new("^" + escaped), "").sub(/\.yml$/, "")
143
150
  end
@@ -3,5 +3,26 @@ module Fontist
3
3
  def self.parse_to_object(data)
4
4
  JSON.parse(data.to_json, object_class: OpenStruct)
5
5
  end
6
+
7
+ def self.run(command)
8
+ Fontist.ui.debug("Run `#{command}`")
9
+
10
+ result = `#{command}`
11
+ unless $CHILD_STATUS.to_i.zero?
12
+ raise Errors::BinaryCallError,
13
+ "Failed to run #{command}, status: #{$CHILD_STATUS}"
14
+ end
15
+
16
+ result
17
+ end
18
+
19
+ def self.silence_stream(stream)
20
+ old_stream = stream.dup
21
+ stream.reopen(RbConfig::CONFIG["host_os"] =~ /mswin|mingw/ ? "NUL:" : "/dev/null") # rubocop:disable Performance/RegexpMatch, Metrics/LineLength
22
+ stream.sync = true
23
+ yield
24
+ ensure
25
+ stream.reopen(old_stream)
26
+ end
6
27
  end
7
28
  end
@@ -55,12 +55,32 @@ module Fontist
55
55
  end
56
56
 
57
57
  def save(builder)
58
- filename = Import.name_to_filename(builder.name)
59
- path = @options[:formula_dir] ? File.join(@options[:formula_dir], filename) : filename
58
+ path = vacant_path
60
59
  yaml = YAML.dump(Helpers::HashHelper.stringify_keys(builder.formula))
61
60
  File.write(path, yaml)
62
61
  path
63
62
  end
63
+
64
+ def vacant_path
65
+ path = path_from_name
66
+ return path unless @options[:keep_existing] && File.exist?(path)
67
+
68
+ 2.upto(9) do |i|
69
+ candidate = path.sub(/\.yml$/, "#{i}.yml")
70
+ return candidate unless File.exist?(candidate)
71
+ end
72
+
73
+ raise Errors::GeneralError, "Formula #{path} already exists."
74
+ end
75
+
76
+ def path_from_name
77
+ filename = Import.name_to_filename(builder.name)
78
+ if @options[:formula_dir]
79
+ File.join(@options[:formula_dir], filename)
80
+ else
81
+ filename
82
+ end
83
+ end
64
84
  end
65
85
  end
66
86
  end
@@ -26,9 +26,9 @@ module Fontist
26
26
 
27
27
  def read
28
28
  switch_to_temp_dir do |tmp_dir|
29
- extract_ttfs(tmp_dir).map do |path|
30
- Otf::FontFile.new(path)
31
- end
29
+ extract_ttfs(tmp_dir)
30
+ .map { |path| Otf::FontFile.new(path) }
31
+ .reject { |font_file| hidden_or_pua_encoded?(font_file) }
32
32
  end
33
33
  end
34
34
 
@@ -47,6 +47,10 @@ module Fontist
47
47
  end
48
48
  end
49
49
 
50
+ def hidden_or_pua_encoded?(font_file)
51
+ font_file.family_name.start_with?(".")
52
+ end
53
+
50
54
  def detect_extension
51
55
  base_extension = "ttc"
52
56
 
@@ -4,9 +4,10 @@ require_relative "text_helper"
4
4
  module Fontist
5
5
  module Import
6
6
  class FormulaBuilder
7
- FORMULA_ATTRIBUTES = %i[description homepage resources
7
+ FORMULA_ATTRIBUTES = %i[platforms description homepage resources
8
8
  font_collections fonts extract copyright
9
- license_url open_license digest command].freeze
9
+ license_url requires_license_agreement
10
+ open_license digest command].freeze
10
11
 
11
12
  attr_writer :archive,
12
13
  :url,
@@ -28,9 +29,15 @@ module Fontist
28
29
  def name
29
30
  return @options[:name] if @options[:name]
30
31
 
31
- unique_names = both_fonts.map(&:family_name).uniq
32
- TextHelper.longest_common_prefix(unique_names) ||
33
- both_fonts.first.family_name
32
+ common = %i[family_name type]
33
+ .map { |attr| both_fonts.map(&attr).uniq }
34
+ .map { |names| TextHelper.longest_common_prefix(names) }
35
+ .map { |prefix| prefix unless prefix == "Regular" }
36
+ .compact
37
+ .join(" ")
38
+ return common unless common.empty?
39
+
40
+ both_fonts.map(&:family_name).first
34
41
  end
35
42
 
36
43
  private
@@ -50,6 +57,10 @@ module Fontist
50
57
  files
51
58
  end
52
59
 
60
+ def platforms
61
+ @options[:platforms]
62
+ end
63
+
53
64
  def description
54
65
  name
55
66
  end
@@ -175,12 +186,17 @@ module Fontist
175
186
  both_fonts.map(&:license_url).compact.first
176
187
  end
177
188
 
189
+ def requires_license_agreement
190
+ @options[:requires_license_agreement]
191
+ end
192
+
178
193
  def open_license
179
- unless @license_text
194
+ unless @license_text || requires_license_agreement
180
195
  Fontist.ui.error("WARN: please add license manually")
181
- return
182
196
  end
183
197
 
198
+ return unless @license_text
199
+
184
200
  Fontist.ui.error("WARN: ensure it's an open license, otherwise " \
185
201
  "change the 'open_license' attribute to " \
186
202
  "'requires_license_agreement'")
@@ -9,3 +9,4 @@
9
9
  - Andika
10
10
  - Harmattan
11
11
  - Padauk
12
+ - STIX Two Math
@@ -4,15 +4,7 @@ module Fontist
4
4
  module SystemHelper
5
5
  class << self
6
6
  def run(command)
7
- Fontist.ui.say("Run `#{command}`") if Fontist.debug?
8
-
9
- result = `#{command}`
10
- unless $CHILD_STATUS.to_i.zero?
11
- raise Errors::BinaryCallError,
12
- "Failed to run #{command}, status: #{$CHILD_STATUS}"
13
- end
14
-
15
- result
7
+ Fontist::Helpers.run(command)
16
8
  end
17
9
  end
18
10
  end