sevgi 0.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/CHANGELOG.md +9 -0
- data/LICENSE +674 -0
- data/README.md +15 -0
- data/bin/sevgi +136 -0
- data/lib/sevgi/errors.rb +11 -0
- data/lib/sevgi/external.rb +8 -0
- data/lib/sevgi/function.rb +35 -0
- data/lib/sevgi/geometry/elements/rect.rb +133 -0
- data/lib/sevgi/geometry/elements/segment.rb +102 -0
- data/lib/sevgi/geometry/elements.rb +41 -0
- data/lib/sevgi/geometry/equation/line/diagonal.rb +90 -0
- data/lib/sevgi/geometry/equation/line/horizontal.rb +15 -0
- data/lib/sevgi/geometry/equation/line/vertical.rb +52 -0
- data/lib/sevgi/geometry/equation/line.rb +38 -0
- data/lib/sevgi/geometry/equation.rb +3 -0
- data/lib/sevgi/geometry/errors.rb +5 -0
- data/lib/sevgi/geometry/external.rb +15 -0
- data/lib/sevgi/geometry/operation/align.rb +40 -0
- data/lib/sevgi/geometry/operation/sweep.rb +52 -0
- data/lib/sevgi/geometry/operation.rb +28 -0
- data/lib/sevgi/geometry/point.rb +62 -0
- data/lib/sevgi/geometry.rb +10 -0
- data/lib/sevgi/graphics/attribute.rb +125 -0
- data/lib/sevgi/graphics/canvas.rb +51 -0
- data/lib/sevgi/graphics/content.rb +72 -0
- data/lib/sevgi/graphics/document/base.rb +51 -0
- data/lib/sevgi/graphics/document/default.rb +19 -0
- data/lib/sevgi/graphics/document/html.rb +11 -0
- data/lib/sevgi/graphics/document/inkscape.rb +17 -0
- data/lib/sevgi/graphics/document/minimal.rb +11 -0
- data/lib/sevgi/graphics/document.rb +53 -0
- data/lib/sevgi/graphics/element.rb +85 -0
- data/lib/sevgi/graphics/external.rb +15 -0
- data/lib/sevgi/graphics/mixtures/core.rb +81 -0
- data/lib/sevgi/graphics/mixtures/duplicate.rb +36 -0
- data/lib/sevgi/graphics/mixtures/hatch.rb +21 -0
- data/lib/sevgi/graphics/mixtures/identify.rb +93 -0
- data/lib/sevgi/graphics/mixtures/inkscape.rb +15 -0
- data/lib/sevgi/graphics/mixtures/lint.rb +33 -0
- data/lib/sevgi/graphics/mixtures/render.rb +149 -0
- data/lib/sevgi/graphics/mixtures/replicate.rb +125 -0
- data/lib/sevgi/graphics/mixtures/save.rb +23 -0
- data/lib/sevgi/graphics/mixtures/transform.rb +78 -0
- data/lib/sevgi/graphics/mixtures/underscore.rb +21 -0
- data/lib/sevgi/graphics/mixtures/validate.rb +27 -0
- data/lib/sevgi/graphics/mixtures/wrappers.rb +63 -0
- data/lib/sevgi/graphics/mixtures.rb +16 -0
- data/lib/sevgi/graphics.rb +10 -0
- data/lib/sevgi/internal/constant.rb +5 -0
- data/lib/sevgi/internal/dim.rb +13 -0
- data/lib/sevgi/internal/function/file.rb +30 -0
- data/lib/sevgi/internal/function/float.rb +35 -0
- data/lib/sevgi/internal/function/formula.rb +71 -0
- data/lib/sevgi/internal/function/math.rb +43 -0
- data/lib/sevgi/internal/function/string.rb +13 -0
- data/lib/sevgi/internal/function.rb +7 -0
- data/lib/sevgi/internal/list.rb +27 -0
- data/lib/sevgi/internal/locate.rb +47 -0
- data/lib/sevgi/internal/margin.rb +23 -0
- data/lib/sevgi/internal/minitest/script.rb +50 -0
- data/lib/sevgi/internal/minitest/shell.rb +71 -0
- data/lib/sevgi/internal/minitest/suite.rb +35 -0
- data/lib/sevgi/internal/minitest.rb +5 -0
- data/lib/sevgi/internal/paper.rb +26 -0
- data/lib/sevgi/internal.rb +10 -0
- data/lib/sevgi/standard/conform.rb +55 -0
- data/lib/sevgi/standard/data/attribute.rb +496 -0
- data/lib/sevgi/standard/data/element.rb +269 -0
- data/lib/sevgi/standard/data/specification.rb +1841 -0
- data/lib/sevgi/standard/data.rb +79 -0
- data/lib/sevgi/standard/errors.rb +28 -0
- data/lib/sevgi/standard/model.rb +82 -0
- data/lib/sevgi/standard.rb +26 -0
- data/lib/sevgi/utensils/external.rb +22 -0
- data/lib/sevgi/utensils/grid.rb +49 -0
- data/lib/sevgi/utensils/ruler.rb +47 -0
- data/lib/sevgi/utensils/tsquare.rb +28 -0
- data/lib/sevgi/utensils.rb +7 -0
- data/lib/sevgi/version.rb +5 -0
- data/lib/sevgi.rb +10 -0
- metadata +127 -0
@@ -0,0 +1,78 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sevgi
|
4
|
+
module Graphics
|
5
|
+
module Mixtures
|
6
|
+
module Transform
|
7
|
+
module InstanceMethods
|
8
|
+
def Translate(x, y = nil)
|
9
|
+
tap do
|
10
|
+
next if x.to_f == 0.0 && (y.nil? || y.to_f == 0.0)
|
11
|
+
|
12
|
+
attributes[:"transform+"] = "translate(#{(y ? [x, y] : [x]).join(" ")})"
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def Scale(x, y = nil)
|
17
|
+
tap do
|
18
|
+
next if x.to_f == 0.0 && (y.nil? || y.to_f == 0.0)
|
19
|
+
|
20
|
+
attributes[:"transform+"] = "scale(#{(y ? [x, y] : [x]).join(", ")})"
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def Scale!(...)
|
25
|
+
Scale(...).tap do
|
26
|
+
attributes[:"vector-effect"] = "non-scaling-stroke" unless attributes[:"vector-effect"]
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def SkewX(a)
|
31
|
+
tap do
|
32
|
+
next if a.to_f == 0.0
|
33
|
+
|
34
|
+
attributes[:"transform+"] = "skewX(#{a})"
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def SkewY(a)
|
39
|
+
tap do
|
40
|
+
next if a.to_f == 0.0
|
41
|
+
|
42
|
+
attributes[:"transform+"] = "skewY(#{a})"
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def Rotate(a, *origin)
|
47
|
+
tap do
|
48
|
+
ArgumentError.("Incorrect origin (two coordinates required): #{origin}") if !origin.empty? && origin.size != 2
|
49
|
+
|
50
|
+
next if a.to_f == 0.0
|
51
|
+
|
52
|
+
attributes[:"transform+"] = "rotate(#{[a, *origin].join(", ")})"
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def Matrix(*values)
|
57
|
+
tap do
|
58
|
+
ArgumentError.("Incorrect transform matrix (six values required): #{values}") if values.size != 6
|
59
|
+
|
60
|
+
next if values.map(&:to_f).all? { _1 == 0.0 }
|
61
|
+
|
62
|
+
attributes[:"transform+"] = "matrix(#{values.join(" ")})"
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def Align(position, inner:, outer:)
|
67
|
+
return self unless position && inner && outer
|
68
|
+
|
69
|
+
case position.to_sym
|
70
|
+
when :center then Translate((outer.width - inner.width) / 2.0, (outer.height - inner.height) / 2.0)
|
71
|
+
else ArgumentError.("Unsupported alignment: #{position}")
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sevgi
|
4
|
+
module Graphics
|
5
|
+
module Mixtures
|
6
|
+
module Underscore
|
7
|
+
module InstanceMethods
|
8
|
+
def _(*contents)
|
9
|
+
self.class.call(:_, parent: self, contents:)
|
10
|
+
end
|
11
|
+
|
12
|
+
def Ancestral
|
13
|
+
{}.tap do |result|
|
14
|
+
Root.Traverse { |element| result.merge!(element[:_]) if element.has?(:_) }
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "sevgi/standard"
|
4
|
+
|
5
|
+
module Sevgi
|
6
|
+
module Graphics
|
7
|
+
module Mixtures
|
8
|
+
module Validate
|
9
|
+
module InstanceMethods
|
10
|
+
def CData
|
11
|
+
return if !contents || contents.empty?
|
12
|
+
|
13
|
+
Graphics.Text(contents)
|
14
|
+
end
|
15
|
+
|
16
|
+
def Validate
|
17
|
+
Traverse do |element|
|
18
|
+
Standard.conform(
|
19
|
+
element.name, attributes: element.attributes.list, cdata: element.CData, elements: element.children.map(&:name)
|
20
|
+
)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sevgi
|
4
|
+
module Graphics
|
5
|
+
module Mixtures
|
6
|
+
module Wrappers
|
7
|
+
module InstanceMethods
|
8
|
+
def Cline(x1: 0, y1: 0, x2:, y2:, **)
|
9
|
+
path(d: "M #{x1} #{y1} L #{x2} #{y2}", **)
|
10
|
+
end
|
11
|
+
|
12
|
+
def Hline(x1: 0, y1: 0, x2:, **)
|
13
|
+
path(d: "M #{x1} #{y1} H #{x2}", **)
|
14
|
+
end
|
15
|
+
|
16
|
+
def Vline(x1: 0, y1: 0, y2:, **)
|
17
|
+
path(d: "M #{x1} #{y1} V #{y2}", **)
|
18
|
+
end
|
19
|
+
|
20
|
+
def cline(x1: 0, y1: 0, angle:, length:, **)
|
21
|
+
dx = length * ::Math.cos(angle.to_f / 180 * ::Math::PI)
|
22
|
+
dy = length * ::Math.sin(angle.to_f / 180 * ::Math::PI)
|
23
|
+
path(d: "M #{x1} #{y1} l #{dx} #{dy}", **)
|
24
|
+
end
|
25
|
+
|
26
|
+
def css(hash, **)
|
27
|
+
style(Content::CSS.new(hash), type: "text/css", **)
|
28
|
+
end
|
29
|
+
|
30
|
+
def cxline(x1: 0, y1: 0, angle:, dx:, **)
|
31
|
+
dy = dx * ::Math.tan(angle.to_f / 180 * ::Math::PI)
|
32
|
+
path(d: "M #{x1} #{y1} l #{dx} #{dy}", **)
|
33
|
+
end
|
34
|
+
|
35
|
+
def cyline(x1: 0, y1: 0, angle:, dy:, **)
|
36
|
+
dx = dy / ::Math.tan(angle.to_f / 180 * ::Math::PI)
|
37
|
+
path(d: "M #{x1} #{y1} l #{dx} #{dy}", **)
|
38
|
+
end
|
39
|
+
|
40
|
+
def hline(x1: 0, y1: 0, length:, **)
|
41
|
+
path(d: "M #{x1} #{y1} h #{length}", **)
|
42
|
+
end
|
43
|
+
|
44
|
+
def layer(...)
|
45
|
+
g(...)
|
46
|
+
end
|
47
|
+
|
48
|
+
def segment(...)
|
49
|
+
Cline(...)
|
50
|
+
end
|
51
|
+
|
52
|
+
def square(length:, **)
|
53
|
+
rect(width: length, height: length, **)
|
54
|
+
end
|
55
|
+
|
56
|
+
def vline(x1: 0, y1: 0, length:, **)
|
57
|
+
path(d: "M #{x1} #{y1} v #{length}", **)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "mixtures/core"
|
4
|
+
|
5
|
+
require_relative "mixtures/duplicate"
|
6
|
+
require_relative "mixtures/hatch"
|
7
|
+
require_relative "mixtures/identify"
|
8
|
+
require_relative "mixtures/inkscape"
|
9
|
+
require_relative "mixtures/lint"
|
10
|
+
require_relative "mixtures/render"
|
11
|
+
require_relative "mixtures/replicate"
|
12
|
+
require_relative "mixtures/save"
|
13
|
+
require_relative "mixtures/transform"
|
14
|
+
require_relative "mixtures/underscore"
|
15
|
+
require_relative "mixtures/validate"
|
16
|
+
require_relative "mixtures/wrappers"
|
@@ -0,0 +1,10 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "graphics/attribute"
|
4
|
+
require_relative "graphics/content"
|
5
|
+
require_relative "graphics/canvas"
|
6
|
+
require_relative "graphics/element"
|
7
|
+
require_relative "graphics/mixtures"
|
8
|
+
require_relative "graphics/document"
|
9
|
+
|
10
|
+
require_relative "graphics/external"
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sevgi
|
4
|
+
Dim = Data.define(:width, :height, :unit) do
|
5
|
+
def initialize(width:, height:, unit: "mm") = super(width: Float(width), height: Float(height), unit:)
|
6
|
+
|
7
|
+
def longest = deconstruct.max
|
8
|
+
|
9
|
+
def rect = Geometry::Rect[width, height]
|
10
|
+
|
11
|
+
def shortest = deconstruct.min
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "digest"
|
4
|
+
|
5
|
+
module Sevgi
|
6
|
+
module Function
|
7
|
+
module File
|
8
|
+
def changed?(file, content)
|
9
|
+
::File.exist?(file) ? Digest::SHA1.digest(::File.read(file)) != Digest::SHA1.digest(content) : true
|
10
|
+
end
|
11
|
+
|
12
|
+
def out(content, *paths, smart: false)
|
13
|
+
if paths.empty?
|
14
|
+
::Kernel.puts(content)
|
15
|
+
else
|
16
|
+
file = ::File.expand_path(::File.join(*paths))
|
17
|
+
output = "#{content.chomp}\n"
|
18
|
+
|
19
|
+
::File.write(file, output) if !smart || changed?(file, output)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def variations(name, dirs, exts)
|
24
|
+
dirs.product(exts).map { |dir, ext| ::File.join(dir, "#{name}.#{ext}") }
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
extend File
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sevgi
|
4
|
+
module Function
|
5
|
+
module Float
|
6
|
+
@precision = PRECISION = 6
|
7
|
+
|
8
|
+
class << self
|
9
|
+
attr_accessor :precision
|
10
|
+
end
|
11
|
+
|
12
|
+
def approx(float, precision = nil) = float.round(precision || Function::Float.precision)
|
13
|
+
|
14
|
+
def eq?(left, right, precision: nil) = approx(left, precision) == approx(right, precision)
|
15
|
+
|
16
|
+
def ge?(left, right, precision: nil) = approx(left, precision) >= approx(right, precision)
|
17
|
+
|
18
|
+
def gt?(left, right, precision: nil) = approx(left, precision) > approx(right, precision)
|
19
|
+
|
20
|
+
def le?(left, right, precision: nil) = approx(left, precision) <= approx(right, precision)
|
21
|
+
|
22
|
+
def lt?(left, right, precision: nil) = approx(left, precision) < approx(right, precision)
|
23
|
+
|
24
|
+
def nonzero?(...) = !zero?(...)
|
25
|
+
|
26
|
+
def prettify(*args) = args.map { (i = _1.to_i) == _1.to_f ? i : _1 }
|
27
|
+
|
28
|
+
def round(float, precision) = precision ? float.round(precision) : float
|
29
|
+
|
30
|
+
def zero?(value, precision: nil) = eq?(value, 0.0, precision:)
|
31
|
+
end
|
32
|
+
|
33
|
+
extend Float
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sevgi
|
4
|
+
module Function
|
5
|
+
module Formula
|
6
|
+
# Y
|
7
|
+
# ^
|
8
|
+
# | slope (Y/X)
|
9
|
+
# | .
|
10
|
+
# | .
|
11
|
+
# | pb +
|
12
|
+
# | . . coangle°
|
13
|
+
# | . .
|
14
|
+
# | . .
|
15
|
+
# | . .
|
16
|
+
# | (hypothenus) d dy
|
17
|
+
# | . .
|
18
|
+
# | . .
|
19
|
+
# | pa . angle° . pc
|
20
|
+
# | + . . dx . . +
|
21
|
+
# | 90° . ..
|
22
|
+
# | . . .
|
23
|
+
# | . . r
|
24
|
+
# | . ry .
|
25
|
+
# | . .
|
26
|
+
# | . + . rx . +
|
27
|
+
# | . . . noangle°
|
28
|
+
# +--------------+------------+---------------------> X
|
29
|
+
# | intercept (Y)
|
30
|
+
# |
|
31
|
+
# |
|
32
|
+
#
|
33
|
+
|
34
|
+
def coangle(angle) = 90.0 - angle # complementary angle
|
35
|
+
|
36
|
+
def angler(dx, dy) = atan(dy / dx)
|
37
|
+
|
38
|
+
def angles(slope) = atan(slope)
|
39
|
+
|
40
|
+
def distance(p, q) = ::Math.sqrt(dxp(p, q)**2 + dyp(p, q)**2)
|
41
|
+
|
42
|
+
def height(pa, pb) = dyp(pa, pb).abs
|
43
|
+
|
44
|
+
def hypothenus(dx, dy) = ::Math.sqrt(dx**2 + dy**2)
|
45
|
+
|
46
|
+
def intercept(point, angle, slope) = point.y - (slope * point.x) # point is pa or pb
|
47
|
+
|
48
|
+
def noangle(angle) = 90.0 + angle # normal angle
|
49
|
+
|
50
|
+
def rx(r, angle) = r * sin(angle)
|
51
|
+
|
52
|
+
def ry(r, angle) = r * cos(angle)
|
53
|
+
|
54
|
+
def slopea(angle) = tan(angle)
|
55
|
+
|
56
|
+
def sloper(dx, dy) = dy / dx
|
57
|
+
|
58
|
+
def width(pa, pb) = dxp(pa, pb).abs
|
59
|
+
|
60
|
+
def dxp(pa, pb) = pb.x - pa.x
|
61
|
+
|
62
|
+
def dyp(pa, pb) = pb.y - pa.y
|
63
|
+
|
64
|
+
def dxa(d, angle) = d * cos(angle)
|
65
|
+
|
66
|
+
def dya(d, angle) = d * sin(angle)
|
67
|
+
end
|
68
|
+
|
69
|
+
extend Formula
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sevgi
|
4
|
+
module Function
|
5
|
+
module Math
|
6
|
+
def acos(value) = to_degrees(::Math.acos(value))
|
7
|
+
|
8
|
+
def acot(value) = 90.0 - to_degrees(::Math.atan(value))
|
9
|
+
|
10
|
+
def asin(value) = to_degrees(::Math.asin(value))
|
11
|
+
|
12
|
+
def atan(value) = to_degrees(::Math.atan(value))
|
13
|
+
|
14
|
+
def complement(degrees) = 90.0 - degrees
|
15
|
+
|
16
|
+
def cos(degrees) = ::Math.cos(to_radians(degrees))
|
17
|
+
|
18
|
+
def cot(degrees) = 1.0 / ::Math.tan(to_radians(degrees))
|
19
|
+
|
20
|
+
def golden = @golden ||= ((1.0 + ::Math.sqrt(5)) / 2.0)
|
21
|
+
|
22
|
+
def horizontal?(degrees, precision = nil) = zero?(degrees % 180.0, precision:)
|
23
|
+
|
24
|
+
def nangle(degrees) = degrees - 90.0
|
25
|
+
|
26
|
+
def sin(degrees) = ::Math.sin(to_radians(degrees))
|
27
|
+
|
28
|
+
def sqrt2 = @sqrt2 ||= ::Math.sqrt(2)
|
29
|
+
|
30
|
+
def sqrt2h = @sqrt2h ||= (sqrt2 / 2.0)
|
31
|
+
|
32
|
+
def tan(degrees) = ::Math.tan(to_radians(degrees))
|
33
|
+
|
34
|
+
def to_degrees(radians) = radians.to_f * 180 / ::Math::PI
|
35
|
+
|
36
|
+
def to_radians(degrees) = degrees.to_f / 180 * ::Math::PI
|
37
|
+
|
38
|
+
def vertical?(degrees, precision = nil) = zero?(degrees % 90.0, precision:)
|
39
|
+
end
|
40
|
+
|
41
|
+
extend Math
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sevgi
|
4
|
+
module List
|
5
|
+
def [](name) = data[name]
|
6
|
+
|
7
|
+
def import(**kwargs) = data.merge!(kwargs.reject { |key, _| data.key?(key) })
|
8
|
+
|
9
|
+
def valid?(name) = data.key?(name)
|
10
|
+
|
11
|
+
class << self
|
12
|
+
def extended(base)
|
13
|
+
super
|
14
|
+
|
15
|
+
base.class_exec do
|
16
|
+
@data = {}
|
17
|
+
|
18
|
+
class << self
|
19
|
+
attr_reader :data
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
private_constant :List
|
27
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sevgi
|
4
|
+
class Locate
|
5
|
+
Location = Data.define(:file, :slug, :dir)
|
6
|
+
|
7
|
+
private_constant :Location
|
8
|
+
|
9
|
+
attr_reader :slugs, :start
|
10
|
+
|
11
|
+
def initialize(slugs, start = Dir.pwd)
|
12
|
+
@slugs = slugs
|
13
|
+
@start = start
|
14
|
+
end
|
15
|
+
|
16
|
+
def call(&block)
|
17
|
+
origin = Dir.pwd
|
18
|
+
Dir.chdir(start)
|
19
|
+
|
20
|
+
here = Dir.pwd
|
21
|
+
until (found = match(&block))
|
22
|
+
Dir.chdir("..")
|
23
|
+
return if Dir.pwd == here
|
24
|
+
|
25
|
+
here = Dir.pwd
|
26
|
+
end
|
27
|
+
|
28
|
+
Location[::File.expand_path(found, here), found, here]
|
29
|
+
ensure
|
30
|
+
Dir.chdir(origin)
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def match(&block)
|
36
|
+
finder = block || proc { |path| ::File.exist?(path) }
|
37
|
+
|
38
|
+
slugs.find { finder.call(_1) }
|
39
|
+
end
|
40
|
+
|
41
|
+
class << self
|
42
|
+
def call(*, &block) = new(*).call(&block)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
private_constant :Locate
|
47
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sevgi
|
4
|
+
Margin = Data.define(:top, :right, :bottom, :left) do
|
5
|
+
def initialize(top: nil, right: nil, bottom: nil, left: nil)
|
6
|
+
case [top, right, bottom, left]
|
7
|
+
in Numeric, Numeric, Numeric, Numeric then # nop
|
8
|
+
in Numeric, Numeric, Numeric, NilClass then left = right
|
9
|
+
in Numeric, Numeric, NilClass, NilClass then bottom, left = top, right
|
10
|
+
in Numeric, NilClass, NilClass, NilClass then bottom, left, right = top, top, top
|
11
|
+
in NilClass, NilClass, NilClass, NilClass then top, bottom, left, right = 0, 0, 0, 0
|
12
|
+
end
|
13
|
+
|
14
|
+
super(top: Float(top), right: Float(right), bottom: Float(bottom), left: Float(left))
|
15
|
+
end
|
16
|
+
|
17
|
+
alias_method :to_a, :deconstruct
|
18
|
+
|
19
|
+
class << self
|
20
|
+
def zero = (@zero ||= self[0.0, 0.0, 0.0, 0.0])
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sevgi
|
4
|
+
module Test
|
5
|
+
class Script
|
6
|
+
attr_reader :file
|
7
|
+
|
8
|
+
def initialize(file) = @file = sanitize(file)
|
9
|
+
|
10
|
+
def dir = @dir ||= ::File.dirname(file)
|
11
|
+
|
12
|
+
def name = @name ||= ::File.basename(file, ".*")
|
13
|
+
|
14
|
+
def run(*) = Shell.run(file, *)
|
15
|
+
|
16
|
+
def suite = @suite ||= ::File.basename(dir)
|
17
|
+
|
18
|
+
def svg? = ::File.exist?(svg)
|
19
|
+
|
20
|
+
def svg = @svg ||= "#{dir}/#{name}.svg"
|
21
|
+
|
22
|
+
# A gross hack to avoid touching filesystem during testing. Intercept the Save methods to display output in stdout
|
23
|
+
# instead of saving an actual file. This hack meets the following criteria:
|
24
|
+
#
|
25
|
+
# - Does not make an invasive change to the library just for the sake of testing (hence the interception technique)
|
26
|
+
# - Do not create a separate file for the interceptor (hence feeding the interceptor through /dev/stdin)
|
27
|
+
|
28
|
+
INTERCEPTOR = <<~LIB
|
29
|
+
class Sevgi::Graphics::Document::Base
|
30
|
+
def Save(*, **) = Out(**)
|
31
|
+
def Save!(...) = Save(...)
|
32
|
+
end
|
33
|
+
LIB
|
34
|
+
|
35
|
+
def run_passive(*)
|
36
|
+
warn(" ==> #{file}")
|
37
|
+
Shell.run("sevgi", "-l", "/dev/stdin", file, *) { puts INTERCEPTOR }
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def sanitize(file)
|
43
|
+
file.tap do
|
44
|
+
ArgumentError.("No such file: #{file}") unless ::File.exist?(file)
|
45
|
+
ArgumentError.("Not an executable: #{file}") unless ::File.executable?(file)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "open3"
|
4
|
+
|
5
|
+
module Sevgi
|
6
|
+
module Test
|
7
|
+
module Shell
|
8
|
+
Result = Data.define(:args, :out, :err, :exit_code) do
|
9
|
+
def cmd = args.join(" ")
|
10
|
+
|
11
|
+
def notok? = !ok?
|
12
|
+
|
13
|
+
def ok? = exit_code&.zero?
|
14
|
+
|
15
|
+
def outline = out.first
|
16
|
+
|
17
|
+
def to_s = out.join("\n")
|
18
|
+
end
|
19
|
+
|
20
|
+
# Adapted to popen3 from github.com/mina-deploy/mina
|
21
|
+
class Runner
|
22
|
+
def initialize
|
23
|
+
@coathooks = 0
|
24
|
+
end
|
25
|
+
|
26
|
+
def run(*args, &block)
|
27
|
+
out, err, status = Open3.popen3(*args) do |stdin, stdout, stderr, thread|
|
28
|
+
inputs(stdin, thread, &block) if block
|
29
|
+
outputs(stdout, stderr, thread)
|
30
|
+
end
|
31
|
+
|
32
|
+
Result[args, out, err, status.exitstatus]
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def inputs(stdin, thread, &block)
|
38
|
+
stdin.instance_exec(thread, &block)
|
39
|
+
stdin.close unless stdin.closed?
|
40
|
+
end
|
41
|
+
|
42
|
+
def outputs(stdout, stderr, thread)
|
43
|
+
trap("INT") { handle_sigint(thread.pid) } # handle `^C`
|
44
|
+
|
45
|
+
out = stdout.readlines.map(&:chomp)
|
46
|
+
err = stderr.readlines.map(&:chomp)
|
47
|
+
|
48
|
+
[out, err, thread.value]
|
49
|
+
end
|
50
|
+
|
51
|
+
def handle_sigint(pid)
|
52
|
+
message, signal = if @coathooks > 1
|
53
|
+
["SIGINT received again. Force quitting...", "KILL"]
|
54
|
+
else
|
55
|
+
["SIGINT received.", "TERM"]
|
56
|
+
end
|
57
|
+
|
58
|
+
warn("\n#{message}")
|
59
|
+
::Process.kill(signal, pid)
|
60
|
+
@coathooks += 1
|
61
|
+
rescue Errno::ESRCH
|
62
|
+
warn("No process to kill.")
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
extend self
|
67
|
+
|
68
|
+
def run(...) = Runner.new.run(...)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|