gloo 6.3.1 → 6.4.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 +4 -4
- data/CLAUDE.md +13 -0
- data/docs/language_objects.md +38 -0
- data/docs/language_scripting.md +41 -0
- data/docs/verbs.md +27 -2
- data/lib/VERSION +1 -1
- data/lib/VERSION_NOTES +6 -0
- data/lib/gloo/core/obj.rb +40 -1
- data/lib/gloo/core/parser.rb +30 -1
- data/lib/gloo/core/tokens.rb +44 -18
- data/lib/gloo/expr/l_string.rb +8 -4
- data/lib/gloo/objs/basic/script.rb +28 -2
- data/lib/gloo/objs/web/json.rb +4 -1
- data/lib/gloo/persist/comment_buffer.rb +58 -0
- data/lib/gloo/persist/declaration_ledger.rb +61 -0
- data/lib/gloo/persist/disc_mech.rb +15 -2
- data/lib/gloo/persist/file_loader.rb +201 -102
- data/lib/gloo/persist/file_saver.rb +251 -6
- data/lib/gloo/persist/file_storage.rb +10 -4
- data/lib/gloo/persist/indent_stack.rb +90 -0
- data/lib/gloo/persist/line_splitter.rb +18 -3
- data/lib/gloo/persist/persist_man.rb +159 -24
- data/lib/gloo/persist/script_body_collector.rb +102 -0
- data/lib/gloo/persist/shorthand_expander.rb +51 -0
- data/lib/gloo/persist/source/blank_node.rb +27 -0
- data/lib/gloo/persist/source/comment_node.rb +27 -0
- data/lib/gloo/persist/source/directive_node.rb +26 -0
- data/lib/gloo/persist/source/obj_node.rb +50 -0
- data/lib/gloo/persist/source/source_doc.rb +41 -0
- data/lib/gloo/verbs/load.rb +5 -0
- data/lib/gloo/verbs/reload.rb +3 -0
- data/lib/gloo/verbs/save.rb +57 -10
- data/test.gloo/lang/shorthand.test.gloo +29 -0
- data/test.gloo/objs/json.test.gloo +9 -0
- data/test.gloo/objs/script.test.gloo +1 -1
- data/test.gloo/string/str.test.gloo +15 -0
- data/test.gloo/verbs/reload.test.gloo +57 -0
- data/test.gloo/verbs/save.test.gloo +115 -0
- metadata +14 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 345f1e1145a1c3e32a2d94f39eda0246c0b36c8a6aa36857cb8a459042b97b0a
|
|
4
|
+
data.tar.gz: a8cc80d6cfe82bd0f938cf9993e3948c7ae0ca1434fc430bb79a357239f8ac70
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1c6f947e4282de5e585543771f30d42ec3ab85197237e805d1c560c06fc286ecd5053b5627b86f7f017368dcf3ae957c1784f74b498e885feacc247a43b17ea1
|
|
7
|
+
data.tar.gz: cd6e0ad73493fd5dd7e56dff790ed22dafa31a32d2f2347fb263cb4d6177aef4e83aaab7127ad28899d30802ffdddd6b83415f57ae27f2fc8dc2001494cd86cf
|
data/CLAUDE.md
CHANGED
|
@@ -58,6 +58,19 @@ Base classes: inherit from `GlooTest` (defined in `test/base_test.rb`).
|
|
|
58
58
|
### Gloo integration tests — `test.gloo/`
|
|
59
59
|
Written in gloo itself. Each file contains `[test]` objects with `on_test` scripts using `assert` and `refute`.
|
|
60
60
|
|
|
61
|
+
Run with `ruby lib/run.rb --test --quiet <paths...>` — **not** `bundle exec` (the Gemfile doesn't carry `gloo-test` / `gloo-md` / the other core-lib gems, so `[test]` and `load lib` fail under bundler; plain `ruby` uses the system gem env where they're installed).
|
|
62
|
+
|
|
63
|
+
- Interpreter suite only:
|
|
64
|
+
`ruby ~/dev/gloo/lib/run.rb --test --quiet ~/dev/gloo/test.gloo/`
|
|
65
|
+
- Interpreter **plus** every extension and core library:
|
|
66
|
+
```
|
|
67
|
+
ruby ~/dev/gloo/lib/run.rb --test --quiet \
|
|
68
|
+
~/dev/gloo/test.gloo/ \
|
|
69
|
+
~/gloo/extensions/{alert,beep,git,ruby,slack,stats,teams}/test/ \
|
|
70
|
+
~/dev/gloo_core_libraries/gloo-{beep,cli,db,email,md,mysql,pg,sqlite,test,web,yaml}/test/
|
|
71
|
+
```
|
|
72
|
+
(brace expansion is shell sugar for the full explicit path list)
|
|
73
|
+
|
|
61
74
|
```
|
|
62
75
|
test.gloo/
|
|
63
76
|
├── basic.test.gloo
|
data/docs/language_objects.md
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
**Contents**
|
|
4
4
|
|
|
5
5
|
- Object Naming
|
|
6
|
+
- Nested Container Shorthand
|
|
6
7
|
- Keywords
|
|
7
8
|
- Literals
|
|
8
9
|
- Value Conversion
|
|
@@ -39,6 +40,36 @@ naming [container] :
|
|
|
39
40
|
|
|
40
41
|
See also: Pathname.
|
|
41
42
|
|
|
43
|
+
## Nested Container Shorthand
|
|
44
|
+
|
|
45
|
+
A declaration whose name is a dotted path creates a container for each
|
|
46
|
+
prefix segment, then declares the real object under the last one. These
|
|
47
|
+
two files are equivalent:
|
|
48
|
+
|
|
49
|
+
```gloo
|
|
50
|
+
page [container] :
|
|
51
|
+
core [container] :
|
|
52
|
+
users [container] :
|
|
53
|
+
list [container] :
|
|
54
|
+
title [string] : Users
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
```gloo
|
|
58
|
+
page.core.users.list [container] :
|
|
59
|
+
title [string] : Users
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
- Each prefix segment (`page`, `core`, `users`) becomes a container,
|
|
63
|
+
unless a sibling object of that name already exists — in which case
|
|
64
|
+
that object is used as-is.
|
|
65
|
+
- The shorthand works at any indent level; the prefix is resolved
|
|
66
|
+
relative to the current parent.
|
|
67
|
+
- Several shorthand lines can share a prefix — `page.core.users [...]`
|
|
68
|
+
and `page.core.settings [...]` both reuse the same `page` and
|
|
69
|
+
`page.core` containers.
|
|
70
|
+
- Indented lines below a shorthand declaration are children of the last
|
|
71
|
+
segment (`list` above), not of any prefix container.
|
|
72
|
+
|
|
42
73
|
## Keywords
|
|
43
74
|
|
|
44
75
|
Gloo doesn't reserve words the way many languages do. A verb keyword like `put` or an object type name like `string` can also be used as an object name — there's no parser conflict, because verbs are only looked up as the first word of a statement, and object type names are only looked up where a type is expected (inside the `[ ]` on a declaration). Everywhere else, the word is just a pathname segment (see Object Naming, above).
|
|
@@ -60,6 +91,11 @@ The following rules apply to literal values:
|
|
|
60
91
|
|
|
61
92
|
- Strings
|
|
62
93
|
- Can be delimited by single or double quotes. (`"` or `'`)
|
|
94
|
+
- A quote of the other kind inside needs no escaping —
|
|
95
|
+
`'{"x":1}'` is the string `{"x":1}`, which is the usual way to
|
|
96
|
+
write a JSON literal.
|
|
97
|
+
- A quote of the same kind inside is escaped with a backslash —
|
|
98
|
+
`"say \"hi\""` is the string `say "hi"`.
|
|
63
99
|
- Numbers
|
|
64
100
|
- Integer and decimal numbers need no delimiters.
|
|
65
101
|
- To refer to a decimal with no fractional value, include `.0` to indicate a decimal value.
|
|
@@ -89,6 +125,8 @@ The following rules apply to literal values:
|
|
|
89
125
|
show ^.s
|
|
90
126
|
put "You're Awesome!" into ^.s
|
|
91
127
|
show ^.s
|
|
128
|
+
put '{"lang":"gloo"}' into ^.s
|
|
129
|
+
show ^.s
|
|
92
130
|
|
|
93
131
|
# Number literals
|
|
94
132
|
put 1 into ^.i
|
data/docs/language_scripting.md
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
**Contents**
|
|
4
4
|
|
|
5
5
|
- Gloo Script Files
|
|
6
|
+
- Comments
|
|
6
7
|
- Gloo Constants
|
|
7
8
|
- Line Continuation
|
|
8
9
|
|
|
@@ -26,6 +27,46 @@ hello [can] :
|
|
|
26
27
|
on_load [script] : show 'hello world'
|
|
27
28
|
```
|
|
28
29
|
|
|
30
|
+
## Comments
|
|
31
|
+
|
|
32
|
+
A line whose first non-blank character is `#` is a comment. Whole-line comments
|
|
33
|
+
can appear anywhere — between objects, inside a container, or inside a script
|
|
34
|
+
body — and are ignored when the file runs.
|
|
35
|
+
|
|
36
|
+
```gloo
|
|
37
|
+
#
|
|
38
|
+
# A whole-line comment.
|
|
39
|
+
#
|
|
40
|
+
demo [can] :
|
|
41
|
+
# a comment inside the container
|
|
42
|
+
msg [string] : hello
|
|
43
|
+
on_load [script] :
|
|
44
|
+
# a comment inside the script body
|
|
45
|
+
show demo.msg
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
A statement line may also end with an inline `# ...` comment. It is stripped
|
|
49
|
+
before the statement runs, so it has no effect on execution:
|
|
50
|
+
|
|
51
|
+
```gloo
|
|
52
|
+
on_load [script] :
|
|
53
|
+
show 3 + 4 # prints 7
|
|
54
|
+
check demo.msg for blank?
|
|
55
|
+
show it # prints false
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Two things are *not* treated as inline comments, and are left alone:
|
|
59
|
+
|
|
60
|
+
- a `#` inside a quoted string — `show 'a # b'`
|
|
61
|
+
- a `#` with no space before it — a URL fragment such as
|
|
62
|
+
`http://example.com/page#section`
|
|
63
|
+
|
|
64
|
+
An inline comment on an object declaration is also left alone — everything after
|
|
65
|
+
the `:` is the object's value, so `note [string] : see item # 5` stores the
|
|
66
|
+
string `see item # 5`.
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
29
70
|
## Gloo Constants
|
|
30
71
|
|
|
31
72
|
There is no gloo language construct for constants. They are simply objects. But by convention, constants are named in all caps. They might be in a container or at the root object level.
|
data/docs/verbs.md
CHANGED
|
@@ -9,8 +9,9 @@ Verbs aren't just for scripts, though. They're also the interactive language of
|
|
|
9
9
|
- Run
|
|
10
10
|
- Tell
|
|
11
11
|
- Put
|
|
12
|
+
- Load & Save
|
|
12
13
|
|
|
13
|
-
This page walks through
|
|
14
|
+
This page walks through the most commonly used verbs to get a feel for how they work together. For the complete list of verbs, their full syntax, and every error they can raise, use the in-app help: enter `help` (or `?`), then `verbs` to list them all, or `verb {name}` for detail on one (see Application, Help).
|
|
14
15
|
|
|
15
16
|
## Run
|
|
16
17
|
|
|
@@ -67,6 +68,30 @@ put {expression} into {dst.path}
|
|
|
67
68
|
|
|
68
69
|
`it` also picks up the result of the evaluation, same as with other verbs — see It.
|
|
69
70
|
|
|
71
|
+
## Load & Save
|
|
72
|
+
|
|
73
|
+
`load` reads a `.gloo` file into the heap and runs its `on_load` script. Give a path relative to the project folder (no extension needed) or a full path (extension required); `*` in place of a file name loads every `.gloo` file in a folder.
|
|
74
|
+
|
|
75
|
+
```gloo
|
|
76
|
+
> load my/project/config
|
|
77
|
+
> load my/app/*
|
|
78
|
+
> load ~/.my_app/settings.gloo
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
`save` writes loaded objects back to their files. With no argument it saves every open file; with an object it saves the file (or files) that object's tree came from; with `to {path}` it saves to a new file and remembers the mapping.
|
|
82
|
+
|
|
83
|
+
```gloo
|
|
84
|
+
> save # every open file
|
|
85
|
+
> save config # just config's file
|
|
86
|
+
> save config to backups/config
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
A save is a **rewrite, not a regeneration**: comments, blank lines, and the original spacing are kept, and only the values you actually changed are re-written. A declaration you never touched comes back byte-for-byte.
|
|
90
|
+
|
|
91
|
+
Several files can contribute to one container — declare `app [container] :` in each and add different children. They merge in the heap, and each file's save only rewrites its own declarations. If two files declare the *same* object with different values, the first one loaded wins and `load` logs a warning.
|
|
92
|
+
|
|
93
|
+
An object can also save itself: `tell config to save`.
|
|
94
|
+
|
|
70
95
|
---
|
|
71
96
|
|
|
72
|
-
`run`, `tell`, and `
|
|
97
|
+
`run`, `tell`, `put`, and `load` / `save` cover a lot of ground, but there are more than two dozen other verbs — `show`, `if`, `create`, `each`, `check`, `reload`, `unload`, and so on — all documented in-app. Enter `help` (or `?`), then `verbs` to browse them. (This page itself is also viewable in-app: `help> doc verbs`.)
|
data/lib/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
6.
|
|
1
|
+
6.4.0
|
data/lib/VERSION_NOTES
CHANGED
data/lib/gloo/core/obj.rb
CHANGED
|
@@ -118,6 +118,29 @@ module Gloo
|
|
|
118
118
|
return false
|
|
119
119
|
end
|
|
120
120
|
|
|
121
|
+
#
|
|
122
|
+
# Serialize this object's value for saving to a file.
|
|
123
|
+
# Returns the text that follows "name [type]" on the declaration
|
|
124
|
+
# line -- including the leading ' :' and, for a multi-line string
|
|
125
|
+
# value, the additional indented lines and the closing END marker.
|
|
126
|
+
# indent is the indentation level of the declaration line itself.
|
|
127
|
+
#
|
|
128
|
+
def serialize_value( indent )
|
|
129
|
+
return " : #{value_display}" unless value_string? && value.include?( "\n" )
|
|
130
|
+
|
|
131
|
+
return serialize_block_value( indent )
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
#
|
|
135
|
+
# Serialize a multi-line string value as a BEGIN/END block.
|
|
136
|
+
#
|
|
137
|
+
def serialize_block_value( indent )
|
|
138
|
+
t = "\t" * indent
|
|
139
|
+
inner = "\t" * ( indent + 1 )
|
|
140
|
+
body = value.split( "\n" ).map { |line| "#{inner}#{line}" }.join( "\n" )
|
|
141
|
+
return " : BEGIN\n#{body}\n#{t}END"
|
|
142
|
+
end
|
|
143
|
+
|
|
121
144
|
#
|
|
122
145
|
# Is the value a String?
|
|
123
146
|
#
|
|
@@ -306,7 +329,7 @@ module Gloo
|
|
|
306
329
|
# Get a list of message names that this object receives.
|
|
307
330
|
#
|
|
308
331
|
def self.messages
|
|
309
|
-
return %w[reload unload blank? contains? responds_to?]
|
|
332
|
+
return %w[save reload unload blank? contains? responds_to?]
|
|
310
333
|
end
|
|
311
334
|
|
|
312
335
|
#
|
|
@@ -356,6 +379,22 @@ module Gloo
|
|
|
356
379
|
@engine.persist_man.unload self
|
|
357
380
|
end
|
|
358
381
|
|
|
382
|
+
#
|
|
383
|
+
# Send the object the save message: tell it to save itself,
|
|
384
|
+
# rather than going through the save verb. Same rules as
|
|
385
|
+
# `save {path.to.object}` -- saves every file that owns a
|
|
386
|
+
# declaration in this object's root, or saves fresh to a default
|
|
387
|
+
# path if it isn't mapped to a file yet.
|
|
388
|
+
#
|
|
389
|
+
def msg_save
|
|
390
|
+
if self.root?
|
|
391
|
+
@engine.err 'Cannot save the root object.'
|
|
392
|
+
return
|
|
393
|
+
end
|
|
394
|
+
|
|
395
|
+
@engine.persist_man.save self.pn
|
|
396
|
+
end
|
|
397
|
+
|
|
359
398
|
#
|
|
360
399
|
# Send the object the reload message.
|
|
361
400
|
# Note that this will only work for objects with file assoications.
|
data/lib/gloo/core/parser.rb
CHANGED
|
@@ -21,7 +21,11 @@ module Gloo
|
|
|
21
21
|
# Parse a command from the immediate execution context.
|
|
22
22
|
#
|
|
23
23
|
def parse_immediate( full_cmd )
|
|
24
|
-
#
|
|
24
|
+
# Drop an inline trailing comment first, then break the
|
|
25
|
+
# command into verb and params.
|
|
26
|
+
full_cmd = strip_comment( full_cmd )
|
|
27
|
+
return nil if full_cmd.strip.empty?
|
|
28
|
+
|
|
25
29
|
cmd, params = split_params full_cmd
|
|
26
30
|
|
|
27
31
|
# Params are the parenthetical part of the command at the end
|
|
@@ -35,6 +39,31 @@ module Gloo
|
|
|
35
39
|
return nil
|
|
36
40
|
end
|
|
37
41
|
|
|
42
|
+
#
|
|
43
|
+
# Remove an inline trailing comment: the first '#' that starts a
|
|
44
|
+
# word (at the start of the command, or right after whitespace)
|
|
45
|
+
# and is not inside a quoted string -- and everything after it.
|
|
46
|
+
#
|
|
47
|
+
# Left alone: a '#' inside single or double quotes (show 'a # b'),
|
|
48
|
+
# a '#' glued to preceding text (a URL fragment like
|
|
49
|
+
# http://x?id=1#frag), and a command with no such '#'. Object
|
|
50
|
+
# declaration values in a .gloo file never reach here -- they're
|
|
51
|
+
# split by LineSplitter, not the parser.
|
|
52
|
+
#
|
|
53
|
+
def strip_comment( full_cmd )
|
|
54
|
+
quote = nil
|
|
55
|
+
full_cmd.each_char.with_index do |ch, i|
|
|
56
|
+
if quote
|
|
57
|
+
quote = nil if ch == quote
|
|
58
|
+
elsif Gloo::Core::Tokens::QUOTE_CHARS.include?( ch )
|
|
59
|
+
quote = ch
|
|
60
|
+
elsif ch == '#' && ( i.zero? || full_cmd[ i - 1 ] =~ /\s/ )
|
|
61
|
+
return full_cmd[ 0...i ].rstrip
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
return full_cmd
|
|
65
|
+
end
|
|
66
|
+
|
|
38
67
|
#
|
|
39
68
|
# If additional params were provided, split them out
|
|
40
69
|
# from the token list.
|
data/lib/gloo/core/tokens.rb
CHANGED
|
@@ -154,8 +154,9 @@ module Gloo
|
|
|
154
154
|
# An inline call (invoke( ... ) / ~>( ... )) is checked for
|
|
155
155
|
# first since it needs to be quote-aware in its own right (a
|
|
156
156
|
# call's args can include a quoted string) - see
|
|
157
|
-
# find_call_range.
|
|
158
|
-
#
|
|
157
|
+
# find_call_range. Otherwise the first quoted run (whichever
|
|
158
|
+
# quote char opens first) becomes one token, and the rest is
|
|
159
|
+
# split on spaces.
|
|
159
160
|
#
|
|
160
161
|
def tokenize( str )
|
|
161
162
|
range = find_call_range( str )
|
|
@@ -163,27 +164,52 @@ module Gloo
|
|
|
163
164
|
tokenize( str[ 0...range.first ] ) if range.first.positive?
|
|
164
165
|
@tokens << str[ range ]
|
|
165
166
|
tokenize( str[ range.last + 1..-1 ] ) if range.last + 1 < str.length
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
i = str.index( "'" )
|
|
176
|
-
j = str.index( "'", i + 1 )
|
|
177
|
-
j ||= str.length
|
|
178
|
-
|
|
179
|
-
tokenize( str[ 0..i - 1 ] ) if i > 1
|
|
180
|
-
@tokens << str[ i..j ]
|
|
181
|
-
tokenize( str[ j + 1..-1 ] ) if j + 1 < str.length
|
|
167
|
+
return
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
qi, qc = first_quote( str )
|
|
171
|
+
if qi
|
|
172
|
+
close = closing_quote( str, qi, qc )
|
|
173
|
+
tokenize( str[ 0...qi ] ) if qi.positive?
|
|
174
|
+
@tokens << str[ qi..close ]
|
|
175
|
+
tokenize( str[ close + 1..-1 ] ) if close + 1 < str.length
|
|
182
176
|
else
|
|
183
177
|
str.strip.split( ' ' ).each { |t| @tokens << t }
|
|
184
178
|
end
|
|
185
179
|
end
|
|
186
180
|
|
|
181
|
+
#
|
|
182
|
+
# Find the first quote character in the string, of either kind
|
|
183
|
+
# -- returns [index, char], or [nil, nil] if there is none. The
|
|
184
|
+
# kind that opens first wins, so a " inside a '...' literal (and
|
|
185
|
+
# vice versa) is treated as ordinary content.
|
|
186
|
+
#
|
|
187
|
+
def first_quote( str )
|
|
188
|
+
found = nil
|
|
189
|
+
QUOTE_CHARS.each do |q|
|
|
190
|
+
i = str.index( q )
|
|
191
|
+
found = [ i, q ] if i && ( found.nil? || i < found[ 0 ] )
|
|
192
|
+
end
|
|
193
|
+
return found || [ nil, nil ]
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
#
|
|
197
|
+
# Index of the quote that closes the one opened at open_index.
|
|
198
|
+
# A backslash-escaped quote (\" or \') does not close the
|
|
199
|
+
# string. Returns str.length if it is never closed.
|
|
200
|
+
#
|
|
201
|
+
def closing_quote( str, open_index, quote_char )
|
|
202
|
+
i = open_index + 1
|
|
203
|
+
while i < str.length
|
|
204
|
+
ch = str[ i ]
|
|
205
|
+
return i if ch == quote_char
|
|
206
|
+
|
|
207
|
+
i += 1
|
|
208
|
+
i += 1 if ch == '\\'
|
|
209
|
+
end
|
|
210
|
+
return str.length
|
|
211
|
+
end
|
|
212
|
+
|
|
187
213
|
#
|
|
188
214
|
# Find the char range of the first top-level (not inside a
|
|
189
215
|
# quoted string) invoke(...)/~>(...) call in str. Returns nil
|
data/lib/gloo/expr/l_string.rb
CHANGED
|
@@ -31,19 +31,23 @@ module Gloo
|
|
|
31
31
|
end
|
|
32
32
|
|
|
33
33
|
#
|
|
34
|
-
# Given a string with leading and trailing quotes,
|
|
35
|
-
#
|
|
34
|
+
# Given a string with leading and trailing quotes, strip them
|
|
35
|
+
# out. A quote of the other kind inside needs nothing done to it
|
|
36
|
+
# ('{"x":1}' -> {"x":1}); an escaped quote of the same kind is
|
|
37
|
+
# unescaped ("say \"hi\"" -> say "hi").
|
|
36
38
|
#
|
|
37
39
|
def self.strip_quotes( str )
|
|
38
40
|
if str.start_with?( '"' )
|
|
39
41
|
str = str[ 1..-1 ]
|
|
40
42
|
str = str[ 0..-2 ] if str.end_with?( '"' )
|
|
41
|
-
return str
|
|
43
|
+
return str.gsub( '\\"', '"' )
|
|
42
44
|
elsif str.start_with?( "'" )
|
|
43
45
|
str = str[ 1..-1 ]
|
|
44
46
|
str = str[ 0..-2 ] if str.end_with?( "'" )
|
|
45
|
-
return str
|
|
47
|
+
return str.gsub( "\\'", "'" )
|
|
46
48
|
end
|
|
49
|
+
|
|
50
|
+
return str
|
|
47
51
|
end
|
|
48
52
|
|
|
49
53
|
#
|
|
@@ -47,7 +47,7 @@ module Gloo
|
|
|
47
47
|
if self.value_string?
|
|
48
48
|
first = self.value
|
|
49
49
|
self.set_array_value []
|
|
50
|
-
self.value << first unless first.empty?
|
|
50
|
+
self.value << first unless first.strip.empty?
|
|
51
51
|
elsif self.value_is_blank?
|
|
52
52
|
self.set_array_value []
|
|
53
53
|
end
|
|
@@ -62,6 +62,29 @@ module Gloo
|
|
|
62
62
|
return true
|
|
63
63
|
end
|
|
64
64
|
|
|
65
|
+
#
|
|
66
|
+
# Serialize this object's value for saving to a file.
|
|
67
|
+
# A script's value is an Array of command lines (once add_line
|
|
68
|
+
# has been called) or a single-line String. Render as indented
|
|
69
|
+
# body lines below the declaration, gloo's convention for
|
|
70
|
+
# scripts -- not a BEGIN/END block, which is reserved for a
|
|
71
|
+
# literal multi-line string value.
|
|
72
|
+
#
|
|
73
|
+
def serialize_value( indent )
|
|
74
|
+
lines = if value_is_array?
|
|
75
|
+
value
|
|
76
|
+
elsif value_string? && !value.strip.empty?
|
|
77
|
+
[ value ]
|
|
78
|
+
else
|
|
79
|
+
[]
|
|
80
|
+
end
|
|
81
|
+
return ' :' if lines.empty?
|
|
82
|
+
|
|
83
|
+
t = "\t" * ( indent + 1 )
|
|
84
|
+
body = lines.map { |line| "#{t}#{line}" }.join( "\n" )
|
|
85
|
+
return " :\n#{body}"
|
|
86
|
+
end
|
|
87
|
+
|
|
65
88
|
#
|
|
66
89
|
# Get the number of lines in this script.
|
|
67
90
|
#
|
|
@@ -105,7 +128,10 @@ module Gloo
|
|
|
105
128
|
{
|
|
106
129
|
:name => KEYWORD,
|
|
107
130
|
:shortcut => KEYWORD_SHORT,
|
|
108
|
-
:description => 'An executable script — a set of commands to
|
|
131
|
+
:description => 'An executable script — a set of commands to ' \
|
|
132
|
+
'be run. A command line may end with an inline `# ...` ' \
|
|
133
|
+
'comment; it is ignored when the line runs but kept when ' \
|
|
134
|
+
'the file is saved.',
|
|
109
135
|
:messages => [
|
|
110
136
|
'run — Run the script. The script can be run by telling the ' \
|
|
111
137
|
'object to run, or via the run verb.'
|
data/lib/gloo/objs/web/json.rb
CHANGED
|
@@ -213,7 +213,10 @@ module Gloo
|
|
|
213
213
|
{
|
|
214
214
|
:name => KEYWORD,
|
|
215
215
|
:shortcut => KEYWORD_SHORT,
|
|
216
|
-
:description => 'JSON data in a text string.'
|
|
216
|
+
:description => 'JSON data in a text string. Declare it ' \
|
|
217
|
+
'inline, in a BEGIN/END block, or assign it at run time: a ' \
|
|
218
|
+
'single-quoted literal keeps its double quotes, so ' \
|
|
219
|
+
'putting a JSON string into the object works.',
|
|
217
220
|
:messages => [
|
|
218
221
|
'get ({path}) — Get a value from the JSON data. Example: tell myjson to get (\'title\'). The parameter is the path in JSON to the value we want. The value is put into it.',
|
|
219
222
|
'set ({obj.path}) — Convert an object to an approximate JSON value. Example: tell myjson to set (my.object). The parameter is the path to the object used as the source. Note this is an approximate conversion — a gloo object can have both a simple value and be a container for child objects, which JSON can\'t represent the same way.',
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# Author:: Eric Crane (mailto:eric.crane@mac.com)
|
|
2
|
+
# Copyright:: Copyright (c) 2026 Eric Crane. All rights reserved.
|
|
3
|
+
#
|
|
4
|
+
# Buffers a run of consecutive whole-line comments while the loader
|
|
5
|
+
# decides whether they become a declaration's leading_doc or float as
|
|
6
|
+
# their own comment nodes.
|
|
7
|
+
#
|
|
8
|
+
|
|
9
|
+
module Gloo
|
|
10
|
+
module Persist
|
|
11
|
+
class CommentBuffer
|
|
12
|
+
|
|
13
|
+
#
|
|
14
|
+
# Set up an empty buffer.
|
|
15
|
+
#
|
|
16
|
+
def initialize
|
|
17
|
+
@pending = []
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
#
|
|
21
|
+
# Buffer one comment line. raw is the full source line (minus
|
|
22
|
+
# its trailing newline); tabs is its indentation level.
|
|
23
|
+
#
|
|
24
|
+
def push( raw, tabs )
|
|
25
|
+
@pending << { :raw => raw, :tabs => tabs }
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
#
|
|
29
|
+
# Detach the buffered run as floating comment nodes, appended in
|
|
30
|
+
# order to the given children array.
|
|
31
|
+
#
|
|
32
|
+
def flush_into( children )
|
|
33
|
+
return if @pending.empty?
|
|
34
|
+
|
|
35
|
+
@pending.each { |c| children << Source::CommentNode.new( c[ :raw ] ) }
|
|
36
|
+
@pending = []
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
#
|
|
40
|
+
# If the buffered run sits at the same indent as line_tabs, claim
|
|
41
|
+
# it as a leading_doc (raw, newline-joined) and clear the buffer.
|
|
42
|
+
# Otherwise flush it into children as floating comments first, so
|
|
43
|
+
# document order is kept, and return nil.
|
|
44
|
+
#
|
|
45
|
+
def take_leading_doc( line_tabs, children )
|
|
46
|
+
if !@pending.empty? && @pending.last[ :tabs ] == line_tabs
|
|
47
|
+
doc = @pending.map { |c| c[ :raw ] }.join( "\n" )
|
|
48
|
+
@pending = []
|
|
49
|
+
return doc
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
flush_into( children )
|
|
53
|
+
return nil
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# Author:: Eric Crane (mailto:eric.crane@mac.com)
|
|
2
|
+
# Copyright:: Copyright (c) 2026 Eric Crane. All rights reserved.
|
|
3
|
+
#
|
|
4
|
+
# Tracks what a single file declares as it loads: its top-level
|
|
5
|
+
# objects (for save resolution), and every object it created -- so a
|
|
6
|
+
# re-declaration of a name an earlier file already used can be
|
|
7
|
+
# flagged, since in that case the second value is silently dropped.
|
|
8
|
+
#
|
|
9
|
+
|
|
10
|
+
module Gloo
|
|
11
|
+
module Persist
|
|
12
|
+
class DeclarationLedger
|
|
13
|
+
|
|
14
|
+
attr_reader :roots
|
|
15
|
+
|
|
16
|
+
#
|
|
17
|
+
# Set up a ledger for the file at pn.
|
|
18
|
+
#
|
|
19
|
+
def initialize( engine, pn )
|
|
20
|
+
@engine = engine
|
|
21
|
+
@pn = pn
|
|
22
|
+
@roots = []
|
|
23
|
+
@mine = {}.compare_by_identity
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
#
|
|
27
|
+
# Record a top-level object this file created or reused (shorthand
|
|
28
|
+
# lines can re-introduce the same root, so dedupe).
|
|
29
|
+
#
|
|
30
|
+
def root( obj )
|
|
31
|
+
@roots << obj unless @roots.include?( obj )
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
#
|
|
35
|
+
# Record an object this file created, at any level.
|
|
36
|
+
#
|
|
37
|
+
def created( obj )
|
|
38
|
+
@mine[ obj ] = true
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
#
|
|
42
|
+
# `prior` already existed when this file declared `name` with
|
|
43
|
+
# `value`. If another file created it and the values differ, the
|
|
44
|
+
# new value is being ignored -- warn so it isn't a surprise. A
|
|
45
|
+
# duplicate name inside one file is left alone (a documented
|
|
46
|
+
# "first one wins").
|
|
47
|
+
#
|
|
48
|
+
def clash( prior, name, value )
|
|
49
|
+
return if @mine.key?( prior )
|
|
50
|
+
return if value.nil? || value.to_s.strip.empty?
|
|
51
|
+
return if prior.value.to_s == value.to_s
|
|
52
|
+
|
|
53
|
+
@engine.log.warn(
|
|
54
|
+
"#{@pn}: '#{name}' is already declared in another loaded file with a " \
|
|
55
|
+
"different value (#{prior.value.inspect} kept, #{value.inspect} ignored)."
|
|
56
|
+
)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -85,13 +85,26 @@ module Gloo
|
|
|
85
85
|
return nil
|
|
86
86
|
end
|
|
87
87
|
|
|
88
|
-
#
|
|
88
|
+
#
|
|
89
89
|
# Read in the contents of a single file.
|
|
90
|
-
#
|
|
90
|
+
#
|
|
91
91
|
def read( file )
|
|
92
92
|
return File.read( file )
|
|
93
93
|
end
|
|
94
94
|
|
|
95
|
+
#
|
|
96
|
+
# Resolve a path to save a new (not-yet-existing) file to, given
|
|
97
|
+
# a name or relative path -- same convention as expand (relative
|
|
98
|
+
# to the project root, .gloo appended if missing), but without
|
|
99
|
+
# requiring the file to already exist.
|
|
100
|
+
#
|
|
101
|
+
def resolve_save_path( name )
|
|
102
|
+
full_name = name.end_with?( file_ext ) ? name : "#{name}#{file_ext}"
|
|
103
|
+
return full_name if full_name.start_with?( '/' )
|
|
104
|
+
|
|
105
|
+
return File.join( @engine.settings.project_path, full_name )
|
|
106
|
+
end
|
|
107
|
+
|
|
95
108
|
#
|
|
96
109
|
# Write data to the file.
|
|
97
110
|
#
|