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
|
@@ -1,7 +1,18 @@
|
|
|
1
1
|
# Author:: Eric Crane (mailto:eric.crane@mac.com)
|
|
2
2
|
# Copyright:: Copyright (c) 2019 Eric Crane. All rights reserved.
|
|
3
3
|
#
|
|
4
|
-
# Helper class used to load a file
|
|
4
|
+
# Helper class used to load a file, create objects in the heap, and
|
|
5
|
+
# build a parallel Source::SourceDoc capturing everything about the
|
|
6
|
+
# file that the heap alone can't represent (comments, blank lines,
|
|
7
|
+
# raw formatting) -- so a later save can rewrite the file instead of
|
|
8
|
+
# regenerating it from scratch.
|
|
9
|
+
#
|
|
10
|
+
# Work is delegated to CommentBuffer (comment buffering),
|
|
11
|
+
# ScriptBodyCollector (script bodies), IndentStack (nesting shared by
|
|
12
|
+
# the heap and source trees), ShorthandExpander (nested-container
|
|
13
|
+
# shorthand), and DeclarationLedger (this file's roots + cross-file
|
|
14
|
+
# clash warnings) -- this class is the orchestrator: per-line dispatch
|
|
15
|
+
# and BEGIN/END handling.
|
|
5
16
|
#
|
|
6
17
|
|
|
7
18
|
module Gloo
|
|
@@ -20,7 +31,7 @@ module Gloo
|
|
|
20
31
|
# afterward.
|
|
21
32
|
LIB_DIRECTIVE = /\A(?:load|ld)\s+(?:lib|ext)\s+\S+\s*\z/i.freeze
|
|
22
33
|
|
|
23
|
-
attr_reader :obj
|
|
34
|
+
attr_reader :obj, :source_doc
|
|
24
35
|
|
|
25
36
|
#
|
|
26
37
|
# Set up a file storage for an object.
|
|
@@ -29,16 +40,25 @@ module Gloo
|
|
|
29
40
|
@engine = engine
|
|
30
41
|
@mech = @engine.platform.get_file_mech( @engine )
|
|
31
42
|
@pn = pn
|
|
32
|
-
@tabs = 0
|
|
33
43
|
@obj = nil
|
|
34
|
-
@
|
|
35
|
-
@
|
|
44
|
+
@source_doc = Gloo::Persist::Source::SourceDoc.new
|
|
45
|
+
@comments = Gloo::Persist::CommentBuffer.new
|
|
46
|
+
@body = Gloo::Persist::ScriptBodyCollector.new
|
|
47
|
+
@shorthand = Gloo::Persist::ShorthandExpander.new( engine )
|
|
48
|
+
@ledger = Gloo::Persist::DeclarationLedger.new( engine, pn )
|
|
36
49
|
@in_block = false
|
|
37
50
|
@block_value = ''
|
|
38
51
|
@body_started = false
|
|
39
52
|
@debug = false
|
|
40
53
|
end
|
|
41
54
|
|
|
55
|
+
#
|
|
56
|
+
# The top-level objects this file declared (for save resolution).
|
|
57
|
+
#
|
|
58
|
+
def roots
|
|
59
|
+
return @ledger.roots
|
|
60
|
+
end
|
|
61
|
+
|
|
42
62
|
#
|
|
43
63
|
# Load the objects from the file.
|
|
44
64
|
#
|
|
@@ -49,27 +69,12 @@ module Gloo
|
|
|
49
69
|
end
|
|
50
70
|
|
|
51
71
|
@engine.log.debug "Loading file '#{@pn}'"
|
|
52
|
-
@
|
|
53
|
-
@parent_stack = []
|
|
54
|
-
@parent = @engine.heap.root
|
|
55
|
-
@parent_stack.push @parent
|
|
72
|
+
@indent_stack = IndentStack.new( @engine.heap.root, @source_doc )
|
|
56
73
|
f = @mech.read( @pn )
|
|
57
74
|
|
|
58
75
|
f = join_continuations( f )
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
# Inside a BEGIN/END block every line is literal content -
|
|
62
|
-
# don't skip blank lines or lines that start with '#'.
|
|
63
|
-
next if !@in_block && skip_line?( line )
|
|
64
|
-
|
|
65
|
-
if !@body_started && line =~ LIB_DIRECTIVE
|
|
66
|
-
run_lib_directive line
|
|
67
|
-
next
|
|
68
|
-
end
|
|
69
|
-
@body_started = true
|
|
70
|
-
|
|
71
|
-
handle_one_line line
|
|
72
|
-
end
|
|
76
|
+
f.each_line { |line| dispatch_line( line ) }
|
|
77
|
+
finish
|
|
73
78
|
end
|
|
74
79
|
|
|
75
80
|
#
|
|
@@ -90,115 +95,195 @@ module Gloo
|
|
|
90
95
|
return data.gsub( "\\\n", '' )
|
|
91
96
|
end
|
|
92
97
|
|
|
98
|
+
# ---------------------------------------------------------------------
|
|
99
|
+
# Per-line dispatch
|
|
100
|
+
# ---------------------------------------------------------------------
|
|
101
|
+
|
|
93
102
|
#
|
|
94
|
-
#
|
|
103
|
+
# Route one line of the file to whichever mode we're currently in:
|
|
104
|
+
# collecting a script body, collecting a BEGIN/END block, trivia
|
|
105
|
+
# (comment/blank), a top-of-file directive, or a normal
|
|
106
|
+
# declaration.
|
|
95
107
|
#
|
|
96
|
-
def
|
|
97
|
-
if
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
else
|
|
106
|
-
@block_value << line
|
|
107
|
-
end
|
|
108
|
-
else
|
|
109
|
-
determine_indent line
|
|
110
|
-
process_line line
|
|
108
|
+
def dispatch_line( line )
|
|
109
|
+
return handle_body_line( line ) if @body.active
|
|
110
|
+
return handle_block_line( line ) if @in_block
|
|
111
|
+
return handle_trivia_line( line ) if skip_line?( line )
|
|
112
|
+
|
|
113
|
+
if !@body_started && line =~ LIB_DIRECTIVE
|
|
114
|
+
@indent_stack.node.children << Source::DirectiveNode.new( line.strip )
|
|
115
|
+
run_lib_directive line
|
|
116
|
+
return
|
|
111
117
|
end
|
|
118
|
+
@body_started = true
|
|
119
|
+
|
|
120
|
+
handle_declaration_line( line )
|
|
112
121
|
end
|
|
113
122
|
|
|
114
123
|
#
|
|
115
|
-
#
|
|
116
|
-
#
|
|
124
|
+
# End of file: close out anything still open so its content
|
|
125
|
+
# isn't silently dropped.
|
|
117
126
|
#
|
|
118
|
-
def
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
return true if line[ 0 ] == '#'
|
|
122
|
-
|
|
123
|
-
return false
|
|
127
|
+
def finish
|
|
128
|
+
@body.finish
|
|
129
|
+
@comments.flush_into( @indent_stack.node.children )
|
|
124
130
|
end
|
|
125
131
|
|
|
132
|
+
# ---------------------------------------------------------------------
|
|
133
|
+
# Trivia (comments and blank lines)
|
|
134
|
+
# ---------------------------------------------------------------------
|
|
135
|
+
|
|
126
136
|
#
|
|
127
|
-
#
|
|
137
|
+
# A comment or blank line, outside of any block/body. A comment
|
|
138
|
+
# is buffered -- it may turn out to be the leading_doc for the
|
|
139
|
+
# declaration that follows. A blank line always breaks that
|
|
140
|
+
# association (detaches any buffered comments as floating nodes)
|
|
141
|
+
# and is itself kept, not discarded.
|
|
128
142
|
#
|
|
129
|
-
def
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
@
|
|
135
|
-
@indent = 1
|
|
136
|
-
elsif tabs < @tabs # outdent
|
|
137
|
-
diff = @tabs - tabs
|
|
138
|
-
@tabs -= diff
|
|
139
|
-
@indent -= diff
|
|
143
|
+
def handle_trivia_line( line )
|
|
144
|
+
if line.strip.empty?
|
|
145
|
+
@comments.flush_into( @indent_stack.node.children )
|
|
146
|
+
@indent_stack.node.children << Source::BlankNode.new( chomped( line ) )
|
|
147
|
+
else
|
|
148
|
+
@comments.push( chomped( line ), tab_count( line ) )
|
|
140
149
|
end
|
|
141
|
-
puts "tabs: #{@tabs}, indent: #{@indent}, line: #{line}" if @debug
|
|
142
150
|
end
|
|
143
151
|
|
|
152
|
+
# ---------------------------------------------------------------------
|
|
153
|
+
# BEGIN / END blocks
|
|
154
|
+
# ---------------------------------------------------------------------
|
|
155
|
+
|
|
144
156
|
#
|
|
145
|
-
#
|
|
157
|
+
# Inside a BEGIN/END block every line is literal content -- kept
|
|
158
|
+
# exactly as-is, comments and blank lines included.
|
|
146
159
|
#
|
|
147
|
-
def
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
@
|
|
152
|
-
@exiting_multiline = true
|
|
153
|
-
end
|
|
154
|
-
|
|
155
|
-
if @in_multiline
|
|
156
|
-
@last.add_line line
|
|
160
|
+
def handle_block_line( line )
|
|
161
|
+
if line.strip == END_BLOCK
|
|
162
|
+
@in_block = false
|
|
163
|
+
@block_end_indent = leading_ws( line )
|
|
164
|
+
finalize_declaration( @save_line )
|
|
157
165
|
else
|
|
158
|
-
|
|
159
|
-
process_obj_line line
|
|
166
|
+
@block_value << line
|
|
160
167
|
end
|
|
161
168
|
end
|
|
162
169
|
|
|
170
|
+
# ---------------------------------------------------------------------
|
|
171
|
+
# Script bodies (indented lines, no BEGIN/END)
|
|
172
|
+
# ---------------------------------------------------------------------
|
|
173
|
+
|
|
163
174
|
#
|
|
164
|
-
#
|
|
175
|
+
# One line while a script body might be open. Re-dispatches the
|
|
176
|
+
# line normally if it turns out to have closed the body.
|
|
165
177
|
#
|
|
166
|
-
def
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
end
|
|
178
|
+
def handle_body_line( line )
|
|
179
|
+
consumed = @body.handle_line( line, tab_count( line ) )
|
|
180
|
+
dispatch_line( line ) unless consumed
|
|
181
|
+
end
|
|
171
182
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
183
|
+
# ---------------------------------------------------------------------
|
|
184
|
+
# Object declarations
|
|
185
|
+
# ---------------------------------------------------------------------
|
|
186
|
+
|
|
187
|
+
#
|
|
188
|
+
# A line that starts (or is) an object declaration.
|
|
189
|
+
#
|
|
190
|
+
def handle_declaration_line( line )
|
|
191
|
+
if line.strip.end_with?( BEGIN_BLOCK )
|
|
192
|
+
@in_block = true
|
|
193
|
+
@save_line = line
|
|
194
|
+
return
|
|
180
195
|
end
|
|
196
|
+
|
|
197
|
+
finalize_declaration( line )
|
|
181
198
|
end
|
|
182
199
|
|
|
183
200
|
#
|
|
184
|
-
#
|
|
201
|
+
# Place the declaration at its indentation level and create the
|
|
202
|
+
# object (and its Source::ObjNode).
|
|
185
203
|
#
|
|
186
|
-
def
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
204
|
+
def finalize_declaration( line )
|
|
205
|
+
line_tabs = tab_count( line )
|
|
206
|
+
@indent_stack.place( line_tabs, @last, @last_node )
|
|
207
|
+
create_declared_obj( line, line_tabs )
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
#
|
|
211
|
+
# Create the heap object and its source node for one declaration
|
|
212
|
+
# line, and start body collection if its type calls for one.
|
|
213
|
+
#
|
|
214
|
+
def create_declared_obj( line, line_tabs )
|
|
215
|
+
name, type, value, style = split_declaration( line )
|
|
216
|
+
leaf, parent, roots, created = @shorthand.expand( name, @indent_stack.parent )
|
|
217
|
+
roots.each { |r| @ledger.root( r ) }
|
|
218
|
+
created.each { |c| @ledger.created( c ) }
|
|
219
|
+
|
|
220
|
+
prior = parent.find_child( leaf )
|
|
221
|
+
params = { :name => leaf, :type => type, :value => value, :parent => parent }
|
|
193
222
|
@last = @engine.factory.create( params )
|
|
223
|
+
@ledger.clash( prior, leaf, value ) if prior
|
|
224
|
+
@ledger.created( @last )
|
|
225
|
+
@ledger.root( @last ) if parent == @engine.heap.root
|
|
194
226
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
227
|
+
node = build_obj_node( leading_ws( line ), name, type, value, style )
|
|
228
|
+
node.leading_doc = @comments.take_leading_doc( line_tabs, @indent_stack.node.children )
|
|
229
|
+
@indent_stack.node.children << node
|
|
230
|
+
@last_node = node
|
|
231
|
+
@obj ||= @ledger.roots.last || @last
|
|
232
|
+
|
|
233
|
+
# A whitespace-only inline value (trailing spaces after the ':'
|
|
234
|
+
# on a `script :` line) counts as no value -- start the body.
|
|
235
|
+
no_value = value.nil? || value.strip.empty?
|
|
236
|
+
@body.start( node, @last, @indent_stack.tabs ) if no_value && @last&.multiline_value?
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
#
|
|
240
|
+
# Split a declaration line into name/type/value, plus a style
|
|
241
|
+
# hash (:block_style, :raw_tail), folding in any BEGIN/END block
|
|
242
|
+
# value collected just before it. raw_tail is the exact source
|
|
243
|
+
# text that followed the type bracket on this line -- eg.
|
|
244
|
+
# " : BEGIN" or " :" -- kept for a save to reproduce the
|
|
245
|
+
# declaration's original spacing when unchanged.
|
|
246
|
+
#
|
|
247
|
+
def split_declaration( line )
|
|
248
|
+
splitter = LineSplitter.new( line, @indent_stack&.tabs || 0 )
|
|
249
|
+
name, type, value = splitter.split
|
|
250
|
+
style = { :raw_tail => splitter.raw_tail, :block_style => :inline }
|
|
251
|
+
return name, type, value, style if @block_value == ''
|
|
252
|
+
|
|
253
|
+
value = @block_value
|
|
254
|
+
@block_value = ''
|
|
255
|
+
style[ :block_style ] = :begin_end
|
|
256
|
+
style[ :raw_end_indent ] = @block_end_indent
|
|
257
|
+
return name, type, value, style
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
#
|
|
261
|
+
# Build the source node for one object declaration. raw_value is
|
|
262
|
+
# kept byte-exact with whatever was handed to the heap object's
|
|
263
|
+
# set_value (trailing newline included, for a BEGIN/END block) so
|
|
264
|
+
# a save can compare the two directly.
|
|
265
|
+
#
|
|
266
|
+
def build_obj_node( raw_indent, name, type, value, style )
|
|
267
|
+
node = Source::ObjNode.new( :name => name, :raw_type => type )
|
|
268
|
+
node.raw_indent = raw_indent
|
|
269
|
+
node.raw_tail = style[ :raw_tail ]
|
|
270
|
+
node.block_style = style[ :block_style ]
|
|
271
|
+
node.raw_value = value
|
|
272
|
+
node.raw_end_indent = style[ :raw_end_indent ] || ''
|
|
273
|
+
node.obj = @last
|
|
274
|
+
return node
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
#
|
|
278
|
+
# Is this line a comment or a blank line?
|
|
279
|
+
# If so we'll skip it.
|
|
280
|
+
#
|
|
281
|
+
def skip_line?( line )
|
|
282
|
+
line = line.strip
|
|
283
|
+
return true if line.empty?
|
|
284
|
+
return true if line[ 0 ] == '#'
|
|
200
285
|
|
|
201
|
-
|
|
286
|
+
return false
|
|
202
287
|
end
|
|
203
288
|
|
|
204
289
|
#
|
|
@@ -222,10 +307,24 @@ module Gloo
|
|
|
222
307
|
# Split the line into 3 parts.
|
|
223
308
|
#
|
|
224
309
|
def split_line( line )
|
|
225
|
-
o = LineSplitter.new( line, @tabs )
|
|
310
|
+
o = LineSplitter.new( line, @indent_stack&.tabs || 0 )
|
|
226
311
|
return o.split
|
|
227
312
|
end
|
|
228
313
|
|
|
314
|
+
#
|
|
315
|
+
# The line's leading whitespace, exactly as written.
|
|
316
|
+
#
|
|
317
|
+
def leading_ws( line )
|
|
318
|
+
return line[ /\A[ \t]*/ ]
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
#
|
|
322
|
+
# A line's raw text with its trailing newline removed.
|
|
323
|
+
#
|
|
324
|
+
def chomped( line )
|
|
325
|
+
return line.chomp( "\n" )
|
|
326
|
+
end
|
|
327
|
+
|
|
229
328
|
end
|
|
230
329
|
end
|
|
231
330
|
end
|
|
@@ -3,26 +3,52 @@
|
|
|
3
3
|
#
|
|
4
4
|
# Helper class used to save an object to a file.
|
|
5
5
|
#
|
|
6
|
+
# Given a Source::SourceDoc (captured at load time), this is a
|
|
7
|
+
# rewriter, not a regenerator: trivia (comments, blank lines,
|
|
8
|
+
# directives) is emitted verbatim, and a declaration's raw text is
|
|
9
|
+
# kept as-is unless the live object's value has actually changed --
|
|
10
|
+
# only then is the value re-rendered, via the object's own
|
|
11
|
+
# serialize_value. Deleted objects drop out; objects created since
|
|
12
|
+
# load are rendered fresh and appended.
|
|
13
|
+
#
|
|
14
|
+
# With no SourceDoc at all (the object was never loaded from a file),
|
|
15
|
+
# this falls back to plain regeneration from the heap, so saving a
|
|
16
|
+
# brand-new in-memory object still works.
|
|
17
|
+
#
|
|
6
18
|
|
|
7
19
|
module Gloo
|
|
8
20
|
module Persist
|
|
9
21
|
class FileSaver
|
|
10
22
|
|
|
11
23
|
#
|
|
12
|
-
# Set up a file
|
|
24
|
+
# Set up a file saver. source_doc is optional -- when given, the
|
|
25
|
+
# save rewrites it in place; otherwise the file is regenerated
|
|
26
|
+
# from the heap.
|
|
27
|
+
#
|
|
28
|
+
# For a multi-file namespace to round-trip, batch carries two sets
|
|
29
|
+
# (PersistMan#save_batch builds them):
|
|
30
|
+
# - :others -- objects declared by *other* loaded files'
|
|
31
|
+
# SourceDocs; this saver leaves them out even though they're
|
|
32
|
+
# live children of a container it shares.
|
|
33
|
+
# - :claimed -- a set shared across the whole save batch; a
|
|
34
|
+
# brand-new object no file owns is written by the first saver to
|
|
35
|
+
# reach it, which adds it here so the rest skip it.
|
|
13
36
|
#
|
|
14
|
-
def initialize( engine, pn, obj )
|
|
37
|
+
def initialize( engine, pn, obj, source_doc = nil, batch = nil )
|
|
15
38
|
@engine = engine
|
|
16
39
|
@mech = @engine.platform.get_file_mech( @engine )
|
|
17
40
|
@pn = pn
|
|
18
41
|
@obj = obj
|
|
42
|
+
@source_doc = source_doc
|
|
43
|
+
@others = batch && batch[ :others ]
|
|
44
|
+
@claimed = batch && batch[ :claimed ]
|
|
19
45
|
end
|
|
20
46
|
|
|
21
47
|
#
|
|
22
48
|
# Save the object to the file.
|
|
23
49
|
#
|
|
24
50
|
def save
|
|
25
|
-
data =
|
|
51
|
+
data = @source_doc ? rewrite : regenerate
|
|
26
52
|
@mech.write( @pn, data )
|
|
27
53
|
end
|
|
28
54
|
|
|
@@ -34,18 +60,237 @@ module Gloo
|
|
|
34
60
|
end
|
|
35
61
|
|
|
36
62
|
#
|
|
37
|
-
#
|
|
38
|
-
#
|
|
63
|
+
# Would saving now actually change anything on disk? True if any
|
|
64
|
+
# known declaration's value no longer matches its raw source, or
|
|
65
|
+
# its object was deleted. (Doesn't detect a brand-new object with
|
|
66
|
+
# no source node of its own -- an approximation, not a guarantee,
|
|
67
|
+
# good enough to warn before a reload discards changes.)
|
|
68
|
+
#
|
|
69
|
+
def dirty?
|
|
70
|
+
return false unless @source_doc
|
|
71
|
+
|
|
72
|
+
return any_dirty?( @source_doc.children, @engine.heap.root )
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
#
|
|
76
|
+
# Convert an object to textual representation from scratch, with
|
|
77
|
+
# no source text to preserve. This is a recursive function, and
|
|
78
|
+
# is also reused by the rewriter for any object created since
|
|
79
|
+
# load (which has no source node of its own to rewrite).
|
|
39
80
|
#
|
|
40
81
|
def get_obj( obj, indent = 0 )
|
|
41
82
|
t = tabs( indent )
|
|
42
|
-
str = "#{t}#{obj.name} [#{obj.type_display}]
|
|
83
|
+
str = "#{t}#{obj.name} [#{obj.type_display}]#{obj.serialize_value( indent )}\n"
|
|
43
84
|
obj.children.each do |child|
|
|
44
85
|
str << get_obj( child, indent + 1 )
|
|
45
86
|
end
|
|
46
87
|
return str
|
|
47
88
|
end
|
|
48
89
|
|
|
90
|
+
private
|
|
91
|
+
|
|
92
|
+
#
|
|
93
|
+
# Regenerate the whole file from the heap (the pre-Source::SourceDoc
|
|
94
|
+
# behavior), used when there's no source text to rewrite.
|
|
95
|
+
#
|
|
96
|
+
def regenerate
|
|
97
|
+
return get_obj( @obj )
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
#
|
|
101
|
+
# Does any node in the given list represent a change from what's
|
|
102
|
+
# on disk -- its object gone from live_parent's children (an
|
|
103
|
+
# untracked deletion counts too), or its value changed?
|
|
104
|
+
#
|
|
105
|
+
def any_dirty?( nodes, live_parent )
|
|
106
|
+
nodes.any? do |node|
|
|
107
|
+
next false unless node.is_a?( Source::ObjNode )
|
|
108
|
+
|
|
109
|
+
_top, leaf = locate( node, live_parent )
|
|
110
|
+
next true unless leaf
|
|
111
|
+
next true unless value_unchanged?( node )
|
|
112
|
+
|
|
113
|
+
any_dirty?( node.children, leaf )
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# ---------------------------------------------------------------------
|
|
118
|
+
# Rewrite (Source::SourceDoc-driven)
|
|
119
|
+
# ---------------------------------------------------------------------
|
|
120
|
+
|
|
121
|
+
#
|
|
122
|
+
# Rewrite the file from its source document: known declarations
|
|
123
|
+
# are patched in place, deleted ones dropped, and anything new
|
|
124
|
+
# appended.
|
|
125
|
+
#
|
|
126
|
+
def rewrite
|
|
127
|
+
return render_children( @source_doc.children, @engine.heap.root, 0 )
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
#
|
|
131
|
+
# Render one level of source nodes against the live children of
|
|
132
|
+
# live_parent: trivia passes through untouched; a declaration
|
|
133
|
+
# whose object is still present is patched in place; one whose
|
|
134
|
+
# object is gone is dropped; anything in live_parent.children
|
|
135
|
+
# with no matching source node is new and gets rendered fresh
|
|
136
|
+
# (unless another file owns it -- see #owned).
|
|
137
|
+
#
|
|
138
|
+
def render_children( nodes, live_parent, indent )
|
|
139
|
+
str = ''
|
|
140
|
+
seen = []
|
|
141
|
+
nodes.each do |node|
|
|
142
|
+
if node.is_a?( Source::ObjNode )
|
|
143
|
+
top, leaf = locate( node, live_parent )
|
|
144
|
+
next unless leaf
|
|
145
|
+
|
|
146
|
+
seen << top
|
|
147
|
+
str << render_obj_node( node, indent )
|
|
148
|
+
else
|
|
149
|
+
str << render_trivia( node )
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
( live_parent.children - seen ).each do |child|
|
|
154
|
+
next unless write_new_child?( child )
|
|
155
|
+
|
|
156
|
+
str << get_obj( child, indent )
|
|
157
|
+
end
|
|
158
|
+
return str
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
#
|
|
162
|
+
# A live child with no source node at this level. Write it unless
|
|
163
|
+
# another file owns it, or another file in this batch already
|
|
164
|
+
# wrote it as a brand-new object. A genuinely new object is
|
|
165
|
+
# written here and claimed.
|
|
166
|
+
#
|
|
167
|
+
def write_new_child?( child )
|
|
168
|
+
return false if @others&.key?( child )
|
|
169
|
+
return false if @claimed&.key?( child )
|
|
170
|
+
|
|
171
|
+
@claimed[ child ] = true if @claimed
|
|
172
|
+
return true
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
#
|
|
176
|
+
# Resolve a source node against the live tree under live_parent.
|
|
177
|
+
# A plain name is a direct child; a dotted name (nested container
|
|
178
|
+
# shorthand, page.core.users.list) walks the chain. Returns
|
|
179
|
+
# [top-level object to mark as handled, the node's own object],
|
|
180
|
+
# or [nil, nil] if the node's object is no longer there.
|
|
181
|
+
#
|
|
182
|
+
def locate( node, live_parent )
|
|
183
|
+
return [ nil, nil ] unless node.obj
|
|
184
|
+
|
|
185
|
+
segments = node.name.split( '.' )
|
|
186
|
+
if segments.length == 1
|
|
187
|
+
return live_parent.children.include?( node.obj ) ? [ node.obj, node.obj ] : [ nil, nil ]
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
top = live_parent.find_child( segments.first )
|
|
191
|
+
return [ nil, nil ] unless top
|
|
192
|
+
|
|
193
|
+
leaf = top
|
|
194
|
+
segments[ 1..-1 ].each do |seg|
|
|
195
|
+
leaf = leaf.find_child( seg )
|
|
196
|
+
return [ nil, nil ] unless leaf
|
|
197
|
+
end
|
|
198
|
+
return leaf.equal?( node.obj ) ? [ top, leaf ] : [ nil, nil ]
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
#
|
|
202
|
+
# A comment, blank line, or top-of-file directive -- passed
|
|
203
|
+
# through exactly as read.
|
|
204
|
+
#
|
|
205
|
+
def render_trivia( node )
|
|
206
|
+
return "#{node.raw}\n"
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
#
|
|
210
|
+
# One declaration whose object is still live: its leading_doc (if
|
|
211
|
+
# any) and its own line are kept (name/type/indent never change
|
|
212
|
+
# once declared), its value is kept raw or re-rendered depending
|
|
213
|
+
# on whether it changed, and its children are rewritten the same
|
|
214
|
+
# way, one level deeper.
|
|
215
|
+
#
|
|
216
|
+
def render_obj_node( node, indent )
|
|
217
|
+
str = node.leading_doc ? "#{node.leading_doc}\n" : ''
|
|
218
|
+
str << "#{node.raw_indent}#{node.name} [#{node.raw_type}]#{declaration_tail( node, indent )}\n"
|
|
219
|
+
str << render_children( node.children, node.obj, indent + 1 )
|
|
220
|
+
return str
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
#
|
|
224
|
+
# The text that follows "name [type]" on the declaration line:
|
|
225
|
+
# the raw source, unless the live value no longer matches it.
|
|
226
|
+
#
|
|
227
|
+
def declaration_tail( node, indent )
|
|
228
|
+
return raw_declaration_tail( node ) if value_unchanged?( node )
|
|
229
|
+
|
|
230
|
+
return node.obj.serialize_value( indent )
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
#
|
|
234
|
+
# Reproduce the original declaration's value text exactly as it
|
|
235
|
+
# was read. raw_tail is the declaration line's own exact tail
|
|
236
|
+
# (eg. " : BEGIN" or " :"); a begin_end/body style has further
|
|
237
|
+
# raw content following it.
|
|
238
|
+
#
|
|
239
|
+
def raw_declaration_tail( node )
|
|
240
|
+
case node.block_style
|
|
241
|
+
when :begin_end
|
|
242
|
+
return "#{node.raw_tail}\n#{node.raw_value}#{node.raw_end_indent}END"
|
|
243
|
+
when :body
|
|
244
|
+
return node.raw_value.to_s.empty? ? node.raw_tail : "#{node.raw_tail}\n#{node.raw_value}"
|
|
245
|
+
else
|
|
246
|
+
return node.raw_tail
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
# ---------------------------------------------------------------------
|
|
251
|
+
# Has the value actually changed since load?
|
|
252
|
+
# ---------------------------------------------------------------------
|
|
253
|
+
|
|
254
|
+
#
|
|
255
|
+
# Does the live object's value still match what raw_value would
|
|
256
|
+
# produce? A script body is compared line by line (raw comments
|
|
257
|
+
# excluded, since they were never run); everything else is
|
|
258
|
+
# compared by re-running the raw text through the same set_value
|
|
259
|
+
# conversion the loader used, and comparing the result.
|
|
260
|
+
#
|
|
261
|
+
def value_unchanged?( node )
|
|
262
|
+
return false unless node.obj
|
|
263
|
+
return body_value_unchanged?( node ) if node.block_style == :body
|
|
264
|
+
|
|
265
|
+
trial = node.obj.class.new( @engine )
|
|
266
|
+
trial.set_value( node.raw_value )
|
|
267
|
+
return trial.value == node.obj.value
|
|
268
|
+
rescue
|
|
269
|
+
return false
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
#
|
|
273
|
+
# A script body's raw text, minus its comment lines (never
|
|
274
|
+
# executed) and with each line stripped, should equal the live
|
|
275
|
+
# object's own command lines.
|
|
276
|
+
#
|
|
277
|
+
def body_value_unchanged?( node )
|
|
278
|
+
raw_lines = node.raw_value.to_s.split( "\n" )
|
|
279
|
+
executable = raw_lines.reject { |l| l.strip.start_with?( '#' ) }.map( &:strip )
|
|
280
|
+
return executable == current_script_lines( node.obj )
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
#
|
|
284
|
+
# The live object's command lines, whatever form its value is
|
|
285
|
+
# currently in.
|
|
286
|
+
#
|
|
287
|
+
def current_script_lines( obj )
|
|
288
|
+
return obj.value if obj.value_is_array?
|
|
289
|
+
return [] if obj.value_is_blank?
|
|
290
|
+
|
|
291
|
+
return [ obj.value.to_s.strip ]
|
|
292
|
+
end
|
|
293
|
+
|
|
49
294
|
end
|
|
50
295
|
end
|
|
51
296
|
end
|