sevgi 0.98.2 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 637d6a0c7a281ad8f1800c9fcf08968266c0e6c6a73a0ad6206a7b41e5c57fc5
4
- data.tar.gz: b01598c764d715a4b381a90a3a6d5de203695459147142575cbd326ce176a07b
3
+ metadata.gz: 27a50b6890ad4f1ae6f47470e8b79896ad1281e2bdb54b7cfabd8310cd683c45
4
+ data.tar.gz: 7839ad48af80ceffb800314422b942406cb4bed1b7bb5c13e2774d121d0746ca
5
5
  SHA512:
6
- metadata.gz: e0c7c633c134254b8eedd6f9d8da45530f4485b0e0b3508ce1e4b76522eca83d3de456a53e956cdba3c38f48fd38e811db70d25b3fb8ed44f4c6c0261319b500
7
- data.tar.gz: b9182c6a0a4d250d0f1947bcf3b8c9e7b3bcebe9268e581d59cb2db4230315eadcf511e6880bd388a74847f441bbf5b0fcf4e37889ddfbc8c7ee7094e124a971
6
+ metadata.gz: 53837c04457e0ea48aa71642ea1c25a6fc352ba737b09dc073f988b4694d9cab561699695e6e2e6e89248a1704ccc659f4009e8aa4396737f7dc5a4b7b2ff5e3
7
+ data.tar.gz: 7f1ffa5979ce5af033451baf13c8746002f719782b633a4af16856d6283f828390eefb92bc2d17fdfbfa387f8e8e2675980dec86b992a483d17db35bdd56552d
data/CHANGELOG.md CHANGED
@@ -3,7 +3,101 @@
3
3
  This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html) and follows
4
4
  [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
5
5
 
6
- ## Unreleased
6
+ ## 1.0.0 - 2026-09-16
7
+
8
+ ### Added
9
+
10
+ - Added Hatch and Orb Showcase examples and displayed both arc lengths in the Arc example.
11
+
12
+ ### Changed
13
+
14
+ - Matched documentation page widths to the homepage Showcase.
15
+ - Aligned Derender user guidance with UTF-8 normalization and safe subtree selection rules.
16
+ - Clarified the packaged skill's script invocation, subtree copying, and Derender selection and encoding rules.
17
+ - Allowed Canvas and Paper values as the first `SVG` argument when using the default document profile.
18
+ - Made element `dup` and `clone` own independent subtrees while retaining IDs and Ruby copy semantics.
19
+ - Made Segment equality compare length and direction; ordering still compares length only.
20
+ - Limited Tile cell, row, and column queries to the requested geometry and accelerated XML character validation.
21
+ - Simplified the first-run guide and aligned library constructor examples.
22
+ - Updated the packaged skill for canvas origins, alignment, Paper lookup, curved geometry, and export constraints.
23
+ - Revised DSL catalog examples and packaged Sevgi skill guidance to use statement-style calls and show inspection results.
24
+ - Refined Showcase example tabs, made Ruby and XML views fill their panels, and hid executable preambles from Ruby views.
25
+ - Documented non-rendering element data and clarified the related DSL catalog examples.
26
+ - Refined Showcase heading spacing, page-title colors, and DSL catalog cards across light and dark themes.
27
+ - Removed an obsolete ruler example and unused site code.
28
+ - Kept the Squared and Copperplate guidesheet examples together at the end of the Showcase.
29
+ - Updated the packaged skill to avoid duplicate semantic shapes and unnecessary paint passes.
30
+
31
+ ### Fixed
32
+
33
+ - Kept horizontal scrollbars visible in Showcase example code panels.
34
+ - Read Derender input in its declared XML encoding and normalized generated source, XML declarations, and markup to UTF-8.
35
+ - Preserved open-path endpoints and derived edges independently of display precision.
36
+ - Kept near-axis and near-parallel line equations and intersections independent of angle and slope rounding.
37
+ - Corrected the Showcase social image dimensions.
38
+ - Added SVG namespaces to standalone drawing examples and regenerated their outputs.
39
+ - Made failed Sevgi Rake tasks stop dependent tasks and captured required-library failures in executor results.
40
+ - Removed executor signal-handler replacement so the host and shell retain SIGINT ownership.
41
+ - Escaped late export CSS as XML text and inserted it only at a supported final root closing tag.
42
+ - Preserved Derender processing instructions and rejected custom entity references and unsupported trailing document nodes before inclusion.
43
+ - Included both box origins in `Align`, unified Tile index validation, and isolated custom numeric attribute snapshots.
44
+ - Prevented avoidable overflow and underflow in point distances.
45
+ - Corrected browser source-panel expectations and added executable documentation, YARD, and packaged-skill checks.
46
+ - Rendered inline code in DSL catalog summaries and kept linked task themes visible below the sticky header.
47
+ - Restored Showcase tab icons with embedded Font Awesome SVGs.
48
+
49
+ ## 0.100.0 - 2026-09-09
50
+
51
+ ### Added
52
+
53
+ - Added `ArcTo` and `ArcBy` for SVG-native elliptical arc paths.
54
+ - Added Geometry `Arc`, `Ellipse`, and `Circle` with bounds, length, predicates, affine transformations, and
55
+ linear/quadratic intersections. Closed ellipses and circles support sweeps and hatching.
56
+ - Added Geometry `Point.midpoint`, `Element#closed?`, lined `vertices`, open-path traversal, and `Rect#center`.
57
+ - Added Geometry aggregate bounding boxes through `Operation.box`, point collinearity through `Point.collinear?`, and
58
+ polygon `simple?`, `convex?`, and `concave?` predicates.
59
+ - Added Protractor and Arc examples and documented the drawing and calculation APIs.
60
+
61
+ ### Changed
62
+
63
+ - Removed redundant parent scans while duplicating element subtrees.
64
+ - Reused source bounds across Tile cells to reduce repeated calculations and object allocations.
65
+
66
+ ### Fixed
67
+
68
+ - Preserved Showcase card HTML through Markdown rendering so all examples and their tabs remain visible.
69
+ - Preserved XML declarations, pre-root comments, CDATA sections, inline comments, and namespace-only leaves in
70
+ Derender output and evaluation.
71
+ - Limited PDF stamp recoloring to replaced text and carried graphics state across a page's content streams.
72
+ - Corrected decimal interval fitting and made Grid point queries honor the active numeric precision.
73
+ - Prevented implicit shell interpretation of single executable arguments in `F.sh` and `F.sh!`, including paths with spaces or shell metacharacters.
74
+ - Preserved inherited namespaces on detached Derender nodes in generated source and direct evaluation, including imports through `EvaluateChildren` and `IncludeChildren`.
75
+ - Prevented sweep and Hatch lines from crossing the exterior between two tangent vertices of a concave polygon.
76
+ - Aligned Tile cells with their declared bounds for circles, rotated shapes, and polygons whose first vertex differs from their bounding-box origin.
77
+ - Limited `Load` chains to 128 active sources with `Executor::LoadDepthError`, preserving cycle detection and nested load diagnostics.
78
+ - Rejected nil and false aggregate-box inputs with the documented geometry error.
79
+ - Restored precision-aware rejection of zero-length Triangle sides.
80
+ - Made point-set collinearity independent of input order and removed eager polygon edge-pair storage.
81
+ - Applied the collinearity tolerance to every three-point subset, removing coordinate-axis-dependent baseline selection.
82
+
83
+ ## 0.99.0 - 2026-08-09
84
+
85
+ ### Added
86
+
87
+ - Added `SVG.Module` for building anonymous callable drawing modules with their contract installed before definition.
88
+ - Added responsive Mermaid diagrams for the Derender round trip and SVG validation lifecycle.
89
+
90
+ ### Changed
91
+
92
+ - Reorganized the documentation around getting started, usage, composition, documents, layout, output, and examples.
93
+ - Documented how the `igves` and `igsev` command names reflect their conversion directions.
94
+ - Normalized DSL catalog provider labels to lowercase and simplified polyfill-backed editor entries to `inkscape`.
95
+
96
+ ### Fixed
97
+
98
+ - Allowed A5-on-A4 pdfcpu conversion to replace an existing output file.
99
+ - Pre-rendered Mermaid diagrams as inline SVG instead of parsing them in site visitors' browsers.
100
+ - Removed duplicate provider and context labels from DSL catalog entries.
7
101
 
8
102
  ## 0.98.2 - 2026-07-25
9
103
 
@@ -212,7 +306,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
212
306
 
213
307
  ### Changed
214
308
 
215
- - Added the checker board example to the documentation showcase flow.
309
+ - Added the checkerboard example to the documentation showcase flow.
216
310
  - Consolidated Victor Book attribution for adapted showcase examples into a single documentation note.
217
311
 
218
312
  ### Fixed
data/README.md CHANGED
@@ -30,7 +30,7 @@ igsev drawing.svg
30
30
  ```
31
31
 
32
32
  Both commands read standard input when the file is omitted or `-`. For `sevgi`, implicit `Save`, `PDF`, and `PNG`
33
- destinations use `output` as the input name; use `--as NAME` to choose another basename:
33
+ destinations use `output` as the input name. Use `--as NAME` to choose another basename:
34
34
 
35
35
  ```sh
36
36
  sevgi --as badge < drawing.sevgi
@@ -16,13 +16,18 @@ module FileUtils
16
16
  # @param kwargs [Hash] keyword arguments exposed to the script as `ARGH`
17
17
  # @return [Sevgi::Executor::Result] immutable execution result
18
18
  # @raise [Sevgi::ArgumentError] when the script file cannot be found
19
+ # @raise [Sevgi::Executor::Error] when execution fails, so Rake stops dependent tasks
19
20
  # @see Sevgi.execute_file
20
21
  def sevgi(file, *args, **kwargs)
21
- Sevgi::Executor.__send__(:execute_file, Sevgi::F.existing!(file, [Sevgi::EXTENSION])) do
22
+ result = Sevgi::Executor.__send__(:execute_file, Sevgi::F.existing!(file, [Sevgi::EXTENSION])) do
22
23
  extend(Sevgi)
23
24
 
24
25
  const_set(:ARGA, args).freeze
25
26
  const_set(:ARGH, kwargs).freeze
26
27
  end
28
+
29
+ raise result.error if result.error?
30
+
31
+ result
27
32
  end
28
33
  end
@@ -83,7 +83,7 @@ module Sevgi
83
83
  return puts(::Sevgi::VERSION) if options.version
84
84
 
85
85
  file = operand(argv)
86
- handle(run(file, options), file, options)
86
+ handle(run(file, options), options)
87
87
 
88
88
  rescue Skill::Error => e
89
89
  abort(e.message)
@@ -93,17 +93,18 @@ module Sevgi
93
93
 
94
94
  private
95
95
 
96
- def die(error, _file)
97
- warn(error.message, "", *error.load_backtrace.map { " #{it}" })
96
+ def die(error)
97
+ message = error.message.empty? ? error.cause.class.to_s : error.message
98
+ warn(message, "", *error.load_backtrace.map { " #{it}" })
98
99
  exit(1)
99
100
  end
100
101
 
101
- def handle(result, file, options)
102
+ def handle(result, options)
102
103
  return unless result&.error?
103
104
 
104
105
  raise result.error if options.vomit || ENV[ENVVOMIT]
105
106
 
106
- die(result.error, file)
107
+ die(result.error)
107
108
  end
108
109
 
109
110
  def help
@@ -3,17 +3,25 @@
3
3
  module Sevgi
4
4
  class Executor
5
5
  # Raised when a source attempts to load another source already active in the same scope.
6
- # @see https://sevgi.roktas.dev/execution/ Execution guide
6
+ # @see https://sevgi.roktas.dev/usage/#execute Execute source guide
7
7
  class CycleError < ::Sevgi::Error
8
8
  end
9
9
 
10
+ # Raised when nested Sevgi loads exceed the executor's supported active-source depth.
11
+ #
12
+ # The limit is 128 active sources, including the entry source. It does not count Ruby stack frames
13
+ # or guarantee protection from stack exhaustion within scripts.
14
+ # @see https://sevgi.roktas.dev/usage/#execute Execute source guide
15
+ class LoadDepthError < ::Sevgi::Error
16
+ end
17
+
10
18
  # Wraps an exception raised while executing Sevgi script source. Its visited source snapshot records every source in
11
- # load order; it is not the active load stack at the instant of failure.
12
- # @see https://sevgi.roktas.dev/execution/ Execution guide
19
+ # load order. It is not the active load stack at the instant of failure.
20
+ # @see https://sevgi.roktas.dev/usage/#execute Execute source guide
13
21
  class Error < ::Sevgi::Error
14
22
  # Builds an executor error wrapper.
15
23
  # @param error [Exception] original exception
16
- # @param stack [Array<String>] source file keys visited in load order; the Array and its String entries are copied
24
+ # @param stack [Array<String>] source file keys visited in load order. The Array and its String entries are copied
17
25
  # and frozen
18
26
  # @return [void]
19
27
  def initialize(error, stack)
@@ -5,9 +5,9 @@ module Sevgi
5
5
  # Describes the outcome of one executor invocation.
6
6
  #
7
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
8
+ # library failure has an {Executor::Error} and can retain a value produced
9
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.
10
+ # order. It is never shared with the executor's internal mutable state.
11
11
  #
12
12
  # @example Inspect successful execution
13
13
  # result = Sevgi.execute("6 * 7")
@@ -22,7 +22,7 @@ module Sevgi
22
22
  #
23
23
  # @see Sevgi.execute
24
24
  # @see Sevgi.execute_file
25
- # @see https://sevgi.roktas.dev/execution/ Execution guide
25
+ # @see https://sevgi.roktas.dev/usage/#execute Execute source guide
26
26
  Result = Data.define(:value, :error, :stack) do
27
27
  # @!attribute [r] value
28
28
  # @return [Object, nil] last value produced, or nil when no value was produced
@@ -8,6 +8,9 @@ module Sevgi
8
8
  # fiber's executor stack. They are not shared between concurrent executions.
9
9
  # @api private
10
10
  class Scope
11
+ MAX_LOAD_DEPTH = 128
12
+ private_constant :MAX_LOAD_DEPTH
13
+
11
14
  # @!attribute [r] scope
12
15
  # @return [Module] isolated module where script source is evaluated
13
16
  # @!attribute [r] recent
@@ -115,8 +118,13 @@ module Sevgi
115
118
  end
116
119
 
117
120
  def enter(source)
118
- if @active.key?(source.identity)
119
- raise Executor::CycleError, "Recursive Sevgi load: #{source.file}"
121
+ raise Executor::CycleError, "Recursive Sevgi load: #{source.file}" if @active.key?(source.identity)
122
+
123
+ if @active.size >= MAX_LOAD_DEPTH
124
+ raise(
125
+ Executor::LoadDepthError,
126
+ "Sevgi load nesting too deep (maximum #{MAX_LOAD_DEPTH} active sources): #{source.file}"
127
+ )
120
128
  end
121
129
 
122
130
  @active[source.identity] = source
@@ -132,11 +140,10 @@ module Sevgi
132
140
  def execute(source, receiver, &boot)
133
141
  active = enter(source)
134
142
  @recent = run(source, receiver, &boot)
135
- # rubocop:disable Lint/RescueException
143
+ # rubocop:disable-next Lint/RescueException
136
144
  rescue Exception => e
137
145
  @error = e
138
146
  throw(:result, self)
139
- # rubocop:enable Lint/RescueException
140
147
  ensure
141
148
  leave(active)
142
149
  end
@@ -14,15 +14,15 @@ module Sevgi
14
14
  # useful load stack while keeping DSL methods out of the caller's global object
15
15
  # whenever possible. Active scope stacks are isolated per Ruby fiber, so
16
16
  # concurrent executions can perform nested `Load` calls without sharing scope
17
- # state. The process SIGINT handler is shared by Ruby, so executor runs guard it
18
- # with a reference-counted critical section and restore the previous handler
19
- # after the last active execution finishes.
17
+ # state. The host owns process signal handlers. Execution captures an Interrupt
18
+ # only when Ruby delivers it inside the executing scope; worker executions do
19
+ # not take ownership of the main thread's signal policy.
20
20
  #
21
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.
22
+ # {Executor::Error}, {Executor::CycleError}, and {Executor::LoadDepthError}. The custom receiver and boot lifecycle is
23
+ # internal plumbing for the top-level API and Rake integration.
24
24
  #
25
- # @see https://sevgi.roktas.dev/execution/ Execution guide
25
+ # @see https://sevgi.roktas.dev/usage/#execute Execute source guide
26
26
  class Executor
27
27
  private_class_method :new
28
28
  private_constant :Scope
@@ -33,30 +33,12 @@ module Sevgi
33
33
  SOURCE_LINE_MAX = (2 ** 31) - 1
34
34
  private_constant :SCOPE_KEY, :SOURCE_LINE_MAX, :Source
35
35
 
36
- # Owns mutable execution and process-signal state outside the public executor surface.
36
+ # Owns mutable execution state outside the public executor surface.
37
37
  # @api private
38
38
  class State
39
- def initialize
40
- @signal_count = 0
41
- @signal_mutex = Mutex.new
42
- @signal_previous = nil
43
- end
44
-
45
39
  def create(scope = nil) = Scope.new(scope).tap { scopes << it }
46
40
  def current = scopes.last
47
41
 
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
42
  def shutdown(scope = nil)
61
43
  return scopes.pop unless scope
62
44
  return scopes.pop if scopes.last.equal?(scope)
@@ -64,13 +46,6 @@ module Sevgi
64
46
  scopes.delete(scope)
65
47
  end
66
48
 
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
49
  private
75
50
 
76
51
  def scopes = Thread.current[SCOPE_KEY] ||= []
@@ -86,7 +61,7 @@ module Sevgi
86
61
  # @note Uses the active executor scope from the current fiber.
87
62
  # @api private
88
63
  def self.load(file, ...)
89
- PanicError.("box stack empty; create a box first") unless STATE.current
64
+ PanicError.("No active executor scope") unless STATE.current
90
65
 
91
66
  STATE.current.load(file, ...)
92
67
  end
@@ -98,18 +73,17 @@ module Sevgi
98
73
  # @param file [String, nil] source file name used for errors and backtraces
99
74
  # @param line [Integer, nil] starting source line used for errors and backtraces
100
75
  # @param require [String, nil] optional Ruby library to require before execution
101
- # @param receiver [Object, nil] receiver used verbatim while booting the DSL; nil selects the isolated execution
76
+ # @param receiver [Object, nil] receiver used verbatim while booting the DSL. Nil selects the isolated execution
102
77
  # module, while false and other executable objects remain explicit receivers
103
78
  # @yield optional boot block that installs DSL methods before evaluation
104
79
  # @yieldreturn [void]
105
80
  # @return [Sevgi::Executor::Result] immutable execution result
106
81
  # @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
82
+ # @note Script and required-library failures are captured in {Sevgi::Executor::Result#error}. Inspect
108
83
  # {Sevgi::Executor::Error#cause} for the original exception.
109
84
  # @note Empty source without `require:` is a strict no-op: no scope is created, the receiver and boot block are
110
85
  # unused, and the result stack is empty. Supplying `require:` uses the normal boot and evaluation lifecycle.
111
- # @note Reentrant and concurrent calls keep independent scope stacks per fiber. The temporary SIGINT handler remains
112
- # process-global while any execution is active.
86
+ # @note Reentrant and concurrent calls keep independent scope stacks per fiber and preserve the host's signal policy.
113
87
  # @api private
114
88
  def self.execute(string, file: nil, line: nil, require: nil, receiver: nil, &block)
115
89
  validate_source!(string, file, line)
@@ -122,18 +96,17 @@ module Sevgi
122
96
  # @param file [String] source file to read and execute
123
97
  # @param as [String, nil] logical source name used for evaluation and diagnostics
124
98
  # @param require [String, nil] optional Ruby library to require before execution
125
- # @param receiver [Object, nil] receiver used verbatim while booting the DSL; nil selects the isolated execution
99
+ # @param receiver [Object, nil] receiver used verbatim while booting the DSL. Nil selects the isolated execution
126
100
  # module, while false and other executable objects remain explicit receivers
127
101
  # @yield optional boot block that installs DSL methods before evaluation
128
102
  # @yieldreturn [void]
129
103
  # @return [Sevgi::Executor::Result] immutable execution result
130
104
  # @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
105
+ # @note File-read, script, and required-library failures are captured in {Sevgi::Executor::Result#error}. Inspect
132
106
  # {Sevgi::Executor::Result#stack} for nested loads.
133
107
  # @note An empty file without `require:` is a strict no-op: no scope is created, the receiver and boot block are
134
108
  # unused, and the result stack is empty. Supplying `require:` uses the normal boot and evaluation lifecycle.
135
- # @note Reentrant and concurrent calls keep independent scope stacks per fiber. The temporary SIGINT handler remains
136
- # process-global while any execution is active.
109
+ # @note Reentrant and concurrent calls keep independent scope stacks per fiber and preserve the host's signal policy.
137
110
  # @api private
138
111
  def self.execute_file(file, as: nil, require: nil, receiver: nil, &block)
139
112
  ArgumentError.("Executor file must be a String") unless file.is_a?(::String)
@@ -156,32 +129,29 @@ module Sevgi
156
129
  private
157
130
 
158
131
  def capture_error(source, error)
159
- acquired = STATE.trap
160
132
  scope = STATE.create
161
133
  scope.capture(source, error).result
162
134
  ensure
163
- STATE.restore if acquired
164
135
  STATE.shutdown(scope) if scope
165
136
  end
166
137
 
167
138
  def execute_source(source, require:, receiver:, &block)
168
- acquired = false
169
139
  return Result.new(value: nil, error: nil, stack: []) if source.string.empty? && require.nil?
170
140
 
171
- acquired = STATE.trap
172
141
  scope = STATE.create
173
142
  catch(:result) { run_source(scope, source, require, receiver, &block) }
174
143
  scope.result
175
144
 
176
145
  ensure
177
- STATE.restore if acquired
178
146
  STATE.shutdown(scope) if scope
179
147
  end
180
148
 
181
149
  def run_source(scope, source, library, receiver, &block)
182
150
  ::Kernel.require(library) if library
183
151
  scope.call(source, receiver, &block)
184
- rescue ::LoadError => e
152
+ # Required libraries use the same failure policy as executable script source.
153
+ # rubocop:disable-next Lint/RescueException
154
+ rescue ::Exception => e
185
155
  scope.capture(source, e)
186
156
  end
187
157
 
data/lib/sevgi/skill.rb CHANGED
@@ -14,7 +14,7 @@ module Sevgi
14
14
  def path
15
15
  spec = ::Gem::Specification.find_by_name("sevgi-appendix", "= #{::Sevgi::VERSION}")
16
16
  packaged = packaged_path(spec)
17
- # Package managers may replace a versioned gem path with their stable prefix.
17
+ # Package managers can replace a versioned gem path with their stable prefix.
18
18
  path = ::File.expand_path(ENV.fetch("SEVGI_SKILL", packaged))
19
19
 
20
20
  Error.("Sevgi skill is unavailable at #{path}.") unless ::File.file?(::File.join(path, "SKILL.md"))
data/lib/sevgi/svg.rb CHANGED
@@ -3,11 +3,12 @@
3
3
  module Sevgi
4
4
  # Public SVG facade installed by `require "sevgi"`.
5
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.
6
+ # Capitalized methods are SVG DSL operations and constructors: {SVG.Canvas}
7
+ # creates a canvas, {SVG.Document} defines a document profile, {SVG.Module}
8
+ # builds a callable drawing module, and {SVG.Paper} registers a paper size.
9
+ # Double-colon names are Ruby constants and types, such as {SVG::Canvas},
10
+ # {SVG::Document}, and {SVG::Module}. The global `SVG(...)` method builds a
11
+ # drawing. There is intentionally no stuttering `SVG.SVG(...)` form.
11
12
  #
12
13
  # Lowercase constructors remain on {Sevgi::Graphics} for focused component
13
14
  # use. Script execution belongs to {Sevgi.execute} and is not part of this
@@ -26,8 +27,9 @@ module Sevgi
26
27
  # @example Distinguish an operation from its result type
27
28
  # canvas = SVG.Canvas width: 24, height: 24, unit: :px
28
29
  # 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
30
+ # @see https://sevgi.roktas.dev/start/ Getting started
31
+ # @see https://sevgi.roktas.dev/usage/ Usage guide
32
+ # @see https://sevgi.roktas.dev/compose/ Composition guide
31
33
  module SVG
32
34
  # SVG attribute collection and normalization API.
33
35
  Attributes = Graphics::Attributes
@@ -114,6 +116,11 @@ module Sevgi
114
116
  # @see Sevgi::Toplevel#Grid
115
117
  def self.Grid(...) = Sevgi.Grid(...)
116
118
 
119
+ # Fits major and minor intervals into a span.
120
+ # @return [Sevgi::Sundries::Ruler] fitted ruler
121
+ # @see Sevgi::Toplevel#Ruler
122
+ def self.Ruler(...) = Sevgi.Ruler(...)
123
+
117
124
  # Loads nested `.sevgi` files through the active executor scope.
118
125
  # @return [Array<String>] requested file names
119
126
  # @see Sevgi::Toplevel#Load
@@ -124,6 +131,28 @@ module Sevgi
124
131
  # @see Sevgi::Toplevel#Mixin
125
132
  def self.Mixin(...) = Sevgi.Mixin(...)
126
133
 
134
+ # Builds an anonymous callable drawing module.
135
+ # The callable contract is installed before the optional definition runs, so `base` and drawing-method tracking
136
+ # are available throughout the block. The definition keeps its ordinary Ruby lexical constant scope.
137
+ # @yield [mod] optionally defines the callable module
138
+ # @yieldparam mod [Module] module being defined
139
+ # @yieldreturn [Object] ignored definition result
140
+ # @return [Module] new anonymous module extended with {SVG::Module}
141
+ # @example Build a callable drawing module
142
+ # label = SVG.Module do
143
+ # base { circle r: 8, fill: "seagreen" }
144
+ # def call(value) = text value
145
+ # end
146
+ #
147
+ # SVG(:minimal) { Call label, "OK" }
148
+ # @see SVG::Module
149
+ def self.Module(&definition)
150
+ mod = ::Module.new
151
+ mod.extend(Graphics::Module)
152
+ mod.module_eval(&definition) if definition
153
+ mod
154
+ end
155
+
127
156
  # Defines or validates a named paper profile.
128
157
  # @return [Symbol, String] original paper profile name
129
158
  # @see Sevgi::Toplevel#Paper
@@ -27,7 +27,8 @@ module Sevgi
27
27
  # @see Sevgi::Derender.decompile_file
28
28
  def DecompileFile(file, id: nil, omit: nil) = Derender.decompile_file(file, id:, omit:)
29
29
 
30
- # Converts inline SVG/XML content into Sevgi DSL Ruby source.
30
+ # Converts inline SVG/XML content into Sevgi DSL Ruby source. The returned String is ordinary Ruby source. Review
31
+ # and integrate it statically rather than using Ruby's raw dynamic evaluation methods.
31
32
  # @param content [String] SVG/XML source content
32
33
  # @param id [String, Symbol, nil] optional SVG id selecting a node inside the source
33
34
  # @param omit [String, Symbol, Array<String, Symbol>, nil] exact attribute name or names omitted from the selected
@@ -39,7 +40,8 @@ module Sevgi
39
40
  # @see Sevgi::Derender.derender
40
41
  def Derender(content, id: nil, omit: nil) = Derender.derender(content, id:, omit:)
41
42
 
42
- # Converts an SVG/XML file into Sevgi DSL Ruby source.
43
+ # Converts an SVG/XML file into Sevgi DSL Ruby source. The returned String is ordinary Ruby source. Review and
44
+ # integrate it statically rather than using Ruby's raw dynamic evaluation methods.
43
45
  # @param file [String] path to the source SVG/XML file
44
46
  # @param id [String, Symbol, nil] optional SVG id selecting a node inside the source
45
47
  # @param omit [String, Symbol, Array<String, Symbol>, nil] exact attribute name or names omitted from the selected
@@ -22,16 +22,17 @@ module Sevgi
22
22
  # @note Script and required-library failures are captured in {Sevgi::Executor::Result#error}.
23
23
  # @note The default isolated mode does not modify Ruby's top-level main object. `main: true` preserves the command-line
24
24
  # default by installing Sevgi through main before evaluating source in the managed script scope.
25
- # @note Empty source without `require:` is a strict no-op; the DSL boot block is unused.
26
- # @note Reentrant and concurrent calls keep independent executor scope stacks per fiber.
27
- # @see https://sevgi.roktas.dev/execution/ Execution guide
25
+ # @note Empty source without `require:` is a strict no-op. The DSL boot block is unused.
26
+ # @note Reentrant and concurrent calls keep independent executor scope stacks per fiber. The host retains its signal
27
+ # handlers. An Interrupt is captured only if it reaches this execution, not redirected from the main thread.
28
+ # @see https://sevgi.roktas.dev/usage/#execute Execute source guide
28
29
  def self.execute(string, file: nil, line: nil, require: nil, main: false)
29
30
  Executor.__send__(:execute, string, file:, line:, require:, receiver: execution_receiver(main), &BootBlock)
30
31
  end
31
32
 
32
33
  # Executes a Sevgi script file with the full top-level DSL installed.
33
34
  # @param file [String] source file to read and execute
34
- # @param as [String, nil] source basename used for evaluation, diagnostics, and caller-derived output defaults;
35
+ # @param as [String, nil] source basename used for evaluation, diagnostics, and caller-derived output defaults.
35
36
  # its extension is replaced with `.sevgi` and the input file's directory is retained
36
37
  # @param require [String, nil] optional Ruby library to require before execution
37
38
  # @param main [Boolean] whether to install the DSL through Ruby's top-level main object
@@ -40,9 +41,10 @@ module Sevgi
40
41
  # @note File-read, script, and required-library failures are captured in {Sevgi::Executor::Result#error}.
41
42
  # @note The default isolated mode does not modify Ruby's top-level main object. `main: true` preserves the command-line
42
43
  # default by installing Sevgi through main before evaluating source in the managed script scope.
43
- # @note An empty file without `require:` is a strict no-op; the DSL boot block is unused.
44
- # @note Reentrant and concurrent calls keep independent executor scope stacks per fiber.
45
- # @see https://sevgi.roktas.dev/execution/ Execution guide
44
+ # @note An empty file without `require:` is a strict no-op. The DSL boot block is unused.
45
+ # @note Reentrant and concurrent calls keep independent executor scope stacks per fiber. The host retains its signal
46
+ # handlers. An Interrupt is captured only if it reaches this execution, not redirected from the main thread.
47
+ # @see https://sevgi.roktas.dev/usage/#execute Execute source guide
46
48
  def self.execute_file(file, as: nil, require: nil, main: false)
47
49
  as = source_name(file, as) if as
48
50
  Executor.__send__(:execute_file, file, as:, require:, receiver: execution_receiver(main), &BootBlock)
@@ -71,7 +73,7 @@ module Sevgi
71
73
  # @raise [Sevgi::PanicError] when called without an active executor scope
72
74
  # @raise [Sevgi::Error] when a file cannot be located
73
75
  # @note `Load` resolves against the active executor scope in the current fiber.
74
- # @note Ordinary library code should use Ruby `require`; `Load` is available only during Sevgi script execution.
76
+ # @note Ordinary library code uses Ruby `require`. `Load` is available only during Sevgi script execution.
75
77
  # @see Sevgi.Load
76
78
  # @see Sevgi.execute_file
77
79
  def Load(*files)
@@ -64,7 +64,8 @@ module Sevgi
64
64
  end
65
65
 
66
66
  # Builds an SVG document through the full Sevgi top-level DSL.
67
- # @param document [Symbol, String, Class] document profile name or document class
67
+ # @param document [Symbol, String, Class, Sevgi::Graphics::Canvas, Sevgi::Graphics::Paper] document profile,
68
+ # document class, or canvas input that uses the default profile
68
69
  # @param canvas [Sevgi::Graphics::Canvas, Sevgi::Graphics::Paper, Symbol, String, Sevgi::Undefined, nil] optional
69
70
  # canvas or paper profile
70
71
  # @param attributes [Hash] root SVG attributes
@@ -4,12 +4,23 @@ require "sevgi/sundries"
4
4
 
5
5
  module Sevgi
6
6
  module Toplevel
7
+ # Fits major and minor intervals into a span without drawing SVG elements.
8
+ # @overload Ruler(brut:, unit:, multiple:, margins: [0.0])
9
+ # @param brut [Numeric] full available span
10
+ # @param unit [Numeric] minor interval length
11
+ # @param multiple [Integer] minor intervals per major interval
12
+ # @param margins [Array<Numeric>] one symmetric or two start/end minimum margins
13
+ # @return [Sevgi::Sundries::Ruler] fitted ruler
14
+ # @raise [Sevgi::ArgumentError] when the span, intervals, or margins are invalid
15
+ # @see Sevgi::Sundries::Ruler#initialize
16
+ def Ruler(...) = Sundries::Ruler.new(...)
17
+
7
18
  # Builds a drawable grid fitted inside a graphics canvas.
8
19
  #
9
20
  # The canvas margins are minimum clearances. Any span left after fitting
10
21
  # whole major intervals is shared equally between the opposite margins, so
11
22
  # their requested difference is preserved. The returned grid starts at
12
- # `(0, 0)`; {Sevgi::Sundries::Grid#canvas} exposes the fitted page margins.
23
+ # `(0, 0)`. {Sevgi::Sundries::Grid#canvas} exposes the fitted page margins.
13
24
  # @param canvas [Sevgi::Graphics::Canvas] canvas defining page size and margins
14
25
  # @param unit [Numeric] minor grid unit
15
26
  # @param multiple [Integer] number of minor units in each major interval
@@ -3,12 +3,11 @@
3
3
  module Sevgi
4
4
  # Shared implementation for the full Sevgi top-level DSL.
5
5
  #
6
- # This module is installed by `include Sevgi` or `extend Sevgi`; it should not
6
+ # This module is installed by `include Sevgi` or `extend Sevgi`. Consumers do not
7
7
  # normally be included directly.
8
8
  #
9
9
  # @see Sevgi
10
- # @see https://sevgi.roktas.dev/library-mode/ Library mode guide
11
- # @see https://sevgi.roktas.dev/script-mode/ Script mode guide
10
+ # @see https://sevgi.roktas.dev/usage/ Usage guide
12
11
  module Toplevel
13
12
  @promotions = {}
14
13
 
data/lib/sevgi/version.rb CHANGED
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Sevgi
4
4
  # Current version of the Sevgi top-level gem.
5
- VERSION = "0.98.2"
5
+ VERSION = "1.0.0"
6
6
  end
data/lib/sevgi.rb CHANGED
@@ -18,13 +18,14 @@ SVG = Sevgi::SVG
18
18
 
19
19
  # @overload SVG(document = :default, canvas = Undefined, **attributes, &block)
20
20
  # Builds an SVG document through the default top-level DSL entrypoint.
21
- # @param document [Symbol, String, Class] document profile name or document class
21
+ # @param document [Symbol, String, Class, Sevgi::Graphics::Canvas, Sevgi::Graphics::Paper] document profile,
22
+ # document class, or canvas input that uses the default profile
22
23
  # @param canvas [Sevgi::Graphics::Canvas, Sevgi::Graphics::Paper, Symbol, String, Sevgi::Undefined, nil] optional
23
24
  # canvas or paper profile
24
25
  # @param attributes [Hash] root SVG attributes
25
26
  # @yield the document block evaluated in the SVG document context
26
27
  # @yieldreturn [void]
27
- # @return [Sevgi::Graphics::Document::Proto] a rendered SVG document object
28
+ # @return [Sevgi::Graphics::Document::Proto] an SVG document object, ready for separate rendering
28
29
  # @raise [Sevgi::ArgumentError] when the document, paper, or canvas arguments are invalid
29
30
  # @example Build through the global library entrypoint
30
31
  # SVG(:minimal) { circle r: 4 }.Render
@@ -35,26 +36,25 @@ def SVG(...) = Sevgi.SVG(...)
35
36
  # Full top-level API for Sevgi library and script consumers.
36
37
  #
37
38
  # `require "sevgi"` installs the global document builder `SVG(...)` and the independent {Sevgi::SVG} facade. SVG-domain
38
- # operations use capitalized method names on that facade, such as `SVG.Canvas`, `SVG.Paper`, and `SVG.Derender`; types
39
- # and namespaces use Ruby's double-colon notation, such as `SVG::Canvas`. The same operations are available on `Sevgi`
40
- # for consumers that prefer the full toolkit namespace. Process-level operations such as {Sevgi.execute} remain on
41
- # `Sevgi` rather than the SVG facade.
39
+ # operations use capitalized method names on that facade, such as `SVG.Canvas`, `SVG.Paper`, and `SVG.Derender`. Types
40
+ # and namespaces use Ruby's double-colon notation, such as `SVG::Canvas`. Promoted toolkit operations are also
41
+ # available on `Sevgi` for consumers that prefer the full namespace. {SVG.Module} is a facade-only convenience
42
+ # constructor. Process-level operations such as {Sevgi.execute} remain on `Sevgi`, not the SVG facade.
42
43
  #
43
44
  # Including this module in a class or module installs the DSL methods plus convenience constants `F`, `Geometry`,
44
- # `Origin`, and `Export`; script execution provides the same promoted scope. Extending a module does the same. Extending
45
+ # `Origin`, and `Export`. Script execution provides the same promoted scope. Extending a module does the same. Extending
45
46
  # an ordinary object installs methods only and does not write promoted constants to `Object`. Focused component requires
46
47
  # expose their namespaced component APIs instead of this full top-level surface.
47
48
  #
48
49
  # `Load` is meaningful only during {Sevgi.execute}, {Sevgi.execute_file}, or command-line script execution. It resolves
49
- # nested `.sevgi` files through the active executor scope; it is not a general-purpose replacement for Ruby `require`.
50
+ # nested `.sevgi` files through the active executor scope. It is not a general-purpose replacement for Ruby `require`.
50
51
  #
51
52
  # @example Include the DSL in an object
52
53
  # class Drawing
53
54
  # include Sevgi
54
55
  # end
55
- # @see https://sevgi.roktas.dev/library-mode/ Library mode guide
56
- # @see https://sevgi.roktas.dev/script-mode/ Script mode guide
57
- # @see https://sevgi.roktas.dev/execution/ Embedded execution guide
56
+ # @see https://sevgi.roktas.dev/usage/ Usage guide
57
+ # @see https://sevgi.roktas.dev/compose/ Composition guide
58
58
  module Sevgi
59
59
  # @!method self.Canvas(...)
60
60
  # Builds a canvas from a paper profile or explicit dimensions.
@@ -88,6 +88,10 @@ module Sevgi
88
88
  # Fits a drawable grid to a graphics canvas.
89
89
  # @return [Sevgi::Sundries::Grid] fitted grid
90
90
  # @see Sevgi::Toplevel#Grid
91
+ # @!method self.Ruler(...)
92
+ # Fits major and minor intervals into a span.
93
+ # @return [Sevgi::Sundries::Ruler] fitted ruler
94
+ # @see Sevgi::Toplevel#Ruler
91
95
  # @!method self.Decompile(...)
92
96
  # Converts inline SVG/XML into an immutable Derender node.
93
97
  # @return [Sevgi::Derender::Node] selected node
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.98.2
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Recai Oktaş
@@ -15,98 +15,98 @@ dependencies:
15
15
  requirements:
16
16
  - - '='
17
17
  - !ruby/object:Gem::Version
18
- version: 0.98.2
18
+ version: 1.0.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.98.2
25
+ version: 1.0.0
26
26
  - !ruby/object:Gem::Dependency
27
27
  name: sevgi-derender
28
28
  requirement: !ruby/object:Gem::Requirement
29
29
  requirements:
30
30
  - - '='
31
31
  - !ruby/object:Gem::Version
32
- version: 0.98.2
32
+ version: 1.0.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.98.2
39
+ version: 1.0.0
40
40
  - !ruby/object:Gem::Dependency
41
41
  name: sevgi-function
42
42
  requirement: !ruby/object:Gem::Requirement
43
43
  requirements:
44
44
  - - '='
45
45
  - !ruby/object:Gem::Version
46
- version: 0.98.2
46
+ version: 1.0.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.98.2
53
+ version: 1.0.0
54
54
  - !ruby/object:Gem::Dependency
55
55
  name: sevgi-geometry
56
56
  requirement: !ruby/object:Gem::Requirement
57
57
  requirements:
58
58
  - - '='
59
59
  - !ruby/object:Gem::Version
60
- version: 0.98.2
60
+ version: 1.0.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.98.2
67
+ version: 1.0.0
68
68
  - !ruby/object:Gem::Dependency
69
69
  name: sevgi-graphics
70
70
  requirement: !ruby/object:Gem::Requirement
71
71
  requirements:
72
72
  - - '='
73
73
  - !ruby/object:Gem::Version
74
- version: 0.98.2
74
+ version: 1.0.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.98.2
81
+ version: 1.0.0
82
82
  - !ruby/object:Gem::Dependency
83
83
  name: sevgi-standard
84
84
  requirement: !ruby/object:Gem::Requirement
85
85
  requirements:
86
86
  - - '='
87
87
  - !ruby/object:Gem::Version
88
- version: 0.98.2
88
+ version: 1.0.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.98.2
95
+ version: 1.0.0
96
96
  - !ruby/object:Gem::Dependency
97
97
  name: sevgi-sundries
98
98
  requirement: !ruby/object:Gem::Requirement
99
99
  requirements:
100
100
  - - '='
101
101
  - !ruby/object:Gem::Version
102
- version: 0.98.2
102
+ version: 1.0.0
103
103
  type: :runtime
104
104
  prerelease: false
105
105
  version_requirements: !ruby/object:Gem::Requirement
106
106
  requirements:
107
107
  - - '='
108
108
  - !ruby/object:Gem::Version
109
- version: 0.98.2
109
+ version: 1.0.0
110
110
  description: Loads the complete Sevgi API and runs .sevgi scripts.
111
111
  email: roktas@gmail.com
112
112
  executables:
@@ -161,7 +161,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
161
161
  - !ruby/object:Gem::Version
162
162
  version: '0'
163
163
  requirements: []
164
- rubygems_version: 4.0.16
164
+ rubygems_version: 4.0.20
165
165
  specification_version: 4
166
166
  summary: Ruby toolkit for creating SVG.
167
167
  test_files: []