sevgi 0.95.0 → 0.98.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +125 -0
- data/README.md +27 -9
- data/bin/igsev +6 -0
- data/bin/sevgi +1 -1
- data/lib/sevgi/binaries/igsev.rb +161 -0
- data/lib/sevgi/binaries/rake.rb +3 -3
- data/lib/sevgi/binaries/sevgi.rb +69 -27
- data/lib/sevgi/executor/error.rb +16 -24
- data/lib/sevgi/executor/result.rb +54 -0
- data/lib/sevgi/executor/scope.rb +16 -5
- data/lib/sevgi/executor/source.rb +16 -7
- data/lib/sevgi/executor.rb +129 -99
- data/lib/sevgi/skill.rb +42 -0
- data/lib/sevgi/svg.rb +137 -0
- data/lib/sevgi/toplevel/derender.rb +92 -6
- data/lib/sevgi/toplevel/executor.rb +55 -13
- data/lib/sevgi/toplevel/graphics.rb +92 -6
- data/lib/sevgi/toplevel/sundries.rb +13 -5
- data/lib/sevgi/toplevel.rb +7 -7
- data/lib/sevgi/version.rb +1 -1
- data/lib/sevgi.rb +100 -13
- metadata +36 -17
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 637d6a0c7a281ad8f1800c9fcf08968266c0e6c6a73a0ad6206a7b41e5c57fc5
|
|
4
|
+
data.tar.gz: b01598c764d715a4b381a90a3a6d5de203695459147142575cbd326ce176a07b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e0c7c633c134254b8eedd6f9d8da45530f4485b0e0b3508ce1e4b76522eca83d3de456a53e956cdba3c38f48fd38e811db70d25b3fb8ed44f4c6c0261319b500
|
|
7
|
+
data.tar.gz: b9182c6a0a4d250d0f1947bcf3b8c9e7b3bcebe9268e581d59cb2db4230315eadcf511e6880bd388a74847f441bbf5b0fcf4e37889ddfbc8c7ee7094e124a971
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,131 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
|
|
|
5
5
|
|
|
6
6
|
## Unreleased
|
|
7
7
|
|
|
8
|
+
## 0.98.2 - 2026-07-25
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- Restored the `sevgi` CLI's main-object scope so bare toolkit calls remain available inside script helper classes.
|
|
13
|
+
|
|
14
|
+
## 0.98.1 - 2026-07-20
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
|
|
18
|
+
- Changed the `sevgi` CLI to use the executor's isolated scope by default.
|
|
19
|
+
|
|
20
|
+
### Removed
|
|
21
|
+
|
|
22
|
+
- Removed the redundant `-n` and `--nomain` CLI options; library consumers can still select `main: true` through
|
|
23
|
+
`Sevgi.execute` and `Sevgi.execute_file` when needed.
|
|
24
|
+
|
|
25
|
+
## 0.98.0 - 2026-07-20
|
|
26
|
+
|
|
27
|
+
### Added
|
|
28
|
+
|
|
29
|
+
- Added standard-input support to `sevgi`, `igves`, and `igsev`; `sevgi --as NAME` and
|
|
30
|
+
`Sevgi.execute_file(..., as: NAME)` supply the logical source basename used by implicit SVG, PDF, and PNG
|
|
31
|
+
destinations without changing physical load identity.
|
|
32
|
+
|
|
33
|
+
## 0.97.0 - 2026-07-19
|
|
34
|
+
|
|
35
|
+
### Added
|
|
36
|
+
|
|
37
|
+
- Added `sevgi-appendix`, containing the Sevgi agent skill and a RuboCop plugin for preserving readable `.sevgi` DSL
|
|
38
|
+
source alongside rubyfmt-formatted Ruby; the umbrella `sevgi` gem installs the matching Appendix version.
|
|
39
|
+
- Added `sevgi --skill` to report the validated path of the matching packaged agent skill for product-neutral setup.
|
|
40
|
+
- Added `igsev` to the umbrella gem for normalizing an SVG file through a complete SVG-to-Sevgi-to-SVG round trip.
|
|
41
|
+
|
|
42
|
+
### Changed
|
|
43
|
+
|
|
44
|
+
- Made Homebrew the recommended complete CLI installation while retaining Bundler and focused component gems for Ruby
|
|
45
|
+
application dependencies.
|
|
46
|
+
|
|
47
|
+
## 0.96.0 - 2026-07-18
|
|
48
|
+
|
|
49
|
+
### Added
|
|
50
|
+
|
|
51
|
+
- Added exact, subtree-wide attribute omission to Derender content/file conversion, evaluation, and inclusion APIs;
|
|
52
|
+
`igves --omit` exposes the same behavior from the command line.
|
|
53
|
+
- Added `Sevgi.SVG` as the explicit namespaced form of the top-level SVG document entrypoint.
|
|
54
|
+
- Added the opt-in recursive `SVG::Modules` contract for callable drawing namespaces.
|
|
55
|
+
- Added callable drawing-module `base` blocks, document and paper registry introspection, Canvas structural equality,
|
|
56
|
+
renderer options on `RenderChildren`, axis translation helpers, and a CC BY RDF helper.
|
|
57
|
+
- Formalized non-rendering `-` metadata and `+` attribute updates as `Attributes::META_PREFIX` and
|
|
58
|
+
`Attributes::UPDATE_SUFFIX`; repeated Array updates now concatenate into a stable flat value.
|
|
59
|
+
|
|
60
|
+
### Changed
|
|
61
|
+
|
|
62
|
+
- Breaking: replaced the `SVG = Sevgi::Graphics` alias with an independent SVG facade. Facade operations now use
|
|
63
|
+
capitalized names such as `SVG.Canvas` and `SVG.Document`; lowercase component helpers remain on
|
|
64
|
+
`Sevgi::Graphics`, and the stuttering `SVG.SVG` form was removed.
|
|
65
|
+
- Breaking: made low-level `Executor.execute` and `execute_file` runners private; consumers use
|
|
66
|
+
`Sevgi.execute` and `Sevgi.execute_file` while retaining the public result and error types.
|
|
67
|
+
- Breaking: removed the abstract `Document::Base` layer from the selectable document-profile registry; `Minimal` and
|
|
68
|
+
`Default` are now sibling concrete profiles, and custom profile hierarchies should derive from `Base`.
|
|
69
|
+
- Breaking: renamed Parallelogram segment and constraint arguments by geometric role: `base`, `side`, and
|
|
70
|
+
`constraint` replace axis-implying constructor names.
|
|
71
|
+
- Made degree-based sine and cosine exact at integer quarter turns, eliminating cardinal Geometry residue.
|
|
72
|
+
- Documented complete file-system failure families for file comparison, output, touch, and upward location helpers.
|
|
73
|
+
- Formalized generated Tile ids, positional CSS classes, template placement, and per-use callback signatures.
|
|
74
|
+
- Formalized String and Symbol ids across Derender selection APIs and documented file-read failures consistently.
|
|
75
|
+
- Breaking: made top-level `Decompile`, `Derender`, `Evaluate`, and `EvaluateChildren` consume inline SVG/XML;
|
|
76
|
+
file inputs now use the corresponding `File`-suffixed entrypoints.
|
|
77
|
+
- Breaking: replaced callable drawing module `call` block registration with argument-independent `base` blocks;
|
|
78
|
+
inherited bases run parent-first, and modules with one drawing method conventionally name it `call`.
|
|
79
|
+
- Breaking: made bracket and call notation the canonical Geometry constructors. Among Data value types, bracket notation
|
|
80
|
+
is public only for Point, Segment, LengthAngle, Margin, and Paper; use `.new` for Location and result carriers, and
|
|
81
|
+
`Stay` for traversal stop tokens.
|
|
82
|
+
- Breaking: aligned Point and Segment comparison with Ruby `Comparable`; malformed or unrelated comparison operands now
|
|
83
|
+
return nil from `<=>`.
|
|
84
|
+
- Breaking: executor entrypoints now return immutable `Executor::Result` values and expose only `execute` and
|
|
85
|
+
`execute_file`; inspect `result.error`, `result.stack`, and `result.value` instead of executor scopes or lifecycle state.
|
|
86
|
+
- Breaking: wrapper attributes and callable arguments use distinct channels, `With` rejects parentless receivers, and
|
|
87
|
+
unsupported direct constructors are private; use the documented factories for Content, concrete Geometry lined
|
|
88
|
+
elements, and Grid query values.
|
|
89
|
+
- Breaking: Canvas uses `.new` for explicit fields, `.from_paper(paper, **overrides)` for paper conversion, and
|
|
90
|
+
`.call`/`Graphics.canvas` for dispatch; the misleading keyword-only `.from_paper(width:, height:)` form was removed.
|
|
91
|
+
- Breaking: `Save`, `Write`, `PNG`, `PDF`, and Sundries native export normalize successful destinations to expanded
|
|
92
|
+
String paths and create missing parent directories; change-aware SVG writes still return nil when unchanged.
|
|
93
|
+
- Breaking: renamed `Derender.evaluate_file_children` to `evaluate_children_file`; decompiled nodes now own immutable
|
|
94
|
+
public state while parser, strategy, and construction plumbing remain private.
|
|
95
|
+
- Expanded runtime/YARD parity checks and exact contracts for inherited, extended, generated, and dynamic API surfaces,
|
|
96
|
+
forwarded options, error channels, path ownership, whitespace, namespaces, nil behavior, and return values.
|
|
97
|
+
|
|
98
|
+
### Fixed
|
|
99
|
+
|
|
100
|
+
- Rejected directories from default file discovery while preserving custom locator matchers.
|
|
101
|
+
- Kept `Ancestral` context in non-rendering `-context` metadata instead of leaking it into SVG attributes.
|
|
102
|
+
- Preserved source Canvas units and names when deriving fitted Grid canvases.
|
|
103
|
+
- Prevented stale documentation assets from mixing old layouts with new HTML, and moved sidebar navigation into the
|
|
104
|
+
tablet menu at 1024px and below.
|
|
105
|
+
- Normalized numeric slots owned by path, shape, transform, tile, and Inkscape page helpers to finite SVG number spelling;
|
|
106
|
+
arbitrary user-supplied attributes remain untouched.
|
|
107
|
+
- Made named callable wrapper and symbol ids stable while omitting defaults for anonymous modules, and indexed every
|
|
108
|
+
rendered id value, including false and numeric values, through its serialized string.
|
|
109
|
+
- Preserved signed Geometry constraint directions, rejected invalid sweep/export channels through Sevgi error families,
|
|
110
|
+
and validated raw output paths before expansion or rendering.
|
|
111
|
+
- Made callable module configuration copy-owned and freeze-aware, document subclasses inherit their nearest profile, and
|
|
112
|
+
false executor boot receivers remain explicit rather than defaulting to an internal scope.
|
|
113
|
+
- Made element trees, attributes, identifiers, locator results, document profiles, Derender nodes, shell results, and
|
|
114
|
+
executor results retain owned immutable snapshots where their public contracts promise value semantics.
|
|
115
|
+
- Corrected Shell combined-output separators, nil export density errors, document render-option routing, executor source
|
|
116
|
+
snapshots, selected-node namespace/whitespace documentation, and Standard character-data validation.
|
|
117
|
+
|
|
118
|
+
### Removed
|
|
119
|
+
|
|
120
|
+
- Removed public access to pluralization tables and the internal SVG save extension; pluralization rules are now deeply
|
|
121
|
+
immutable.
|
|
122
|
+
- Removed public documentation and constant access for command-line implementation modules; the `sevgi` and `igves`
|
|
123
|
+
executables remain unchanged.
|
|
124
|
+
- Removed eager loading and public documentation of private Showcase build/test support; explicit support entrypoints
|
|
125
|
+
now keep the harness under the Showcase namespace.
|
|
126
|
+
- Removed public access to document profile name normalizers; registry operations retain them as private plumbing.
|
|
127
|
+
- Removed the redundant `Margin.margin` constructor; use canonical bracket notation.
|
|
128
|
+
- Removed public executor orchestration, obsolete callable-module hooks, the old public attribute syntax constants, and
|
|
129
|
+
direct construction of abstract or internally wired Content, Element, and Grid query types.
|
|
130
|
+
- Removed accidental public access to abstract Lined factories, internal element-name/export maps, result/location
|
|
131
|
+
bracket constructors, and direct Stop construction.
|
|
132
|
+
|
|
8
133
|
## 0.95.0 - 2026-07-11
|
|
9
134
|
|
|
10
135
|
### Security
|
data/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Sevgi
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The `sevgi` gem provides the top-level API and the `.sevgi` script runner.
|
|
4
4
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
@@ -17,20 +17,38 @@ require "sevgi"
|
|
|
17
17
|
## Example
|
|
18
18
|
|
|
19
19
|
```ruby
|
|
20
|
-
SVG
|
|
21
|
-
rect
|
|
20
|
+
SVG :minimal do
|
|
21
|
+
rect width: 3, height: 5
|
|
22
22
|
end.call
|
|
23
23
|
```
|
|
24
24
|
|
|
25
|
-
##
|
|
25
|
+
## Executables
|
|
26
26
|
|
|
27
27
|
```sh
|
|
28
28
|
sevgi drawing.sevgi
|
|
29
|
+
igsev drawing.svg
|
|
29
30
|
```
|
|
30
31
|
|
|
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:
|
|
34
|
+
|
|
35
|
+
```sh
|
|
36
|
+
sevgi --as badge < drawing.sevgi
|
|
37
|
+
igsev < drawing.svg > normalized.svg
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
`NAME` cannot include a directory. An explicit output path or `default:` argument in the script remains authoritative.
|
|
41
|
+
With a file operand, `--as` keeps the file's source directory so relative `Load` calls continue to resolve there.
|
|
42
|
+
|
|
43
|
+
`sevgi --skill` prints the validated path of the matching packaged agent skill. See the
|
|
44
|
+
[Appendix documentation](https://github.com/roktas/sevgi/tree/main/appendix) for installation guidance.
|
|
45
|
+
|
|
46
|
+
`igsev` converts an SVG file to Sevgi source, evaluates it with the complete DSL, and prints the resulting normalized
|
|
47
|
+
SVG. Use `igves` from `sevgi-derender` when the generated Sevgi source itself is the desired output.
|
|
48
|
+
|
|
31
49
|
## Ruby compatibility
|
|
32
50
|
|
|
33
|
-
Requires Ruby 3.4.0 or newer. CI verifies Ruby 3.4
|
|
51
|
+
Requires Ruby 3.4.0 or newer. CI verifies the current Ruby 3.4 release and the development Ruby from `.ruby-version`.
|
|
34
52
|
|
|
35
53
|
## Native prerequisites
|
|
36
54
|
|
|
@@ -55,7 +73,7 @@ gem install cairo rsvg2 hexapdf
|
|
|
55
73
|
|
|
56
74
|
## Links
|
|
57
75
|
|
|
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
|
|
76
|
+
- Documentation: <https://sevgi.roktas.dev>
|
|
77
|
+
- API documentation: <https://www.rubydoc.info/gems/sevgi>
|
|
78
|
+
- Source: <https://github.com/roktas/sevgi/tree/main/toplevel>
|
|
79
|
+
- Changelog: <https://github.com/roktas/sevgi/blob/main/CHANGELOG.md>
|
data/bin/igsev
ADDED
data/bin/sevgi
CHANGED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "sevgi"
|
|
4
|
+
|
|
5
|
+
module Sevgi
|
|
6
|
+
# Command-line entrypoint implementations shipped with Sevgi.
|
|
7
|
+
# @api private
|
|
8
|
+
module Binaries
|
|
9
|
+
# Implements the `igsev` executable that round-trips SVG files through Sevgi.
|
|
10
|
+
# @api private
|
|
11
|
+
module Igsev
|
|
12
|
+
extend self
|
|
13
|
+
|
|
14
|
+
# Executable name used in help output.
|
|
15
|
+
PROGNAME = "igsev"
|
|
16
|
+
|
|
17
|
+
# Error raised for invalid command-line usage.
|
|
18
|
+
Error = Class.new(::Sevgi::Error)
|
|
19
|
+
|
|
20
|
+
FLAGS = {
|
|
21
|
+
"--exception" => :vomit,
|
|
22
|
+
"--help" => :help,
|
|
23
|
+
"--version" => :version,
|
|
24
|
+
"-h" => :help,
|
|
25
|
+
"-v" => :version,
|
|
26
|
+
"-x" => :vomit
|
|
27
|
+
}.freeze
|
|
28
|
+
private_constant :FLAGS
|
|
29
|
+
|
|
30
|
+
# Parsed command-line options for the `igsev` executable.
|
|
31
|
+
# @api private
|
|
32
|
+
Options = Struct.new(:require, :vomit, :help, :version, :omit) do
|
|
33
|
+
# Parses command-line options and removes them from the argv array.
|
|
34
|
+
# @param argv [Array<String>] mutable command-line argument array
|
|
35
|
+
# @return [Sevgi::Binaries::Igsev::Options] parsed options
|
|
36
|
+
# @raise [Sevgi::Binaries::Igsev::Error] when an option is not recognized or a required value is missing
|
|
37
|
+
def self.parse(argv)
|
|
38
|
+
new.tap do |options|
|
|
39
|
+
until argv.empty? || argv.first == "-" || !argv.first.start_with?("-")
|
|
40
|
+
if argv.first == "--"
|
|
41
|
+
argv.shift
|
|
42
|
+
break
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
option(argv, options)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
class << self
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
def option(argv, options)
|
|
54
|
+
arg = argv.shift
|
|
55
|
+
return options[FLAGS[arg]] = true if FLAGS.key?(arg)
|
|
56
|
+
if ["-r", "--require"].include?(arg)
|
|
57
|
+
return options.require = argv.shift || Error.("Option requires a library: #{arg}")
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
return (options.omit ||= []) << (argv.shift || Error.("No attribute given for --omit")) if arg == "--omit"
|
|
61
|
+
|
|
62
|
+
Error.("Not a valid option: #{arg}")
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
private_constant :Options
|
|
68
|
+
|
|
69
|
+
# Runs the `igsev` command-line interface.
|
|
70
|
+
# @param argv [Array<String>, String, nil] command-line arguments
|
|
71
|
+
# @return [nil]
|
|
72
|
+
# @raise [StandardError] when `--exception` or `SEVGI_VOMIT` requests raw errors
|
|
73
|
+
# @raise [SystemExit] when argv does not match `[options...] [--] [file|-]` or conversion aborts
|
|
74
|
+
def call(argv)
|
|
75
|
+
dispatch(Array(argv))
|
|
76
|
+
rescue Binaries::Igsev::Error => e
|
|
77
|
+
abort("#{e.message}\n\n#{help}")
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
private
|
|
81
|
+
|
|
82
|
+
def backtrace(error)
|
|
83
|
+
return error.load_backtrace if error.respond_to?(:load_backtrace)
|
|
84
|
+
|
|
85
|
+
error.backtrace || []
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def die(error)
|
|
89
|
+
warn(error.message)
|
|
90
|
+
warn("")
|
|
91
|
+
backtrace(error).each { warn(" #{it}") }
|
|
92
|
+
|
|
93
|
+
exit(1)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def dispatch(argv)
|
|
97
|
+
options = Options.parse(argv)
|
|
98
|
+
return puts(help) if options.help
|
|
99
|
+
return puts(::Sevgi::VERSION) if options.version
|
|
100
|
+
|
|
101
|
+
emit(operand(argv), options)
|
|
102
|
+
rescue Binaries::Igsev::Error
|
|
103
|
+
raise
|
|
104
|
+
rescue ::StandardError => e
|
|
105
|
+
raise if raw_error?(options)
|
|
106
|
+
|
|
107
|
+
die(e)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def emit(file, options)
|
|
111
|
+
result = run(file, options)
|
|
112
|
+
unless result.success?
|
|
113
|
+
raise result.error if raw_error?(options)
|
|
114
|
+
|
|
115
|
+
die(result.error)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
result.value.Out()
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def help
|
|
122
|
+
<<~HELP
|
|
123
|
+
Usage: #{PROGNAME} [options...] [--] [SVG file|-]
|
|
124
|
+
|
|
125
|
+
See documentation for detailed help.
|
|
126
|
+
|
|
127
|
+
Options:
|
|
128
|
+
|
|
129
|
+
--omit ATTRIBUTE Omit an attribute (repeatable)
|
|
130
|
+
-r, --require LIB Require Ruby LIB while evaluating generated source
|
|
131
|
+
-x, --exception Raise exception instead of abort
|
|
132
|
+
-- Stop option parsing
|
|
133
|
+
|
|
134
|
+
-h, --help Show this help
|
|
135
|
+
-v, --version Display version
|
|
136
|
+
HELP
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def operand(argv)
|
|
140
|
+
file = argv.shift
|
|
141
|
+
Error.("Unexpected argument: #{argv.first}") unless argv.empty?
|
|
142
|
+
|
|
143
|
+
file unless file == "-"
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def raw_error?(options) = options&.vomit || ENV.fetch(ENVVOMIT, nil)
|
|
147
|
+
|
|
148
|
+
def run(file, options)
|
|
149
|
+
source = if file
|
|
150
|
+
::Sevgi::Derender.derender_file(file, omit: options.omit)
|
|
151
|
+
else
|
|
152
|
+
::Sevgi::Derender.derender($stdin.read, omit: options.omit)
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
::Sevgi.execute(source, require: options.require)
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
private_constant :Binaries
|
|
161
|
+
end
|
data/lib/sevgi/binaries/rake.rb
CHANGED
|
@@ -14,11 +14,11 @@ module FileUtils
|
|
|
14
14
|
# @param file [String] Sevgi script file, with or without `.sevgi` extension
|
|
15
15
|
# @param args [Array] positional arguments exposed to the script as `ARGA`
|
|
16
16
|
# @param kwargs [Hash] keyword arguments exposed to the script as `ARGH`
|
|
17
|
-
# @return [Sevgi::Executor::
|
|
17
|
+
# @return [Sevgi::Executor::Result] immutable execution result
|
|
18
18
|
# @raise [Sevgi::ArgumentError] when the script file cannot be found
|
|
19
|
-
# @see Sevgi
|
|
19
|
+
# @see Sevgi.execute_file
|
|
20
20
|
def sevgi(file, *args, **kwargs)
|
|
21
|
-
Sevgi::Executor.execute_file
|
|
21
|
+
Sevgi::Executor.__send__(:execute_file, Sevgi::F.existing!(file, [Sevgi::EXTENSION])) do
|
|
22
22
|
extend(Sevgi)
|
|
23
23
|
|
|
24
24
|
const_set(:ARGA, args).freeze
|
data/lib/sevgi/binaries/sevgi.rb
CHANGED
|
@@ -1,30 +1,53 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "sevgi"
|
|
4
|
+
require "sevgi/skill"
|
|
4
5
|
|
|
5
6
|
module Sevgi
|
|
6
7
|
# Command-line entrypoint implementations shipped with Sevgi.
|
|
8
|
+
# @api private
|
|
7
9
|
module Binaries
|
|
8
10
|
# Implements the `sevgi` executable.
|
|
11
|
+
# @api private
|
|
9
12
|
module Sevgi
|
|
10
13
|
extend self
|
|
11
14
|
|
|
12
15
|
# Executable name used in help output.
|
|
13
16
|
PROGNAME = "sevgi"
|
|
14
17
|
|
|
18
|
+
# Logical source name used for standard input.
|
|
19
|
+
STDIN_NAME = "output.sevgi"
|
|
20
|
+
|
|
15
21
|
# Error raised for invalid command-line usage.
|
|
16
22
|
Error = Class.new(::Sevgi::Error)
|
|
17
23
|
|
|
24
|
+
FLAGS = {
|
|
25
|
+
"-x" => :vomit,
|
|
26
|
+
"--exception" => :vomit,
|
|
27
|
+
"-h" => :help,
|
|
28
|
+
"--help" => :help,
|
|
29
|
+
"--skill" => :skill,
|
|
30
|
+
"-v" => :version,
|
|
31
|
+
"--version" => :version
|
|
32
|
+
}.freeze
|
|
33
|
+
|
|
18
34
|
# Parsed command-line options for the `sevgi` executable.
|
|
19
35
|
# @api private
|
|
20
|
-
Options = Struct.new(:require, :
|
|
36
|
+
Options = Struct.new(:require, :vomit, :help, :skill, :version, :as) do
|
|
21
37
|
# Parses command-line options and removes them from the argv array.
|
|
22
38
|
# @param argv [Array<String>] mutable command-line argument array
|
|
23
39
|
# @return [Sevgi::Binaries::Sevgi::Options] parsed options
|
|
24
|
-
# @raise [Sevgi::Binaries::Sevgi::Error] when an option is not recognized
|
|
40
|
+
# @raise [Sevgi::Binaries::Sevgi::Error] when an option is not recognized or a required value is missing
|
|
25
41
|
def self.parse(argv)
|
|
26
42
|
new.tap do |options|
|
|
27
|
-
argv.
|
|
43
|
+
until argv.empty? || argv.first == "-" || !argv.first.start_with?("-")
|
|
44
|
+
if argv.first == "--"
|
|
45
|
+
argv.shift
|
|
46
|
+
break
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
option(argv, options)
|
|
50
|
+
end
|
|
28
51
|
end
|
|
29
52
|
end
|
|
30
53
|
|
|
@@ -32,18 +55,14 @@ module Sevgi
|
|
|
32
55
|
private
|
|
33
56
|
|
|
34
57
|
def option(argv, options)
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
options
|
|
38
|
-
|
|
39
|
-
options.
|
|
40
|
-
|
|
41
|
-
options.
|
|
42
|
-
|
|
43
|
-
when "-h", "--help"
|
|
44
|
-
options.help = true
|
|
45
|
-
when "-v", "--version"
|
|
46
|
-
options.version = true
|
|
58
|
+
arg = argv.shift
|
|
59
|
+
if (flag = FLAGS[arg])
|
|
60
|
+
options[flag] = true
|
|
61
|
+
elsif ["-r", "--require"].include?(arg)
|
|
62
|
+
options.require = argv.shift || Error.("Option requires a library: #{arg}")
|
|
63
|
+
elsif arg == "--as"
|
|
64
|
+
options.as = argv.shift
|
|
65
|
+
Error.("Option requires a name: --as") if options.as.nil? || options.as.empty?
|
|
47
66
|
else
|
|
48
67
|
Error.("Not a valid option: #{arg}")
|
|
49
68
|
end
|
|
@@ -51,31 +70,31 @@ module Sevgi
|
|
|
51
70
|
end
|
|
52
71
|
end
|
|
53
72
|
|
|
54
|
-
private_constant :Options
|
|
73
|
+
private_constant :FLAGS, :Options, :STDIN_NAME
|
|
55
74
|
|
|
56
75
|
# Runs the `sevgi` command-line interface.
|
|
57
76
|
# @param argv [Array<String>, String, nil] command-line arguments
|
|
58
77
|
# @return [nil]
|
|
59
78
|
# @raise [Sevgi::Executor::Error] when `--exception` or `SEVGI_VOMIT` requests raw executor errors
|
|
60
|
-
# @raise [SystemExit] when
|
|
79
|
+
# @raise [SystemExit] when argv does not match `[options...] [--] [file|-]` or script execution aborts
|
|
61
80
|
def call(argv)
|
|
62
81
|
return puts(help) if (options = Options.parse(argv = Array(argv))).help
|
|
82
|
+
return puts(Skill.path) if options.skill
|
|
63
83
|
return puts(::Sevgi::VERSION) if options.version
|
|
64
84
|
|
|
65
|
-
file = argv
|
|
85
|
+
file = operand(argv)
|
|
66
86
|
handle(run(file, options), file, options)
|
|
67
87
|
|
|
68
|
-
rescue
|
|
88
|
+
rescue Skill::Error => e
|
|
69
89
|
abort(e.message)
|
|
90
|
+
rescue Binaries::Sevgi::Error => e
|
|
91
|
+
abort("#{e.message}\n\n#{help}")
|
|
70
92
|
end
|
|
71
93
|
|
|
72
94
|
private
|
|
73
95
|
|
|
74
96
|
def die(error, _file)
|
|
75
|
-
warn(error.message)
|
|
76
|
-
warn("")
|
|
77
|
-
error.backtrace!.each { warn(" #{it}") }
|
|
78
|
-
|
|
97
|
+
warn(error.message, "", *error.load_backtrace.map { " #{it}" })
|
|
79
98
|
exit(1)
|
|
80
99
|
end
|
|
81
100
|
|
|
@@ -89,26 +108,49 @@ module Sevgi
|
|
|
89
108
|
|
|
90
109
|
def help
|
|
91
110
|
<<~HELP
|
|
92
|
-
Usage: #{PROGNAME} [options...]
|
|
111
|
+
Usage: #{PROGNAME} [options...] [--] [Sevgi file|-]
|
|
93
112
|
|
|
94
113
|
See documentation for detailed help.
|
|
95
114
|
|
|
96
115
|
Options:
|
|
97
116
|
|
|
98
|
-
|
|
117
|
+
--as NAME Evaluate input as NAME for implicit output names
|
|
99
118
|
-r, --require LIB Require Ruby LIB
|
|
119
|
+
--skill Display the packaged agent skill path
|
|
100
120
|
-x, --exception Raise exception instead of abort
|
|
121
|
+
-- Stop option parsing
|
|
101
122
|
|
|
102
123
|
-h, --help Show this help
|
|
103
124
|
-v, --version Display version
|
|
104
125
|
HELP
|
|
105
126
|
end
|
|
106
127
|
|
|
128
|
+
def operand(argv)
|
|
129
|
+
file = argv.shift
|
|
130
|
+
Error.("Unexpected argument: #{argv.first}") unless argv.empty?
|
|
131
|
+
|
|
132
|
+
file unless file == "-"
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def execute_file(file, options)
|
|
136
|
+
name = source_name(options.as) if options.as
|
|
137
|
+
::Sevgi.execute_file(file, as: name, require: options.require, main: true)
|
|
138
|
+
end
|
|
139
|
+
|
|
107
140
|
def run(file, options)
|
|
108
|
-
|
|
141
|
+
return execute_file(file, options) if file
|
|
109
142
|
|
|
110
|
-
::Sevgi.
|
|
143
|
+
::Sevgi.execute($stdin.read, file: source_name(options.as), require: options.require, main: true)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def source_name(name)
|
|
147
|
+
return STDIN_NAME unless name
|
|
148
|
+
|
|
149
|
+
Error.("Option requires a name, not a path: --as") unless ::File.basename(name) == name
|
|
150
|
+
F.subext(".sevgi", name)
|
|
111
151
|
end
|
|
112
152
|
end
|
|
113
153
|
end
|
|
154
|
+
|
|
155
|
+
private_constant :Binaries
|
|
114
156
|
end
|
data/lib/sevgi/executor/error.rb
CHANGED
|
@@ -3,48 +3,40 @@
|
|
|
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
7
|
class CycleError < ::Sevgi::Error
|
|
7
8
|
end
|
|
8
9
|
|
|
9
|
-
# Wraps an exception raised while executing Sevgi script source.
|
|
10
|
+
# 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
|
|
10
13
|
class Error < ::Sevgi::Error
|
|
11
|
-
# Returns the original script exception.
|
|
12
|
-
# @return [Exception]
|
|
13
|
-
attr_reader :error
|
|
14
|
-
|
|
15
|
-
# Returns the executor scope active when the error was captured.
|
|
16
|
-
# @return [Sevgi::Executor::Scope]
|
|
17
|
-
attr_reader :scope
|
|
18
|
-
|
|
19
14
|
# Builds an executor error wrapper.
|
|
20
15
|
# @param error [Exception] original exception
|
|
21
|
-
# @param
|
|
16
|
+
# @param stack [Array<String>] source file keys visited in load order; the Array and its String entries are copied
|
|
17
|
+
# and frozen
|
|
22
18
|
# @return [void]
|
|
23
|
-
def initialize(error,
|
|
24
|
-
@
|
|
25
|
-
@
|
|
19
|
+
def initialize(error, stack)
|
|
20
|
+
@cause = error
|
|
21
|
+
@stack = stack.map { it.dup.freeze }.freeze
|
|
26
22
|
|
|
27
23
|
super(error.message)
|
|
28
24
|
end
|
|
29
25
|
|
|
30
|
-
# Returns backtrace entries that belong to the Sevgi
|
|
31
|
-
# @return [Array<String>] filtered backtrace lines relative to the current directory
|
|
32
|
-
|
|
33
|
-
|
|
26
|
+
# Returns backtrace entries that belong to the visited Sevgi source set.
|
|
27
|
+
# @return [Array<String>] filtered backtrace lines relative to the current directory, or an empty Array when the
|
|
28
|
+
# original exception has no backtrace
|
|
29
|
+
def load_backtrace
|
|
30
|
+
sources = @stack.map { ::File.expand_path(it) }
|
|
34
31
|
|
|
35
|
-
|
|
36
|
-
.backtrace
|
|
32
|
+
Array(cause.backtrace)
|
|
37
33
|
.select { sources.include?(::File.expand_path(it.split(":", 2).first)) }
|
|
38
34
|
.map { |line| line.delete_prefix("#{::Dir.pwd}/") }
|
|
39
35
|
end
|
|
40
36
|
|
|
41
37
|
# Returns the original exception as the wrapped cause.
|
|
42
38
|
# @return [Exception]
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
# Returns the script load stack active at failure time.
|
|
46
|
-
# @return [Array<String>] script file names in load order
|
|
47
|
-
def stack = scope.stack
|
|
39
|
+
attr_reader :cause
|
|
48
40
|
end
|
|
49
41
|
end
|
|
50
42
|
end
|