sevgi 0.0.0
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.
- 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
|