fontist 1.20.0 → 1.21.2

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.
@@ -1,49 +1,70 @@
1
1
  require "shellwords"
2
2
  require_relative "text_helper"
3
+ require_relative "helpers/hash_helper"
3
4
 
4
5
  module Fontist
5
6
  module Import
6
7
  class FormulaBuilder
7
- FORMULA_ATTRIBUTES = %i[platforms description homepage resources
8
+ FORMULA_ATTRIBUTES = %i[name platforms description homepage resources
8
9
  font_collections fonts extract copyright
9
10
  license_url requires_license_agreement
10
11
  open_license digest command].freeze
11
12
 
12
- attr_writer :archive,
13
- :url,
14
- :extractor,
13
+ attr_writer :resources,
15
14
  :options,
16
15
  :font_files,
17
16
  :font_collection_files,
18
17
  :license_text,
19
- :homepage
18
+ :operations
20
19
 
21
20
  def initialize
22
21
  @options = {}
22
+ @font_files = []
23
+ @font_collection_files = []
23
24
  end
24
25
 
25
26
  def formula
26
27
  formula_attributes.map { |name| [name, send(name)] }.to_h.compact
27
28
  end
28
29
 
30
+ def save
31
+ path = vacant_path
32
+ yaml = YAML.dump(Helpers::HashHelper.stringify_keys(formula))
33
+ File.write(path, yaml)
34
+ path
35
+ end
36
+
37
+ private
38
+
39
+ def formula_attributes
40
+ FORMULA_ATTRIBUTES
41
+ end
42
+
29
43
  def name
44
+ @name ||= generate_name
45
+ end
46
+
47
+ def generate_name
30
48
  return @options[:name] if @options[:name]
31
49
 
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(" ")
50
+ common = common_prefix
38
51
  return common unless common.empty?
39
52
 
40
53
  both_fonts.map(&:family_name).first
41
54
  end
42
55
 
43
- private
56
+ def common_prefix
57
+ family_prefix = common_prefix_by_attr(:family_name)
58
+ style_prefix = common_prefix_by_attr(:type)
44
59
 
45
- def formula_attributes
46
- FORMULA_ATTRIBUTES
60
+ [family_prefix, style_prefix].compact.join(" ")
61
+ end
62
+
63
+ def common_prefix_by_attr(attr)
64
+ names = both_fonts.map(&attr).uniq
65
+ prefix = TextHelper.longest_common_prefix(names)
66
+
67
+ prefix unless prefix == "Regular"
47
68
  end
48
69
 
49
70
  def both_fonts
@@ -70,69 +91,7 @@ module Fontist
70
91
  end
71
92
 
72
93
  def resources
73
- filename = name.gsub(" ", "_") + "." + @extractor.extension
74
-
75
- { filename => resource_options }
76
- end
77
-
78
- def resource_options
79
- if @options[:skip_sha]
80
- resource_options_without_sha
81
- else
82
- resource_options_with_sha
83
- end
84
- end
85
-
86
- def resource_options_without_sha
87
- { urls: [@url] + mirrors, file_size: file_size }
88
- end
89
-
90
- def resource_options_with_sha
91
- urls = []
92
- sha = []
93
- downloads do |url, path|
94
- urls << url
95
- sha << Digest::SHA256.file(path).to_s
96
- end
97
-
98
- sha = prepare_sha256(sha)
99
-
100
- { urls: urls, sha256: sha, file_size: file_size }
101
- end
102
-
103
- def downloads
104
- yield @url, @archive
105
-
106
- mirrors.each do |url|
107
- path = download(url)
108
- next unless path
109
-
110
- yield url, path
111
- end
112
- end
113
-
114
- def mirrors
115
- @options[:mirror] || []
116
- end
117
-
118
- def download(url)
119
- Fontist::Utils::Downloader.download(url, progress_bar: true).path
120
- rescue Errors::InvalidResourceError
121
- Fontist.ui.error("WARN: a mirror is not found '#{url}'")
122
- nil
123
- end
124
-
125
- def prepare_sha256(input)
126
- output = input.uniq
127
- return output.first if output.size == 1
128
-
129
- checksums = output.join(", ")
130
- Fontist.ui.error("WARN: SHA256 differs (#{checksums})")
131
- output
132
- end
133
-
134
- def file_size
135
- File.size(@archive)
94
+ @resources || raise("Resources should be set.")
136
95
  end
137
96
 
138
97
  def font_collections
@@ -175,7 +134,7 @@ module Fontist
175
134
  end
176
135
 
177
136
  def extract
178
- @extractor.operations
137
+ @operations || {}
179
138
  end
180
139
 
181
140
  def copyright
@@ -197,9 +156,11 @@ module Fontist
197
156
 
198
157
  return unless @license_text
199
158
 
200
- Fontist.ui.error("WARN: ensure it's an open license, otherwise " \
201
- "change the 'open_license' attribute to " \
202
- "'requires_license_agreement'")
159
+ unless @options[:open_license]
160
+ Fontist.ui.error("WARN: ensure it's an open license, otherwise " \
161
+ "change the 'open_license' attribute to " \
162
+ "'requires_license_agreement'")
163
+ end
203
164
 
204
165
  TextHelper.cleanup(@license_text)
205
166
  end
@@ -211,6 +172,27 @@ module Fontist
211
172
  def command
212
173
  Shellwords.shelljoin(ARGV)
213
174
  end
175
+
176
+ def vacant_path
177
+ path = path_from_name
178
+ return path unless @options[:keep_existing] && File.exist?(path)
179
+
180
+ 2.upto(9) do |i|
181
+ candidate = path.sub(/\.yml$/, "#{i}.yml")
182
+ return candidate unless File.exist?(candidate)
183
+ end
184
+
185
+ raise Errors::GeneralError, "Formula #{path} already exists."
186
+ end
187
+
188
+ def path_from_name
189
+ filename = Import.name_to_filename(name)
190
+ if @options[:formula_dir]
191
+ File.join(@options[:formula_dir], filename)
192
+ else
193
+ filename
194
+ end
195
+ end
214
196
  end
215
197
  end
216
198
  end
@@ -0,0 +1,25 @@
1
+ module Fontist
2
+ module Import
3
+ module Google
4
+ class Api
5
+ class << self
6
+ def items
7
+ db["items"]
8
+ end
9
+
10
+ def db
11
+ @db ||= JSON.parse(Net::HTTP.get(URI(url)))
12
+ end
13
+
14
+ def url
15
+ "https://www.googleapis.com/webfonts/v1/webfonts?key=#{api_key}"
16
+ end
17
+
18
+ def api_key
19
+ Fontist.google_fonts_key
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,89 @@
1
+ require "fontist/import"
2
+ require "fontist/import/formula_builder"
3
+ require "fontist/import/otf/font_file"
4
+
5
+ module Fontist
6
+ module Import
7
+ module Google
8
+ class CreateGoogleFormula
9
+ REPO_PATH = Fontist.fontist_path.join("google", "fonts")
10
+ POSSIBLE_LICENSE_FILES = ["LICENSE.txt",
11
+ "LICENCE.txt",
12
+ "OFL.txt",
13
+ "UFL.txt"].freeze
14
+
15
+ def initialize(item, options = {})
16
+ @item = item
17
+ @options = options
18
+ end
19
+
20
+ def call
21
+ builder = FormulaBuilder.new
22
+ builder.options = options
23
+ builder.resources = resources
24
+ builder.font_files = font_files
25
+ builder.license_text = license_text
26
+ builder.save
27
+ end
28
+
29
+ private
30
+
31
+ def options
32
+ @options.merge(name: formula_name, open_license: true)
33
+ end
34
+
35
+ def formula_name
36
+ @item["family"]
37
+ end
38
+
39
+ def resources
40
+ {
41
+ @item["family"] => {
42
+ source: "google",
43
+ family: @item["family"],
44
+ files: @item["files"].values,
45
+ },
46
+ }
47
+ end
48
+
49
+ def font_files
50
+ @font_files ||= @item["files"].map do |_key, url|
51
+ font_file(url)
52
+ end
53
+ end
54
+
55
+ def license_text
56
+ @license_text ||= find_license_text
57
+ end
58
+
59
+ def font_file(url)
60
+ path = Utils::Downloader.download(url, use_content_length: false).path
61
+ Otf::FontFile.new(path)
62
+ end
63
+
64
+ def find_license_text
65
+ file = license_file
66
+ return unless file
67
+
68
+ File.read(file)
69
+ end
70
+
71
+ def license_file
72
+ dir = @item["family"].gsub(" ", "").downcase
73
+ path = repo_paths(dir).first
74
+ return unless path
75
+
76
+ full_paths = POSSIBLE_LICENSE_FILES.map { |f| File.join(path, f) }
77
+
78
+ Dir[*full_paths].first
79
+ end
80
+
81
+ def repo_paths(dir)
82
+ Dir[File.join(REPO_PATH, "apache", dir),
83
+ File.join(REPO_PATH, "ofl", dir),
84
+ File.join(REPO_PATH, "ufl", dir)]
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end
@@ -1,54 +1,92 @@
1
- require "erb"
2
1
  require_relative "google"
3
- require_relative "google/new_fonts_fetcher"
4
- require_relative "create_formula"
2
+ require_relative "google/api"
3
+ require_relative "google/create_google_formula"
5
4
 
6
5
  module Fontist
7
6
  module Import
8
7
  class GoogleImport
8
+ REPO_PATH = Fontist.fontist_path.join("google", "fonts")
9
+ REPO_URL = "https://github.com/google/fonts.git".freeze
10
+
9
11
  def initialize(options)
10
12
  @max_count = options[:max_count] || Google::DEFAULT_MAX_COUNT
11
13
  end
12
14
 
13
15
  def call
14
- fonts = new_fonts
15
- create_formulas(fonts)
16
- rebuild_index unless fonts.empty?
16
+ update_repo
17
+ count = update_formulas
18
+ rebuild_index if count.positive?
17
19
  end
18
20
 
19
21
  private
20
22
 
21
- def new_fonts
22
- Fontist::Import::Google::NewFontsFetcher.new(logging: true,
23
- limit: @max_count).call
23
+ def update_repo
24
+ if Dir.exist?(REPO_PATH)
25
+ `cd #{REPO_PATH} && git pull`
26
+ else
27
+ FileUtils.mkdir_p(File.dirname(REPO_PATH))
28
+ `git clone --depth 1 #{REPO_URL} #{REPO_PATH}`
29
+ end
24
30
  end
25
31
 
26
- def create_formulas(fonts)
27
- return puts("Nothing to update") if fonts.empty?
32
+ def update_formulas
33
+ Fontist.ui.say "Updating formulas..."
34
+
35
+ items = api_items
28
36
 
29
- puts "Creating formulas..."
30
- fonts.each do |path|
31
- create_formula(path)
37
+ count = 0
38
+ items.each do |item|
39
+ break if count >= @max_count
40
+
41
+ path = update_formula(item)
42
+ count += 1 if path
32
43
  end
44
+
45
+ count
46
+ end
47
+
48
+ def api_items
49
+ Google::Api.items
50
+ end
51
+
52
+ def update_formula(item)
53
+ family = item["family"]
54
+ Fontist.ui.say "Checking #{family}"
55
+ unless new_changes?(item)
56
+ Fontist.ui.say "Skip, no changes"
57
+ return
58
+ end
59
+
60
+ create_formula(item)
33
61
  end
34
62
 
35
- def create_formula(font_path)
36
- puts font_path
63
+ def new_changes?(item)
64
+ formula = formula(item["family"])
65
+ return true unless formula
37
66
 
38
- path = Fontist::Import::CreateFormula.new(
39
- url(font_path),
40
- name: Google.metadata_name(font_path),
67
+ item["files"].values != formula.resources.first.files
68
+ end
69
+
70
+ def formula(font_name)
71
+ path = formula_path(font_name)
72
+ Formula.new_from_file(path) if File.exist?(path)
73
+ end
74
+
75
+ def formula_path(name)
76
+ snake_case = name.downcase.gsub(" ", "_")
77
+ filename = "#{snake_case}.yml"
78
+ Fontist.formulas_path.join("google", filename)
79
+ end
80
+
81
+ def create_formula(item)
82
+ path = Google::CreateGoogleFormula.new(
83
+ item,
41
84
  formula_dir: formula_dir,
42
- skip_sha: variable_style?(font_path),
43
- digest: Google.digest(font_path),
44
85
  ).call
45
86
 
46
87
  Fontist.ui.success("Formula has been successfully created: #{path}")
47
- end
48
88
 
49
- def url(path)
50
- name = Google.metadata_name(path)
51
- "https://fonts.google.com/download?family=#{ERB::Util.url_encode(name)}"
89
+ path
52
90
  end
53
91
 
54
92
  def formula_dir
@@ -57,13 +95,6 @@ module Fontist
57
95
  end
58
96
  end
59
97
 
60
- def variable_style?(path)
61
- fonts = Dir.glob(File.join(path, "*.{ttf,otf}"))
62
- fonts.any? do |font|
63
- File.basename(font).match?(/\[(.+,)?(wght|opsz)\]/)
64
- end
65
- end
66
-
67
98
  def rebuild_index
68
99
  Fontist::Index.rebuild
69
100
  end
@@ -19,10 +19,6 @@ module Fontist
19
19
  save_operation_subdir
20
20
  end
21
21
 
22
- def extension
23
- fetch_extension(@archive)
24
- end
25
-
26
22
  def font_files
27
23
  ensure_extracted
28
24
  @font_files
@@ -52,18 +48,6 @@ module Fontist
52
48
  @operations[:options][:fonts_sub_dir] = @subdir
53
49
  end
54
50
 
55
- def fetch_extension(file)
56
- File.extname(filename(file)).sub(/^\./, "")
57
- end
58
-
59
- def filename(file)
60
- if file.respond_to?(:original_filename)
61
- file.original_filename
62
- else
63
- File.basename(file)
64
- end
65
- end
66
-
67
51
  def ensure_extracted
68
52
  return if @extracted
69
53
 
@@ -2,6 +2,8 @@ module Fontist
2
2
  module Manifest
3
3
  class Locations
4
4
  def initialize(manifest)
5
+ Fontist.ui.debug("Manifest: #{manifest}")
6
+
5
7
  @manifest = manifest
6
8
  end
7
9
 
@@ -0,0 +1,55 @@
1
+ module Fontist
2
+ module Resources
3
+ class ArchiveResource
4
+ def initialize(resource, options = {})
5
+ @resource = resource
6
+ @options = options
7
+ end
8
+
9
+ def files(_source_names, &block)
10
+ excavate.files(recursive_packages: true, &block)
11
+ end
12
+
13
+ private
14
+
15
+ def excavate
16
+ Excavate::Archive.new(archive.path)
17
+ end
18
+
19
+ def archive
20
+ download_file(@resource)
21
+ end
22
+
23
+ def download_file(source)
24
+ errors = []
25
+ source.urls.each do |request|
26
+ result = try_download_file(request, source)
27
+ return result unless result.is_a?(Errors::InvalidResourceError)
28
+
29
+ errors << result
30
+ end
31
+
32
+ raise Errors::InvalidResourceError, errors.join(" ")
33
+ end
34
+
35
+ def try_download_file(request, source)
36
+ info_log(request)
37
+
38
+ Fontist::Utils::Downloader.download(
39
+ request,
40
+ sha: source.sha256,
41
+ file_size: source.file_size,
42
+ progress_bar: !@options[:no_progress],
43
+ )
44
+ rescue Errors::InvalidResourceError => e
45
+ Fontist.ui.say(e.message)
46
+ e
47
+ end
48
+
49
+ def info_log(request)
50
+ url = request.respond_to?(:url) ? request.url : request
51
+ Fontist.ui.say(%(Downloading from #{url}))
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,64 @@
1
+ module Fontist
2
+ module Resources
3
+ class GoogleResource
4
+ def initialize(resource, options = {})
5
+ @resource = resource
6
+ @options = options
7
+ end
8
+
9
+ def files(source_names)
10
+ cached_paths = download_fonts(source_names)
11
+
12
+ cached_paths.map do |path|
13
+ Dir.mktmpdir do |dir|
14
+ FileUtils.cp(path, dir)
15
+
16
+ yield File.join(dir, File.basename(path))
17
+ end
18
+ end
19
+ end
20
+
21
+ private
22
+
23
+ def download_fonts(source_names)
24
+ urls = font_urls(source_names)
25
+
26
+ urls.map do |url|
27
+ download(url)
28
+ end
29
+ end
30
+
31
+ def font_urls(source_names)
32
+ @resource.files.select do |url|
33
+ source_names.include?(path_to_source_file(url))
34
+ end
35
+ end
36
+
37
+ def path_to_source_file(path)
38
+ format_filename(File.basename(path))
39
+ end
40
+
41
+ # TODO: remove duplication, another in Cache
42
+ def format_filename(filename)
43
+ return filename unless filename.length > 255
44
+
45
+ ext = File.extname(filename)
46
+ target_size = 255 - ext.length
47
+ cut_filename = filename.slice(0, target_size)
48
+ "#{cut_filename}#{ext}"
49
+ end
50
+
51
+ def download(url)
52
+ Fontist.ui.say(%(Downloading from #{url}))
53
+
54
+ file = Utils::Downloader.download(
55
+ url,
56
+ use_content_length: false,
57
+ progress_bar: !@options[:no_progress],
58
+ )
59
+
60
+ file.path
61
+ end
62
+ end
63
+ end
64
+ end
@@ -4,6 +4,10 @@ module Fontist
4
4
  @text = text
5
5
  end
6
6
 
7
+ def to_s
8
+ value.join(" . ")
9
+ end
10
+
7
11
  def value
8
12
  @value ||= numbers || default_value
9
13
  end
@@ -1,6 +1,8 @@
1
1
  module Fontist
2
2
  module Utils
3
3
  class Cache
4
+ MAX_FILENAME_SIZE = 255
5
+
4
6
  include Locking
5
7
 
6
8
  def self.lock_path(path)
@@ -110,6 +112,11 @@ module Fontist
110
112
  end
111
113
 
112
114
  def filename(source)
115
+ filename = response_to_filename(source)
116
+ format_filename(filename)
117
+ end
118
+
119
+ def response_to_filename(source)
113
120
  if File.extname(source.original_filename).empty? && source.content_type
114
121
  require "mime/types"
115
122
  ext = MIME::Types[source.content_type].first&.preferred_extension
@@ -119,6 +126,15 @@ module Fontist
119
126
  source.original_filename
120
127
  end
121
128
 
129
+ def format_filename(filename)
130
+ return filename unless filename.length > MAX_FILENAME_SIZE
131
+
132
+ ext = File.extname(filename)
133
+ target_size = MAX_FILENAME_SIZE - ext.length
134
+ cut_filename = filename.slice(0, target_size)
135
+ "#{cut_filename}#{ext}"
136
+ end
137
+
122
138
  def move(source_file, target_path)
123
139
  # Windows requires file descriptors to be closed before files are moved
124
140
  source_file.close
@@ -10,12 +10,17 @@ module Fontist
10
10
  ruby2_keywords :download if respond_to?(:ruby2_keywords, true)
11
11
  end
12
12
 
13
- def initialize(file, file_size: nil, sha: nil, progress_bar: nil)
13
+ def initialize(file,
14
+ file_size: nil,
15
+ sha: nil,
16
+ progress_bar: nil,
17
+ use_content_length: true)
14
18
  # TODO: If the first mirror fails, try the second one
15
19
  @file = file
16
20
  @sha = [sha].flatten.compact
17
21
  @file_size = file_size.to_i if file_size
18
22
  @progress_bar = progress_bar
23
+ @use_content_length = use_content_length
19
24
  @cache = Cache.new
20
25
  end
21
26
 
@@ -85,7 +90,9 @@ module Fontist
85
90
  max_redirects: 10,
86
91
  headers: headers,
87
92
  content_length_proc: ->(content_length) {
88
- progress_bar.total = content_length if content_length
93
+ if @use_content_length && content_length
94
+ progress_bar.total = content_length
95
+ end
89
96
  },
90
97
  progress_proc: -> (progress) {
91
98
  progress_bar.increment(progress)