sevgi 0.73.2 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4696b409358c595a5196f8e4e037edf1cf6f2e171d3f083fa56438d36dd5750f
4
- data.tar.gz: 857df564a95208647e8f3d5de0ae8f8289d4afcbe7433b74be05f2b433293256
3
+ metadata.gz: ceb2e9987cc3e1ee86f9a7e8961214a60b81685e89b2f2d9dc5986e3b2eed8f6
4
+ data.tar.gz: 50240698f875654431f4caf9681ddf8b163971c5f7d4ef9781849be17541e37d
5
5
  SHA512:
6
- metadata.gz: a2687e5ec21afdc64e569a814a1ab0ff78436d93eb929bc661f5462f621da5305bdfada1fd256dd3960dbbc82bd4e64ea4f5d098a39870a83493b76b5a1844f2
7
- data.tar.gz: 267238636b3ee84ada012dfd585889071aa0552abae6418922b423f6ee15bafe150ad721ae8d6718e45966f659669ac38972985c033dc77252d2ec2c8c14c627
6
+ metadata.gz: 9ddd81f3154b0bab5f2fa17890301f97fc960572874621bc42634f04caf826664c73a19082b9e8222a7155f2dcb5132f99a01afdb8ee841c6d6376bf162020b2
7
+ data.tar.gz: a8902dc527a3bcc3f26bce6f2eb8ecd442a26b4628db70c39011523bf409628f148c3288c7efc357b96b57fc67bc8aa640f7420d278494f8c32d2616bf9b615e
data/CHANGELOG.md ADDED
@@ -0,0 +1,4 @@
1
+ # Changelog
2
+
3
+ Sevgi follows the root Sevgi release notes:
4
+ https://github.com/roktas/sevgi/blob/main/CHANGELOG.md
data/LICENSE ADDED
@@ -0,0 +1,5 @@
1
+ Sevgi is distributed under the GNU General Public License v3.0 or later.
2
+
3
+ SPDX-License-Identifier: GPL-3.0-or-later
4
+
5
+ Full project license: https://github.com/roktas/sevgi/blob/main/LICENSE
data/README.md CHANGED
@@ -0,0 +1,61 @@
1
+ # Sevgi
2
+
3
+ Top-level API and `.sevgi` script runner.
4
+
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
@@ -2,14 +2,27 @@
2
2
 
3
3
  require "sevgi"
4
4
 
5
+ # Extends FileUtils with Sevgi script helpers for Rake tasks.
5
6
  module FileUtils
6
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
+ #
14
+ # @param file [String] Sevgi script file, with or without `.sevgi` extension
15
+ # @param args [Array] positional arguments exposed to the script as `ARGA`
16
+ # @param kwargs [Hash] keyword arguments exposed to the script as `ARGH`
17
+ # @return [Sevgi::Executor::Scope, nil] execution scope, or nil for an empty file
18
+ # @raise [Sevgi::ArgumentError] when the script file cannot be found
19
+ # @see Sevgi::Executor.execute_file
7
20
  def sevgi(file, *args, **kwargs)
8
- Sevgi.execute_file(F.existing!(file, [EXTENSION])) do |mod|
9
- include(Sevgi)
21
+ Sevgi::Executor.execute_file(Sevgi::F.existing!(file, [Sevgi::EXTENSION])) do
22
+ extend(Sevgi)
10
23
 
11
- mod.const_set(:ARGA, args).freeze
12
- mod.const_set(:ARGH, kwargs).freeze
24
+ const_set(:ARGA, args).freeze
25
+ const_set(:ARGH, kwargs).freeze
13
26
  end
14
27
  end
15
28
  end
@@ -3,15 +3,25 @@
3
3
  require "sevgi"
4
4
 
5
5
  module Sevgi
6
+ # Command-line entrypoint implementations shipped with Sevgi.
6
7
  module Binaries
8
+ # Implements the `sevgi` executable.
7
9
  module Sevgi
8
10
  extend self
9
11
 
12
+ # Executable name used in help output.
10
13
  PROGNAME = "sevgi"
11
14
 
15
+ # Error raised for invalid command-line usage.
12
16
  Error = Class.new(::Sevgi::Error)
13
17
 
18
+ # Parsed command-line options for the `sevgi` executable.
19
+ # @api private
14
20
  Options = Struct.new(:require, :nomain, :vomit, :help, :version) do
21
+ # Parses command-line options and removes them from the argv array.
22
+ # @param argv [Array<String>] mutable command-line argument array
23
+ # @return [Sevgi::Binaries::Sevgi::Options] parsed options
24
+ # @raise [Sevgi::Binaries::Sevgi::Error] when an option is not recognized
15
25
  def self.parse(argv)
16
26
  new.tap do |options|
17
27
  argv.first.start_with?("-") ? option(argv, options) : break until argv.empty?
@@ -25,7 +35,7 @@ module Sevgi
25
35
  case (arg = argv.shift)
26
36
  when "-r", "--require"
27
37
  options.require = argv.shift
28
- when "-m", "--nomain"
38
+ when "-n", "--nomain"
29
39
  options.nomain = true
30
40
  when "-x", "--exception"
31
41
  options.vomit = true
@@ -43,6 +53,11 @@ module Sevgi
43
53
 
44
54
  private_constant :Options
45
55
 
56
+ # Runs the `sevgi` command-line interface.
57
+ # @param argv [Array<String>, String, nil] command-line arguments
58
+ # @return [nil]
59
+ # @raise [Sevgi::Executor::Error] when `--exception` or `SEVGI_VOMIT` requests raw executor errors
60
+ # @raise [SystemExit] when command-line usage or script execution aborts
46
61
  def call(argv)
47
62
  return puts(help) if (options = Options.parse(argv = Array(argv))).help
48
63
  return puts(::Sevgi::VERSION) if options.version
@@ -2,9 +2,18 @@
2
2
 
3
3
  module Sevgi
4
4
  class Executor
5
+ # Wraps an exception raised while executing Sevgi script source.
5
6
  class Error < ::Sevgi::Error
7
+ # @!attribute [r] error
8
+ # @return [Exception] original exception raised by script execution
9
+ # @!attribute [r] scope
10
+ # @return [Sevgi::Executor::Scope] executor scope active when the error was captured
6
11
  attr_reader :error, :scope
7
12
 
13
+ # Builds an executor error wrapper.
14
+ # @param error [Exception] original exception
15
+ # @param scope [Sevgi::Executor::Scope] executor scope active at failure time
16
+ # @return [void]
8
17
  def initialize(error, scope)
9
18
  @error = error
10
19
  @scope = scope
@@ -12,13 +21,23 @@ module Sevgi
12
21
  super(error.message)
13
22
  end
14
23
 
24
+ # Returns backtrace entries that belong to the Sevgi load stack.
25
+ # @return [Array<String>] filtered backtrace lines relative to the current directory
15
26
  def backtrace!
27
+ sources = stack.map { ::File.expand_path(it) }
28
+
16
29
  error
17
30
  .backtrace
18
- .select { |line| stack.any? { line.start_with?(::File.expand_path(it)) } }
31
+ .select { sources.include?(::File.expand_path(it.split(":", 2).first)) }
19
32
  .map { |line| line.delete_prefix("#{::Dir.pwd}/") }
20
33
  end
21
34
 
35
+ # Returns the original exception as the wrapped cause.
36
+ # @return [Exception]
37
+ def cause = error
38
+
39
+ # Returns the script load stack active at failure time.
40
+ # @return [Array<String>] script file names in load order
22
41
  def stack = scope.stack
23
42
  end
24
43
  end
@@ -2,11 +2,24 @@
2
2
 
3
3
  module Sevgi
4
4
  class Executor
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.
5
9
  class Scope
6
- Error = Class.new(::Sevgi::Error)
7
-
10
+ # @!attribute [r] scope
11
+ # @return [Module] isolated module where script source is evaluated
12
+ # @!attribute [r] recent
13
+ # @return [Object, nil] last expression result from the executed source
14
+ # @!attribute [r] error
15
+ # @return [Sevgi::Executor::Error, nil] captured execution error
8
16
  attr_reader :scope, :recent, :error
9
17
 
18
+ # Creates a script execution scope.
19
+ # @param scope [Module, nil] existing module to evaluate source in
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.
10
23
  def initialize(scope = nil)
11
24
  @scope = scope || main
12
25
  @recent = nil
@@ -14,16 +27,22 @@ module Sevgi
14
27
  @stack = {}
15
28
  end
16
29
 
30
+ # Reports whether execution captured an error.
31
+ # @return [Boolean] true when execution failed
17
32
  def error? = !error.nil?
18
33
 
34
+ # Executes one source object in this scope.
35
+ # @param source [Sevgi::Executor::Source] source to evaluate
36
+ # @param receiver [Object, nil] receiver used while booting the DSL
37
+ # @yield optional boot block that installs DSL methods before evaluation
38
+ # @yieldreturn [void]
39
+ # @return [Sevgi::Executor::Scope] self, with recent or error populated
40
+ # @api private
19
41
  def call(source, receiver = nil, &boot)
20
42
  push(source)
21
43
 
22
44
  tap do
23
- @recent = begin
24
- boot(receiver, &boot)
25
- evaluate(source)
26
- end
45
+ @recent = run(source, receiver, &boot)
27
46
 
28
47
  # rubocop:disable Lint/RescueException
29
48
  rescue Exception => e
@@ -34,14 +53,41 @@ module Sevgi
34
53
  end
35
54
  end
36
55
 
37
- def load(file, &block) = call(Source.load(file), &block)
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
+
67
+ # Loads a file into this existing execution scope.
68
+ # @param file [String] source file to read and evaluate
69
+ # @yield optional boot block that installs DSL methods before evaluation
70
+ # @yieldreturn [void]
71
+ # @return [Sevgi::Executor::Scope] self, with recent or error populated
72
+ # @note File-read failures are captured as {Sevgi::Executor::Error} on this scope.
73
+ # @api private
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
38
80
 
81
+ # Returns the unique source stack for this execution.
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.
39
84
  def stack = @stack.keys
40
85
 
86
+ # Returns the most recently pushed source.
87
+ # @return [Sevgi::Executor::Source, nil] most recent source object
88
+ # @api private
41
89
  def peek = @stack[@stack.keys.last]
42
90
 
43
- MAIN_MODULE = :Main
44
-
45
91
  private
46
92
 
47
93
  def boot(receiver, &boot)
@@ -56,15 +102,17 @@ module Sevgi
56
102
  end
57
103
 
58
104
  def main
59
- Module.new.tap do |mod|
60
- Sevgi.send(:remove_const, MAIN_MODULE) if Sevgi.const_defined?(MAIN_MODULE)
61
- Sevgi.const_set(MAIN_MODULE, mod)
62
- end
105
+ Module.new.tap { |mod| mod.define_singleton_method(:name) { "Sevgi::Main" } }
63
106
  end
64
107
 
65
108
  def push(source)
66
109
  tap { @stack[source.key] = source }
67
110
  end
111
+
112
+ def run(source, receiver, &boot)
113
+ boot(receiver, &boot)
114
+ evaluate(source)
115
+ end
68
116
  end
69
117
  end
70
118
  end
@@ -2,13 +2,32 @@
2
2
 
3
3
  module Sevgi
4
4
  class Executor
5
+ # Describes Ruby source evaluated by the Sevgi executor.
6
+ # @api private
5
7
  Source = Data.define(:string, :file, :line) do
8
+ # @overload call(string:, file: nil, line: nil)
9
+ # Builds a source object.
10
+ # @param string [String] Ruby source string
11
+ # @param file [String, nil] source file name for diagnostics
12
+ # @param line [Integer, nil] starting source line for diagnostics
13
+ # @return [Sevgi::Executor::Source] source object
6
14
  def self.call(...) = new(...)
7
15
 
16
+ # Builds a source object from a file.
17
+ # @param file [String] source file to read
18
+ # @return [Sevgi::Executor::Source] source object with file contents
19
+ # @raise [Errno::ENOENT] when the file cannot be read
8
20
  def self.load(file) = new(string: ::File.read(file), file: file, line: 1)
9
21
 
22
+ # Creates a source object.
23
+ # @param string [String] Ruby source string
24
+ # @param file [String, nil] source file name for diagnostics
25
+ # @param line [Integer, nil] starting source line for diagnostics
26
+ # @return [void]
10
27
  def initialize(string:, file: nil, line: nil) = super(string:, file: file || "sevgi", line: line || 1)
11
28
 
29
+ # Returns the stack key used for this source.
30
+ # @return [String] source file name
12
31
  def key = file
13
32
  end
14
33
  end
@@ -7,44 +7,165 @@ require_relative "executor/scope"
7
7
  require_relative "executor/source"
8
8
 
9
9
  module Sevgi
10
+ # Executes Sevgi script source inside an isolated module scope.
11
+ #
12
+ # The executor is used by script mode and by the `Load` DSL word to preserve a
13
+ # useful load stack while keeping DSL methods out of the caller's global object
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.
10
19
  class Executor
11
20
  include Singleton
12
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
+
27
+ # Loads a script file inside the current executor scope.
28
+ # @param file [String] path to a Sevgi script file
29
+ # @return [Sevgi::Executor::Scope] current execution scope
30
+ # @raise [Sevgi::PanicError] when there is no active executor scope
31
+ # @note Uses the active executor scope from the current fiber.
32
+ # @api private
13
33
  def self.load(file, ...)
14
34
  PanicError.("box stack empty; create a box first") unless instance.current
15
35
 
16
36
  instance.current.load(file, ...)
17
37
  end
18
38
 
39
+ # Executes Ruby source inside a managed Sevgi script scope.
40
+ # @param string [String] source to evaluate
41
+ # @param file [String, nil] source file name used for errors and backtraces
42
+ # @param line [Integer, nil] starting source line used for errors and backtraces
43
+ # @param require [String, nil] optional Ruby library to require before execution
44
+ # @param receiver [Object, nil] receiver used while booting the DSL
45
+ # @yield optional boot block that installs DSL methods before evaluation
46
+ # @yieldreturn [void]
47
+ # @return [Sevgi::Executor::Scope, nil] execution scope, or nil for empty source
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.
19
51
  def self.execute(string, file: nil, line: nil, require: nil, receiver: nil, &block)
20
- return if string.empty?
52
+ execute_source(Source.new(string:, file:, line:), require:, receiver:, &block)
53
+ end
21
54
 
22
- Signal.trap("INT") { Kernel.abort("") }
55
+ # Executes a file inside a managed Sevgi script scope.
56
+ # @param file [String] source file to read and execute
57
+ # @param require [String, nil] optional Ruby library to require before execution
58
+ # @param receiver [Object, nil] receiver used while booting the DSL
59
+ # @yield optional boot block that installs DSL methods before evaluation
60
+ # @yieldreturn [void]
61
+ # @return [Sevgi::Executor::Scope, nil] execution scope, or nil for an empty file
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.
65
+ def self.execute_file(file, require: nil, receiver: nil, &block)
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
23
72
 
24
- ::Kernel.require(require) if require
73
+ execute_source(source, require:, receiver:, &block)
74
+ end
25
75
 
26
- catch(:result) do
27
- instance.create.call(Source.new(string:, file:, line:), receiver, &block)
76
+ # Removes the current executor scope.
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
88
+
89
+ # Creates and pushes a new executor scope.
90
+ # @param scope [Module, nil] existing module scope to reuse
91
+ # @return [Sevgi::Executor::Scope] created scope
92
+ # @api private
93
+ def create(scope = nil) = Scope.new(scope).tap { scopes << it }
94
+
95
+ # Returns the active executor scope.
96
+ # @return [Sevgi::Executor::Scope, nil] active scope, if any
97
+ # @api private
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
28
112
  end
113
+ end
29
114
 
30
- ensure
31
- instance.shutdown
115
+ # Removes the active executor scope.
116
+ # @param scope [Sevgi::Executor::Scope, nil] exact scope to remove, or nil to pop the current scope
117
+ # @return [Sevgi::Executor::Scope, nil] removed scope
118
+ # @api private
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)
32
124
  end
33
125
 
34
- def self.execute_file(file, require: nil, receiver: nil, &block)
35
- execute(::File.read(file), file: file, line: 1, require:, receiver:, &block)
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
36
135
  end
37
136
 
38
- def self.shutdown
39
- instance.shutdown
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
40
144
  end
41
145
 
42
- def initialize = @scopes = []
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
43
164
 
44
- def create(scope = nil) = Scope.new(scope).tap { @scopes << it }
165
+ private_class_method :capture_error, :execute_source, :run_source
45
166
 
46
- def current = @scopes.last
167
+ private
47
168
 
48
- def shutdown = @scopes.pop
169
+ def scopes = Thread.current[SCOPE_KEY] ||= []
49
170
  end
50
171
  end
@@ -4,12 +4,20 @@ require "sevgi/derender"
4
4
 
5
5
  module Sevgi
6
6
  module Toplevel
7
- def Decompile(file, id) = Derender.decompile_file(file, id:)
7
+ # Converts an SVG/XML file into a derender node.
8
+ # @param file [String] path to the source SVG/XML file
9
+ # @param id [String, nil] optional SVG id selecting a node inside the source
10
+ # @return [Sevgi::Derender::Node] selected node in the derender tree
11
+ # @raise [Sevgi::ArgumentError] when the file cannot be found or the id is absent
12
+ # @see Sevgi::Derender.decompile_file
13
+ def Decompile(file, id = nil) = Derender.decompile_file(file, id:)
8
14
 
9
- def Decompile!(file, id) = Derender.decompile_file!(file, id:)
10
-
11
- def Derender(file, id) = Derender.derender_file(file, id:)
12
-
13
- def Derender!(file, id) = Derender.derender_file!(file, id:)
15
+ # Converts an SVG/XML file into Sevgi DSL Ruby source.
16
+ # @param file [String] path to the source SVG/XML file
17
+ # @param id [String, nil] optional SVG id selecting a node inside the source
18
+ # @return [String] formatted Sevgi DSL source
19
+ # @raise [Sevgi::ArgumentError] when the file cannot be found or the id is absent
20
+ # @see Sevgi::Derender.derender_file
21
+ def Derender(file, id = nil) = Derender.derender_file(file, id:)
14
22
  end
15
23
  end
@@ -7,11 +7,31 @@ module Sevgi
7
7
 
8
8
  private_constant :BootBlock
9
9
 
10
+ # Executes Sevgi script source with the full top-level DSL installed.
11
+ # @param args [Array] arguments forwarded to {Sevgi::Executor.execute}
12
+ # @param kwargs [Hash] keyword arguments forwarded to {Sevgi::Executor.execute}
13
+ # @return [Sevgi::Executor::Scope, nil] execution result, or nil for empty source
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.
16
+ # @see Sevgi::Executor.execute
10
17
  def self.execute(*args, **kwargs) = Executor.execute(*args, **kwargs, &BootBlock)
11
18
 
19
+ # Executes a Sevgi script file with the full top-level DSL installed.
20
+ # @param args [Array] arguments forwarded to {Sevgi::Executor.execute_file}
21
+ # @param kwargs [Hash] keyword arguments forwarded to {Sevgi::Executor.execute_file}
22
+ # @return [Sevgi::Executor::Scope, nil] execution result, or nil for an empty file
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.
25
+ # @see Sevgi::Executor.execute_file
12
26
  def self.execute_file(*args, **kwargs) = Executor.execute_file(*args, **kwargs, &BootBlock)
13
27
 
14
28
  module Toplevel
29
+ # Loads one or more Sevgi files relative to the caller's source file.
30
+ # @param files [Array<String>] Sevgi script files to locate and execute
31
+ # @return [Array<String>] the input file list
32
+ # @raise [Sevgi::PanicError] when called without an active executor scope
33
+ # @raise [Sevgi::Error] when a file cannot be located
34
+ # @note `Load` resolves against the active executor scope in the current fiber.
15
35
  def Load(*files)
16
36
  start = ::File.dirname(caller_locations(1..1).first.path)
17
37
 
@@ -4,14 +4,13 @@ require "sevgi/function"
4
4
 
5
5
  module Sevgi
6
6
  module Toplevel
7
- module Function
8
- extend Sevgi::Function::Color
9
- extend Sevgi::Function::Math
10
- extend Sevgi::Function::Pluralize
11
- extend Sevgi::Function::Shell
12
- extend Sevgi::Function::UI
13
- end
14
-
15
- promote Function, :F
7
+ # Promotes the canonical function helper namespace as `F` in the full top-level DSL.
8
+ #
9
+ # @example Use helper functions inside script mode
10
+ # SVG do
11
+ # text F.pluralize("axis")
12
+ # end
13
+ # @see Sevgi::Function
14
+ promote Sevgi::Function, :F
16
15
  end
17
16
  end
@@ -4,14 +4,45 @@ require "sevgi/graphics"
4
4
 
5
5
  module Sevgi
6
6
  module Toplevel
7
+ # @overload Mixin(mod, document = Sevgi::Graphics::Document::Base, &block)
8
+ # Adds graphics mixture methods to a document class.
9
+ # @param mod [Symbol, String] named mixture to mix into the document
10
+ # @param document [Class] document class receiving the mixture
11
+ # @yield optional anonymous mixture module body
12
+ # @yieldreturn [void]
13
+ # @return [void]
14
+ # @raise [NameError] when a named mixture cannot be resolved
15
+ # @see Sevgi::Graphics::Mixtures.mixin
16
+ # @overload Mixin(document = Sevgi::Graphics::Document::Base, &block)
17
+ # Adds an anonymous graphics mixture to a document class.
18
+ # @param document [Class] document class receiving the mixture
19
+ # @yield anonymous mixture module body
20
+ # @yieldreturn [void]
21
+ # @return [Module] anonymous mixture
22
+ # @raise [Sevgi::ArgumentError] when no named mixture or block is given
23
+ # @see Sevgi::Graphics::Mixtures.mixin
7
24
  def Mixin(...) = Graphics::Mixtures.mixin(...)
8
25
 
9
- def Paper(width, height, name = :custom, unit: "mm")
10
- name.tap { Graphics::Paper.define(name, width:, height:, unit:) unless Graphics::Paper.exist?(name) }
11
- end
26
+ # @overload Paper(width, height, name = :custom, unit: "mm")
27
+ # Defines or validates a named paper profile for DSL use.
28
+ # @param width [Numeric] paper width
29
+ # @param height [Numeric] paper height
30
+ # @param name [Symbol] paper profile name
31
+ # @param unit [String, Symbol] size unit
32
+ # @return [Symbol] the paper profile name
33
+ # @raise [Sevgi::ArgumentError] when the profile is invalid or an existing profile differs
34
+ # @see Sevgi::Graphics#paper
35
+ def Paper(...) = Graphics.paper(...)
12
36
 
13
- def Paper!(width, height, name = :custom, unit: "mm")
14
- name.tap { Graphics::Paper.define(name, width:, height:, unit:) }
15
- end
37
+ # @overload Paper!(width, height, name = :custom, unit: "mm")
38
+ # Defines or overwrites a named paper profile for DSL use.
39
+ # @param width [Numeric] paper width
40
+ # @param height [Numeric] paper height
41
+ # @param name [Symbol] paper profile name
42
+ # @param unit [String, Symbol] size unit
43
+ # @return [Symbol] the paper profile name
44
+ # @raise [Sevgi::ArgumentError] when the profile is invalid or the paper name is reserved
45
+ # @see Sevgi::Graphics#paper!
46
+ def Paper!(...) = Graphics.paper!(...)
16
47
  end
17
48
  end
@@ -4,6 +4,15 @@ require "sevgi/sundries"
4
4
 
5
5
  module Sevgi
6
6
  module Toplevel
7
+ # Builds a drawable grid from a graphics canvas.
8
+ # @param canvas [Sevgi::Graphics::Canvas] canvas defining page size and margins
9
+ # @param unit [Numeric] minor grid unit
10
+ # @param multiple [Integer] number of minor units in each major interval
11
+ # @return [Sevgi::Sundries::Grid] grid fitted to the 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
7
16
  def Grid(canvas, unit:, multiple:)
8
17
  ArgumentError.("Must be a Canvas: #{canvas}") unless canvas.is_a?(Graphics::Canvas)
9
18
 
@@ -1,6 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Sevgi
4
+ # Shared implementation for the full Sevgi top-level DSL.
5
+ #
6
+ # This module is installed by `include Sevgi` or `extend Sevgi`; it should not
7
+ # normally be included directly.
8
+ #
9
+ # @see Sevgi
4
10
  module Toplevel
5
11
  @constants = {}
6
12
 
@@ -12,7 +18,12 @@ module Sevgi
12
18
  def inject(base)
13
19
  return if base.instance_variable_defined?("@_toplevel_injected_")
14
20
 
15
- @constants.each { |args| (base.is_a?(::Module) ? base : base.class).const_set(*args) }
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
25
+ end
26
+
16
27
  base.instance_variable_set("@_toplevel_injected_", true)
17
28
  end
18
29
 
@@ -21,11 +32,21 @@ module Sevgi
21
32
  end
22
33
  end
23
34
 
35
+ # Injects promoted constants when the DSL is included in a module or class.
36
+ # @param base [Module] the class or module receiving promoted constants
37
+ # @return [void]
38
+ # @api private
24
39
  def self.included(base)
25
40
  super
26
41
  inject(base)
27
42
  end
28
43
 
44
+ # Injects promoted constants when the DSL is extended by a module.
45
+ # @param base [Object] the object or module receiving the DSL methods
46
+ # @return [void]
47
+ # @note Constants are promoted only to modules/classes. Extending an ordinary object installs methods without
48
+ # writing constants to `Object`.
49
+ # @api private
29
50
  def self.extended(base)
30
51
  super
31
52
  inject(base)
data/lib/sevgi/version.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Sevgi
4
- VERSION = "0.73.2"
4
+ # Current version of the Sevgi top-level gem.
5
+ VERSION = "0.94.0"
5
6
  end
data/lib/sevgi.rb CHANGED
@@ -9,20 +9,51 @@ require "sevgi/sundries"
9
9
 
10
10
  require "sevgi/version"
11
11
 
12
- # Minimal Toplevel Interface (activated with 'require "sevgi"')
12
+ # Minimal top-level SVG interface installed by `require "sevgi"`.
13
+ # @return [Module] the graphics module used as the SVG DSL namespace
13
14
  SVG = Sevgi::Graphics
15
+
16
+ # @overload SVG(document = :default, canvas = Undefined, **attributes, &block)
17
+ # Builds an SVG document through the default top-level DSL entrypoint.
18
+ # @param document [Symbol, Class] document profile name or document class
19
+ # @param canvas [Object] optional canvas or paper profile argument
20
+ # @param attributes [Hash] root SVG attributes
21
+ # @yield the document block evaluated in the SVG document context
22
+ # @yieldreturn [void]
23
+ # @return [Sevgi::Graphics::Document::Proto] a rendered SVG document object
24
+ # @raise [Sevgi::ArgumentError] when the document, paper, or canvas arguments are invalid
14
25
  def SVG(...) = Sevgi::Graphics.SVG(...)
15
26
 
16
- # Maximal Toplevel Interface (activated with 'include Sevgi')
27
+ # Full top-level API for Sevgi library and script consumers.
28
+ #
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`.
34
+ #
35
+ # @example Include the DSL in an object
36
+ # class Drawing
37
+ # include Sevgi
38
+ # end
17
39
  module Sevgi
18
40
  # See sevgi/toplevel/*.rb files for details
19
41
  require_relative "sevgi/toplevel"
20
42
 
43
+ # Installs the full toplevel DSL into an including class or module.
44
+ # @param base [Module] the class or module receiving the DSL methods
45
+ # @return [void]
46
+ # @api private
21
47
  def self.included(base)
22
48
  super
23
49
  base.include(Toplevel)
24
50
  end
25
51
 
52
+ # Installs the full toplevel DSL into an extending object or module.
53
+ # @param base [Object] the receiver extended with the DSL methods
54
+ # @return [void]
55
+ # @note Promoted constants are written only when base is a module or class.
56
+ # @api private
26
57
  def self.extended(base)
27
58
  super
28
59
  base.extend(Toplevel)
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.73.2
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.73.2
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.73.2
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.73.2
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.73.2
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.73.2
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.73.2
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.73.2
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.73.2
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.73.2
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.73.2
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.73.2
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.73.2
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.pre.preview1
138
+ version: 3.4.0
137
139
  required_rubygems_version: !ruby/object:Gem::Requirement
138
140
  requirements:
139
141
  - - ">="