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
|
@@ -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,25 +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
|
-
next if skip_line? line
|
|
62
|
-
|
|
63
|
-
if !@body_started && line =~ LIB_DIRECTIVE
|
|
64
|
-
run_lib_directive line
|
|
65
|
-
next
|
|
66
|
-
end
|
|
67
|
-
@body_started = true
|
|
68
|
-
|
|
69
|
-
handle_one_line line
|
|
70
|
-
end
|
|
76
|
+
f.each_line { |line| dispatch_line( line ) }
|
|
77
|
+
finish
|
|
71
78
|
end
|
|
72
79
|
|
|
73
80
|
#
|
|
@@ -88,115 +95,195 @@ module Gloo
|
|
|
88
95
|
return data.gsub( "\\\n", '' )
|
|
89
96
|
end
|
|
90
97
|
|
|
98
|
+
# ---------------------------------------------------------------------
|
|
99
|
+
# Per-line dispatch
|
|
100
|
+
# ---------------------------------------------------------------------
|
|
101
|
+
|
|
91
102
|
#
|
|
92
|
-
#
|
|
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.
|
|
93
107
|
#
|
|
94
|
-
def
|
|
95
|
-
if
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
else
|
|
104
|
-
@block_value << line
|
|
105
|
-
end
|
|
106
|
-
else
|
|
107
|
-
determine_indent line
|
|
108
|
-
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
|
|
109
117
|
end
|
|
118
|
+
@body_started = true
|
|
119
|
+
|
|
120
|
+
handle_declaration_line( line )
|
|
110
121
|
end
|
|
111
122
|
|
|
112
123
|
#
|
|
113
|
-
#
|
|
114
|
-
#
|
|
124
|
+
# End of file: close out anything still open so its content
|
|
125
|
+
# isn't silently dropped.
|
|
115
126
|
#
|
|
116
|
-
def
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
return true if line[ 0 ] == '#'
|
|
120
|
-
|
|
121
|
-
return false
|
|
127
|
+
def finish
|
|
128
|
+
@body.finish
|
|
129
|
+
@comments.flush_into( @indent_stack.node.children )
|
|
122
130
|
end
|
|
123
131
|
|
|
132
|
+
# ---------------------------------------------------------------------
|
|
133
|
+
# Trivia (comments and blank lines)
|
|
134
|
+
# ---------------------------------------------------------------------
|
|
135
|
+
|
|
124
136
|
#
|
|
125
|
-
#
|
|
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.
|
|
126
142
|
#
|
|
127
|
-
def
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
@
|
|
133
|
-
@indent = 1
|
|
134
|
-
elsif tabs < @tabs # outdent
|
|
135
|
-
diff = @tabs - tabs
|
|
136
|
-
@tabs -= diff
|
|
137
|
-
@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 ) )
|
|
138
149
|
end
|
|
139
|
-
puts "tabs: #{@tabs}, indent: #{@indent}, line: #{line}" if @debug
|
|
140
150
|
end
|
|
141
151
|
|
|
152
|
+
# ---------------------------------------------------------------------
|
|
153
|
+
# BEGIN / END blocks
|
|
154
|
+
# ---------------------------------------------------------------------
|
|
155
|
+
|
|
142
156
|
#
|
|
143
|
-
#
|
|
157
|
+
# Inside a BEGIN/END block every line is literal content -- kept
|
|
158
|
+
# exactly as-is, comments and blank lines included.
|
|
144
159
|
#
|
|
145
|
-
def
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
@
|
|
150
|
-
@exiting_multiline = true
|
|
151
|
-
end
|
|
152
|
-
|
|
153
|
-
if @in_multiline
|
|
154
|
-
@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 )
|
|
155
165
|
else
|
|
156
|
-
|
|
157
|
-
process_obj_line line
|
|
166
|
+
@block_value << line
|
|
158
167
|
end
|
|
159
168
|
end
|
|
160
169
|
|
|
170
|
+
# ---------------------------------------------------------------------
|
|
171
|
+
# Script bodies (indented lines, no BEGIN/END)
|
|
172
|
+
# ---------------------------------------------------------------------
|
|
173
|
+
|
|
161
174
|
#
|
|
162
|
-
#
|
|
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.
|
|
163
177
|
#
|
|
164
|
-
def
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
end
|
|
178
|
+
def handle_body_line( line )
|
|
179
|
+
consumed = @body.handle_line( line, tab_count( line ) )
|
|
180
|
+
dispatch_line( line ) unless consumed
|
|
181
|
+
end
|
|
169
182
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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
|
|
178
195
|
end
|
|
196
|
+
|
|
197
|
+
finalize_declaration( line )
|
|
179
198
|
end
|
|
180
199
|
|
|
181
200
|
#
|
|
182
|
-
#
|
|
201
|
+
# Place the declaration at its indentation level and create the
|
|
202
|
+
# object (and its Source::ObjNode).
|
|
183
203
|
#
|
|
184
|
-
def
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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 }
|
|
191
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
|
|
192
226
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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 ] == '#'
|
|
198
285
|
|
|
199
|
-
|
|
286
|
+
return false
|
|
200
287
|
end
|
|
201
288
|
|
|
202
289
|
#
|
|
@@ -220,10 +307,24 @@ module Gloo
|
|
|
220
307
|
# Split the line into 3 parts.
|
|
221
308
|
#
|
|
222
309
|
def split_line( line )
|
|
223
|
-
o = LineSplitter.new( line, @tabs )
|
|
310
|
+
o = LineSplitter.new( line, @indent_stack&.tabs || 0 )
|
|
224
311
|
return o.split
|
|
225
312
|
end
|
|
226
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
|
+
|
|
227
328
|
end
|
|
228
329
|
end
|
|
229
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
|