fontist 1.21.4 → 2.0.1

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.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/rake.yml +4 -0
  3. data/Gemfile +11 -0
  4. data/README.adoc +6 -6
  5. data/docs/guide/api-ruby.md +8 -9
  6. data/fontist.gemspec +14 -24
  7. data/formula_filename_index.yml +6210 -0
  8. data/formula_index.yml +2568 -0
  9. data/lib/fontist/cli.rb +14 -14
  10. data/lib/fontist/config.rb +75 -14
  11. data/lib/fontist/errors.rb +2 -0
  12. data/lib/fontist/extract.rb +25 -0
  13. data/lib/fontist/font.rb +6 -8
  14. data/lib/fontist/font_collection.rb +16 -0
  15. data/lib/fontist/font_installer.rb +4 -4
  16. data/lib/fontist/font_model.rb +15 -0
  17. data/lib/fontist/font_path.rb +1 -1
  18. data/lib/fontist/font_style.rb +37 -0
  19. data/lib/fontist/formula.rb +169 -112
  20. data/lib/fontist/import/formula_serializer.rb +4 -4
  21. data/lib/fontist/import/google_import.rb +1 -1
  22. data/lib/fontist/index.rb +47 -8
  23. data/lib/fontist/indexes/default_family_font_index.rb +28 -9
  24. data/lib/fontist/indexes/filename_index.rb +45 -8
  25. data/lib/fontist/indexes/font_index.rb +3 -3
  26. data/lib/fontist/indexes/formula_key_to_path.rb +35 -0
  27. data/lib/fontist/indexes/index_mixin.rb +109 -0
  28. data/lib/fontist/indexes/preferred_family_font_index.rb +28 -9
  29. data/lib/fontist/manifest.rb +144 -2
  30. data/lib/fontist/manifest_request.rb +64 -0
  31. data/lib/fontist/manifest_response.rb +66 -0
  32. data/lib/fontist/repo.rb +1 -1
  33. data/lib/fontist/system_font.rb +7 -25
  34. data/lib/fontist/system_index.rb +137 -126
  35. data/lib/fontist/utils/cache.rb +54 -4
  36. data/lib/fontist/version.rb +1 -1
  37. data/lib/fontist.rb +33 -13
  38. metadata +16 -136
  39. data/lib/fontist/indexes/base_index.rb +0 -92
  40. data/lib/fontist/indexes/index_formula.rb +0 -36
  41. data/lib/fontist/manifest/install.rb +0 -35
  42. data/lib/fontist/manifest/locations.rb +0 -84
@@ -0,0 +1,35 @@
1
+
2
+ module Fontist
3
+ module Indexes
4
+ class FormulaKeyToPath < Lutaml::Model::Serializable
5
+ attribute :key, :string
6
+ attribute :formula_path, :string, collection: true
7
+
8
+ key_value do
9
+ map "key", to: :key
10
+ map "formula_path", to: :formula_path
11
+ end
12
+
13
+ def to_full
14
+ formula_path.map { |p| Formula.from_file(full_path(p)) }
15
+ end
16
+
17
+ def name
18
+ formula_path.map { |p| normalized(p) }
19
+ end
20
+
21
+ def normalized(path)
22
+ return "" unless path
23
+
24
+ escaped = Regexp.escape("#{Fontist.formulas_path}/")
25
+ path.sub(Regexp.new("^#{escaped}"), "").sub(/\.yml$/, "").to_s
26
+ end
27
+
28
+ private
29
+
30
+ def full_path(path)
31
+ Fontist.formulas_path.join(path).to_s
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,109 @@
1
+ module Fontist
2
+ module Indexes
3
+ module IndexMixin
4
+ def self.included(base)
5
+ base.extend(ClassMethods)
6
+ end
7
+
8
+ module ClassMethods
9
+ def from_file(file_path = self.path)
10
+ Fontist.ui.debug("Index: #{file_path}")
11
+
12
+ Fontist.formulas_repo_path_exists!
13
+
14
+ rebuild unless File.exist?(file_path)
15
+
16
+ file_content = File.read(file_path).strip
17
+
18
+ if file_content.empty?
19
+ raise Fontist::Errors::FontIndexCorrupted, "Index file is empty: #{file_path}"
20
+ end
21
+
22
+ from_yaml(file_content)
23
+ end
24
+
25
+ def rebuild
26
+ # puts "Rebuilding index..."
27
+ new.build
28
+ end
29
+ end
30
+
31
+ def build
32
+ Formula.all.each do |formula|
33
+ add_formula(formula)
34
+ end
35
+
36
+ to_file
37
+
38
+ self
39
+ end
40
+
41
+ def add_formula(formula)
42
+ raise unless formula.is_a?(Formula)
43
+
44
+ formula.all_fonts.each do |font|
45
+ font.styles.each do |style|
46
+ add_index_formula(style, formula.path)
47
+ end
48
+ end
49
+
50
+ entries
51
+ end
52
+
53
+ def index_key_for_style(_style)
54
+ raise NotImplementedError, "index_key_for_style(style) must be implemented in including class"
55
+ end
56
+
57
+ def add_index_formula(style, formula_path)
58
+ key = index_key_for_style(style)
59
+ raise if key.nil? || key.empty?
60
+
61
+ key = normalize_key(key)
62
+ formula_path = Array(formula_path)
63
+ paths = formula_path.map { |p| relative_formula_path(p) }
64
+
65
+ if index_formula(key)
66
+ index_formula(key).formula_path.concat(paths).uniq!
67
+ return
68
+ end
69
+
70
+ entries << FormulaKeyToPath.new(
71
+ key: key,
72
+ formula_path: paths,
73
+ )
74
+ end
75
+
76
+ def load_formulas(key)
77
+ index_formulas(key).flat_map(&:to_full)
78
+ end
79
+
80
+ def load_index_formulas(key)
81
+ index_formulas(key)
82
+ end
83
+
84
+ def to_file(file_path = self.class.path)
85
+ # Use default path if file_path is nil
86
+ file_path = self.class.path if file_path.nil?
87
+ # puts "Writing index to #{file_path}"
88
+
89
+ FileUtils.mkdir_p(File.dirname(file_path))
90
+ File.write(file_path, to_yaml)
91
+ end
92
+
93
+ private
94
+
95
+ def index_formula(key)
96
+ Array(entries).detect { |f| normalize_key(f.key) == normalize_key(key) }
97
+ end
98
+
99
+ def index_formulas(key)
100
+ Array(entries).select { |f| normalize_key(f.key) == normalize_key(key) }
101
+ end
102
+
103
+ def relative_formula_path(path)
104
+ escaped = Regexp.escape("#{Fontist.formulas_path}/")
105
+ path.sub(Regexp.new("^#{escaped}"), "")
106
+ end
107
+ end
108
+ end
109
+ end
@@ -1,19 +1,38 @@
1
- require_relative "base_index"
1
+ require "lutaml/model"
2
+ require_relative "index_mixin"
3
+ require_relative "formula_key_to_path"
2
4
 
3
5
  module Fontist
4
6
  module Indexes
5
- class PreferredFamilyFontIndex < BaseIndex
7
+ # YAML file structure:
8
+ # ---
9
+ # adobe arabic:
10
+ # - adobe_reader_19.yml
11
+ # myriad pro:
12
+ # - adobe_reader_20.yml
13
+ # akabara-cinderella:
14
+ # - akabara-cinderella.yml
15
+ # andale mono:
16
+ # - andale.yml
17
+ # - macos/andale_mono.yml
18
+ # - opensuse_webcore_fonts.yml
19
+ # - pclinuxos_webcore_fonts.yml
20
+ class PreferredFamilyFontIndex < Lutaml::Model::Collection
21
+ include IndexMixin
22
+ instances :entries, FormulaKeyToPath
23
+
24
+ key_value do
25
+ map_key to_instance: :key
26
+ map_value as_attribute: :formula_path
27
+ map_instances to: :entries
28
+ end
29
+
6
30
  def self.path
7
31
  Fontist.formula_preferred_family_index_path
8
32
  end
9
33
 
10
- def add_formula(formula)
11
- formula.fonts.each do |font|
12
- font.styles.each do |style|
13
- font_name = style.preferred_family_name || font.name
14
- add_index_formula(font_name, formula.to_index_formula)
15
- end
16
- end
34
+ def index_key_for_style(style)
35
+ style.preferred_family_name || style.family_name
17
36
  end
18
37
 
19
38
  def normalize_key(key)
@@ -1,2 +1,144 @@
1
- require_relative "manifest/locations"
2
- require_relative "manifest/install"
1
+ require "lutaml/model"
2
+
3
+ module Fontist
4
+ class ManifestFont < Lutaml::Model::Serializable
5
+ attribute :name, :string
6
+ attribute :styles, :string, collection: true
7
+
8
+ def style_paths(locations: false)
9
+ ary = Array(styles)
10
+ (ary.empty? ? [nil] : ary).flat_map do |style|
11
+ find_font_with_name(name, style).tap do |x|
12
+ raise Errors::MissingFontError.new(name, style) if x.nil? && locations
13
+ end
14
+ end.compact
15
+ end
16
+
17
+ def group_paths(locations: false)
18
+ style_paths(locations: locations).group_by(&:type)
19
+ .transform_values { |group| style(group) }
20
+ end
21
+
22
+ def style(styles_ary)
23
+ { "full_name" => styles_ary.first.full_name,
24
+ "paths" => styles_ary.filter_map(&:path) }
25
+ end
26
+
27
+ def find_font_with_name(font, style)
28
+ Fontist::SystemFont.find_styles(font, style)
29
+ end
30
+
31
+ def install(confirmation: "no", hide_licenses: false, no_progress: false)
32
+ Fontist::Font.install(
33
+ name,
34
+ force: true,
35
+ confirmation: confirmation,
36
+ hide_licenses: hide_licenses,
37
+ no_progress: no_progress,
38
+ )
39
+ end
40
+
41
+ def to_response(locations: false)
42
+ groups = group_paths(locations: locations)
43
+
44
+ return self if groups.empty?
45
+
46
+ ManifestResponseFont.new(
47
+ name: name,
48
+ styles: groups.map do |type, details|
49
+ ManifestResponseFontStyle.new(
50
+ type: type,
51
+ full_name: details["full_name"],
52
+ paths: details["paths"],
53
+ )
54
+ end,
55
+ )
56
+ end
57
+
58
+ def group_paths_empty?
59
+ group_paths.compact.empty?
60
+ end
61
+ end
62
+
63
+ # Manifest class for managing font manifests.
64
+ class Manifest < Lutaml::Model::Collection
65
+ instances :fonts, ManifestFont
66
+
67
+ # TODO: Re-enable these when
68
+ key_value do
69
+ map to: :fonts, root_mappings: {
70
+ name: :key,
71
+ styles: :value,
72
+ }
73
+ end
74
+
75
+ def self.from_file(path, locations: false)
76
+ Fontist.ui.debug("Manifest: #{path}")
77
+
78
+ unless File.exist?(path)
79
+ raise Fontist::Errors::ManifestCouldNotBeFoundError,
80
+ "Manifest file not found: #{path}"
81
+ end
82
+
83
+ file_content = File.read(path).strip
84
+
85
+ if file_content.empty?
86
+ raise Fontist::Errors::ManifestCouldNotBeReadError,
87
+ "Manifest file is empty: #{path}"
88
+ end
89
+
90
+ manifest_model = begin
91
+ from_yaml(file_content)
92
+ rescue StandardError => e
93
+ raise Fontist::Errors::ManifestCouldNotBeReadError,
94
+ "Manifest file could not be read: #{e.message}"
95
+ end
96
+
97
+ manifest_model.to_response(locations: locations)
98
+ end
99
+
100
+ def self.from_hash(data, options = {})
101
+ locations = options.delete(:locations) || false
102
+
103
+ model = super(data, options)
104
+
105
+ model.to_response(locations: locations)
106
+ end
107
+
108
+ def self.font_class
109
+ ManifestFont
110
+ end
111
+
112
+ def fonts_casted
113
+ Array(fonts).map do |font|
114
+ self.class.font_class === font ? font : self.class.font_class.new(font.to_h)
115
+ end
116
+ end
117
+
118
+ def install(confirmation: "no", hide_licenses: false, no_progress: false)
119
+ fonts_casted.each do |font|
120
+ paths = font.group_paths
121
+ if paths.length < fonts_casted.length
122
+ font.install(confirmation: confirmation,
123
+ hide_licenses: hide_licenses, no_progress: no_progress)
124
+ end
125
+ end
126
+ to_response
127
+ end
128
+
129
+ def to_response(locations: false)
130
+ return self if fonts_casted.any?(&:group_paths_empty?) && !locations
131
+
132
+ ManifestResponse.new.tap do |response|
133
+ response.fonts = fonts_casted.map do |font|
134
+ font.to_response(locations: locations)
135
+ end
136
+ end
137
+ end
138
+
139
+ def to_file(path)
140
+ FileUtils.mkdir_p(File.dirname(path))
141
+ File.write(path, to_yaml)
142
+ end
143
+ end
144
+ end
@@ -0,0 +1,64 @@
1
+ require "lutaml/model"
2
+ require_relative "manifest"
3
+
4
+ module Fontist
5
+ class ManifestRequestFont < ManifestFont
6
+ def to_response
7
+ font_styles = locate_styles.map do |style, detailed_styles|
8
+ # puts "Detailed styles for #{name}: #{detailed_styles.inspect}"
9
+
10
+ if detailed_styles.nil? || detailed_styles.empty?
11
+ Fontist.ui.error("Font #{name} with style #{style} not found, skipping")
12
+ ManifestResponseFontStyle.new(
13
+ type: style,
14
+ )
15
+ else
16
+ ManifestResponseFontStyle.new(
17
+ full_name: detailed_styles.first.full_name,
18
+ type: detailed_styles.first.type,
19
+ paths: detailed_styles.map(&:path),
20
+ )
21
+ end
22
+ end
23
+
24
+ ManifestResponseFont.new(
25
+ name: name,
26
+ styles: font_styles,
27
+ )
28
+ end
29
+
30
+ private
31
+
32
+ # Returns an array of paths for the font and all its styles
33
+ # A SystemIndexFont array is returned, which has the following structure:
34
+ # [{:path=>"/Library/Fonts/Arial Unicode.ttf",
35
+ # :full_name=>"Arial Unicode MS",
36
+ # :family_name=>"Arial Unicode MS",
37
+ # :type=>"Regular"}, ...],
38
+ def locate_styles
39
+ Array(styles).map do |style|
40
+ [
41
+ style,
42
+ Fontist::SystemFont.find_styles(name, style),
43
+ ]
44
+ end
45
+ end
46
+ end
47
+
48
+ # ---
49
+ # Andale Mono:
50
+ # - Regular
51
+ # Arial Black:
52
+ # - Regular
53
+ # Arial:
54
+ # - Bold
55
+ # - Italic
56
+ # - Bold Italic
57
+ class ManifestRequest < Manifest
58
+ instances :fonts, ManifestRequestFont
59
+
60
+ def self.font_class
61
+ ManifestRequestFont
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,66 @@
1
+ require "lutaml/model"
2
+ require_relative "manifest"
3
+
4
+ module Fontist
5
+ class ManifestResponseFontStyle < Lutaml::Model::Serializable
6
+ attribute :type, :string
7
+ attribute :full_name, :string
8
+ attribute :paths, :string, collection: true
9
+ end
10
+
11
+ class ManifestResponseFont < ManifestFont
12
+ attribute :name, :string
13
+ attribute :styles, ManifestResponseFontStyle, collection: true
14
+
15
+ key_value do
16
+ map "name", to: :name
17
+ map "styles", to: :styles, child_mappings: {
18
+ type: :key,
19
+ full_name: :full_name,
20
+ paths: :paths,
21
+ }
22
+ end
23
+
24
+ def install(confirmation: "no", hide_licenses: false, no_progress: false)
25
+ styles.each do |style|
26
+ if style.paths.nil?
27
+ # If no paths are found, notify the user but continue with the
28
+ # installation
29
+ Fontist.ui.error("Font #{name} with style #{style} not found, skipping installation.")
30
+ end
31
+ end
32
+
33
+ Fontist::Font.install(
34
+ name,
35
+ force: false,
36
+ confirmation: confirmation,
37
+ hide_licenses: hide_licenses,
38
+ no_progress: no_progress,
39
+ )
40
+ end
41
+ end
42
+
43
+ # Yu Gothic:
44
+ # Bold:
45
+ # full_name: Yu Gothic Bold
46
+ # paths:
47
+ # - "/Applications/Microsoft Excel.app/Contents/Resources/DFonts/YuGothB.ttc"
48
+ # - "/Applications/Microsoft OneNote.app/Contents/Resources/DFonts/YuGothB.ttc"
49
+ # Regular:
50
+ # full_name: Yu Gothic Regular
51
+ # paths:
52
+ # - "/Applications/Microsoft Excel.app/Contents/Resources/DFonts/YuGothR.ttc"
53
+ # - "/Applications/Microsoft OneNote.app/Contents/Resources/DFonts/YuGothR.ttc"
54
+ # Noto Sans Condensed:
55
+ # Regular:
56
+ # full_name: Noto Sans Condensed
57
+ # paths:
58
+ # - "/Users/foo/.fontist/fonts/NotoSans-Condensed.ttf"
59
+ class ManifestResponse < Manifest
60
+ instances :fonts, ManifestResponseFont
61
+
62
+ def self.font_class
63
+ ManifestResponseFont
64
+ end
65
+ end
66
+ end
data/lib/fontist/repo.rb CHANGED
@@ -35,7 +35,7 @@ module Fontist
35
35
 
36
36
  def build_formulas(path)
37
37
  path.glob("*.yml").map do |formula_path|
38
- formula = Formula.new_from_file(formula_path)
38
+ formula = Formula.from_file(formula_path)
39
39
  Struct.new(:name, :description).new(formula.key, formula.description)
40
40
  end
41
41
  end
@@ -1,12 +1,8 @@
1
1
  require_relative "system_index"
2
2
 
3
3
  module Fontist
4
- class SystemFont
5
- def initialize(font:, style: nil)
6
- @font = font
7
- @style = style
8
- end
9
-
4
+ # TODO: This is actually a SystemIndex font entry
5
+ class SystemFont < Lutaml::Model::Serializable
10
6
  def self.font_paths
11
7
  system_font_paths + fontist_font_paths
12
8
  end
@@ -15,6 +11,7 @@ module Fontist
15
11
  @system_font_paths ||= load_system_font_paths
16
12
  end
17
13
 
14
+ # This detects all fonts on the system from the configuration file.
18
15
  def self.load_system_font_paths
19
16
  os = Fontist::Utils::System.user_os.to_s
20
17
  templates = system_config["system"][os]["paths"]
@@ -48,29 +45,14 @@ module Fontist
48
45
  end
49
46
 
50
47
  def self.find(font)
51
- new(font: font).find
52
- end
53
-
54
- def self.find_styles(font, style)
55
- new(font: font, style: style).find_styles
56
- end
57
-
58
- def find
59
- styles = find_styles
48
+ styles = find_styles(font)
60
49
  return unless styles
61
50
 
62
- styles.map { |x| x[:path] }
63
- end
64
-
65
- def find_styles
66
- find_by_index
51
+ styles.map(&:path)
67
52
  end
68
53
 
69
- private
70
-
71
- attr_reader :font, :style
72
-
73
- def find_by_index
54
+ # This returns a SystemIndexEntry
55
+ def self.find_styles(font, style = nil)
74
56
  SystemIndex.system_index.find(font, style)
75
57
  end
76
58
  end