dieses 0.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 (55) hide show
  1. checksums.yaml +7 -0
  2. data/BEN/304/260OKU.md +0 -0
  3. data/CHANGELOG.md +14 -0
  4. data/LICENSE.md +675 -0
  5. data/README.md +18 -0
  6. data/bin/dieses +10 -0
  7. data/bin/diesis +10 -0
  8. data/dieses.gemspec +36 -0
  9. data/lib/dieses.rb +7 -0
  10. data/lib/dieses/application.rb +24 -0
  11. data/lib/dieses/application/batch.rb +127 -0
  12. data/lib/dieses/application/canvas.rb +51 -0
  13. data/lib/dieses/application/cli.rb +6 -0
  14. data/lib/dieses/application/cli/multi.rb +106 -0
  15. data/lib/dieses/application/cli/single.rb +100 -0
  16. data/lib/dieses/application/common.rb +27 -0
  17. data/lib/dieses/application/mixins.rb +5 -0
  18. data/lib/dieses/application/mixins/lines.rb +105 -0
  19. data/lib/dieses/application/mixins/scribes.rb +91 -0
  20. data/lib/dieses/application/mixins/squares.rb +23 -0
  21. data/lib/dieses/application/paper.rb +146 -0
  22. data/lib/dieses/application/pen.rb +161 -0
  23. data/lib/dieses/application/sheet.rb +111 -0
  24. data/lib/dieses/application/sheets.rb +58 -0
  25. data/lib/dieses/application/sheets/copperplate.rb +20 -0
  26. data/lib/dieses/application/sheets/cursive.rb +19 -0
  27. data/lib/dieses/application/sheets/graph.rb +27 -0
  28. data/lib/dieses/application/sheets/italics.rb +19 -0
  29. data/lib/dieses/application/sheets/lettering.rb +30 -0
  30. data/lib/dieses/application/sheets/lined.rb +24 -0
  31. data/lib/dieses/application/sheets/print.rb +19 -0
  32. data/lib/dieses/application/sheets/ruled.rb +25 -0
  33. data/lib/dieses/application/sheets/spencerian.rb +20 -0
  34. data/lib/dieses/application/sheets/thumbnail.rb +37 -0
  35. data/lib/dieses/error.rb +5 -0
  36. data/lib/dieses/geometry.rb +8 -0
  37. data/lib/dieses/geometry/element.rb +66 -0
  38. data/lib/dieses/geometry/equation.rb +57 -0
  39. data/lib/dieses/geometry/equation/slant.rb +88 -0
  40. data/lib/dieses/geometry/equation/steep.rb +70 -0
  41. data/lib/dieses/geometry/error.rb +7 -0
  42. data/lib/dieses/geometry/line.rb +67 -0
  43. data/lib/dieses/geometry/point.rb +98 -0
  44. data/lib/dieses/geometry/rect.rb +173 -0
  45. data/lib/dieses/geometry/support.rb +3 -0
  46. data/lib/dieses/support.rb +9 -0
  47. data/lib/dieses/support/class.rb +100 -0
  48. data/lib/dieses/support/const.rb +78 -0
  49. data/lib/dieses/support/enum.rb +63 -0
  50. data/lib/dieses/support/float.rb +43 -0
  51. data/lib/dieses/support/hash.rb +19 -0
  52. data/lib/dieses/support/kernel.rb +36 -0
  53. data/lib/dieses/support/math.rb +20 -0
  54. data/lib/dieses/version.rb +5 -0
  55. metadata +226 -0
data/README.md ADDED
@@ -0,0 +1,18 @@
1
+ Diesis
2
+ ======
3
+
4
+ [![test](https://github.com/alaturka/dieses/actions/workflows/test.yml/badge.svg)](https://github.com/alaturka/dieses/actions/workflows/test.yml)
5
+ [![codebeat badge](https://codebeat.co/badges/7e57d6fa-d244-4359-a324-c4239217b749)](https://codebeat.co/projects/github-com-alaturka-dieses-master)
6
+
7
+ Guide sheets for penmanship, calligraphy, lettering, and sketching.
8
+ [Click](https://github.com/alaturka/dieses/tree/master/docs/sheets) to reach the all available guide sheet combinations.
9
+
10
+ Couldn't find the guide sheet you were looking for? [Open an
11
+ issue](https://github.com/alaturka/dieses/issues/new/choose) and let's see what we can do.
12
+
13
+ Install
14
+ -------
15
+
16
+ ```sh
17
+ gem install dieses
18
+ ```
data/bin/dieses ADDED
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'dieses/application/cli'
5
+
6
+ def main
7
+ Dieses::Application::CLI::Multi.(*ARGV)
8
+ end
9
+
10
+ main
data/bin/diesis ADDED
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'dieses/application/cli'
5
+
6
+ def main
7
+ Dieses::Application::CLI::Single.(*ARGV)
8
+ end
9
+
10
+ main
data/dieses.gemspec ADDED
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ $LOAD_PATH.push File.expand_path('lib', __dir__)
4
+
5
+ require 'dieses/version'
6
+
7
+ Gem::Specification.new do |s|
8
+ s.name = 'dieses'
9
+ s.author = 'Recai Oktaş'
10
+ s.email = 'roktas@gmail.com'
11
+ s.license = 'GPL-3.0-or-later'
12
+ s.version = Dieses::VERSION.dup
13
+ s.summary = 'Guide sheets generator for penmanship, calligraphy, lettering, and sketching'
14
+ s.description = 'Guide sheets generator for penmanship, calligraphy, lettering, and sketching'
15
+
16
+ s.homepage = 'https://alaturka.github.io/dieses'
17
+ s.files = Dir['CHANGELOG.md', 'LICENSE.md', 'README.md', 'BENİOKU.md', 'dieses.gemspec', 'lib/**/*']
18
+ s.executables = %w[diesis dieses]
19
+ s.require_paths = %w[lib]
20
+
21
+ s.metadata['changelog_uri'] = 'https://github.com/alaturka/dieses/blob/master/CHANGELOG.md'
22
+ s.metadata['source_code_uri'] = 'https://github.com/alaturka/dieses'
23
+ s.metadata['bug_tracker_uri'] = 'https://github.com/alaturka/dieses/issues'
24
+
25
+ s.required_ruby_version = '>= 2.7.0' # rubocop:disable Gemspec/RequiredRubyVersion
26
+
27
+ s.add_development_dependency 'bundler'
28
+ s.add_development_dependency 'minitest-focus', '>= 1.2.1'
29
+ s.add_development_dependency 'minitest-reporters', '>= 1.4.3'
30
+ s.add_development_dependency 'rake'
31
+ s.add_development_dependency 'rubocop'
32
+ s.add_development_dependency 'rubocop-minitest'
33
+ s.add_development_dependency 'rubocop-performance'
34
+ s.add_development_dependency 'rubocop-rake'
35
+ s.add_development_dependency 'rubygems-tasks'
36
+ end
data/lib/dieses.rb ADDED
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'dieses/support'
4
+ require_relative 'dieses/error'
5
+ require_relative 'dieses/geometry'
6
+ require_relative 'dieses/application'
7
+ require_relative 'dieses/version'
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'application/common'
4
+ require_relative 'application/paper'
5
+ require_relative 'application/canvas'
6
+ require_relative 'application/pen'
7
+ require_relative 'application/mixins'
8
+ require_relative 'application/sheet'
9
+ require_relative 'application/sheets'
10
+ require_relative 'application/batch'
11
+
12
+ module Dieses
13
+ module Application
14
+ module_function
15
+
16
+ def produce(sheet, variant: Undefined, paper: Paper.default, orientation: Orientation.default, **render_args)
17
+ sheet(sheet, variant: variant, paper: paper, orientation: orientation).produce(**render_args)
18
+ end
19
+
20
+ def sheet(sheet, variant: Undefined, paper: Paper.default, orientation: Orientation.default)
21
+ Sheets.sheet(sheet.to_sym).new(Paper.public_send(paper.to_sym).orient(orientation.to_sym), variant: variant)
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,127 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+ require 'json'
5
+
6
+ module Dieses
7
+ module Application
8
+ module Batch
9
+ Production = Struct.new :sheet, :variant, :desc, :paper, :orientation, :file, :produce, keyword_init: true do
10
+ def self.call(**kwargs)
11
+ new(**kwargs.slice(*members)).tap do |production|
12
+ production.file = production.relfile unless production.file
13
+ end
14
+ end
15
+
16
+ def to_s
17
+ suffix = orientation.to_s == 'landscape' ? 'l' : ''
18
+ "#{sheet}-#{variant}@#{paper}#{suffix}"
19
+ end
20
+
21
+ def key
22
+ to_s.to_sym
23
+ end
24
+
25
+ def relfile
26
+ File.join(reldir, "#{self}.svg")
27
+ end
28
+
29
+ def call
30
+ Application.produce(sheet, variant: variant, paper: paper, orientation: orientation)
31
+ end
32
+
33
+ def run(basedir)
34
+ unless produce
35
+ warn "Undefined production request; skipping: #{self}" if produce.nil?
36
+
37
+ return
38
+ end
39
+
40
+ outfile = File.join(File.expand_path(basedir), file)
41
+ content = call
42
+
43
+ FileUtils.mkdir_p(File.dirname(outfile))
44
+ File.write(outfile, content)
45
+ end
46
+
47
+ private
48
+
49
+ def reldir
50
+ File.join(sheet, paper)
51
+ end
52
+ end
53
+
54
+ private_constant :Production
55
+
56
+ module_function
57
+
58
+ PAPERS = %i[a4 a5].freeze
59
+ ORIENTATIONS = Orientation.values
60
+
61
+ # rubocop:disable Metrics/MethodLength
62
+ # codebeat:disable[BLOCK_NESTING]
63
+ def all(papers: PAPERS, orientations: ORIENTATIONS)
64
+ Set.new.tap do |productions|
65
+ papers.each do |paper|
66
+ Sheets.available.each do |sheet, proto|
67
+ proto.variants.each do |variant|
68
+ orientations.each do |orientation|
69
+ productions << Production.(sheet: sheet.to_s,
70
+ variant: variant.to_s,
71
+ desc: variant.desc,
72
+ paper: paper.to_s,
73
+ orientation: orientation.to_s)
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
80
+ # codebeat:enable[BLOCK_NESTING]
81
+
82
+ def defaults
83
+ Set.new(
84
+ Sheets.defaults.map do |sheet, variant|
85
+ Production.(sheet: sheet.to_s,
86
+ variant: variant.to_s,
87
+ desc: variant.desc,
88
+ paper: Paper.default.to_s,
89
+ orientation: Orientation.default.to_s)
90
+ end
91
+ )
92
+ end
93
+
94
+ def index(productions)
95
+ previous = Support.hashify_by productions, :key
96
+ current = Support.hashify_by Batch.all, :key
97
+
98
+ unprocessed = []
99
+ current.each do |name, production|
100
+ unless previous.key?(name)
101
+ unprocessed << production
102
+
103
+ next
104
+ end
105
+
106
+ production.produce = previous[name].produce
107
+ end
108
+
109
+ [current.values, unprocessed]
110
+ end
111
+ # rubocop:enable Metrics/MethodLength
112
+
113
+ def from_json_file(file)
114
+ JSON.load_file(file).map do |hash|
115
+ hash.transform_keys!(&:to_sym)
116
+ Production.(**hash.slice(*Production.members))
117
+ end
118
+ end
119
+
120
+ def to_json_file(file, productions)
121
+ content = JSON.pretty_generate(productions.map(&:to_h)).chomp
122
+
123
+ File.write(file, "#{content}\n")
124
+ end
125
+ end
126
+ end
127
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'delegate'
4
+
5
+ module Dieses
6
+ module Application
7
+ class Canvas < DelegateClass(Geometry::Rect)
8
+ TEMPLATE = <<~XML
9
+ <?xml version="1.0" standalone="no"?>
10
+ <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
11
+ <svg xmlns="http://www.w3.org/2000/svg" width="%{width}mm" height="%{height}mm" viewBox="0 0 %{width} %{height}" %{header}>
12
+ %{style}
13
+ <g id="sheet">
14
+ %{content}
15
+ </g>
16
+ </svg>
17
+ XML
18
+
19
+ attr_reader :elements, :paper
20
+
21
+ def initialize(paper = Paper.a4)
22
+ super(paper.inner)
23
+
24
+ @paper = paper
25
+ @elements = Set.new
26
+ end
27
+
28
+ def to_h
29
+ @paper.to_h
30
+ end
31
+
32
+ def <<(items)
33
+ [*items].each do |item|
34
+ case item
35
+ when Array then item.each { |element| elements << element }
36
+ when Geometry::Element then elements << item
37
+ else raise Error, 'Item must be an Array or Element'
38
+ end
39
+ end
40
+ end
41
+
42
+ def render(header: EMPTY_STRING, style: EMPTY_STRING)
43
+ # We avoid prettifying XML through REXML which is pretty slow, at the cost of some ugly hacks.
44
+ format(TEMPLATE, **to_h,
45
+ content: Geometry.to_svg(elements, paper, prefix: ' ' * 4),
46
+ header: header,
47
+ style: style.empty? ? '' : format('<style>%{style}</style>', style: style))
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'diesis'
4
+
5
+ require_relative 'cli/single'
6
+ require_relative 'cli/multi'
@@ -0,0 +1,106 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'optparse'
4
+ require 'ostruct'
5
+
6
+ module Dieses
7
+ module Application
8
+ module CLI
9
+ module Multi
10
+ OPTIONS = EMPTY_HASH
11
+
12
+ def self.call(*argv, **options)
13
+ options = OpenStruct.new(OPTIONS.merge(options))
14
+ args options(argv, options), argv
15
+
16
+ index_file = argv.first
17
+
18
+ return build_index(index_file, options) if options.index
19
+
20
+ batch_run(index_file, options)
21
+ rescue OptionParser::InvalidOption, Dieses::Error => e
22
+ abort(e.message)
23
+ end
24
+
25
+ class << self
26
+ private
27
+
28
+ def options(argv, options) # rubocop:disable Metrics/MethodLength
29
+ Signal.trap('INT') { Kernel.abort '' }
30
+
31
+ OptionParser.new do |option|
32
+ program_name = option.program_name
33
+ option.banner = <<~BANNER
34
+ Usage: #{program_name} [options...] <INDEXFILE>
35
+
36
+ See #{program_name}(1) manual page for detailed help.
37
+
38
+ Options:
39
+
40
+ BANNER
41
+
42
+ option.on('--index', 'Create index for all variants without producing sheets') do |opt|
43
+ options.index = opt
44
+ end
45
+
46
+ option.on('--destdir DIR', 'Destination directory') do |opt|
47
+ options.destdir = opt
48
+ end
49
+
50
+ option.on('--no-clobber', 'Do not overwrite an existing file or directory') do
51
+ options.no_clobber = true
52
+ end
53
+
54
+ option.on('--pdf', 'Generate PDF') do |_opt|
55
+ raise NotImplementedError
56
+ end
57
+
58
+ option.on_tail('-h', '--help', 'Show this message') do
59
+ abort option.help
60
+ end
61
+
62
+ option.on_tail('-v', '--version', 'Show version') do
63
+ warn VERSION
64
+ exit
65
+ end
66
+ end.tap { |parser| parser.parse!(argv) } # rubocop:disable Style/MultilineBlockChain
67
+ end
68
+
69
+ def args(parser, argv)
70
+ if argv.empty?
71
+ warn parser.help
72
+ warn ''
73
+ abort 'Error: Index file required.'
74
+ end
75
+
76
+ return if argv.size <= 1
77
+
78
+ warn parser.help
79
+ warn ''
80
+ abort 'Error: Too many arguments.'
81
+ end
82
+
83
+ def build_index(index_file, options)
84
+ abort("Index file exists: #{index_file}") if options.no_clobber && File.exist?(index_file)
85
+
86
+ productions, unprocessed = Batch.index Batch.from_json_file(index_file)
87
+
88
+ warn "Warning: There are #{unprocessed.size} new sheets which should be processed" unless unprocessed.empty?
89
+
90
+ Batch.to_json_file(index_file, productions)
91
+ end
92
+
93
+ def batch_run(index_file, options)
94
+ abort("Index file not found: #{index_file}") unless File.exist?(index_file)
95
+
96
+ destdir = options.destdir || File.join(File.dirname(index_file))
97
+ abort("Destination directory exists: #{destdir}") if Dir.exist?(destdir) && options.no_clobber
98
+
99
+ FileUtils.rm_rf(Dir[File.join(destdir, '*')].select { |file| File.directory?(file) })
100
+ Batch.from_json_file(index_file).each { |production| production.run(destdir) }
101
+ end
102
+ end
103
+ end
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dieses
4
+ module Application
5
+ module CLI
6
+ module Single
7
+ def self.call(*argv, **options)
8
+ options = OpenStruct.new(options)
9
+ args options(argv, options), argv
10
+
11
+ sheet = argv.first
12
+
13
+ return puts(out = Application.produce(sheet, **options.to_h)) unless options[:output]
14
+
15
+ File.write(options[:output], out)
16
+ rescue OptionParser::InvalidOption, Dieses::Error => e
17
+ abort(e.message)
18
+ end
19
+
20
+ class << self
21
+ private
22
+
23
+ # rubocop:disable Metrics/MethodLength,Metrics/AbcSize
24
+ # codebeat:disable[LOC]
25
+ def options(argv, options)
26
+ Signal.trap('INT') { Kernel.abort '' }
27
+
28
+ OptionParser.new do |option| # rubocop:disable Metrics/BlockLength
29
+ program_name = option.program_name
30
+ option.banner = <<~BANNER
31
+ Usage: #{program_name} [options...] <SHEET>
32
+
33
+ See #{program_name}(1) manual page for detailed help.
34
+
35
+ Options:
36
+
37
+ BANNER
38
+
39
+ option.on('--paper PAPER', 'Paper size [default: a4]', String) do |opt|
40
+ options.paper = opt
41
+ end
42
+
43
+ option.on('--variant VARIANT', 'Sheet variant', String) do |opt|
44
+ options.variant = opt
45
+ end
46
+
47
+ option.on('--landscape', 'Use landscape orientation') do
48
+ options.orientation = :landscape
49
+ end
50
+
51
+ option.on('-o', '--output FILE', 'Output file', String) do |opt|
52
+ options.output = opt
53
+ end
54
+
55
+ option.on('--pdf', 'Generate PDF') do |_opt|
56
+ raise NotImplementedError
57
+ end
58
+
59
+ option.on('-l', '--list', 'List guidesheets') do
60
+ list
61
+ exit
62
+ end
63
+
64
+ option.on_tail('-h', '--help', 'Show this message') do
65
+ abort option.help
66
+ end
67
+
68
+ option.on_tail('-v', '--version', 'Show version') do
69
+ warn VERSION
70
+ exit
71
+ end
72
+ end.tap { |parser| parser.parse!(argv) } # rubocop:disable Style/MultilineBlockChain
73
+ end
74
+ # codebeat:enable[LOC]
75
+ # rubocop:enable Metrics/MethodLength,Metrics/AbcSize
76
+
77
+ def args(parser, argv)
78
+ if argv.empty?
79
+ warn parser.help
80
+ warn ''
81
+ abort "Error: Guidesheet type required. Type #{parser.program_name} -l to list all available guidesheets."
82
+ end
83
+
84
+ return if argv.size <= 1
85
+
86
+ warn parser.help
87
+ warn ''
88
+ abort 'Error: Too many arguments.'
89
+ end
90
+
91
+ def list
92
+ warn 'Sheets:'
93
+ warn ''
94
+ warn Sheets.dump(prefix: "\t")
95
+ end
96
+ end
97
+ end
98
+ end
99
+ end
100
+ end