fontist 1.11.7 → 1.13.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,126 @@
1
+ require "fontist/style_version"
2
+
3
+ module Fontist
4
+ class FormulaPicker
5
+ def initialize(font_name, size_limit:, version:, smallest:, newest:)
6
+ @font_name = font_name
7
+ @size_limit = size_limit || Fontist.formula_size_limit_in_megabytes
8
+ @version = version
9
+ @smallest = smallest
10
+ @newest = newest
11
+ end
12
+
13
+ def call(formulas)
14
+ return [] if formulas.size.zero?
15
+ return formulas if contain_different_styles?(formulas)
16
+ return by_version(formulas) if version_is_passed?
17
+ return newest(formulas) if newest_is_passed?
18
+ return smallest(formulas) if smallest_is_passed?
19
+
20
+ default_way(formulas)
21
+ end
22
+
23
+ private
24
+
25
+ def version_is_passed?
26
+ !@version.nil?
27
+ end
28
+
29
+ def by_version(formulas)
30
+ formulas.each do |formula|
31
+ fonts = formula.fonts_by_name(@font_name)
32
+ fonts.each do |font|
33
+ font.styles.each do |style|
34
+ version = StyleVersion.new(style.version)
35
+ return [formula] if version == passed_version
36
+ end
37
+ end
38
+ end
39
+
40
+ []
41
+ end
42
+
43
+ def passed_version
44
+ @passed_version ||= StyleVersion.new(@version)
45
+ end
46
+
47
+ def newest_is_passed?
48
+ @newest
49
+ end
50
+
51
+ def newest(formulas)
52
+ newest_formulas = filter_by_max_version(formulas)
53
+ smallest(newest_formulas)
54
+ end
55
+
56
+ def smallest_is_passed?
57
+ @smallest
58
+ end
59
+
60
+ def smallest(formulas)
61
+ [choose_smallest_formula(formulas)]
62
+ end
63
+
64
+ def default_way(formulas)
65
+ size_limited_formulas = filter_by_size_limit(formulas)
66
+ raise_size_limit_error if size_limited_formulas.empty?
67
+ newest(size_limited_formulas)
68
+ end
69
+
70
+ def contain_different_styles?(formulas)
71
+ styles_by_formula = formulas.map do |formula|
72
+ fonts = formula.fonts_by_name(@font_name)
73
+ styles = fonts.flat_map do |font|
74
+ font.styles.map(&:type)
75
+ end
76
+
77
+ styles.uniq.sort
78
+ end
79
+
80
+ styles_by_formula.uniq.size > 1
81
+ end
82
+
83
+ def filter_by_size_limit(formulas)
84
+ formulas.select do |formula|
85
+ formula.file_size.nil? || formula.file_size < size_limit_in_bytes
86
+ end
87
+ end
88
+
89
+ def size_limit_in_bytes
90
+ @size_limit_in_bytes ||= @size_limit * 1024 * 1024
91
+ end
92
+
93
+ def raise_size_limit_error
94
+ raise Errors::SizeLimitError,
95
+ "There are only formulas above the size limit " \
96
+ "(#{@size_limit} MB)."
97
+ end
98
+
99
+ def filter_by_max_version(formulas)
100
+ formulas_with_version = detect_formula_version(formulas)
101
+ max_version = formulas_with_version.map(&:first).max
102
+ formulas_with_version.select do |version, _formula|
103
+ version == max_version
104
+ end.map(&:last)
105
+ end
106
+
107
+ def detect_formula_version(formulas)
108
+ formulas.map do |formula|
109
+ fonts = formula.fonts_by_name(@font_name)
110
+ versions = fonts.flat_map do |font|
111
+ font.styles.map do |style|
112
+ StyleVersion.new(style.version)
113
+ end
114
+ end
115
+
116
+ [versions.max, formula]
117
+ end
118
+ end
119
+
120
+ def choose_smallest_formula(formulas)
121
+ formulas.min_by do |formula|
122
+ formula.file_size || 0
123
+ end
124
+ end
125
+ end
126
+ end
@@ -1,6 +1,6 @@
1
1
  module Fontist
2
2
  class GoogleCLI < Thor
3
- class_option :formulas_path, type: :string, desc: "Path to formulas"
3
+ include CLI::ClassOptions
4
4
 
5
5
  desc "check", "Check Google fonts for updates"
6
6
  def check
@@ -17,13 +17,5 @@ module Fontist
17
17
  Fontist::Import::GoogleImport.new.call
18
18
  CLI::STATUS_SUCCESS
19
19
  end
20
-
21
- private
22
-
23
- def handle_class_options(options)
24
- if options[:formulas_path]
25
- Fontist.formulas_path = Pathname.new(options[:formulas_path])
26
- end
27
- end
28
20
  end
29
21
  end
@@ -1,7 +1,5 @@
1
1
  require "fontist/import"
2
2
  require_relative "recursive_extraction"
3
- require_relative "otf/font_file"
4
- require_relative "files/collection_file"
5
3
  require_relative "helpers/hash_helper"
6
4
  require_relative "formula_builder"
7
5
 
@@ -14,21 +12,29 @@ module Fontist
14
12
  end
15
13
 
16
14
  def call
17
- save(formula)
15
+ save(builder)
18
16
  end
19
17
 
20
18
  private
21
19
 
22
- def formula
20
+ def builder
23
21
  builder = FormulaBuilder.new
24
- builder.url = @url
22
+ setup_strings(builder, archive)
23
+ setup_files(builder)
24
+ builder
25
+ end
26
+
27
+ def setup_strings(builder, archive)
25
28
  builder.archive = archive
26
- builder.extractor = extractor
29
+ builder.url = @url
27
30
  builder.options = @options
31
+ end
32
+
33
+ def setup_files(builder)
34
+ builder.extractor = extractor
28
35
  builder.font_files = extractor.font_files
29
36
  builder.font_collection_files = extractor.font_collection_files
30
37
  builder.license_text = extractor.license_text
31
- builder.formula
32
38
  end
33
39
 
34
40
  def extractor
@@ -48,10 +54,10 @@ module Fontist
48
54
  Fontist::Utils::Downloader.download(url, progress_bar: true).path
49
55
  end
50
56
 
51
- def save(hash)
52
- filename = Import.name_to_filename(hash[:name])
57
+ def save(builder)
58
+ filename = Import.name_to_filename(builder.name)
53
59
  path = @options[:formula_dir] ? File.join(@options[:formula_dir], filename) : filename
54
- yaml = YAML.dump(Helpers::HashHelper.stringify_keys(hash))
60
+ yaml = YAML.dump(Helpers::HashHelper.stringify_keys(builder.formula))
55
61
  File.write(path, yaml)
56
62
  path
57
63
  end
@@ -9,11 +9,13 @@ module Fontist
9
9
  FONT_LABELS = ["OpenType font data",
10
10
  "TrueType Font data"].freeze
11
11
 
12
- COLLECTION_LABEL = "TrueType font collection data".freeze
12
+ COLLECTION_LABELS = ["OpenType font collection data",
13
+ "TrueType font collection data"].freeze
13
14
 
14
15
  FONT_EXTENSIONS = {
15
16
  "OpenType font data" => "otf",
16
17
  "TrueType Font data" => "ttf",
18
+ "OpenType font collection data" => "ttc",
17
19
  "TrueType font collection data" => "ttc",
18
20
  }.freeze
19
21
 
@@ -22,7 +24,7 @@ module Fontist
22
24
 
23
25
  if brief.start_with?(*FONT_LABELS)
24
26
  :font
25
- elsif brief.start_with?(COLLECTION_LABEL)
27
+ elsif brief.start_with?(*COLLECTION_LABELS)
26
28
  :collection
27
29
  else
28
30
  :other
@@ -4,36 +4,41 @@ require_relative "text_helper"
4
4
  module Fontist
5
5
  module Import
6
6
  class FormulaBuilder
7
- FORMULA_ATTRIBUTES = %i[name description homepage resources
7
+ FORMULA_ATTRIBUTES = %i[description homepage resources
8
8
  font_collections fonts extract copyright
9
9
  license_url open_license digest command].freeze
10
10
 
11
- attr_accessor :archive,
12
- :url,
13
- :extractor,
14
- :options,
15
- :font_files,
16
- :font_collection_files,
17
- :license_text
11
+ attr_writer :archive,
12
+ :url,
13
+ :extractor,
14
+ :options,
15
+ :font_files,
16
+ :font_collection_files,
17
+ :license_text,
18
+ :homepage
18
19
 
19
20
  def initialize
20
21
  @options = {}
21
22
  end
22
23
 
23
24
  def formula
24
- FORMULA_ATTRIBUTES.map { |name| [name, send(name)] }.to_h.compact
25
+ formula_attributes.map { |name| [name, send(name)] }.to_h.compact
25
26
  end
26
27
 
27
- private
28
-
29
28
  def name
30
- return options[:name] if options[:name]
29
+ return @options[:name] if @options[:name]
31
30
 
32
31
  unique_names = both_fonts.map(&:family_name).uniq
33
32
  TextHelper.longest_common_prefix(unique_names) ||
34
33
  both_fonts.first.family_name
35
34
  end
36
35
 
36
+ private
37
+
38
+ def formula_attributes
39
+ FORMULA_ATTRIBUTES
40
+ end
41
+
37
42
  def both_fonts
38
43
  @both_fonts ||= group_fonts
39
44
  end
@@ -50,7 +55,7 @@ module Fontist
50
55
  end
51
56
 
52
57
  def homepage
53
- both_fonts.map(&:homepage).compact.first
58
+ @options[:homepage] || both_fonts.map(&:homepage).compact.first
54
59
  end
55
60
 
56
61
  def resources
@@ -68,7 +73,7 @@ module Fontist
68
73
  end
69
74
 
70
75
  def resource_options_without_sha
71
- { urls: [@url] + mirrors }
76
+ { urls: [@url] + mirrors, file_size: file_size }
72
77
  end
73
78
 
74
79
  def resource_options_with_sha
@@ -81,7 +86,7 @@ module Fontist
81
86
 
82
87
  sha = prepare_sha256(sha)
83
88
 
84
- { urls: urls, sha256: sha }
89
+ { urls: urls, sha256: sha, file_size: file_size }
85
90
  end
86
91
 
87
92
  def downloads
@@ -115,6 +120,10 @@ module Fontist
115
120
  output
116
121
  end
117
122
 
123
+ def file_size
124
+ File.size(@archive)
125
+ end
126
+
118
127
  def font_collections
119
128
  return if @font_collection_files.empty?
120
129
 
@@ -9,3 +9,4 @@
9
9
  - Andika
10
10
  - Harmattan
11
11
  - Padauk
12
+ - STIX Two Math
@@ -4,7 +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?
7
+ Fontist.ui.debug("Run `#{command}`")
8
8
 
9
9
  result = `#{command}`
10
10
  unless $CHILD_STATUS.to_i.zero?
@@ -0,0 +1,148 @@
1
+ require "plist"
2
+ require "nokogiri"
3
+ require "fontist/import"
4
+ require_relative "recursive_extraction"
5
+ require_relative "helpers/hash_helper"
6
+ require_relative "manual_formula_builder"
7
+
8
+ module Fontist
9
+ module Import
10
+ class Macos
11
+ FONT_XML = "/System/Library/AssetsV2/com_apple_MobileAsset_Font6/com_apple_MobileAsset_Font6.xml".freeze # rubocop:disable Layout/LineLength
12
+ DESCRIPTION = "Fonts included with macOS %<name>s".freeze
13
+
14
+ INSTRUCTIONS = <<~INSTRUCTIONS.freeze
15
+ To download and enable any of these fonts:
16
+
17
+ 1. Open Font Book, which is in your Applications folder.
18
+ 2. Select All Fonts in the sidebar, or use the Search field to find the font that you want to download. Fonts that are not already downloaded appear dimmed in the list of fonts.
19
+ 3. Select the dimmed font and choose Edit > Download, or Control-click it and choose Download from the pop-up menu.
20
+ INSTRUCTIONS
21
+
22
+ def initialize(options = {})
23
+ @options = options
24
+ end
25
+
26
+ def call
27
+ downloadable_fonts = fetch_fonts_list
28
+ links = fetch_links(downloadable_fonts)
29
+ archives = download(links)
30
+ store_in_dir(archives) do |dir|
31
+ create_formula(dir)
32
+ end
33
+ end
34
+
35
+ private
36
+
37
+ def fetch_fonts_list
38
+ html = Net::HTTP.get(URI.parse(@options[:fonts_link]))
39
+
40
+ document = Nokogiri::HTML.parse(html)
41
+ document.css("#sections div.grid2col:nth-of-type(3) div ul > li",
42
+ "#sections div.grid2col:nth-of-type(4) div ul > li")
43
+ .map(&:text)
44
+ end
45
+
46
+ def fetch_links(downloadable_fonts)
47
+ data = Plist.parse_xml(FONT_XML)
48
+ assets = downloadable_assets(data, downloadable_fonts)
49
+ assets_links(assets)
50
+ end
51
+
52
+ def downloadable_assets(data, downloadable_fonts)
53
+ data["Assets"].select do |x|
54
+ x["FontInfo4"].any? do |i|
55
+ downloadable_fonts.find do |d|
56
+ d.start_with?(i["FontFamilyName"])
57
+ end
58
+ end
59
+ end
60
+ end
61
+
62
+ def assets_links(assets)
63
+ assets.map do |x|
64
+ x.values_at("__BaseURL", "__RelativePath").join
65
+ end
66
+ end
67
+
68
+ def download(links)
69
+ links.map do |url|
70
+ Fontist::Utils::Downloader.download(url, progress_bar: true).path
71
+ end
72
+ end
73
+
74
+ def store_in_dir(archives)
75
+ Dir.mktmpdir do |dir|
76
+ archives.each do |archive|
77
+ FileUtils.ln(archive, dir)
78
+ end
79
+
80
+ yield dir
81
+ end
82
+ end
83
+
84
+ def create_formula(archives_dir)
85
+ extractor = RecursiveExtraction.new(archives_dir)
86
+ path = save(formula(archives_dir, extractor))
87
+ Fontist.ui.success("Formula has been successfully created: #{path}")
88
+
89
+ path
90
+ end
91
+
92
+ def formula(archive, extractor)
93
+ builder = ManualFormulaBuilder.new
94
+ setup_strings(builder, archive)
95
+ setup_files(builder, extractor)
96
+ builder.formula
97
+ end
98
+
99
+ def setup_strings(builder, archive)
100
+ builder.url = archive
101
+ builder.archive = archive
102
+ builder.platforms = platforms
103
+ builder.instructions = instructions
104
+ builder.description = description
105
+ builder.options = builder_options
106
+ end
107
+
108
+ def platforms
109
+ major_version = Sys::Uname.release.split(".").first
110
+
111
+ ["macos-#{major_version}"]
112
+ end
113
+
114
+ def instructions
115
+ INSTRUCTIONS.strip
116
+ end
117
+
118
+ def description
119
+ format(DESCRIPTION, name: @options[:name])
120
+ end
121
+
122
+ def builder_options
123
+ @options.merge(homepage: @options[:fonts_link])
124
+ end
125
+
126
+ def setup_files(builder, extractor)
127
+ builder.extractor = extractor
128
+ builder.font_files = extractor.font_files
129
+ builder.font_collection_files = extractor.font_collection_files
130
+ builder.license_text = extractor.license_text
131
+ end
132
+
133
+ def save(hash)
134
+ filename = Import.name_to_filename(@options[:name])
135
+ path = File.join(formula_dir, filename)
136
+ yaml = YAML.dump(Helpers::HashHelper.stringify_keys(hash))
137
+ File.write(path, yaml)
138
+ path
139
+ end
140
+
141
+ def formula_dir
142
+ @formula_dir ||= Fontist.formulas_path.join("macos").tap do |path|
143
+ FileUtils.mkdir_p(path) unless File.exist?(path)
144
+ end
145
+ end
146
+ end
147
+ end
148
+ end
@@ -0,0 +1,24 @@
1
+ require_relative "formula_builder"
2
+
3
+ module Fontist
4
+ module Import
5
+ class ManualFormulaBuilder < FormulaBuilder
6
+ attr_accessor :description,
7
+ :platforms,
8
+ :instructions
9
+
10
+ private
11
+
12
+ def formula_attributes
13
+ @formula_attributes ||= super.dup.tap do |attrs|
14
+ attrs.delete(:resources)
15
+ attrs.delete(:open_license)
16
+ attrs.delete(:license_url)
17
+ attrs.delete(:copyright)
18
+
19
+ attrs.insert(attrs.index(:homepage) + 1, :platforms, :instructions)
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -1,3 +1,5 @@
1
+ require_relative "otf/font_file"
2
+ require_relative "files/collection_file"
1
3
  require_relative "files/font_detector"
2
4
 
3
5
  module Fontist
@@ -0,0 +1,17 @@
1
+ module Fontist
2
+ class ImportCLI < Thor
3
+ include CLI::ClassOptions
4
+
5
+ desc "macos", "Create formula for on-demand macOS fonts"
6
+ option :name, desc: "Example: Big Sur", required: true
7
+ option :fonts_link,
8
+ desc: "A link to a list of available fonts in a current OS",
9
+ required: true
10
+ def macos
11
+ handle_class_options(options)
12
+ require_relative "import/macos"
13
+ Import::Macos.new(options).call
14
+ CLI::STATUS_SUCCESS
15
+ end
16
+ end
17
+ end
@@ -9,7 +9,10 @@ module Fontist
9
9
 
10
10
  def add_formula(formula)
11
11
  formula.fonts.each do |font|
12
- add_index_formula(font.name, formula.to_index_formula)
12
+ font.styles.each do |style|
13
+ font_name = style.default_family_name || font.name
14
+ add_index_formula(font_name, formula.to_index_formula)
15
+ end
13
16
  end
14
17
  end
15
18
 
@@ -1,9 +1,12 @@
1
1
  module Fontist
2
2
  class RepoCLI < Thor
3
+ include CLI::ClassOptions
4
+
3
5
  desc "setup NAME URL",
4
6
  "Setup a custom fontist repo named NAME for the repository at URL " \
5
7
  "and fetches its formulas"
6
8
  def setup(name, url)
9
+ handle_class_options(options)
7
10
  Repo.setup(name, url)
8
11
  Fontist.ui.success(
9
12
  "Fontist repo '#{name}' from '#{url}' has been successfully set up.",
@@ -13,6 +16,7 @@ module Fontist
13
16
 
14
17
  desc "update NAME", "Update formulas in a fontist repo named NAME"
15
18
  def update(name)
19
+ handle_class_options(options)
16
20
  Repo.update(name)
17
21
  Fontist.ui.success(
18
22
  "Fontist repo '#{name}' has been successfully updated.",
@@ -24,6 +28,7 @@ module Fontist
24
28
 
25
29
  desc "remove NAME", "Remove fontist repo named NAME"
26
30
  def remove(name)
31
+ handle_class_options(options)
27
32
  Repo.remove(name)
28
33
  Fontist.ui.success(
29
34
  "Fontist repo '#{name}' has been successfully removed.",
@@ -35,6 +40,7 @@ module Fontist
35
40
 
36
41
  desc "list", "List fontist repos"
37
42
  def list
43
+ handle_class_options(options)
38
44
  Repo.list.each do |name|
39
45
  Fontist.ui.say(name)
40
46
  end
@@ -0,0 +1,39 @@
1
+ module Fontist
2
+ class StyleVersion
3
+ def initialize(text)
4
+ @text = text
5
+ end
6
+
7
+ def value
8
+ @value ||= numbers || default_value
9
+ end
10
+
11
+ def numbers
12
+ string_version&.split(".")&.map(&:strip)
13
+ end
14
+
15
+ def string_version
16
+ @text&.split(";")&.first
17
+ end
18
+
19
+ def default_value
20
+ ["0"]
21
+ end
22
+
23
+ def <=>(other)
24
+ value <=> other.value
25
+ end
26
+
27
+ def ==(other)
28
+ value == other.value
29
+ end
30
+
31
+ def eql?(other)
32
+ value.eql?(other.value)
33
+ end
34
+
35
+ def hash
36
+ value.hash
37
+ end
38
+ end
39
+ end
@@ -14,6 +14,7 @@ system:
14
14
  - /System/Library/Fonts/**/**.{ttf,ttc}
15
15
  - /Users/{username}/Library/Fonts/**.{ttf,ttc}
16
16
  - /Applications/Microsoft**/Contents/Resources/**/**.{ttf,ttc}
17
+ - /System/Library/AssetsV2/com_apple_MobileAsset_Font6/**/**.{ttf,ttc}
17
18
 
18
19
  unix:
19
20
  paths:
@@ -1,9 +1,7 @@
1
1
  module Fontist
2
2
  class Update
3
- VERSION = "v2".freeze
4
-
5
3
  def self.call
6
- new(VERSION).call
4
+ new(Fontist.formulas_version).call
7
5
  end
8
6
 
9
7
  def initialize(branch = "main")
@@ -24,7 +24,7 @@ module Fontist
24
24
  download_file
25
25
  end
26
26
 
27
- raise_if_tampered(file)
27
+ check_tampered(file)
28
28
 
29
29
  file
30
30
  end
@@ -33,15 +33,12 @@ module Fontist
33
33
 
34
34
  attr_reader :file, :sha, :file_size
35
35
 
36
- def raise_if_tampered(file)
36
+ def check_tampered(file)
37
37
  file_checksum = Digest::SHA256.file(file).to_s
38
38
  if !sha.empty? && !sha.include?(file_checksum)
39
- raise(
40
- Fontist::Errors::TamperedFileError.new(
41
- "The downloaded file from #{@file} doesn't " \
42
- "match with the expected sha256 checksum (#{file_checksum})!\n" \
43
- "Beginning of content: #{File.read(file, 3000)}",
44
- ),
39
+ Fontist.ui.error(
40
+ "SHA256 checksum mismatch for #{url}: #{file_checksum}, " \
41
+ "should be #{sha.join(', or ')}.",
45
42
  )
46
43
  end
47
44
  end
@@ -1,3 +1,5 @@
1
+ require "sys/uname"
2
+
1
3
  module Fontist
2
4
  module Utils
3
5
  module System
@@ -18,6 +20,14 @@ module Fontist
18
20
  end
19
21
  end
20
22
  end
23
+
24
+ def self.user_os_with_version
25
+ "#{user_os}-#{Sys::Uname.release}"
26
+ end
27
+
28
+ def self.match?(platform)
29
+ user_os_with_version.start_with?(platform)
30
+ end
21
31
  end
22
32
  end
23
33
  end