fontist 1.19.0 → 1.21.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/deploy-pages.yml +48 -0
  3. data/.github/workflows/tebako-pack.yml +61 -0
  4. data/.github/workflows/test-and-release.yml +2 -1
  5. data/LICENSE.txt +1 -2
  6. data/README.adoc +24 -2
  7. data/docs/.gitignore +136 -0
  8. data/docs/.vitepress/config.ts +83 -0
  9. data/docs/guide/api-ruby.md +190 -0
  10. data/docs/guide/ci.md +29 -0
  11. data/docs/guide/fontconfig.md +23 -0
  12. data/docs/guide/index.md +67 -0
  13. data/docs/guide/proxy.md +47 -0
  14. data/docs/guide/why.md +7 -0
  15. data/docs/index.md +40 -0
  16. data/docs/package-lock.json +1791 -0
  17. data/docs/package.json +17 -0
  18. data/docs/public/hero.png +0 -0
  19. data/docs/public/logo.png +0 -0
  20. data/docs/reference/index.md +143 -0
  21. data/exe/fontist +1 -2
  22. data/fontist.gemspec +3 -0
  23. data/lib/fontist/cli/class_options.rb +7 -0
  24. data/lib/fontist/cli/thor_ext.rb +79 -0
  25. data/lib/fontist/cli.rb +2 -0
  26. data/lib/fontist/config.rb +2 -1
  27. data/lib/fontist/font.rb +55 -10
  28. data/lib/fontist/font_installer.rb +22 -51
  29. data/lib/fontist/formula.rb +77 -3
  30. data/lib/fontist/formula_suggestion.rb +55 -0
  31. data/lib/fontist/helpers.rb +2 -0
  32. data/lib/fontist/import/create_formula.rb +77 -35
  33. data/lib/fontist/import/formula_builder.rb +63 -81
  34. data/lib/fontist/import/google/api.rb +25 -0
  35. data/lib/fontist/import/google/create_google_formula.rb +89 -0
  36. data/lib/fontist/import/google_import.rb +63 -32
  37. data/lib/fontist/import/recursive_extraction.rb +0 -16
  38. data/lib/fontist/manifest/locations.rb +2 -0
  39. data/lib/fontist/resources/archive_resource.rb +55 -0
  40. data/lib/fontist/resources/google_resource.rb +64 -0
  41. data/lib/fontist/style_version.rb +4 -0
  42. data/lib/fontist/utils/cache.rb +16 -0
  43. data/lib/fontist/utils/downloader.rb +9 -2
  44. data/lib/fontist/utils/ui.rb +10 -2
  45. data/lib/fontist/version.rb +1 -1
  46. data/lib/fontist.rb +13 -1
  47. metadata +67 -6
  48. data/lib/fontist/import/google/new_fonts_fetcher.rb +0 -146
  49. data/lib/fontist/import/google/skiplist.yml +0 -12
  50. data/lib/fontist/import/google_check.rb +0 -27
@@ -0,0 +1,55 @@
1
+ require "fuzzy_match"
2
+
3
+ module Fontist
4
+ class FormulaSuggestion
5
+ MINIMUM_REQUIRED_SCORE = 0.6
6
+
7
+ def initialize
8
+ @fuzzy_match = prepare_search_engine
9
+ end
10
+
11
+ def find(name)
12
+ @fuzzy_match.find_all_with_score(normalize(name))
13
+ .tap { |res| Fontist.ui.debug(prettify_result(res)) }
14
+ .select { |_key, score, _| score >= MINIMUM_REQUIRED_SCORE }
15
+ .take(10)
16
+ .map(&:first)
17
+ .map { |x| Formula.find_by_key_or_name(x) }
18
+ .select(&:downloadable?)
19
+ end
20
+
21
+ private
22
+
23
+ def normalize(name)
24
+ name.gsub(" ", "_")
25
+ end
26
+
27
+ def prepare_search_engine
28
+ dict = Formula.all_keys
29
+ stop_words = namespaces(dict).map { |ns| /^#{Regexp.escape(ns)}/i }
30
+
31
+ FuzzyMatch.new(dict, stop_words: stop_words)
32
+ end
33
+
34
+ def namespaces(keys)
35
+ keys.map do |key|
36
+ parts = key.split("/")
37
+ parts.size
38
+ parts.take(parts.size - 1).join("/")
39
+ end.uniq
40
+ end
41
+
42
+ def prettify_result(result)
43
+ list = result.map do |key, dice, leve|
44
+ sprintf(
45
+ "%<dice>.3f %<leve>.3f %<key>s",
46
+ dice: dice,
47
+ leve: leve,
48
+ key: key,
49
+ )
50
+ end
51
+
52
+ "FuzzyMatch:\n#{list.join("\n")}"
53
+ end
54
+ end
55
+ end
@@ -1,3 +1,5 @@
1
+ require "ostruct"
2
+
1
3
  module Fontist
2
4
  module Helpers
3
5
  def self.parse_to_object(data)
@@ -1,6 +1,5 @@
1
1
  require "fontist/import"
2
2
  require_relative "recursive_extraction"
3
- require_relative "helpers/hash_helper"
4
3
  require_relative "formula_builder"
5
4
 
6
5
  module Fontist
@@ -12,31 +11,102 @@ module Fontist
12
11
  end
13
12
 
14
13
  def call
15
- save(builder)
14
+ builder.save
16
15
  end
17
16
 
18
17
  private
19
18
 
20
19
  def builder
21
20
  builder = FormulaBuilder.new
22
- setup_strings(builder, archive)
21
+ setup_strings(builder)
23
22
  setup_files(builder)
24
23
  builder
25
24
  end
26
25
 
27
- def setup_strings(builder, archive)
28
- builder.archive = archive
29
- builder.url = @url
26
+ def setup_strings(builder)
30
27
  builder.options = @options
28
+ builder.resources = resources
31
29
  end
32
30
 
33
31
  def setup_files(builder)
34
- builder.extractor = extractor
32
+ builder.operations = extractor.operations
35
33
  builder.font_files = extractor.font_files
36
34
  builder.font_collection_files = extractor.font_collection_files
37
35
  builder.license_text = extractor.license_text
38
36
  end
39
37
 
38
+ def resources
39
+ @resources ||= { filename(archive) => resource_options }
40
+ end
41
+
42
+ def filename(file)
43
+ if file.respond_to?(:original_filename)
44
+ file.original_filename
45
+ else
46
+ File.basename(file)
47
+ end
48
+ end
49
+
50
+ def resource_options
51
+ if @options[:skip_sha]
52
+ resource_options_without_sha
53
+ else
54
+ resource_options_with_sha
55
+ end
56
+ end
57
+
58
+ def resource_options_without_sha
59
+ { urls: [@url] + mirrors, file_size: file_size }
60
+ end
61
+
62
+ def resource_options_with_sha
63
+ urls = []
64
+ sha = []
65
+ downloads do |url, path|
66
+ urls << url
67
+ sha << Digest::SHA256.file(path).to_s
68
+ end
69
+
70
+ sha = prepare_sha256(sha)
71
+
72
+ { urls: urls, sha256: sha, file_size: file_size }
73
+ end
74
+
75
+ def downloads
76
+ yield @url, archive
77
+
78
+ mirrors.each do |url|
79
+ path = download_mirror(url)
80
+ next unless path
81
+
82
+ yield url, path
83
+ end
84
+ end
85
+
86
+ def mirrors
87
+ @options[:mirror] || []
88
+ end
89
+
90
+ def download_mirror(url)
91
+ Fontist::Utils::Downloader.download(url, progress_bar: true).path
92
+ rescue Errors::InvalidResourceError
93
+ Fontist.ui.error("WARN: a mirror is not found '#{url}'")
94
+ nil
95
+ end
96
+
97
+ def prepare_sha256(input)
98
+ output = input.uniq
99
+ return output.first if output.size == 1
100
+
101
+ checksums = output.join(", ")
102
+ Fontist.ui.error("WARN: SHA256 differs (#{checksums})")
103
+ output
104
+ end
105
+
106
+ def file_size
107
+ File.size(archive)
108
+ end
109
+
40
110
  def extractor
41
111
  @extractor ||=
42
112
  RecursiveExtraction.new(archive,
@@ -53,34 +123,6 @@ module Fontist
53
123
 
54
124
  Fontist::Utils::Downloader.download(url, progress_bar: true).path
55
125
  end
56
-
57
- def save(builder)
58
- path = vacant_path
59
- yaml = YAML.dump(Helpers::HashHelper.stringify_keys(builder.formula))
60
- File.write(path, yaml)
61
- path
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
84
126
  end
85
127
  end
86
128
  end
@@ -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