sevgi 0.93.1 → 0.94.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 +4 -4
- data/CHANGELOG.md +4 -0
- data/LICENSE +5 -0
- data/README.md +58 -2
- data/lib/sevgi/binaries/rake.rb +11 -4
- data/lib/sevgi/binaries/sevgi.rb +1 -2
- data/lib/sevgi/executor/error.rb +4 -0
- data/lib/sevgi/executor/scope.rb +25 -12
- data/lib/sevgi/executor.rb +100 -21
- data/lib/sevgi/toplevel/executor.rb +5 -3
- data/lib/sevgi/toplevel/function.rb +3 -10
- data/lib/sevgi/toplevel/sundries.rb +5 -2
- data/lib/sevgi/toplevel.rb +9 -6
- data/lib/sevgi/version.rb +1 -1
- data/lib/sevgi.rb +6 -3
- metadata +16 -14
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ceb2e9987cc3e1ee86f9a7e8961214a60b81685e89b2f2d9dc5986e3b2eed8f6
|
|
4
|
+
data.tar.gz: 50240698f875654431f4caf9681ddf8b163971c5f7d4ef9781849be17541e37d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9ddd81f3154b0bab5f2fa17890301f97fc960572874621bc42634f04caf826664c73a19082b9e8222a7155f2dcb5132f99a01afdb8ee841c6d6376bf162020b2
|
|
7
|
+
data.tar.gz: a8902dc527a3bcc3f26bce6f2eb8ecd442a26b4628db70c39011523bf409628f148c3288c7efc357b96b57fc67bc8aa640f7420d278494f8c32d2616bf9b615e
|
data/CHANGELOG.md
ADDED
data/LICENSE
ADDED
data/README.md
CHANGED
|
@@ -1,5 +1,61 @@
|
|
|
1
1
|
# Sevgi
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Top-level API and `.sevgi` script runner.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
gem install sevgi
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Require
|
|
12
|
+
|
|
13
|
+
```ruby
|
|
14
|
+
require "sevgi"
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Example
|
|
18
|
+
|
|
19
|
+
```ruby
|
|
20
|
+
SVG(:minimal) do
|
|
21
|
+
rect(width: 3, height: 5)
|
|
22
|
+
end.call
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Executable
|
|
26
|
+
|
|
27
|
+
```sh
|
|
28
|
+
sevgi drawing.sevgi
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Ruby compatibility
|
|
32
|
+
|
|
33
|
+
Requires Ruby 3.4.0 or newer. CI verifies Ruby 3.4 and the current development Ruby from `.ruby-version`.
|
|
34
|
+
|
|
35
|
+
## Native prerequisites
|
|
36
|
+
|
|
37
|
+
The top-level gem installs Sevgi's standard components without native export gems. SVG-only scripts need no native
|
|
38
|
+
packages beyond the standard Ruby dependencies.
|
|
39
|
+
|
|
40
|
+
PDF/PNG export helpers come from `sevgi-sundries` and lazily load the optional Ruby gems `cairo`, `rsvg2`, and
|
|
41
|
+
`hexapdf`. On Debian/Ubuntu:
|
|
42
|
+
|
|
43
|
+
```sh
|
|
44
|
+
sudo apt-get update
|
|
45
|
+
sudo apt-get install -y libcairo2-dev libgdk-pixbuf-2.0-dev libgirepository1.0-dev libglib2.0-dev librsvg2-dev pkg-config
|
|
46
|
+
gem install cairo rsvg2 hexapdf
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
On macOS with Homebrew:
|
|
50
|
+
|
|
51
|
+
```sh
|
|
52
|
+
brew install cairo gdk-pixbuf gobject-introspection librsvg pkg-config
|
|
53
|
+
gem install cairo rsvg2 hexapdf
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Links
|
|
57
|
+
|
|
58
|
+
- Documentation: https://sevgi.roktas.dev
|
|
59
|
+
- API documentation: https://www.rubydoc.info/gems/sevgi
|
|
60
|
+
- Source: https://github.com/roktas/sevgi/tree/main/toplevel
|
|
61
|
+
- Changelog: https://github.com/roktas/sevgi/blob/main/CHANGELOG.md
|
data/lib/sevgi/binaries/rake.rb
CHANGED
|
@@ -5,17 +5,24 @@ require "sevgi"
|
|
|
5
5
|
# Extends FileUtils with Sevgi script helpers for Rake tasks.
|
|
6
6
|
module FileUtils
|
|
7
7
|
# Thin DSL wrapper to call a script without spawning a shell.
|
|
8
|
+
#
|
|
9
|
+
# The script receives positional arguments through `ARGA` and keyword arguments through `ARGH`.
|
|
10
|
+
#
|
|
11
|
+
# @example Run a Sevgi script from a Rake task
|
|
12
|
+
# sevgi "drawings/card", "front", theme: :dark
|
|
13
|
+
#
|
|
8
14
|
# @param file [String] Sevgi script file, with or without `.sevgi` extension
|
|
9
15
|
# @param args [Array] positional arguments exposed to the script as `ARGA`
|
|
10
16
|
# @param kwargs [Hash] keyword arguments exposed to the script as `ARGH`
|
|
11
17
|
# @return [Sevgi::Executor::Scope, nil] execution scope, or nil for an empty file
|
|
12
18
|
# @raise [Sevgi::ArgumentError] when the script file cannot be found
|
|
19
|
+
# @see Sevgi::Executor.execute_file
|
|
13
20
|
def sevgi(file, *args, **kwargs)
|
|
14
|
-
Sevgi.execute_file(F.existing!(file, [EXTENSION])) do
|
|
15
|
-
|
|
21
|
+
Sevgi::Executor.execute_file(Sevgi::F.existing!(file, [Sevgi::EXTENSION])) do
|
|
22
|
+
extend(Sevgi)
|
|
16
23
|
|
|
17
|
-
|
|
18
|
-
|
|
24
|
+
const_set(:ARGA, args).freeze
|
|
25
|
+
const_set(:ARGH, kwargs).freeze
|
|
19
26
|
end
|
|
20
27
|
end
|
|
21
28
|
end
|
data/lib/sevgi/binaries/sevgi.rb
CHANGED
|
@@ -56,8 +56,7 @@ module Sevgi
|
|
|
56
56
|
# Runs the `sevgi` command-line interface.
|
|
57
57
|
# @param argv [Array<String>, String, nil] command-line arguments
|
|
58
58
|
# @return [nil]
|
|
59
|
-
# @raise [
|
|
60
|
-
# @raise [Sevgi::Executor::Error] when `--exception` or `SEVGI_VOMIT` requests raw errors
|
|
59
|
+
# @raise [Sevgi::Executor::Error] when `--exception` or `SEVGI_VOMIT` requests raw executor errors
|
|
61
60
|
# @raise [SystemExit] when command-line usage or script execution aborts
|
|
62
61
|
def call(argv)
|
|
63
62
|
return puts(help) if (options = Options.parse(argv = Array(argv))).help
|
data/lib/sevgi/executor/error.rb
CHANGED
|
@@ -32,6 +32,10 @@ module Sevgi
|
|
|
32
32
|
.map { |line| line.delete_prefix("#{::Dir.pwd}/") }
|
|
33
33
|
end
|
|
34
34
|
|
|
35
|
+
# Returns the original exception as the wrapped cause.
|
|
36
|
+
# @return [Exception]
|
|
37
|
+
def cause = error
|
|
38
|
+
|
|
35
39
|
# Returns the script load stack active at failure time.
|
|
36
40
|
# @return [Array<String>] script file names in load order
|
|
37
41
|
def stack = scope.stack
|
data/lib/sevgi/executor/scope.rb
CHANGED
|
@@ -3,10 +3,10 @@
|
|
|
3
3
|
module Sevgi
|
|
4
4
|
class Executor
|
|
5
5
|
# Holds one isolated Sevgi script execution scope and its result.
|
|
6
|
+
#
|
|
7
|
+
# Scope objects belong to one executor run and are pushed onto the current
|
|
8
|
+
# fiber's executor stack. They are not shared between concurrent executions.
|
|
6
9
|
class Scope
|
|
7
|
-
# Error raised by low-level executor scope operations.
|
|
8
|
-
Error = Class.new(::Sevgi::Error)
|
|
9
|
-
|
|
10
10
|
# @!attribute [r] scope
|
|
11
11
|
# @return [Module] isolated module where script source is evaluated
|
|
12
12
|
# @!attribute [r] recent
|
|
@@ -18,6 +18,8 @@ module Sevgi
|
|
|
18
18
|
# Creates a script execution scope.
|
|
19
19
|
# @param scope [Module, nil] existing module to evaluate source in
|
|
20
20
|
# @return [void]
|
|
21
|
+
# @note When no module is supplied, the internal module reports its name as `Sevgi::Main` for readable Ruby error
|
|
22
|
+
# messages without publishing a process-global `Sevgi::Main` constant.
|
|
21
23
|
def initialize(scope = nil)
|
|
22
24
|
@scope = scope || main
|
|
23
25
|
@recent = nil
|
|
@@ -51,17 +53,34 @@ module Sevgi
|
|
|
51
53
|
end
|
|
52
54
|
end
|
|
53
55
|
|
|
56
|
+
# Captures a preprocessing failure for this scope.
|
|
57
|
+
# @param source [Sevgi::Executor::Source] source active when preprocessing failed
|
|
58
|
+
# @param error [Exception] original preprocessing exception
|
|
59
|
+
# @return [Sevgi::Executor::Scope] self, with error populated
|
|
60
|
+
# @api private
|
|
61
|
+
def capture(source, error)
|
|
62
|
+
push(source)
|
|
63
|
+
@error = Executor::Error.new(error, self)
|
|
64
|
+
self
|
|
65
|
+
end
|
|
66
|
+
|
|
54
67
|
# Loads a file into this existing execution scope.
|
|
55
68
|
# @param file [String] source file to read and evaluate
|
|
56
69
|
# @yield optional boot block that installs DSL methods before evaluation
|
|
57
70
|
# @yieldreturn [void]
|
|
58
71
|
# @return [Sevgi::Executor::Scope] self, with recent or error populated
|
|
59
|
-
# @
|
|
72
|
+
# @note File-read failures are captured as {Sevgi::Executor::Error} on this scope.
|
|
60
73
|
# @api private
|
|
61
|
-
def load(file, &block)
|
|
74
|
+
def load(file, &block)
|
|
75
|
+
call(Source.load(file), &block)
|
|
76
|
+
rescue ::SystemCallError => e
|
|
77
|
+
capture(Source.new(string: "", file:, line: 1), e)
|
|
78
|
+
throw(:result, self)
|
|
79
|
+
end
|
|
62
80
|
|
|
63
81
|
# Returns the unique source stack for this execution.
|
|
64
82
|
# @return [Array<String>] source file keys in load order
|
|
83
|
+
# @note The stack is owned by this scope and is not shared with concurrent executions.
|
|
65
84
|
def stack = @stack.keys
|
|
66
85
|
|
|
67
86
|
# Returns the most recently pushed source.
|
|
@@ -69,9 +88,6 @@ module Sevgi
|
|
|
69
88
|
# @api private
|
|
70
89
|
def peek = @stack[@stack.keys.last]
|
|
71
90
|
|
|
72
|
-
# Constant name used for the temporary script module under {Sevgi}.
|
|
73
|
-
MAIN_MODULE = :Main
|
|
74
|
-
|
|
75
91
|
private
|
|
76
92
|
|
|
77
93
|
def boot(receiver, &boot)
|
|
@@ -86,10 +102,7 @@ module Sevgi
|
|
|
86
102
|
end
|
|
87
103
|
|
|
88
104
|
def main
|
|
89
|
-
Module.new.tap
|
|
90
|
-
Sevgi.send(:remove_const, MAIN_MODULE) if Sevgi.const_defined?(MAIN_MODULE)
|
|
91
|
-
Sevgi.const_set(MAIN_MODULE, mod)
|
|
92
|
-
end
|
|
105
|
+
Module.new.tap { |mod| mod.define_singleton_method(:name) { "Sevgi::Main" } }
|
|
93
106
|
end
|
|
94
107
|
|
|
95
108
|
def push(source)
|
data/lib/sevgi/executor.rb
CHANGED
|
@@ -11,14 +11,24 @@ module Sevgi
|
|
|
11
11
|
#
|
|
12
12
|
# The executor is used by script mode and by the `Load` DSL word to preserve a
|
|
13
13
|
# useful load stack while keeping DSL methods out of the caller's global object
|
|
14
|
-
# whenever possible.
|
|
14
|
+
# whenever possible. Active scope stacks are isolated per Ruby fiber, so
|
|
15
|
+
# concurrent executions can perform nested `Load` calls without sharing scope
|
|
16
|
+
# state. The process SIGINT handler is shared by Ruby, so executor runs guard it
|
|
17
|
+
# with a reference-counted critical section and restore the previous handler
|
|
18
|
+
# after the last active execution finishes.
|
|
15
19
|
class Executor
|
|
16
20
|
include Singleton
|
|
17
21
|
|
|
22
|
+
# Thread-current key used for the fiber-local executor scope stack.
|
|
23
|
+
# @api private
|
|
24
|
+
SCOPE_KEY = :sevgi_executor_scopes
|
|
25
|
+
private_constant :SCOPE_KEY, :Source
|
|
26
|
+
|
|
18
27
|
# Loads a script file inside the current executor scope.
|
|
19
28
|
# @param file [String] path to a Sevgi script file
|
|
20
29
|
# @return [Sevgi::Executor::Scope] current execution scope
|
|
21
30
|
# @raise [Sevgi::PanicError] when there is no active executor scope
|
|
31
|
+
# @note Uses the active executor scope from the current fiber.
|
|
22
32
|
# @api private
|
|
23
33
|
def self.load(file, ...)
|
|
24
34
|
PanicError.("box stack empty; create a box first") unless instance.current
|
|
@@ -35,20 +45,11 @@ module Sevgi
|
|
|
35
45
|
# @yield optional boot block that installs DSL methods before evaluation
|
|
36
46
|
# @yieldreturn [void]
|
|
37
47
|
# @return [Sevgi::Executor::Scope, nil] execution scope, or nil for empty source
|
|
38
|
-
# @
|
|
48
|
+
# @note Required-library load failures are captured as {Sevgi::Executor::Error} on the returned scope.
|
|
49
|
+
# @note Reentrant and concurrent calls keep independent scope stacks per fiber. The temporary SIGINT handler remains
|
|
50
|
+
# process-global while any execution is active.
|
|
39
51
|
def self.execute(string, file: nil, line: nil, require: nil, receiver: nil, &block)
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
interrupt = Signal.trap("INT") { Kernel.abort("") }
|
|
43
|
-
|
|
44
|
-
::Kernel.require(require) if require
|
|
45
|
-
|
|
46
|
-
scope = instance.create
|
|
47
|
-
catch(:result) { scope.call(Source.new(string:, file:, line:), receiver, &block) }
|
|
48
|
-
|
|
49
|
-
ensure
|
|
50
|
-
Signal.trap("INT", interrupt) if interrupt
|
|
51
|
-
instance.shutdown if scope
|
|
52
|
+
execute_source(Source.new(string:, file:, line:), require:, receiver:, &block)
|
|
52
53
|
end
|
|
53
54
|
|
|
54
55
|
# Executes a file inside a managed Sevgi script scope.
|
|
@@ -58,10 +59,18 @@ module Sevgi
|
|
|
58
59
|
# @yield optional boot block that installs DSL methods before evaluation
|
|
59
60
|
# @yieldreturn [void]
|
|
60
61
|
# @return [Sevgi::Executor::Scope, nil] execution scope, or nil for an empty file
|
|
61
|
-
# @
|
|
62
|
-
# @
|
|
62
|
+
# @note File-read and required-library load failures are captured as {Sevgi::Executor::Error} on the returned scope.
|
|
63
|
+
# @note Reentrant and concurrent calls keep independent scope stacks per fiber. The temporary SIGINT handler remains
|
|
64
|
+
# process-global while any execution is active.
|
|
63
65
|
def self.execute_file(file, require: nil, receiver: nil, &block)
|
|
64
|
-
|
|
66
|
+
source = nil
|
|
67
|
+
begin
|
|
68
|
+
source = Source.load(file)
|
|
69
|
+
rescue ::SystemCallError => e
|
|
70
|
+
return capture_error(Source.new(string: "", file:, line: 1), e)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
execute_source(source, require:, receiver:, &block)
|
|
65
74
|
end
|
|
66
75
|
|
|
67
76
|
# Removes the current executor scope.
|
|
@@ -71,22 +80,92 @@ module Sevgi
|
|
|
71
80
|
instance.shutdown
|
|
72
81
|
end
|
|
73
82
|
|
|
74
|
-
def initialize
|
|
83
|
+
def initialize
|
|
84
|
+
@signal_count = 0
|
|
85
|
+
@signal_mutex = Mutex.new
|
|
86
|
+
@signal_previous = nil
|
|
87
|
+
end
|
|
75
88
|
|
|
76
89
|
# Creates and pushes a new executor scope.
|
|
77
90
|
# @param scope [Module, nil] existing module scope to reuse
|
|
78
91
|
# @return [Sevgi::Executor::Scope] created scope
|
|
79
92
|
# @api private
|
|
80
|
-
def create(scope = nil) = Scope.new(scope).tap {
|
|
93
|
+
def create(scope = nil) = Scope.new(scope).tap { scopes << it }
|
|
81
94
|
|
|
82
95
|
# Returns the active executor scope.
|
|
83
96
|
# @return [Sevgi::Executor::Scope, nil] active scope, if any
|
|
84
97
|
# @api private
|
|
85
|
-
def current =
|
|
98
|
+
def current = scopes.last
|
|
99
|
+
|
|
100
|
+
# Restores the process SIGINT handler when the last active execution ends.
|
|
101
|
+
# @return [void]
|
|
102
|
+
# @api private
|
|
103
|
+
def restore
|
|
104
|
+
@signal_mutex.synchronize do
|
|
105
|
+
next if @signal_count.zero?
|
|
106
|
+
|
|
107
|
+
@signal_count -= 1
|
|
108
|
+
next unless @signal_count.zero?
|
|
109
|
+
|
|
110
|
+
Signal.trap("INT", @signal_previous)
|
|
111
|
+
@signal_previous = nil
|
|
112
|
+
end
|
|
113
|
+
end
|
|
86
114
|
|
|
87
115
|
# Removes the active executor scope.
|
|
116
|
+
# @param scope [Sevgi::Executor::Scope, nil] exact scope to remove, or nil to pop the current scope
|
|
88
117
|
# @return [Sevgi::Executor::Scope, nil] removed scope
|
|
89
118
|
# @api private
|
|
90
|
-
def shutdown =
|
|
119
|
+
def shutdown(scope = nil)
|
|
120
|
+
return scopes.pop unless scope
|
|
121
|
+
return scopes.pop if scopes.last.equal?(scope)
|
|
122
|
+
|
|
123
|
+
scopes.delete(scope)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Installs the process SIGINT handler while one or more executions are active.
|
|
127
|
+
# @return [void]
|
|
128
|
+
# @api private
|
|
129
|
+
def trap
|
|
130
|
+
@signal_mutex.synchronize do
|
|
131
|
+
@signal_previous = Signal.trap("INT") { Kernel.abort("") } if @signal_count.zero?
|
|
132
|
+
|
|
133
|
+
@signal_count += 1
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def self.capture_error(source, error)
|
|
138
|
+
instance.trap
|
|
139
|
+
scope = instance.create
|
|
140
|
+
scope.capture(source, error)
|
|
141
|
+
ensure
|
|
142
|
+
instance.restore
|
|
143
|
+
instance.shutdown(scope) if scope
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def self.execute_source(source, require:, receiver:, &block)
|
|
147
|
+
return if source.string.empty?
|
|
148
|
+
|
|
149
|
+
instance.trap
|
|
150
|
+
scope = instance.create
|
|
151
|
+
catch(:result) { run_source(scope, source, require, receiver, &block) }
|
|
152
|
+
|
|
153
|
+
ensure
|
|
154
|
+
instance.restore
|
|
155
|
+
instance.shutdown(scope) if scope
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def self.run_source(scope, source, library, receiver, &block)
|
|
159
|
+
::Kernel.require(library) if library
|
|
160
|
+
scope.call(source, receiver, &block)
|
|
161
|
+
rescue ::LoadError => e
|
|
162
|
+
scope.capture(source, e)
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
private_class_method :capture_error, :execute_source, :run_source
|
|
166
|
+
|
|
167
|
+
private
|
|
168
|
+
|
|
169
|
+
def scopes = Thread.current[SCOPE_KEY] ||= []
|
|
91
170
|
end
|
|
92
171
|
end
|
|
@@ -11,7 +11,8 @@ module Sevgi
|
|
|
11
11
|
# @param args [Array] arguments forwarded to {Sevgi::Executor.execute}
|
|
12
12
|
# @param kwargs [Hash] keyword arguments forwarded to {Sevgi::Executor.execute}
|
|
13
13
|
# @return [Sevgi::Executor::Scope, nil] execution result, or nil for empty source
|
|
14
|
-
# @
|
|
14
|
+
# @note Required-library load failures are captured as {Sevgi::Executor::Error} on the returned scope.
|
|
15
|
+
# @note Reentrant and concurrent calls keep independent executor scope stacks per fiber.
|
|
15
16
|
# @see Sevgi::Executor.execute
|
|
16
17
|
def self.execute(*args, **kwargs) = Executor.execute(*args, **kwargs, &BootBlock)
|
|
17
18
|
|
|
@@ -19,8 +20,8 @@ module Sevgi
|
|
|
19
20
|
# @param args [Array] arguments forwarded to {Sevgi::Executor.execute_file}
|
|
20
21
|
# @param kwargs [Hash] keyword arguments forwarded to {Sevgi::Executor.execute_file}
|
|
21
22
|
# @return [Sevgi::Executor::Scope, nil] execution result, or nil for an empty file
|
|
22
|
-
# @
|
|
23
|
-
# @
|
|
23
|
+
# @note File-read and required-library load failures are captured as {Sevgi::Executor::Error} on the returned scope.
|
|
24
|
+
# @note Reentrant and concurrent calls keep independent executor scope stacks per fiber.
|
|
24
25
|
# @see Sevgi::Executor.execute_file
|
|
25
26
|
def self.execute_file(*args, **kwargs) = Executor.execute_file(*args, **kwargs, &BootBlock)
|
|
26
27
|
|
|
@@ -30,6 +31,7 @@ module Sevgi
|
|
|
30
31
|
# @return [Array<String>] the input file list
|
|
31
32
|
# @raise [Sevgi::PanicError] when called without an active executor scope
|
|
32
33
|
# @raise [Sevgi::Error] when a file cannot be located
|
|
34
|
+
# @note `Load` resolves against the active executor scope in the current fiber.
|
|
33
35
|
def Load(*files)
|
|
34
36
|
start = ::File.dirname(caller_locations(1..1).first.path)
|
|
35
37
|
|
|
@@ -4,20 +4,13 @@ require "sevgi/function"
|
|
|
4
4
|
|
|
5
5
|
module Sevgi
|
|
6
6
|
module Toplevel
|
|
7
|
-
#
|
|
7
|
+
# Promotes the canonical function helper namespace as `F` in the full top-level DSL.
|
|
8
8
|
#
|
|
9
9
|
# @example Use helper functions inside script mode
|
|
10
10
|
# SVG do
|
|
11
11
|
# text F.pluralize("axis")
|
|
12
12
|
# end
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
extend Sevgi::Function::Math
|
|
16
|
-
extend Sevgi::Function::Pluralize
|
|
17
|
-
extend Sevgi::Function::Shell
|
|
18
|
-
extend Sevgi::Function::UI
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
promote Function, :F
|
|
13
|
+
# @see Sevgi::Function
|
|
14
|
+
promote Sevgi::Function, :F
|
|
22
15
|
end
|
|
23
16
|
end
|
|
@@ -7,9 +7,12 @@ module Sevgi
|
|
|
7
7
|
# Builds a drawable grid from a graphics canvas.
|
|
8
8
|
# @param canvas [Sevgi::Graphics::Canvas] canvas defining page size and margins
|
|
9
9
|
# @param unit [Numeric] minor grid unit
|
|
10
|
-
# @param multiple [
|
|
10
|
+
# @param multiple [Integer] number of minor units in each major interval
|
|
11
11
|
# @return [Sevgi::Sundries::Grid] grid fitted to the canvas
|
|
12
|
-
# @raise [Sevgi::ArgumentError] when canvas
|
|
12
|
+
# @raise [Sevgi::ArgumentError] when canvas is not a graphics canvas
|
|
13
|
+
# @raise [Sevgi::ArgumentError] when unit is not a finite positive number
|
|
14
|
+
# @raise [Sevgi::ArgumentError] when multiple is not a positive integer
|
|
15
|
+
# @raise [Sevgi::ArgumentError] when canvas dimensions, margins, and grid intervals cannot fit
|
|
13
16
|
def Grid(canvas, unit:, multiple:)
|
|
14
17
|
ArgumentError.("Must be a Canvas: #{canvas}") unless canvas.is_a?(Graphics::Canvas)
|
|
15
18
|
|
data/lib/sevgi/toplevel.rb
CHANGED
|
@@ -18,9 +18,10 @@ module Sevgi
|
|
|
18
18
|
def inject(base)
|
|
19
19
|
return if base.instance_variable_defined?("@_toplevel_injected_")
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
if base.is_a?(::Module)
|
|
22
|
+
@constants.each do |name, constant|
|
|
23
|
+
base.const_set(name, constant) unless base.const_defined?(name, false)
|
|
24
|
+
end
|
|
24
25
|
end
|
|
25
26
|
|
|
26
27
|
base.instance_variable_set("@_toplevel_injected_", true)
|
|
@@ -31,7 +32,7 @@ module Sevgi
|
|
|
31
32
|
end
|
|
32
33
|
end
|
|
33
34
|
|
|
34
|
-
# Injects promoted constants when the DSL is included.
|
|
35
|
+
# Injects promoted constants when the DSL is included in a module or class.
|
|
35
36
|
# @param base [Module] the class or module receiving promoted constants
|
|
36
37
|
# @return [void]
|
|
37
38
|
# @api private
|
|
@@ -40,9 +41,11 @@ module Sevgi
|
|
|
40
41
|
inject(base)
|
|
41
42
|
end
|
|
42
43
|
|
|
43
|
-
# Injects promoted constants when the DSL is extended.
|
|
44
|
-
# @param base [Object] the object or module receiving
|
|
44
|
+
# Injects promoted constants when the DSL is extended by a module.
|
|
45
|
+
# @param base [Object] the object or module receiving the DSL methods
|
|
45
46
|
# @return [void]
|
|
47
|
+
# @note Constants are promoted only to modules/classes. Extending an ordinary object installs methods without
|
|
48
|
+
# writing constants to `Object`.
|
|
46
49
|
# @api private
|
|
47
50
|
def self.extended(base)
|
|
48
51
|
super
|
data/lib/sevgi/version.rb
CHANGED
data/lib/sevgi.rb
CHANGED
|
@@ -26,9 +26,11 @@ def SVG(...) = Sevgi::Graphics.SVG(...)
|
|
|
26
26
|
|
|
27
27
|
# Full top-level API for Sevgi library and script consumers.
|
|
28
28
|
#
|
|
29
|
-
# Including
|
|
30
|
-
# and `Grid`, plus convenience constants such as `F`,
|
|
31
|
-
# `Export`.
|
|
29
|
+
# Including this module in a class or module installs DSL methods such as
|
|
30
|
+
# `Paper`, `Load`, and `Grid`, plus convenience constants such as `F`,
|
|
31
|
+
# `Geometry`, `Origin`, and `Export`. Extending a module does the same.
|
|
32
|
+
# Extending an ordinary object installs the methods only and does not write
|
|
33
|
+
# promoted constants to `Object`.
|
|
32
34
|
#
|
|
33
35
|
# @example Include the DSL in an object
|
|
34
36
|
# class Drawing
|
|
@@ -50,6 +52,7 @@ module Sevgi
|
|
|
50
52
|
# Installs the full toplevel DSL into an extending object or module.
|
|
51
53
|
# @param base [Object] the receiver extended with the DSL methods
|
|
52
54
|
# @return [void]
|
|
55
|
+
# @note Promoted constants are written only when base is a module or class.
|
|
53
56
|
# @api private
|
|
54
57
|
def self.extended(base)
|
|
55
58
|
super
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: sevgi
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.94.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Recai Oktaş
|
|
@@ -15,84 +15,84 @@ dependencies:
|
|
|
15
15
|
requirements:
|
|
16
16
|
- - '='
|
|
17
17
|
- !ruby/object:Gem::Version
|
|
18
|
-
version: 0.
|
|
18
|
+
version: 0.94.0
|
|
19
19
|
type: :runtime
|
|
20
20
|
prerelease: false
|
|
21
21
|
version_requirements: !ruby/object:Gem::Requirement
|
|
22
22
|
requirements:
|
|
23
23
|
- - '='
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
|
-
version: 0.
|
|
25
|
+
version: 0.94.0
|
|
26
26
|
- !ruby/object:Gem::Dependency
|
|
27
27
|
name: sevgi-function
|
|
28
28
|
requirement: !ruby/object:Gem::Requirement
|
|
29
29
|
requirements:
|
|
30
30
|
- - '='
|
|
31
31
|
- !ruby/object:Gem::Version
|
|
32
|
-
version: 0.
|
|
32
|
+
version: 0.94.0
|
|
33
33
|
type: :runtime
|
|
34
34
|
prerelease: false
|
|
35
35
|
version_requirements: !ruby/object:Gem::Requirement
|
|
36
36
|
requirements:
|
|
37
37
|
- - '='
|
|
38
38
|
- !ruby/object:Gem::Version
|
|
39
|
-
version: 0.
|
|
39
|
+
version: 0.94.0
|
|
40
40
|
- !ruby/object:Gem::Dependency
|
|
41
41
|
name: sevgi-geometry
|
|
42
42
|
requirement: !ruby/object:Gem::Requirement
|
|
43
43
|
requirements:
|
|
44
44
|
- - '='
|
|
45
45
|
- !ruby/object:Gem::Version
|
|
46
|
-
version: 0.
|
|
46
|
+
version: 0.94.0
|
|
47
47
|
type: :runtime
|
|
48
48
|
prerelease: false
|
|
49
49
|
version_requirements: !ruby/object:Gem::Requirement
|
|
50
50
|
requirements:
|
|
51
51
|
- - '='
|
|
52
52
|
- !ruby/object:Gem::Version
|
|
53
|
-
version: 0.
|
|
53
|
+
version: 0.94.0
|
|
54
54
|
- !ruby/object:Gem::Dependency
|
|
55
55
|
name: sevgi-graphics
|
|
56
56
|
requirement: !ruby/object:Gem::Requirement
|
|
57
57
|
requirements:
|
|
58
58
|
- - '='
|
|
59
59
|
- !ruby/object:Gem::Version
|
|
60
|
-
version: 0.
|
|
60
|
+
version: 0.94.0
|
|
61
61
|
type: :runtime
|
|
62
62
|
prerelease: false
|
|
63
63
|
version_requirements: !ruby/object:Gem::Requirement
|
|
64
64
|
requirements:
|
|
65
65
|
- - '='
|
|
66
66
|
- !ruby/object:Gem::Version
|
|
67
|
-
version: 0.
|
|
67
|
+
version: 0.94.0
|
|
68
68
|
- !ruby/object:Gem::Dependency
|
|
69
69
|
name: sevgi-standard
|
|
70
70
|
requirement: !ruby/object:Gem::Requirement
|
|
71
71
|
requirements:
|
|
72
72
|
- - '='
|
|
73
73
|
- !ruby/object:Gem::Version
|
|
74
|
-
version: 0.
|
|
74
|
+
version: 0.94.0
|
|
75
75
|
type: :runtime
|
|
76
76
|
prerelease: false
|
|
77
77
|
version_requirements: !ruby/object:Gem::Requirement
|
|
78
78
|
requirements:
|
|
79
79
|
- - '='
|
|
80
80
|
- !ruby/object:Gem::Version
|
|
81
|
-
version: 0.
|
|
81
|
+
version: 0.94.0
|
|
82
82
|
- !ruby/object:Gem::Dependency
|
|
83
83
|
name: sevgi-sundries
|
|
84
84
|
requirement: !ruby/object:Gem::Requirement
|
|
85
85
|
requirements:
|
|
86
86
|
- - '='
|
|
87
87
|
- !ruby/object:Gem::Version
|
|
88
|
-
version: 0.
|
|
88
|
+
version: 0.94.0
|
|
89
89
|
type: :runtime
|
|
90
90
|
prerelease: false
|
|
91
91
|
version_requirements: !ruby/object:Gem::Requirement
|
|
92
92
|
requirements:
|
|
93
93
|
- - '='
|
|
94
94
|
- !ruby/object:Gem::Version
|
|
95
|
-
version: 0.
|
|
95
|
+
version: 0.94.0
|
|
96
96
|
description: Provides a scriptable DSL with utilities for creating SVG content with
|
|
97
97
|
Ruby.
|
|
98
98
|
email: roktas@gmail.com
|
|
@@ -101,6 +101,8 @@ executables:
|
|
|
101
101
|
extensions: []
|
|
102
102
|
extra_rdoc_files: []
|
|
103
103
|
files:
|
|
104
|
+
- CHANGELOG.md
|
|
105
|
+
- LICENSE
|
|
104
106
|
- README.md
|
|
105
107
|
- bin/sevgi
|
|
106
108
|
- lib/sevgi.rb
|
|
@@ -133,7 +135,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
133
135
|
requirements:
|
|
134
136
|
- - ">="
|
|
135
137
|
- !ruby/object:Gem::Version
|
|
136
|
-
version: 3.4.0
|
|
138
|
+
version: 3.4.0
|
|
137
139
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
138
140
|
requirements:
|
|
139
141
|
- - ">="
|