sevgi 0.94.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 +275 -2
- data/LICENSE +672 -3
- 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 +78 -33
- data/lib/sevgi/executor/error.rb +20 -22
- data/lib/sevgi/executor/result.rb +54 -0
- data/lib/sevgi/executor/scope.rb +46 -14
- data/lib/sevgi/executor/source.rb +21 -4
- data/lib/sevgi/executor.rb +131 -97
- 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 +56 -14
- data/lib/sevgi/toplevel/graphics.rb +92 -6
- data/lib/sevgi/toplevel/sundries.rb +13 -5
- data/lib/sevgi/toplevel.rb +7 -5
- data/lib/sevgi/version.rb +1 -1
- data/lib/sevgi.rb +100 -13
- metadata +36 -17
|
@@ -4,31 +4,48 @@ 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
|
|
39
|
+
|
|
40
|
+
# Returns the canonical identity used for active-load cycle detection.
|
|
41
|
+
# @return [Integer, String] object identity for inline source, or canonical physical identity for loaded source
|
|
42
|
+
def identity
|
|
43
|
+
return object_id unless origin
|
|
44
|
+
|
|
45
|
+
::File.realpath(origin)
|
|
46
|
+
rescue ::SystemCallError
|
|
47
|
+
::File.expand_path(origin)
|
|
48
|
+
end
|
|
32
49
|
end
|
|
33
50
|
end
|
|
34
51
|
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,156 +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
|
-
|
|
27
|
+
private_class_method :new
|
|
28
|
+
private_constant :Scope
|
|
21
29
|
|
|
22
30
|
# Thread-current key used for the fiber-local executor scope stack.
|
|
23
31
|
# @api private
|
|
24
32
|
SCOPE_KEY = :sevgi_executor_scopes
|
|
25
|
-
|
|
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
|
|
26
81
|
|
|
27
82
|
# Loads a script file inside the current executor scope.
|
|
28
83
|
# @param file [String] path to a Sevgi script file
|
|
29
|
-
# @return [Sevgi::Executor::Scope] current execution scope
|
|
84
|
+
# @return [Sevgi::Executor::Scope] current internal execution scope
|
|
30
85
|
# @raise [Sevgi::PanicError] when there is no active executor scope
|
|
31
86
|
# @note Uses the active executor scope from the current fiber.
|
|
32
87
|
# @api private
|
|
33
88
|
def self.load(file, ...)
|
|
34
|
-
PanicError.("box stack empty; create a box first") unless
|
|
89
|
+
PanicError.("box stack empty; create a box first") unless STATE.current
|
|
35
90
|
|
|
36
|
-
|
|
91
|
+
STATE.current.load(file, ...)
|
|
37
92
|
end
|
|
38
93
|
|
|
94
|
+
private_class_method :load
|
|
95
|
+
|
|
39
96
|
# Executes Ruby source inside a managed Sevgi script scope.
|
|
40
97
|
# @param string [String] source to evaluate
|
|
41
98
|
# @param file [String, nil] source file name used for errors and backtraces
|
|
42
99
|
# @param line [Integer, nil] starting source line used for errors and backtraces
|
|
43
100
|
# @param require [String, nil] optional Ruby library to require before execution
|
|
44
|
-
# @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
|
|
45
103
|
# @yield optional boot block that installs DSL methods before evaluation
|
|
46
104
|
# @yieldreturn [void]
|
|
47
|
-
# @return [Sevgi::Executor::
|
|
48
|
-
# @
|
|
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.
|
|
49
111
|
# @note Reentrant and concurrent calls keep independent scope stacks per fiber. The temporary SIGINT handler remains
|
|
50
112
|
# process-global while any execution is active.
|
|
113
|
+
# @api private
|
|
51
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
|
+
|
|
52
118
|
execute_source(Source.new(string:, file:, line:), require:, receiver:, &block)
|
|
53
119
|
end
|
|
54
120
|
|
|
55
121
|
# Executes a file inside a managed Sevgi script scope.
|
|
56
122
|
# @param file [String] source file to read and execute
|
|
123
|
+
# @param as [String, nil] logical source name used for evaluation and diagnostics
|
|
57
124
|
# @param require [String, nil] optional Ruby library to require before execution
|
|
58
|
-
# @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
|
|
59
127
|
# @yield optional boot block that installs DSL methods before evaluation
|
|
60
128
|
# @yieldreturn [void]
|
|
61
|
-
# @return [Sevgi::Executor::
|
|
62
|
-
# @
|
|
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.
|
|
63
135
|
# @note Reentrant and concurrent calls keep independent scope stacks per fiber. The temporary SIGINT handler remains
|
|
64
136
|
# process-global while any execution is active.
|
|
65
|
-
|
|
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
|
+
|
|
66
143
|
source = nil
|
|
67
144
|
begin
|
|
68
|
-
source = Source.load(file)
|
|
145
|
+
source = Source.load(file, as:)
|
|
69
146
|
rescue ::SystemCallError => e
|
|
70
|
-
return capture_error(Source.new(string: "", file
|
|
147
|
+
return capture_error(Source.new(string: "", file: as || file, line: 1, origin: file), e)
|
|
71
148
|
end
|
|
72
149
|
|
|
73
150
|
execute_source(source, require:, receiver:, &block)
|
|
74
151
|
end
|
|
75
152
|
|
|
76
|
-
|
|
77
|
-
# @return [Sevgi::Executor::Scope, nil] removed scope
|
|
78
|
-
# @api private
|
|
79
|
-
def self.shutdown
|
|
80
|
-
instance.shutdown
|
|
81
|
-
end
|
|
82
|
-
|
|
83
|
-
def initialize
|
|
84
|
-
@signal_count = 0
|
|
85
|
-
@signal_mutex = Mutex.new
|
|
86
|
-
@signal_previous = nil
|
|
87
|
-
end
|
|
153
|
+
private_class_method :execute, :execute_file
|
|
88
154
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
# @return [Sevgi::Executor::Scope] created scope
|
|
92
|
-
# @api private
|
|
93
|
-
def create(scope = nil) = Scope.new(scope).tap { scopes << it }
|
|
155
|
+
class << self
|
|
156
|
+
private
|
|
94
157
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
|
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
|
|
112
165
|
end
|
|
113
|
-
end
|
|
114
166
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
# @api private
|
|
119
|
-
def shutdown(scope = nil)
|
|
120
|
-
return scopes.pop unless scope
|
|
121
|
-
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?
|
|
122
170
|
|
|
123
|
-
|
|
124
|
-
|
|
171
|
+
acquired = STATE.trap
|
|
172
|
+
scope = STATE.create
|
|
173
|
+
catch(:result) { run_source(scope, source, require, receiver, &block) }
|
|
174
|
+
scope.result
|
|
125
175
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
|
176
|
+
ensure
|
|
177
|
+
STATE.restore if acquired
|
|
178
|
+
STATE.shutdown(scope) if scope
|
|
134
179
|
end
|
|
135
|
-
end
|
|
136
180
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
instance.shutdown(scope) if scope
|
|
144
|
-
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
|
|
145
187
|
|
|
146
|
-
|
|
147
|
-
|
|
188
|
+
def validate_context!(library, receiver)
|
|
189
|
+
ArgumentError.("Executor library must be a String or nil") unless library.nil? || library.is_a?(::String)
|
|
148
190
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
|
152
194
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
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)
|
|
157
198
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
rescue ::LoadError => e
|
|
162
|
-
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
|
|
163
202
|
end
|
|
164
|
-
|
|
165
|
-
private_class_method :capture_error, :execute_source, :run_source
|
|
166
|
-
|
|
167
|
-
private
|
|
168
|
-
|
|
169
|
-
def scopes = Thread.current[SCOPE_KEY] ||= []
|
|
170
203
|
end
|
|
204
|
+
|
|
171
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
|
|
@@ -4,20 +4,106 @@ require "sevgi/derender"
|
|
|
4
4
|
|
|
5
5
|
module Sevgi
|
|
6
6
|
module Toplevel
|
|
7
|
+
# Converts inline SVG/XML content into a derender node.
|
|
8
|
+
# @param content [String] SVG/XML source content
|
|
9
|
+
# @param id [String, Symbol, nil] optional SVG id selecting a node inside the source
|
|
10
|
+
# @param omit [String, Symbol, Array<String, Symbol>, nil] exact attribute name or names omitted from the selected
|
|
11
|
+
# subtree after id selection
|
|
12
|
+
# @return [Sevgi::Derender::Node] selected node in the derender tree
|
|
13
|
+
# @raise [Sevgi::ArgumentError] when content is malformed or rootless, or the id is absent
|
|
14
|
+
# @see Sevgi.Decompile
|
|
15
|
+
# @see Sevgi::Derender.decompile
|
|
16
|
+
def Decompile(content, id: nil, omit: nil) = Derender.decompile(content, id:, omit:)
|
|
17
|
+
|
|
7
18
|
# Converts an SVG/XML file into a derender node.
|
|
8
19
|
# @param file [String] path to the source SVG/XML file
|
|
9
|
-
# @param id [String, nil] optional SVG id selecting a node inside the source
|
|
20
|
+
# @param id [String, Symbol, nil] optional SVG id selecting a node inside the source
|
|
21
|
+
# @param omit [String, Symbol, Array<String, Symbol>, nil] exact attribute name or names omitted from the selected
|
|
22
|
+
# subtree after id selection
|
|
10
23
|
# @return [Sevgi::Derender::Node] selected node in the derender tree
|
|
11
|
-
# @raise [Sevgi::ArgumentError] when the file
|
|
24
|
+
# @raise [Sevgi::ArgumentError] when the file is absent, malformed, or rootless, or the id is absent
|
|
25
|
+
# @raise [SystemCallError] when the file cannot be read
|
|
26
|
+
# @see Sevgi.DecompileFile
|
|
12
27
|
# @see Sevgi::Derender.decompile_file
|
|
13
|
-
def
|
|
28
|
+
def DecompileFile(file, id: nil, omit: nil) = Derender.decompile_file(file, id:, omit:)
|
|
29
|
+
|
|
30
|
+
# Converts inline SVG/XML content into Sevgi DSL Ruby source.
|
|
31
|
+
# @param content [String] SVG/XML source content
|
|
32
|
+
# @param id [String, Symbol, nil] optional SVG id selecting a node inside the source
|
|
33
|
+
# @param omit [String, Symbol, Array<String, Symbol>, nil] exact attribute name or names omitted from the selected
|
|
34
|
+
# subtree after id selection
|
|
35
|
+
# @return [String] formatted Sevgi DSL source
|
|
36
|
+
# @raise [Sevgi::ArgumentError] when content is malformed or rootless, or the id is absent
|
|
37
|
+
# @raise [Sevgi::PanicError] when generated Ruby source cannot be formatted
|
|
38
|
+
# @see Sevgi.Derender
|
|
39
|
+
# @see Sevgi::Derender.derender
|
|
40
|
+
def Derender(content, id: nil, omit: nil) = Derender.derender(content, id:, omit:)
|
|
14
41
|
|
|
15
42
|
# Converts an SVG/XML file into Sevgi DSL Ruby source.
|
|
16
43
|
# @param file [String] path to the source SVG/XML file
|
|
17
|
-
# @param id [String, nil] optional SVG id selecting a node inside the source
|
|
44
|
+
# @param id [String, Symbol, nil] optional SVG id selecting a node inside the source
|
|
45
|
+
# @param omit [String, Symbol, Array<String, Symbol>, nil] exact attribute name or names omitted from the selected
|
|
46
|
+
# subtree after id selection
|
|
18
47
|
# @return [String] formatted Sevgi DSL source
|
|
19
|
-
# @raise [Sevgi::ArgumentError] when the file
|
|
48
|
+
# @raise [Sevgi::ArgumentError] when the file is absent, malformed, or rootless, or the id is absent
|
|
49
|
+
# @raise [Sevgi::PanicError] when generated Ruby source cannot be formatted
|
|
50
|
+
# @raise [SystemCallError] when the file cannot be read
|
|
51
|
+
# @see Sevgi.DerenderFile
|
|
20
52
|
# @see Sevgi::Derender.derender_file
|
|
21
|
-
def
|
|
53
|
+
def DerenderFile(file, id: nil, omit: nil) = Derender.derender_file(file, id:, omit:)
|
|
54
|
+
|
|
55
|
+
# Evaluates inline SVG/XML content under a graphics element, including the selected node.
|
|
56
|
+
# @param content [String] SVG/XML source content
|
|
57
|
+
# @param element [Sevgi::Graphics::Element] target graphics element
|
|
58
|
+
# @param id [String, Symbol, nil] optional SVG id selecting a node inside the source
|
|
59
|
+
# @param omit [String, Symbol, Array<String, Symbol>, nil] exact attribute name or names omitted from the selected
|
|
60
|
+
# subtree after id selection
|
|
61
|
+
# @return [Sevgi::Graphics::Element, nil] included selected/root element, or nil when it produces no output
|
|
62
|
+
# @raise [Sevgi::ArgumentError] when content is malformed or rootless, or the id is absent
|
|
63
|
+
# @see Sevgi.Evaluate
|
|
64
|
+
# @see Sevgi::Derender.evaluate
|
|
65
|
+
def Evaluate(content, element, id: nil, omit: nil) = Derender.evaluate(content, element, id:, omit:)
|
|
66
|
+
|
|
67
|
+
# Evaluates only the selected node's children from inline SVG/XML content under a graphics element.
|
|
68
|
+
# @param content [String] SVG/XML source content
|
|
69
|
+
# @param element [Sevgi::Graphics::Element] target graphics element
|
|
70
|
+
# @param id [String, Symbol, nil] optional SVG id selecting a node inside the source
|
|
71
|
+
# @param omit [String, Symbol, Array<String, Symbol>, nil] exact attribute name or names omitted from the selected
|
|
72
|
+
# subtree after id selection
|
|
73
|
+
# @return [Array<Sevgi::Graphics::Element>] immutable included-child snapshot
|
|
74
|
+
# @raise [Sevgi::ArgumentError] when content is malformed or rootless, or the id is absent
|
|
75
|
+
# @see Sevgi.EvaluateChildren
|
|
76
|
+
# @see Sevgi::Derender.evaluate_children
|
|
77
|
+
def EvaluateChildren(content, element, id: nil, omit: nil)
|
|
78
|
+
Derender.evaluate_children(content, element, id:, omit:)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Evaluates only the selected node's children from an SVG/XML file under a graphics element.
|
|
82
|
+
# @param file [String] path to the source SVG/XML file
|
|
83
|
+
# @param element [Sevgi::Graphics::Element] target graphics element
|
|
84
|
+
# @param id [String, Symbol, nil] optional SVG id selecting a node inside the source
|
|
85
|
+
# @param omit [String, Symbol, Array<String, Symbol>, nil] exact attribute name or names omitted from the selected
|
|
86
|
+
# subtree after id selection
|
|
87
|
+
# @return [Array<Sevgi::Graphics::Element>] immutable included-child snapshot
|
|
88
|
+
# @raise [Sevgi::ArgumentError] when the file is absent, malformed, or rootless, or the id is absent
|
|
89
|
+
# @raise [SystemCallError] when the file cannot be read
|
|
90
|
+
# @see Sevgi.EvaluateChildrenFile
|
|
91
|
+
# @see Sevgi::Derender.evaluate_children_file
|
|
92
|
+
def EvaluateChildrenFile(file, element, id: nil, omit: nil)
|
|
93
|
+
Derender.evaluate_children_file(file, element, id:, omit:)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Evaluates an SVG/XML file under a graphics element, including the selected node.
|
|
97
|
+
# @param file [String] path to the source SVG/XML file
|
|
98
|
+
# @param element [Sevgi::Graphics::Element] target graphics element
|
|
99
|
+
# @param id [String, Symbol, nil] optional SVG id selecting a node inside the source
|
|
100
|
+
# @param omit [String, Symbol, Array<String, Symbol>, nil] exact attribute name or names omitted from the selected
|
|
101
|
+
# subtree after id selection
|
|
102
|
+
# @return [Sevgi::Graphics::Element, nil] included selected/root element, or nil when it produces no output
|
|
103
|
+
# @raise [Sevgi::ArgumentError] when the file is absent, malformed, or rootless, or the id is absent
|
|
104
|
+
# @raise [SystemCallError] when the file cannot be read
|
|
105
|
+
# @see Sevgi.EvaluateFile
|
|
106
|
+
# @see Sevgi::Derender.evaluate_file
|
|
107
|
+
def EvaluateFile(file, element, id: nil, omit: nil) = Derender.evaluate_file(file, element, id:, omit:)
|
|
22
108
|
end
|
|
23
109
|
end
|