fontist 1.20.0 → 1.21.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -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)