sevgi 0.95.0 → 0.98.2
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 +125 -0
- data/README.md +27 -9
- data/bin/igsev +6 -0
- data/bin/sevgi +1 -1
- data/lib/sevgi/binaries/igsev.rb +161 -0
- data/lib/sevgi/binaries/rake.rb +3 -3
- data/lib/sevgi/binaries/sevgi.rb +69 -27
- data/lib/sevgi/executor/error.rb +16 -24
- data/lib/sevgi/executor/result.rb +54 -0
- data/lib/sevgi/executor/scope.rb +16 -5
- data/lib/sevgi/executor/source.rb +16 -7
- data/lib/sevgi/executor.rb +129 -99
- data/lib/sevgi/skill.rb +42 -0
- data/lib/sevgi/svg.rb +137 -0
- data/lib/sevgi/toplevel/derender.rb +92 -6
- data/lib/sevgi/toplevel/executor.rb +55 -13
- data/lib/sevgi/toplevel/graphics.rb +92 -6
- data/lib/sevgi/toplevel/sundries.rb +13 -5
- data/lib/sevgi/toplevel.rb +7 -7
- data/lib/sevgi/version.rb +1 -1
- data/lib/sevgi.rb +100 -13
- metadata +36 -17
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Sevgi
|
|
4
|
+
class Executor
|
|
5
|
+
# Describes the outcome of one executor invocation.
|
|
6
|
+
#
|
|
7
|
+
# A successful result has a value and no error. A captured script, file, or
|
|
8
|
+
# library failure has an {Executor::Error} and may retain a value produced
|
|
9
|
+
# before the failure. The source stack is an immutable snapshot in load
|
|
10
|
+
# order; it is never shared with the executor's internal mutable state.
|
|
11
|
+
#
|
|
12
|
+
# @example Inspect successful execution
|
|
13
|
+
# result = Sevgi.execute("6 * 7")
|
|
14
|
+
# result.success? #=> true
|
|
15
|
+
# result.value #=> 42
|
|
16
|
+
#
|
|
17
|
+
# @example Inspect a captured failure
|
|
18
|
+
# result = Sevgi.execute("missing", file: "drawing.sevgi")
|
|
19
|
+
# result.error? #=> true
|
|
20
|
+
# result.error.cause #=> #<NameError ...>
|
|
21
|
+
# result.stack #=> ["drawing.sevgi"]
|
|
22
|
+
#
|
|
23
|
+
# @see Sevgi.execute
|
|
24
|
+
# @see Sevgi.execute_file
|
|
25
|
+
# @see https://sevgi.roktas.dev/execution/ Execution guide
|
|
26
|
+
Result = Data.define(:value, :error, :stack) do
|
|
27
|
+
# @!attribute [r] value
|
|
28
|
+
# @return [Object, nil] last value produced, or nil when no value was produced
|
|
29
|
+
# @!attribute [r] error
|
|
30
|
+
# @return [Sevgi::Executor::Error, nil] captured failure, or nil after successful execution
|
|
31
|
+
# @!attribute [r] stack
|
|
32
|
+
# @return [Array<String>] frozen owned source-path snapshot in load order
|
|
33
|
+
|
|
34
|
+
# Creates an execution result.
|
|
35
|
+
# @param value [Object, nil] last value produced before execution finished
|
|
36
|
+
# @param error [Sevgi::Executor::Error, nil] captured execution failure
|
|
37
|
+
# @param stack [Array<String>] source files visited in load order
|
|
38
|
+
# @return [void]
|
|
39
|
+
def initialize(value:, error:, stack:)
|
|
40
|
+
super(value:, error:, stack: Array(stack).map { it.dup.freeze }.freeze)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private_class_method :[]
|
|
44
|
+
|
|
45
|
+
# Reports whether execution completed without a captured error.
|
|
46
|
+
# @return [Boolean] true when execution succeeded
|
|
47
|
+
def success? = error.nil?
|
|
48
|
+
|
|
49
|
+
# Reports whether execution captured an error.
|
|
50
|
+
# @return [Boolean] true when execution failed
|
|
51
|
+
def error? = !success?
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
data/lib/sevgi/executor/scope.rb
CHANGED
|
@@ -13,7 +13,7 @@ module Sevgi
|
|
|
13
13
|
# @!attribute [r] recent
|
|
14
14
|
# @return [Object, nil] last expression result from the executed source
|
|
15
15
|
# @!attribute [r] error
|
|
16
|
-
# @return [
|
|
16
|
+
# @return [Exception, nil] captured execution error
|
|
17
17
|
attr_reader :scope, :recent, :error
|
|
18
18
|
|
|
19
19
|
# Creates a script execution scope.
|
|
@@ -35,7 +35,7 @@ module Sevgi
|
|
|
35
35
|
|
|
36
36
|
# Executes one source object in this scope.
|
|
37
37
|
# @param source [Sevgi::Executor::Source] source to evaluate
|
|
38
|
-
# @param receiver [Object, nil] receiver
|
|
38
|
+
# @param receiver [Object, nil] explicit boot receiver, or nil to use the isolated scope module
|
|
39
39
|
# @yield optional boot block that installs DSL methods before evaluation
|
|
40
40
|
# @yieldreturn [void]
|
|
41
41
|
# @return [Sevgi::Executor::Scope] self, with recent or error populated
|
|
@@ -54,7 +54,7 @@ module Sevgi
|
|
|
54
54
|
# @api private
|
|
55
55
|
def capture(source, error)
|
|
56
56
|
push(source)
|
|
57
|
-
@error =
|
|
57
|
+
@error = error
|
|
58
58
|
self
|
|
59
59
|
end
|
|
60
60
|
|
|
@@ -77,6 +77,16 @@ module Sevgi
|
|
|
77
77
|
# @note The stack is owned by this scope and is not shared with concurrent executions.
|
|
78
78
|
def stack = @stack.keys
|
|
79
79
|
|
|
80
|
+
# Builds the immutable public result for this scope.
|
|
81
|
+
# @return [Sevgi::Executor::Result] execution result snapshot
|
|
82
|
+
# @api private
|
|
83
|
+
def result
|
|
84
|
+
sources = stack.freeze
|
|
85
|
+
error = Executor::Error.new(@error, sources) if @error
|
|
86
|
+
|
|
87
|
+
Result.new(value: recent, error:, stack: sources)
|
|
88
|
+
end
|
|
89
|
+
|
|
80
90
|
# Returns the most recently pushed source.
|
|
81
91
|
# @return [Sevgi::Executor::Source, nil] most recent source object
|
|
82
92
|
# @api private
|
|
@@ -87,7 +97,8 @@ module Sevgi
|
|
|
87
97
|
def boot(receiver, &boot)
|
|
88
98
|
return unless boot
|
|
89
99
|
|
|
90
|
-
|
|
100
|
+
receiver = scope if receiver.nil?
|
|
101
|
+
receiver.public_send(receiver.is_a?(::Module) ? :module_exec : :instance_exec, &boot)
|
|
91
102
|
end
|
|
92
103
|
|
|
93
104
|
def evaluate(source)
|
|
@@ -123,7 +134,7 @@ module Sevgi
|
|
|
123
134
|
@recent = run(source, receiver, &boot)
|
|
124
135
|
# rubocop:disable Lint/RescueException
|
|
125
136
|
rescue Exception => e
|
|
126
|
-
@error =
|
|
137
|
+
@error = e
|
|
127
138
|
throw(:result, self)
|
|
128
139
|
# rubocop:enable Lint/RescueException
|
|
129
140
|
ensure
|
|
@@ -4,38 +4,47 @@ module Sevgi
|
|
|
4
4
|
class Executor
|
|
5
5
|
# Describes Ruby source evaluated by the Sevgi executor.
|
|
6
6
|
# @api private
|
|
7
|
-
Source = Data.define(:string, :file, :line) do
|
|
8
|
-
# @overload call(string:, file: nil, line: nil)
|
|
7
|
+
Source = Data.define(:string, :file, :line, :origin) do
|
|
8
|
+
# @overload call(string:, file: nil, line: nil, origin: nil)
|
|
9
9
|
# Builds a source object.
|
|
10
10
|
# @param string [String] Ruby source string
|
|
11
11
|
# @param file [String, nil] source file name for diagnostics
|
|
12
12
|
# @param line [Integer, nil] starting source line for diagnostics
|
|
13
|
+
# @param origin [String, nil] physical source path used for load-cycle identity
|
|
13
14
|
# @return [Sevgi::Executor::Source] source object
|
|
14
15
|
def self.call(...) = new(...)
|
|
15
16
|
|
|
16
17
|
# Builds a source object from a file.
|
|
17
18
|
# @param file [String] source file to read
|
|
19
|
+
# @param as [String, nil] logical source name used for evaluation and diagnostics
|
|
18
20
|
# @return [Sevgi::Executor::Source] source object with file contents
|
|
19
21
|
# @raise [Errno::ENOENT] when the file cannot be read
|
|
20
|
-
def self.load(file) = new(string: ::File.read(file), file: file, line: 1)
|
|
22
|
+
def self.load(file, as: nil) = new(string: ::File.read(file), file: as || file, line: 1, origin: file)
|
|
21
23
|
|
|
22
24
|
# Creates a source object.
|
|
23
25
|
# @param string [String] Ruby source string
|
|
24
26
|
# @param file [String, nil] source file name for diagnostics
|
|
25
27
|
# @param line [Integer, nil] starting source line for diagnostics
|
|
28
|
+
# @param origin [String, nil] physical source path used for load-cycle identity
|
|
26
29
|
# @return [void]
|
|
27
|
-
def initialize(string:, file: nil, line: nil
|
|
30
|
+
def initialize(string:, file: nil, line: nil, origin: nil)
|
|
31
|
+
super(string:, file: file || "sevgi", line: line || 1, origin:)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private_class_method :[]
|
|
28
35
|
|
|
29
36
|
# Returns the stack key used for this source.
|
|
30
37
|
# @return [String] source file name
|
|
31
38
|
def key = file
|
|
32
39
|
|
|
33
40
|
# Returns the canonical identity used for active-load cycle detection.
|
|
34
|
-
# @return [String]
|
|
41
|
+
# @return [Integer, String] object identity for inline source, or canonical physical identity for loaded source
|
|
35
42
|
def identity
|
|
36
|
-
|
|
43
|
+
return object_id unless origin
|
|
44
|
+
|
|
45
|
+
::File.realpath(origin)
|
|
37
46
|
rescue ::SystemCallError
|
|
38
|
-
::File.expand_path(
|
|
47
|
+
::File.expand_path(origin)
|
|
39
48
|
end
|
|
40
49
|
end
|
|
41
50
|
end
|
data/lib/sevgi/executor.rb
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "
|
|
3
|
+
require "sevgi/function"
|
|
4
4
|
|
|
5
5
|
require_relative "executor/error"
|
|
6
|
+
require_relative "executor/result"
|
|
6
7
|
require_relative "executor/scope"
|
|
7
8
|
require_relative "executor/source"
|
|
8
9
|
|
|
9
10
|
module Sevgi
|
|
10
|
-
#
|
|
11
|
+
# Internal Sevgi script runtime and namespace for public execution result types.
|
|
11
12
|
#
|
|
12
13
|
# The executor is used by script mode and by the `Load` DSL word to preserve a
|
|
13
14
|
# useful load stack while keeping DSL methods out of the caller's global object
|
|
@@ -16,160 +17,189 @@ module Sevgi
|
|
|
16
17
|
# state. The process SIGINT handler is shared by Ruby, so executor runs guard it
|
|
17
18
|
# with a reference-counted critical section and restore the previous handler
|
|
18
19
|
# after the last active execution finishes.
|
|
20
|
+
#
|
|
21
|
+
# Consumers execute the full DSL through {Sevgi.execute} or {Sevgi.execute_file}, then inspect {Executor::Result},
|
|
22
|
+
# {Executor::Error}, and {Executor::CycleError}. The custom receiver and boot lifecycle is internal plumbing for the
|
|
23
|
+
# top-level API and Rake integration.
|
|
24
|
+
#
|
|
25
|
+
# @see https://sevgi.roktas.dev/execution/ Execution guide
|
|
19
26
|
class Executor
|
|
20
|
-
|
|
21
|
-
|
|
27
|
+
private_class_method :new
|
|
22
28
|
private_constant :Scope
|
|
23
29
|
|
|
24
30
|
# Thread-current key used for the fiber-local executor scope stack.
|
|
25
31
|
# @api private
|
|
26
32
|
SCOPE_KEY = :sevgi_executor_scopes
|
|
27
|
-
|
|
33
|
+
SOURCE_LINE_MAX = (2 ** 31) - 1
|
|
34
|
+
private_constant :SCOPE_KEY, :SOURCE_LINE_MAX, :Source
|
|
35
|
+
|
|
36
|
+
# Owns mutable execution and process-signal state outside the public executor surface.
|
|
37
|
+
# @api private
|
|
38
|
+
class State
|
|
39
|
+
def initialize
|
|
40
|
+
@signal_count = 0
|
|
41
|
+
@signal_mutex = Mutex.new
|
|
42
|
+
@signal_previous = nil
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def create(scope = nil) = Scope.new(scope).tap { scopes << it }
|
|
46
|
+
def current = scopes.last
|
|
47
|
+
|
|
48
|
+
def restore
|
|
49
|
+
@signal_mutex.synchronize do
|
|
50
|
+
next if @signal_count.zero?
|
|
51
|
+
|
|
52
|
+
@signal_count -= 1
|
|
53
|
+
next unless @signal_count.zero?
|
|
54
|
+
|
|
55
|
+
Signal.trap("INT", @signal_previous)
|
|
56
|
+
@signal_previous = nil
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def shutdown(scope = nil)
|
|
61
|
+
return scopes.pop unless scope
|
|
62
|
+
return scopes.pop if scopes.last.equal?(scope)
|
|
63
|
+
|
|
64
|
+
scopes.delete(scope)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def trap
|
|
68
|
+
@signal_mutex.synchronize do
|
|
69
|
+
@signal_previous = Signal.trap("INT") { Kernel.abort("") } if @signal_count.zero?
|
|
70
|
+
@signal_count += 1
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
private
|
|
75
|
+
|
|
76
|
+
def scopes = Thread.current[SCOPE_KEY] ||= []
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
STATE = State.new
|
|
80
|
+
private_constant :STATE, :State
|
|
28
81
|
|
|
29
82
|
# Loads a script file inside the current executor scope.
|
|
30
83
|
# @param file [String] path to a Sevgi script file
|
|
31
|
-
# @return [Sevgi::Executor::Scope] current execution scope
|
|
84
|
+
# @return [Sevgi::Executor::Scope] current internal execution scope
|
|
32
85
|
# @raise [Sevgi::PanicError] when there is no active executor scope
|
|
33
86
|
# @note Uses the active executor scope from the current fiber.
|
|
34
87
|
# @api private
|
|
35
88
|
def self.load(file, ...)
|
|
36
|
-
PanicError.("box stack empty; create a box first") unless
|
|
89
|
+
PanicError.("box stack empty; create a box first") unless STATE.current
|
|
37
90
|
|
|
38
|
-
|
|
91
|
+
STATE.current.load(file, ...)
|
|
39
92
|
end
|
|
40
93
|
|
|
94
|
+
private_class_method :load
|
|
95
|
+
|
|
41
96
|
# Executes Ruby source inside a managed Sevgi script scope.
|
|
42
97
|
# @param string [String] source to evaluate
|
|
43
98
|
# @param file [String, nil] source file name used for errors and backtraces
|
|
44
99
|
# @param line [Integer, nil] starting source line used for errors and backtraces
|
|
45
100
|
# @param require [String, nil] optional Ruby library to require before execution
|
|
46
|
-
# @param receiver [Object, nil] receiver used while booting the DSL
|
|
101
|
+
# @param receiver [Object, nil] receiver used verbatim while booting the DSL; nil selects the isolated execution
|
|
102
|
+
# module, while false and other executable objects remain explicit receivers
|
|
47
103
|
# @yield optional boot block that installs DSL methods before evaluation
|
|
48
104
|
# @yieldreturn [void]
|
|
49
|
-
# @return [Sevgi::Executor::
|
|
50
|
-
# @
|
|
105
|
+
# @return [Sevgi::Executor::Result] immutable execution result
|
|
106
|
+
# @raise [Sevgi::ArgumentError] when source, file, line, required library, or receiver is invalid
|
|
107
|
+
# @note Script and required-library failures are captured in {Sevgi::Executor::Result#error}; inspect
|
|
108
|
+
# {Sevgi::Executor::Error#cause} for the original exception.
|
|
109
|
+
# @note Empty source without `require:` is a strict no-op: no scope is created, the receiver and boot block are
|
|
110
|
+
# unused, and the result stack is empty. Supplying `require:` uses the normal boot and evaluation lifecycle.
|
|
51
111
|
# @note Reentrant and concurrent calls keep independent scope stacks per fiber. The temporary SIGINT handler remains
|
|
52
112
|
# process-global while any execution is active.
|
|
113
|
+
# @api private
|
|
53
114
|
def self.execute(string, file: nil, line: nil, require: nil, receiver: nil, &block)
|
|
115
|
+
validate_source!(string, file, line)
|
|
116
|
+
validate_context!(require, receiver)
|
|
117
|
+
|
|
54
118
|
execute_source(Source.new(string:, file:, line:), require:, receiver:, &block)
|
|
55
119
|
end
|
|
56
120
|
|
|
57
121
|
# Executes a file inside a managed Sevgi script scope.
|
|
58
122
|
# @param file [String] source file to read and execute
|
|
123
|
+
# @param as [String, nil] logical source name used for evaluation and diagnostics
|
|
59
124
|
# @param require [String, nil] optional Ruby library to require before execution
|
|
60
|
-
# @param receiver [Object, nil] receiver used while booting the DSL
|
|
125
|
+
# @param receiver [Object, nil] receiver used verbatim while booting the DSL; nil selects the isolated execution
|
|
126
|
+
# module, while false and other executable objects remain explicit receivers
|
|
61
127
|
# @yield optional boot block that installs DSL methods before evaluation
|
|
62
128
|
# @yieldreturn [void]
|
|
63
|
-
# @return [Sevgi::Executor::
|
|
64
|
-
# @
|
|
129
|
+
# @return [Sevgi::Executor::Result] immutable execution result
|
|
130
|
+
# @raise [Sevgi::ArgumentError] when file, logical source name, required library, or receiver is invalid
|
|
131
|
+
# @note File-read, script, and required-library failures are captured in {Sevgi::Executor::Result#error}; inspect
|
|
132
|
+
# {Sevgi::Executor::Result#stack} for nested loads.
|
|
133
|
+
# @note An empty file without `require:` is a strict no-op: no scope is created, the receiver and boot block are
|
|
134
|
+
# unused, and the result stack is empty. Supplying `require:` uses the normal boot and evaluation lifecycle.
|
|
65
135
|
# @note Reentrant and concurrent calls keep independent scope stacks per fiber. The temporary SIGINT handler remains
|
|
66
136
|
# process-global while any execution is active.
|
|
67
|
-
|
|
137
|
+
# @api private
|
|
138
|
+
def self.execute_file(file, as: nil, require: nil, receiver: nil, &block)
|
|
139
|
+
ArgumentError.("Executor file must be a String") unless file.is_a?(::String)
|
|
140
|
+
ArgumentError.("Executor logical file must be a String or nil") unless as.nil? || as.is_a?(::String)
|
|
141
|
+
validate_context!(require, receiver)
|
|
142
|
+
|
|
68
143
|
source = nil
|
|
69
144
|
begin
|
|
70
|
-
source = Source.load(file)
|
|
145
|
+
source = Source.load(file, as:)
|
|
71
146
|
rescue ::SystemCallError => e
|
|
72
|
-
return capture_error(Source.new(string: "", file
|
|
147
|
+
return capture_error(Source.new(string: "", file: as || file, line: 1, origin: file), e)
|
|
73
148
|
end
|
|
74
149
|
|
|
75
150
|
execute_source(source, require:, receiver:, &block)
|
|
76
151
|
end
|
|
77
152
|
|
|
78
|
-
|
|
79
|
-
# @return [Sevgi::Executor::Scope, nil] removed scope
|
|
80
|
-
# @api private
|
|
81
|
-
def self.shutdown
|
|
82
|
-
instance.shutdown
|
|
83
|
-
end
|
|
84
|
-
|
|
85
|
-
def initialize
|
|
86
|
-
@signal_count = 0
|
|
87
|
-
@signal_mutex = Mutex.new
|
|
88
|
-
@signal_previous = nil
|
|
89
|
-
end
|
|
153
|
+
private_class_method :execute, :execute_file
|
|
90
154
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
# @return [Sevgi::Executor::Scope] created scope
|
|
94
|
-
# @api private
|
|
95
|
-
def create(scope = nil) = Scope.new(scope).tap { scopes << it }
|
|
96
|
-
|
|
97
|
-
# Returns the active executor scope.
|
|
98
|
-
# @return [Sevgi::Executor::Scope, nil] active scope, if any
|
|
99
|
-
# @api private
|
|
100
|
-
def current = scopes.last
|
|
155
|
+
class << self
|
|
156
|
+
private
|
|
101
157
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
@signal_count -= 1
|
|
110
|
-
next unless @signal_count.zero?
|
|
111
|
-
|
|
112
|
-
Signal.trap("INT", @signal_previous)
|
|
113
|
-
@signal_previous = nil
|
|
158
|
+
def capture_error(source, error)
|
|
159
|
+
acquired = STATE.trap
|
|
160
|
+
scope = STATE.create
|
|
161
|
+
scope.capture(source, error).result
|
|
162
|
+
ensure
|
|
163
|
+
STATE.restore if acquired
|
|
164
|
+
STATE.shutdown(scope) if scope
|
|
114
165
|
end
|
|
115
|
-
end
|
|
116
166
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
# @api private
|
|
121
|
-
def shutdown(scope = nil)
|
|
122
|
-
return scopes.pop unless scope
|
|
123
|
-
return scopes.pop if scopes.last.equal?(scope)
|
|
167
|
+
def execute_source(source, require:, receiver:, &block)
|
|
168
|
+
acquired = false
|
|
169
|
+
return Result.new(value: nil, error: nil, stack: []) if source.string.empty? && require.nil?
|
|
124
170
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
# @return [void]
|
|
130
|
-
# @api private
|
|
131
|
-
def trap
|
|
132
|
-
@signal_mutex.synchronize do
|
|
133
|
-
@signal_previous = Signal.trap("INT") { Kernel.abort("") } if @signal_count.zero?
|
|
171
|
+
acquired = STATE.trap
|
|
172
|
+
scope = STATE.create
|
|
173
|
+
catch(:result) { run_source(scope, source, require, receiver, &block) }
|
|
174
|
+
scope.result
|
|
134
175
|
|
|
135
|
-
|
|
176
|
+
ensure
|
|
177
|
+
STATE.restore if acquired
|
|
178
|
+
STATE.shutdown(scope) if scope
|
|
136
179
|
end
|
|
137
|
-
end
|
|
138
180
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
instance.shutdown(scope) if scope
|
|
146
|
-
end
|
|
181
|
+
def run_source(scope, source, library, receiver, &block)
|
|
182
|
+
::Kernel.require(library) if library
|
|
183
|
+
scope.call(source, receiver, &block)
|
|
184
|
+
rescue ::LoadError => e
|
|
185
|
+
scope.capture(source, e)
|
|
186
|
+
end
|
|
147
187
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
return if source.string.empty? && require.nil?
|
|
188
|
+
def validate_context!(library, receiver)
|
|
189
|
+
ArgumentError.("Executor library must be a String or nil") unless library.nil? || library.is_a?(::String)
|
|
151
190
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
191
|
+
valid = (receiver in ::Object) && receiver.respond_to?(:public_send) && receiver.respond_to?(:instance_exec)
|
|
192
|
+
ArgumentError.("Executor receiver must be an executable Object or nil") unless valid
|
|
193
|
+
end
|
|
155
194
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
end
|
|
195
|
+
def validate_source!(string, file, line)
|
|
196
|
+
ArgumentError.("Executor source must be a String") unless string.is_a?(::String)
|
|
197
|
+
ArgumentError.("Executor file must be a String or nil") unless file.nil? || file.is_a?(::String)
|
|
160
198
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
rescue ::LoadError => e
|
|
165
|
-
scope.capture(source, e)
|
|
199
|
+
valid = line.nil? || (line.is_a?(::Integer) && line.between?(1, SOURCE_LINE_MAX))
|
|
200
|
+
ArgumentError.("Executor line must be between 1 and #{SOURCE_LINE_MAX}") unless valid
|
|
201
|
+
end
|
|
166
202
|
end
|
|
167
|
-
|
|
168
|
-
private_class_method :capture_error, :execute_source, :run_source
|
|
169
|
-
|
|
170
|
-
private
|
|
171
|
-
|
|
172
|
-
def scopes = Thread.current[SCOPE_KEY] ||= []
|
|
173
203
|
end
|
|
174
204
|
|
|
175
205
|
end
|
data/lib/sevgi/skill.rb
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rubygems"
|
|
4
|
+
require "sevgi"
|
|
5
|
+
|
|
6
|
+
module Sevgi
|
|
7
|
+
# Locates the agent skill packaged for the installed Sevgi version.
|
|
8
|
+
# @api private
|
|
9
|
+
module Skill
|
|
10
|
+
extend self
|
|
11
|
+
|
|
12
|
+
Error = Class.new(::Sevgi::Error)
|
|
13
|
+
|
|
14
|
+
def path
|
|
15
|
+
spec = ::Gem::Specification.find_by_name("sevgi-appendix", "= #{::Sevgi::VERSION}")
|
|
16
|
+
packaged = packaged_path(spec)
|
|
17
|
+
# Package managers may replace a versioned gem path with their stable prefix.
|
|
18
|
+
path = ::File.expand_path(ENV.fetch("SEVGI_SKILL", packaged))
|
|
19
|
+
|
|
20
|
+
Error.("Sevgi skill is unavailable at #{path}.") unless ::File.file?(::File.join(path, "SKILL.md"))
|
|
21
|
+
|
|
22
|
+
path
|
|
23
|
+
rescue ::Gem::MissingSpecError
|
|
24
|
+
Error.("sevgi-appendix #{::Sevgi::VERSION} is not installed.")
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def packaged_path(spec)
|
|
30
|
+
relative = spec.metadata["sevgi_skill_path"]
|
|
31
|
+
root = ::File.expand_path(spec.full_gem_path)
|
|
32
|
+
path = ::File.expand_path(relative, root) if relative.is_a?(String) && !relative.empty?
|
|
33
|
+
inside = path&.start_with?("#{root}#{::File::SEPARATOR}")
|
|
34
|
+
|
|
35
|
+
Error.("sevgi-appendix #{::Sevgi::VERSION} does not declare a valid skill path.") unless inside
|
|
36
|
+
|
|
37
|
+
path
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private_constant :Skill
|
|
42
|
+
end
|
data/lib/sevgi/svg.rb
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Sevgi
|
|
4
|
+
# Public SVG facade installed by `require "sevgi"`.
|
|
5
|
+
#
|
|
6
|
+
# Capitalized methods are SVG DSL operations: {SVG.Canvas} creates a canvas,
|
|
7
|
+
# {SVG.Document} defines a document profile, and {SVG.Paper} registers a
|
|
8
|
+
# paper size. Double-colon names are Ruby constants and types, such as
|
|
9
|
+
# {SVG::Canvas} and {SVG::Document}. The global `SVG(...)` method builds a
|
|
10
|
+
# drawing; there is intentionally no stuttering `SVG.SVG(...)` form.
|
|
11
|
+
#
|
|
12
|
+
# Lowercase constructors remain on {Sevgi::Graphics} for focused component
|
|
13
|
+
# use. Script execution belongs to {Sevgi.execute} and is not part of this
|
|
14
|
+
# SVG-domain facade.
|
|
15
|
+
#
|
|
16
|
+
# @example Compose a drawing through the facade
|
|
17
|
+
# SVG.Paper 85, 55, :card
|
|
18
|
+
# canvas = SVG.Canvas :card, margins: 4
|
|
19
|
+
# profile = SVG.Document attributes: {viewBox: "0 0 85 55"}
|
|
20
|
+
#
|
|
21
|
+
# drawing = SVG profile, canvas do
|
|
22
|
+
# rect width: 85, height: 55, rx: 3
|
|
23
|
+
# end
|
|
24
|
+
#
|
|
25
|
+
# drawing.Render
|
|
26
|
+
# @example Distinguish an operation from its result type
|
|
27
|
+
# canvas = SVG.Canvas width: 24, height: 24, unit: :px
|
|
28
|
+
# canvas.is_a?(SVG::Canvas) #=> true
|
|
29
|
+
# @see https://sevgi.roktas.dev/getting-started/ Getting started
|
|
30
|
+
# @see https://sevgi.roktas.dev/library-mode/ Library mode guide
|
|
31
|
+
module SVG
|
|
32
|
+
# SVG attribute collection and normalization API.
|
|
33
|
+
Attributes = Graphics::Attributes
|
|
34
|
+
# Canvas value type returned by {SVG.Canvas}.
|
|
35
|
+
Canvas = Graphics::Canvas
|
|
36
|
+
# Explicit SVG content serialization types.
|
|
37
|
+
Content = Graphics::Content
|
|
38
|
+
# Document profiles and document classes.
|
|
39
|
+
Document = Graphics::Document
|
|
40
|
+
# Base SVG/XML element type.
|
|
41
|
+
Element = Graphics::Element
|
|
42
|
+
# Duplicate-id and structural lint failure.
|
|
43
|
+
LintError = Graphics::LintError
|
|
44
|
+
# CSS-like canvas margin value.
|
|
45
|
+
Margin = Graphics::Margin
|
|
46
|
+
# Graphics mixtures available for document composition.
|
|
47
|
+
Mixtures = Graphics::Mixtures
|
|
48
|
+
# Contract for one callable drawing module.
|
|
49
|
+
Module = Graphics::Module
|
|
50
|
+
# Recursive contract for namespaces of callable drawing modules.
|
|
51
|
+
Modules = Graphics::Modules
|
|
52
|
+
# Immutable paper-size value and profile registry.
|
|
53
|
+
Paper = Graphics::Paper
|
|
54
|
+
# Current Sevgi toolkit version.
|
|
55
|
+
VERSION = Sevgi::VERSION
|
|
56
|
+
|
|
57
|
+
# Builds a canvas from a paper profile or explicit dimensions.
|
|
58
|
+
# @return [Sevgi::Graphics::Canvas] canvas value
|
|
59
|
+
# @see Sevgi::Toplevel#Canvas
|
|
60
|
+
def self.Canvas(...) = Sevgi.Canvas(...)
|
|
61
|
+
|
|
62
|
+
# Converts inline SVG/XML into an immutable Derender node.
|
|
63
|
+
# @return [Sevgi::Derender::Node] selected node
|
|
64
|
+
# @see Sevgi::Toplevel#Decompile
|
|
65
|
+
def self.Decompile(...) = Sevgi.Decompile(...)
|
|
66
|
+
|
|
67
|
+
# Converts an SVG/XML file into an immutable Derender node.
|
|
68
|
+
# @return [Sevgi::Derender::Node] selected node
|
|
69
|
+
# @see Sevgi::Toplevel#DecompileFile
|
|
70
|
+
def self.DecompileFile(...) = Sevgi.DecompileFile(...)
|
|
71
|
+
|
|
72
|
+
# Converts inline SVG/XML into formatted Sevgi DSL source.
|
|
73
|
+
# @return [String] formatted Sevgi DSL source
|
|
74
|
+
# @see Sevgi::Toplevel#Derender
|
|
75
|
+
def self.Derender(...) = Sevgi.Derender(...)
|
|
76
|
+
|
|
77
|
+
# Converts an SVG/XML file into formatted Sevgi DSL source.
|
|
78
|
+
# @return [String] formatted Sevgi DSL source
|
|
79
|
+
# @see Sevgi::Toplevel#DerenderFile
|
|
80
|
+
def self.DerenderFile(...) = Sevgi.DerenderFile(...)
|
|
81
|
+
|
|
82
|
+
# Defines, validates, or looks up a document profile.
|
|
83
|
+
# @return [Class] document class
|
|
84
|
+
# @see Sevgi::Toplevel#Document
|
|
85
|
+
def self.Document(...) = Sevgi.Document(...)
|
|
86
|
+
|
|
87
|
+
# Defines or replaces a document profile.
|
|
88
|
+
# @return [Class] document class
|
|
89
|
+
# @see Sevgi::Toplevel#Document!
|
|
90
|
+
def self.Document!(...) = Sevgi.Document!(...)
|
|
91
|
+
|
|
92
|
+
# Includes an inline SVG/XML node under a graphics element.
|
|
93
|
+
# @return [Sevgi::Graphics::Element, nil] included element, or nil when no output is produced
|
|
94
|
+
# @see Sevgi::Toplevel#Evaluate
|
|
95
|
+
def self.Evaluate(...) = Sevgi.Evaluate(...)
|
|
96
|
+
|
|
97
|
+
# Includes only an inline SVG/XML node's children under a graphics element.
|
|
98
|
+
# @return [Array<Sevgi::Graphics::Element>] immutable included-child snapshot
|
|
99
|
+
# @see Sevgi::Toplevel#EvaluateChildren
|
|
100
|
+
def self.EvaluateChildren(...) = Sevgi.EvaluateChildren(...)
|
|
101
|
+
|
|
102
|
+
# Includes only an SVG/XML file node's children under a graphics element.
|
|
103
|
+
# @return [Array<Sevgi::Graphics::Element>] immutable included-child snapshot
|
|
104
|
+
# @see Sevgi::Toplevel#EvaluateChildrenFile
|
|
105
|
+
def self.EvaluateChildrenFile(...) = Sevgi.EvaluateChildrenFile(...)
|
|
106
|
+
|
|
107
|
+
# Includes an SVG/XML file node under a graphics element.
|
|
108
|
+
# @return [Sevgi::Graphics::Element, nil] included element, or nil when no output is produced
|
|
109
|
+
# @see Sevgi::Toplevel#EvaluateFile
|
|
110
|
+
def self.EvaluateFile(...) = Sevgi.EvaluateFile(...)
|
|
111
|
+
|
|
112
|
+
# Fits a drawable grid to a graphics canvas.
|
|
113
|
+
# @return [Sevgi::Sundries::Grid] fitted grid
|
|
114
|
+
# @see Sevgi::Toplevel#Grid
|
|
115
|
+
def self.Grid(...) = Sevgi.Grid(...)
|
|
116
|
+
|
|
117
|
+
# Loads nested `.sevgi` files through the active executor scope.
|
|
118
|
+
# @return [Array<String>] requested file names
|
|
119
|
+
# @see Sevgi::Toplevel#Load
|
|
120
|
+
def self.Load(...) = Sevgi.Load(...)
|
|
121
|
+
|
|
122
|
+
# Extends a document profile with a named or anonymous graphics mixture.
|
|
123
|
+
# @return [Module, nil] anonymous mixture when supplied, otherwise nil
|
|
124
|
+
# @see Sevgi::Toplevel#Mixin
|
|
125
|
+
def self.Mixin(...) = Sevgi.Mixin(...)
|
|
126
|
+
|
|
127
|
+
# Defines or validates a named paper profile.
|
|
128
|
+
# @return [Symbol, String] original paper profile name
|
|
129
|
+
# @see Sevgi::Toplevel#Paper
|
|
130
|
+
def self.Paper(...) = Sevgi.Paper(...)
|
|
131
|
+
|
|
132
|
+
# Defines or overwrites a named paper profile.
|
|
133
|
+
# @return [Symbol, String] original paper profile name
|
|
134
|
+
# @see Sevgi::Toplevel#Paper!
|
|
135
|
+
def self.Paper!(...) = Sevgi.Paper!(...)
|
|
136
|
+
end
|
|
137
|
+
end
|