gloo 6.3.0 → 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 +10 -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 -100
- 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/objs/text.test.gloo +22 -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
|
@@ -8,7 +8,7 @@ module Gloo
|
|
|
8
8
|
module Persist
|
|
9
9
|
class FileStorage
|
|
10
10
|
|
|
11
|
-
attr_reader :obj, :pn
|
|
11
|
+
attr_reader :obj, :pn, :roots, :source_doc
|
|
12
12
|
|
|
13
13
|
#
|
|
14
14
|
# Set up a file storage for an object.
|
|
@@ -17,13 +17,17 @@ module Gloo
|
|
|
17
17
|
@engine = engine
|
|
18
18
|
@obj = obj
|
|
19
19
|
@pn = pn
|
|
20
|
+
@roots = obj ? [ obj ] : []
|
|
21
|
+
@source_doc = nil
|
|
20
22
|
end
|
|
21
23
|
|
|
22
24
|
#
|
|
23
|
-
# Save the object to the file.
|
|
25
|
+
# Save the object to the file. batch, when given, lets a
|
|
26
|
+
# multi-file namespace round-trip -- see PersistMan#save_batch and
|
|
27
|
+
# FileSaver.
|
|
24
28
|
#
|
|
25
|
-
def save
|
|
26
|
-
fs = FileSaver.new( @engine, @pn, @obj )
|
|
29
|
+
def save( batch = nil )
|
|
30
|
+
fs = FileSaver.new( @engine, @pn, @obj, @source_doc, batch )
|
|
27
31
|
fs.save
|
|
28
32
|
end
|
|
29
33
|
|
|
@@ -34,6 +38,8 @@ module Gloo
|
|
|
34
38
|
fl = FileLoader.new( @engine, @pn )
|
|
35
39
|
fl.load
|
|
36
40
|
@obj = fl.obj
|
|
41
|
+
@roots = fl.roots
|
|
42
|
+
@source_doc = fl.source_doc
|
|
37
43
|
if @obj
|
|
38
44
|
@engine.log.debug "Loaded object: #{@obj.name}"
|
|
39
45
|
else
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# Author:: Eric Crane (mailto:eric.crane@mac.com)
|
|
2
|
+
# Copyright:: Copyright (c) 2026 Eric Crane. All rights reserved.
|
|
3
|
+
#
|
|
4
|
+
# Tracks the current nesting level shared by the heap tree and the
|
|
5
|
+
# source tree as declaration lines are processed, pushing and popping
|
|
6
|
+
# both stacks in lockstep so their nesting always matches -- an object
|
|
7
|
+
# only becomes a parent once a deeper line actually follows it.
|
|
8
|
+
#
|
|
9
|
+
|
|
10
|
+
module Gloo
|
|
11
|
+
module Persist
|
|
12
|
+
class IndentStack
|
|
13
|
+
|
|
14
|
+
attr_reader :tabs
|
|
15
|
+
|
|
16
|
+
#
|
|
17
|
+
# Set up a stack rooted at the given heap object and source node.
|
|
18
|
+
#
|
|
19
|
+
def initialize( root_obj, root_node )
|
|
20
|
+
@tabs = 0
|
|
21
|
+
@parent_stack = [ root_obj ]
|
|
22
|
+
@node_stack = [ root_node ]
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
#
|
|
26
|
+
# The heap object new children should be created under.
|
|
27
|
+
#
|
|
28
|
+
def parent
|
|
29
|
+
return @parent_stack.last
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
#
|
|
33
|
+
# The source node new declarations/trivia should be appended to.
|
|
34
|
+
#
|
|
35
|
+
def node
|
|
36
|
+
return @node_stack.last
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
#
|
|
40
|
+
# Move the stacks to the right depth for a line at the given
|
|
41
|
+
# indentation, then -- if it turned out to be deeper than the
|
|
42
|
+
# previous line -- push the previous line's object/node as the
|
|
43
|
+
# new parent. last_obj/last_node are whatever was created for the
|
|
44
|
+
# previous declaration line.
|
|
45
|
+
#
|
|
46
|
+
def place( line_tabs, last_obj, last_node )
|
|
47
|
+
indent = indent_delta( line_tabs )
|
|
48
|
+
if indent.positive?
|
|
49
|
+
# last_obj is nil when the previous line failed to make an
|
|
50
|
+
# object (eg. an unknown type in a malformed file) -- keep
|
|
51
|
+
# the current parent rather than pushing nil.
|
|
52
|
+
@parent_stack.push( last_obj || @parent_stack.last )
|
|
53
|
+
@node_stack.push( last_node || @node_stack.last )
|
|
54
|
+
elsif indent.negative?
|
|
55
|
+
indent.abs.times do
|
|
56
|
+
# never pop the root -- a file with erratic (mixed
|
|
57
|
+
# tab/space) indentation can outdent further than it ever
|
|
58
|
+
# indented; clamp instead of emptying the stack.
|
|
59
|
+
break if @parent_stack.length <= 1
|
|
60
|
+
|
|
61
|
+
@parent_stack.pop
|
|
62
|
+
@node_stack.pop
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
private
|
|
68
|
+
|
|
69
|
+
#
|
|
70
|
+
# Update @tabs for the new line and return the signed change in
|
|
71
|
+
# nesting depth: positive if it's more indented than the last
|
|
72
|
+
# line placed, negative if less, zero if the same.
|
|
73
|
+
#
|
|
74
|
+
def indent_delta( line_tabs )
|
|
75
|
+
if line_tabs > @tabs
|
|
76
|
+
# TODO: What if indent is more than one more level?
|
|
77
|
+
@tabs = line_tabs
|
|
78
|
+
return 1
|
|
79
|
+
elsif line_tabs < @tabs
|
|
80
|
+
diff = @tabs - line_tabs
|
|
81
|
+
@tabs -= diff
|
|
82
|
+
return -diff
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
return 0
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
@@ -12,7 +12,7 @@ module Gloo
|
|
|
12
12
|
BEGIN_BLOCK = 'BEGIN'.freeze
|
|
13
13
|
END_BLOCK = 'END'.freeze
|
|
14
14
|
|
|
15
|
-
attr_reader :obj
|
|
15
|
+
attr_reader :obj, :raw_tail
|
|
16
16
|
|
|
17
17
|
#
|
|
18
18
|
# Set up a line splitter
|
|
@@ -20,6 +20,7 @@ module Gloo
|
|
|
20
20
|
def initialize( line, tabs )
|
|
21
21
|
@line = line
|
|
22
22
|
@tabs = tabs
|
|
23
|
+
@raw_tail = ''
|
|
23
24
|
end
|
|
24
25
|
|
|
25
26
|
#
|
|
@@ -36,15 +37,26 @@ module Gloo
|
|
|
36
37
|
#
|
|
37
38
|
# Detect the object name.
|
|
38
39
|
#
|
|
40
|
+
# Only leading whitespace (the indentation, captured separately by
|
|
41
|
+
# the loader) and the trailing newline are removed here --
|
|
42
|
+
# trailing spaces/tabs are left on the line so they survive into
|
|
43
|
+
# a string value, which can matter when the value is used to
|
|
44
|
+
# build HTML.
|
|
45
|
+
#
|
|
39
46
|
def detect_name
|
|
40
|
-
@line = @line.
|
|
47
|
+
@line = @line.lstrip.chomp
|
|
41
48
|
@idx = @line.index( ' ' )
|
|
42
49
|
@idx = 0 unless @idx
|
|
43
50
|
@name = @line[ 0..@idx - 1 ]
|
|
44
51
|
end
|
|
45
52
|
|
|
46
53
|
#
|
|
47
|
-
# Detect the object type
|
|
54
|
+
# Detect the object type, and capture the exact source text that
|
|
55
|
+
# follows it -- everything from the closing ']' (or, for an
|
|
56
|
+
# untyped declaration with no brackets at all, from right after
|
|
57
|
+
# the name) to the end of the line. Kept byte-exact so a save can
|
|
58
|
+
# reproduce a declaration's original spacing when its value
|
|
59
|
+
# hasn't changed.
|
|
48
60
|
#
|
|
49
61
|
def detect_type
|
|
50
62
|
@line = @line[ @idx + 1..-1 ]
|
|
@@ -52,12 +64,15 @@ module Gloo
|
|
|
52
64
|
|
|
53
65
|
if @line[ 0 ] == ':'
|
|
54
66
|
@type = 'untyped'
|
|
67
|
+
@raw_tail = @line
|
|
55
68
|
return
|
|
56
69
|
end
|
|
57
70
|
|
|
58
71
|
@type = @line[ 0..( @idx ? @idx - 1 : -1 ) ]
|
|
59
72
|
@type = @type[ 1..-1 ] if @type[ 0 ] == '['
|
|
60
73
|
@type = @type[ 0..-2 ] if @type[ -1 ] == ']'
|
|
74
|
+
close = @line.index( ']' )
|
|
75
|
+
@raw_tail = close ? @line[ close + 1..-1 ] : ''
|
|
61
76
|
end
|
|
62
77
|
|
|
63
78
|
#
|
|
@@ -12,6 +12,10 @@ module Gloo
|
|
|
12
12
|
|
|
13
13
|
attr_reader :maps, :mech
|
|
14
14
|
|
|
15
|
+
OBJ_NOT_FOUND_ERR = 'Could not resolve object to save: '.freeze
|
|
16
|
+
PATH_EXISTS_ERR = 'Will not overwrite a file not already saved there: '.freeze
|
|
17
|
+
RELOAD_DIRTY_WARNING = 'Reloading will discard unsaved changes in: '.freeze
|
|
18
|
+
|
|
15
19
|
#
|
|
16
20
|
# Constructor for the persistence manager.
|
|
17
21
|
#
|
|
@@ -22,31 +26,52 @@ module Gloo
|
|
|
22
26
|
end
|
|
23
27
|
|
|
24
28
|
#
|
|
25
|
-
# Save one object to the file
|
|
29
|
+
# Save one object to the file, or every open file if no name is
|
|
30
|
+
# given.
|
|
26
31
|
#
|
|
27
32
|
def save( name = '' )
|
|
28
33
|
name.blank? ? save_all : save_one( name )
|
|
29
34
|
end
|
|
30
35
|
|
|
31
36
|
#
|
|
32
|
-
# Save
|
|
37
|
+
# Save every open file.
|
|
33
38
|
#
|
|
34
39
|
def save_all
|
|
35
|
-
@maps
|
|
40
|
+
save_batch( @maps )
|
|
36
41
|
end
|
|
37
42
|
|
|
38
43
|
#
|
|
39
|
-
# Save
|
|
44
|
+
# Save the given object: every file that owns a declaration in
|
|
45
|
+
# its root's tree (usually one -- more when the root is a
|
|
46
|
+
# multi-file namespace). If it isn't mapped to any file yet, it's
|
|
47
|
+
# saved fresh to a default path derived from its root's name.
|
|
40
48
|
#
|
|
41
49
|
def save_one( name )
|
|
42
|
-
|
|
43
|
-
|
|
50
|
+
obj = resolve_for_save( name )
|
|
51
|
+
return unless obj
|
|
52
|
+
|
|
53
|
+
fs_list = find_file_storages( obj )
|
|
54
|
+
return save_new( obj, @mech.resolve_save_path( root_of( obj ).name ) ) if fs_list.empty?
|
|
44
55
|
|
|
45
|
-
# Send an on_save event
|
|
46
56
|
@engine.event_manager.on_save obj
|
|
57
|
+
save_batch( fs_list )
|
|
58
|
+
end
|
|
47
59
|
|
|
48
|
-
|
|
49
|
-
|
|
60
|
+
#
|
|
61
|
+
# Save the given object to an explicit path. Registers the
|
|
62
|
+
# mapping so a future bare save includes it. Refuses to overwrite
|
|
63
|
+
# a file that exists but isn't already mapped to this object.
|
|
64
|
+
#
|
|
65
|
+
def save_to( name, path )
|
|
66
|
+
obj = resolve_for_save( name )
|
|
67
|
+
return unless obj
|
|
68
|
+
|
|
69
|
+
pn = @mech.resolve_save_path( path )
|
|
70
|
+
mapped = @maps.find { |fs| fs.pn == pn }
|
|
71
|
+
return save_mapped( obj, mapped, pn ) if mapped
|
|
72
|
+
return @engine.err( "#{PATH_EXISTS_ERR}#{pn}" ) if @mech.exist?( pn )
|
|
73
|
+
|
|
74
|
+
save_new( obj, pn )
|
|
50
75
|
end
|
|
51
76
|
|
|
52
77
|
#
|
|
@@ -61,6 +86,8 @@ module Gloo
|
|
|
61
86
|
begin
|
|
62
87
|
fs = Gloo::Persist::FileStorage.new( @engine, pn )
|
|
63
88
|
fs.load
|
|
89
|
+
next unless fs.obj # a failed load -- don't map it
|
|
90
|
+
|
|
64
91
|
@maps << fs
|
|
65
92
|
@engine.event_manager.on_load fs.obj
|
|
66
93
|
rescue => ex
|
|
@@ -74,8 +101,8 @@ module Gloo
|
|
|
74
101
|
# The engine is reset to a clean state.
|
|
75
102
|
#
|
|
76
103
|
def unload_all
|
|
77
|
-
objs = self.maps.map
|
|
78
|
-
objs.each
|
|
104
|
+
objs = self.maps.map( &:obj ).compact
|
|
105
|
+
objs.each( &:msg_unload )
|
|
79
106
|
@engine.reset_state
|
|
80
107
|
end
|
|
81
108
|
|
|
@@ -88,22 +115,21 @@ module Gloo
|
|
|
88
115
|
def unload( obj )
|
|
89
116
|
@engine.event_manager.on_unload obj
|
|
90
117
|
@engine.heap.unload obj
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
end
|
|
96
|
-
end
|
|
118
|
+
# Drop every mapping for this object -- more than one when the
|
|
119
|
+
# root came from several files (the namespace pattern) -- and
|
|
120
|
+
# sweep out any mapping whose object is already gone.
|
|
121
|
+
@maps.reject! { |o| o.obj.nil? || ( o.obj.pn === obj.pn ) }
|
|
97
122
|
end
|
|
98
123
|
|
|
99
|
-
#
|
|
124
|
+
#
|
|
100
125
|
# Reload all objects.
|
|
101
126
|
# First send a message to each object to let it know it is being reloaded.
|
|
102
127
|
# Then let the engine restart to reload the files.
|
|
103
|
-
#
|
|
128
|
+
#
|
|
104
129
|
def reload_all
|
|
105
130
|
return unless @maps
|
|
106
|
-
|
|
131
|
+
|
|
132
|
+
@maps.each { |fs| warn_if_dirty( fs ) }
|
|
107
133
|
@maps.each do |fs|
|
|
108
134
|
@engine.event_manager.on_reload fs.obj
|
|
109
135
|
end
|
|
@@ -112,23 +138,24 @@ module Gloo
|
|
|
112
138
|
@engine.restart
|
|
113
139
|
end
|
|
114
140
|
|
|
115
|
-
#
|
|
141
|
+
#
|
|
116
142
|
# Re-load the given object from file.
|
|
117
143
|
# This is used to reload a single object and comes from a
|
|
118
144
|
# message sent to the object.
|
|
119
|
-
#
|
|
145
|
+
#
|
|
120
146
|
def reload( obj )
|
|
121
147
|
fs = find_file_storage( obj )
|
|
122
148
|
return unless fs
|
|
123
149
|
|
|
150
|
+
warn_if_dirty( fs )
|
|
124
151
|
@engine.event_manager.on_reload obj
|
|
125
152
|
@engine.heap.unload obj
|
|
126
153
|
fs.load
|
|
127
154
|
end
|
|
128
155
|
|
|
129
|
-
#
|
|
156
|
+
#
|
|
130
157
|
# Find the objects FileStorage in the list.
|
|
131
|
-
#
|
|
158
|
+
#
|
|
132
159
|
def find_file_storage( obj )
|
|
133
160
|
@maps.each do |o|
|
|
134
161
|
return o if ( o.obj.pn === obj.pn )
|
|
@@ -138,6 +165,16 @@ module Gloo
|
|
|
138
165
|
return nil
|
|
139
166
|
end
|
|
140
167
|
|
|
168
|
+
#
|
|
169
|
+
# Find every FileStorage that owns a declaration in the given
|
|
170
|
+
# object's root -- usually one, more when the root is shared
|
|
171
|
+
# across files (the multi-file namespace pattern).
|
|
172
|
+
#
|
|
173
|
+
def find_file_storages( obj )
|
|
174
|
+
root = root_of( obj )
|
|
175
|
+
return @maps.select { |fs| fs.roots.include?( root ) }
|
|
176
|
+
end
|
|
177
|
+
|
|
141
178
|
|
|
142
179
|
#
|
|
143
180
|
# Get the full path and name of the file.
|
|
@@ -176,6 +213,104 @@ module Gloo
|
|
|
176
213
|
end
|
|
177
214
|
end
|
|
178
215
|
|
|
216
|
+
private
|
|
217
|
+
|
|
218
|
+
#
|
|
219
|
+
# Resolve a name to an object for saving, reporting an error and
|
|
220
|
+
# returning nil if it can't be found.
|
|
221
|
+
#
|
|
222
|
+
def resolve_for_save( name )
|
|
223
|
+
obj = Gloo::Core::Pn.new( @engine, name ).resolve
|
|
224
|
+
@engine.err( "#{OBJ_NOT_FOUND_ERR}#{name}" ) unless obj
|
|
225
|
+
return obj
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
#
|
|
229
|
+
# Walk up to the top-level declared object that contains obj --
|
|
230
|
+
# its parent is the heap root, not another declared object.
|
|
231
|
+
#
|
|
232
|
+
def root_of( obj )
|
|
233
|
+
o = obj
|
|
234
|
+
o = o.parent until o.parent.nil? || o.parent.root?
|
|
235
|
+
return o
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
#
|
|
239
|
+
# Save a set of files as one batch so a multi-file namespace
|
|
240
|
+
# round-trips: each file only rewrites the declarations its own
|
|
241
|
+
# SourceDoc holds and leaves other files' declarations alone; a
|
|
242
|
+
# brand-new object no file owns is written once, by the first file
|
|
243
|
+
# in the batch to reach it.
|
|
244
|
+
#
|
|
245
|
+
def save_batch( files )
|
|
246
|
+
claimed = {}.compare_by_identity
|
|
247
|
+
files.each do |fs|
|
|
248
|
+
fs.save( :others => owned_elsewhere( fs ), :claimed => claimed )
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
#
|
|
253
|
+
# Every heap object declared by some loaded file's SourceDoc
|
|
254
|
+
# other than fs.
|
|
255
|
+
#
|
|
256
|
+
def owned_elsewhere( fs )
|
|
257
|
+
owned = {}.compare_by_identity
|
|
258
|
+
@maps.each do |other|
|
|
259
|
+
next if other.equal?( fs )
|
|
260
|
+
|
|
261
|
+
collect_owned( other.source_doc&.children, owned )
|
|
262
|
+
end
|
|
263
|
+
return owned
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
#
|
|
267
|
+
# Recursively collect the objects declared by the given source
|
|
268
|
+
# nodes into owned.
|
|
269
|
+
#
|
|
270
|
+
def collect_owned( nodes, owned )
|
|
271
|
+
return unless nodes
|
|
272
|
+
|
|
273
|
+
nodes.each do |node|
|
|
274
|
+
next unless node.is_a?( Gloo::Persist::Source::ObjNode )
|
|
275
|
+
|
|
276
|
+
owned[ node.obj ] = true if node.obj
|
|
277
|
+
collect_owned( node.children, owned )
|
|
278
|
+
end
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
#
|
|
282
|
+
# Save to a path that's already mapped to some file. If that
|
|
283
|
+
# mapping is for a different root, it's a real collision -- the
|
|
284
|
+
# path belongs to something else.
|
|
285
|
+
#
|
|
286
|
+
def save_mapped( obj, mapped, pn )
|
|
287
|
+
return @engine.err( "#{PATH_EXISTS_ERR}#{pn}" ) unless mapped.roots.include?( root_of( obj ) )
|
|
288
|
+
|
|
289
|
+
@engine.event_manager.on_save obj
|
|
290
|
+
save_batch( [ mapped ] )
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
#
|
|
294
|
+
# Save an object's root fresh to a new path, and register the
|
|
295
|
+
# resulting mapping.
|
|
296
|
+
#
|
|
297
|
+
def save_new( obj, pn )
|
|
298
|
+
@engine.event_manager.on_save obj
|
|
299
|
+
fs = Gloo::Persist::FileStorage.new( @engine, pn, root_of( obj ) )
|
|
300
|
+
fs.save
|
|
301
|
+
@maps << fs
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
#
|
|
305
|
+
# Warn (without blocking) if reloading fs would discard changes
|
|
306
|
+
# that haven't been saved -- any object whose value no longer
|
|
307
|
+
# round-trips to what's on disk.
|
|
308
|
+
#
|
|
309
|
+
def warn_if_dirty( fs )
|
|
310
|
+
saver = Gloo::Persist::FileSaver.new( @engine, fs.pn, fs.obj, fs.source_doc )
|
|
311
|
+
@engine.log.warn "#{RELOAD_DIRTY_WARNING}#{fs.pn}" if saver.dirty?
|
|
312
|
+
end
|
|
313
|
+
|
|
179
314
|
end
|
|
180
315
|
end
|
|
181
316
|
end
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# Author:: Eric Crane (mailto:eric.crane@mac.com)
|
|
2
|
+
# Copyright:: Copyright (c) 2026 Eric Crane. All rights reserved.
|
|
3
|
+
#
|
|
4
|
+
# Collects an indented, unparsed script body (no BEGIN/END) for the
|
|
5
|
+
# object that was just declared. We don't yet know whether a body
|
|
6
|
+
# actually follows -- that's decided by the next line's indentation,
|
|
7
|
+
# so this may close immediately with nothing collected (an empty
|
|
8
|
+
# script).
|
|
9
|
+
#
|
|
10
|
+
|
|
11
|
+
module Gloo
|
|
12
|
+
module Persist
|
|
13
|
+
class ScriptBodyCollector
|
|
14
|
+
|
|
15
|
+
attr_reader :active
|
|
16
|
+
|
|
17
|
+
#
|
|
18
|
+
# Set up an inactive collector.
|
|
19
|
+
#
|
|
20
|
+
def initialize
|
|
21
|
+
@active = false
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
#
|
|
25
|
+
# Start tentatively collecting a body for the object just
|
|
26
|
+
# declared. node is its Source::ObjNode, heap_obj the object
|
|
27
|
+
# itself, base_tabs the indentation of the declaration line
|
|
28
|
+
# (body lines must be indented deeper than this).
|
|
29
|
+
#
|
|
30
|
+
def start( node, heap_obj, base_tabs )
|
|
31
|
+
@active = true
|
|
32
|
+
@node = node
|
|
33
|
+
@obj = heap_obj
|
|
34
|
+
@base_tabs = base_tabs
|
|
35
|
+
@lines = []
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
#
|
|
39
|
+
# One line while a body might be open. line_tabs is its
|
|
40
|
+
# indentation (ignored for a blank line, which is always kept).
|
|
41
|
+
# Returns true if the line was consumed as body content, or
|
|
42
|
+
# false if it closed the body and still needs to be dispatched
|
|
43
|
+
# normally by the caller.
|
|
44
|
+
#
|
|
45
|
+
def handle_line( line, line_tabs )
|
|
46
|
+
if line.strip.empty?
|
|
47
|
+
@lines << chomped( line )
|
|
48
|
+
return true
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
if line_tabs <= @base_tabs
|
|
52
|
+
close
|
|
53
|
+
return false
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
add_line( line )
|
|
57
|
+
return true
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
#
|
|
61
|
+
# End of file with a body still open (eg. the last object in the
|
|
62
|
+
# file is an empty or in-progress script) -- close it so its
|
|
63
|
+
# content isn't silently dropped.
|
|
64
|
+
#
|
|
65
|
+
def finish
|
|
66
|
+
close if @active
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
private
|
|
70
|
+
|
|
71
|
+
#
|
|
72
|
+
# Record one real body line: kept verbatim for the source model;
|
|
73
|
+
# added as a runnable command only if it isn't a comment (a
|
|
74
|
+
# comment inside a script body is documentation, not something
|
|
75
|
+
# to execute).
|
|
76
|
+
#
|
|
77
|
+
def add_line( line )
|
|
78
|
+
@lines << chomped( line )
|
|
79
|
+
@obj.add_line( line ) unless line.strip.start_with?( '#' )
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
#
|
|
83
|
+
# Close out the body, recording its raw content on the node.
|
|
84
|
+
#
|
|
85
|
+
def close
|
|
86
|
+
@active = false
|
|
87
|
+
@node.raw_value = @lines.join( "\n" )
|
|
88
|
+
@node.block_style = :body unless @lines.empty?
|
|
89
|
+
@node = nil
|
|
90
|
+
@obj = nil
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
#
|
|
94
|
+
# A line's raw text with its trailing newline removed.
|
|
95
|
+
#
|
|
96
|
+
def chomped( line )
|
|
97
|
+
return line.chomp( "\n" )
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Author:: Eric Crane (mailto:eric.crane@mac.com)
|
|
2
|
+
# Copyright:: Copyright (c) 2026 Eric Crane. All rights reserved.
|
|
3
|
+
#
|
|
4
|
+
# Nested container shorthand: a dotted name in a declaration
|
|
5
|
+
# (page.core.users.list [type] :) means "find or create a container
|
|
6
|
+
# for each prefix segment, then declare the real object under the
|
|
7
|
+
# last one". A segment that already names some other kind of object
|
|
8
|
+
# is used as-is, not forced to a container.
|
|
9
|
+
#
|
|
10
|
+
|
|
11
|
+
module Gloo
|
|
12
|
+
module Persist
|
|
13
|
+
class ShorthandExpander
|
|
14
|
+
|
|
15
|
+
#
|
|
16
|
+
# Set up an expander for the given engine.
|
|
17
|
+
#
|
|
18
|
+
def initialize( engine )
|
|
19
|
+
@engine = engine
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
#
|
|
23
|
+
# Split a possibly-dotted name against the given parent. Returns
|
|
24
|
+
# [leaf_name, parent_for_the_leaf, roots_touched, created]:
|
|
25
|
+
# - roots_touched: containers created OR reused directly under
|
|
26
|
+
# the heap root, for the loader to register as this file's roots
|
|
27
|
+
# - created: containers this call actually created (any level),
|
|
28
|
+
# for the loader to remember as its own
|
|
29
|
+
#
|
|
30
|
+
def expand( name, parent )
|
|
31
|
+
return [ name, parent, [], [] ] unless name.include?( '.' )
|
|
32
|
+
|
|
33
|
+
segments = name.split( '.' )
|
|
34
|
+
leaf = segments.pop
|
|
35
|
+
roots = []
|
|
36
|
+
created = []
|
|
37
|
+
segments.each do |seg|
|
|
38
|
+
child = parent.find_child( seg )
|
|
39
|
+
unless child
|
|
40
|
+
child = @engine.factory.create_can( seg, parent )
|
|
41
|
+
created << child
|
|
42
|
+
end
|
|
43
|
+
roots << child if parent == @engine.heap.root
|
|
44
|
+
parent = child
|
|
45
|
+
end
|
|
46
|
+
return [ leaf, parent, roots, created ]
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Author:: Eric Crane (mailto:eric.crane@mac.com)
|
|
2
|
+
# Copyright:: Copyright (c) 2026 Eric Crane. All rights reserved.
|
|
3
|
+
#
|
|
4
|
+
# A blank line between declarations. Kept (not discarded) so a save
|
|
5
|
+
# can reproduce the file's original spacing.
|
|
6
|
+
#
|
|
7
|
+
|
|
8
|
+
module Gloo
|
|
9
|
+
module Persist
|
|
10
|
+
module Source
|
|
11
|
+
class BlankNode
|
|
12
|
+
|
|
13
|
+
attr_reader :raw
|
|
14
|
+
|
|
15
|
+
#
|
|
16
|
+
# Set up a blank line node. raw is the full source line, minus
|
|
17
|
+
# its trailing newline (may be non-empty if the blank line had
|
|
18
|
+
# trailing whitespace -- preserved rather than normalized).
|
|
19
|
+
#
|
|
20
|
+
def initialize( raw = '' )
|
|
21
|
+
@raw = raw
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Author:: Eric Crane (mailto:eric.crane@mac.com)
|
|
2
|
+
# Copyright:: Copyright (c) 2026 Eric Crane. All rights reserved.
|
|
3
|
+
#
|
|
4
|
+
# A whole-line comment that is not attached to any object declaration
|
|
5
|
+
# as its leading_doc -- either because a blank line separated it from
|
|
6
|
+
# the declaration that follows, or nothing follows it at all.
|
|
7
|
+
#
|
|
8
|
+
|
|
9
|
+
module Gloo
|
|
10
|
+
module Persist
|
|
11
|
+
module Source
|
|
12
|
+
class CommentNode
|
|
13
|
+
|
|
14
|
+
attr_reader :raw
|
|
15
|
+
|
|
16
|
+
#
|
|
17
|
+
# Set up a comment node. raw is the full source line, minus
|
|
18
|
+
# its trailing newline.
|
|
19
|
+
#
|
|
20
|
+
def initialize( raw )
|
|
21
|
+
@raw = raw
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Author:: Eric Crane (mailto:eric.crane@mac.com)
|
|
2
|
+
# Copyright:: Copyright (c) 2026 Eric Crane. All rights reserved.
|
|
3
|
+
#
|
|
4
|
+
# A top-of-file 'load lib {name}' (or 'load ext {name}') directive,
|
|
5
|
+
# run by the loader itself before the declarations that follow it.
|
|
6
|
+
#
|
|
7
|
+
|
|
8
|
+
module Gloo
|
|
9
|
+
module Persist
|
|
10
|
+
module Source
|
|
11
|
+
class DirectiveNode
|
|
12
|
+
|
|
13
|
+
attr_reader :raw
|
|
14
|
+
|
|
15
|
+
#
|
|
16
|
+
# Set up a directive node. raw is the full source line, minus
|
|
17
|
+
# its trailing newline.
|
|
18
|
+
#
|
|
19
|
+
def initialize( raw )
|
|
20
|
+
@raw = raw
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|