gloo 6.3.1 → 6.5.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/application.md +9 -0
- data/docs/language_objects.md +78 -0
- data/docs/language_scripting.md +41 -0
- data/docs/objects.md +1 -1
- data/docs/verbs.md +36 -2
- data/lib/VERSION +1 -1
- data/lib/VERSION_NOTES +13 -0
- data/lib/gloo/app/engine.rb +5 -1
- data/lib/gloo/app/log.rb +5 -5
- data/lib/gloo/app/settings.rb +4 -1
- data/lib/gloo/core/obj.rb +60 -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 +208 -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 +193 -26
- 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 +91 -0
- data/lib/gloo/persist/source/source_doc.rb +41 -0
- data/lib/gloo/verbs/list.rb +24 -2
- data/lib/gloo/verbs/load.rb +9 -1
- data/lib/gloo/verbs/reload.rb +3 -0
- data/lib/gloo/verbs/save.rb +57 -10
- data/test.gloo/ctrl/each.test.gloo +28 -30
- data/test.gloo/dt/date.test.gloo +69 -71
- data/test.gloo/dt/datetime.test.gloo +55 -57
- data/test.gloo/dt/time.test.gloo +70 -72
- data/test.gloo/lang/continuation.test.gloo +25 -27
- data/test.gloo/lang/convert.test.gloo +50 -52
- data/test.gloo/lang/dt_comparisons.test.gloo +87 -89
- data/test.gloo/lang/exceptions.test.gloo +42 -44
- data/test.gloo/lang/gloo_sys.test.gloo +100 -102
- data/test.gloo/lang/here.test.gloo +32 -34
- data/test.gloo/lang/it.test.gloo +27 -29
- data/test.gloo/lang/literal.test.gloo +55 -57
- data/test.gloo/lang/load_lib_directive.test.gloo +18 -20
- data/test.gloo/lang/naming.test.gloo +28 -30
- data/test.gloo/lang/ops.test.gloo +53 -55
- data/test.gloo/lang/shorthand.test.gloo +27 -0
- data/test.gloo/math/add.test.gloo +40 -42
- data/test.gloo/math/div.test.gloo +23 -25
- data/test.gloo/math/mult.test.gloo +23 -25
- data/test.gloo/math/sub.test.gloo +23 -25
- data/test.gloo/objs/alias.test.gloo +15 -17
- data/test.gloo/objs/bool.test.gloo +54 -56
- data/test.gloo/objs/can.test.gloo +33 -35
- data/test.gloo/objs/cipher.test.gloo +39 -41
- data/test.gloo/objs/decimal.test.gloo +27 -29
- data/test.gloo/objs/erb.test.gloo +47 -49
- data/test.gloo/objs/file.test.gloo +68 -70
- data/test.gloo/objs/function.test.gloo +37 -39
- data/test.gloo/objs/int.test.gloo +54 -56
- data/test.gloo/objs/json.test.gloo +57 -50
- data/test.gloo/objs/obj.test.gloo +68 -48
- data/test.gloo/objs/outline.test.gloo +48 -50
- data/test.gloo/objs/password.test.gloo +34 -36
- data/test.gloo/objs/repeat.test.gloo +42 -44
- data/test.gloo/objs/script.test.gloo +13 -15
- data/test.gloo/objs/string.test.gloo +82 -84
- data/test.gloo/objs/text.test.gloo +90 -92
- data/test.gloo/objs/untyped.test.gloo +36 -38
- data/test.gloo/objs/uri.test.gloo +37 -39
- data/test.gloo/string/str.test.gloo +15 -0
- data/test.gloo/verbs/break.test.gloo +12 -14
- data/test.gloo/verbs/check.test.gloo +22 -24
- data/test.gloo/verbs/context.test.gloo +11 -13
- data/test.gloo/verbs/create.test.gloo +7 -9
- data/test.gloo/verbs/eval.test.gloo +10 -12
- data/test.gloo/verbs/exists.test.gloo +43 -45
- data/test.gloo/verbs/if.test.gloo +8 -10
- data/test.gloo/verbs/invoke.test.gloo +73 -75
- data/test.gloo/verbs/list.test.gloo +21 -23
- data/test.gloo/verbs/load.test.gloo +27 -21
- data/test.gloo/verbs/log.test.gloo +7 -9
- data/test.gloo/verbs/move.test.gloo +12 -14
- data/test.gloo/verbs/put.test.gloo +10 -12
- data/test.gloo/verbs/reload.test.gloo +55 -0
- data/test.gloo/verbs/run.test.gloo +8 -10
- data/test.gloo/verbs/save.test.gloo +111 -0
- data/test.gloo/verbs/show.test.gloo +26 -28
- data/test.gloo/verbs/tell.test.gloo +8 -10
- data/test.gloo/verbs/throw.test.gloo +31 -33
- data/test.gloo/verbs/unless.test.gloo +13 -15
- data/test.gloo/verbs/unload.test.gloo +10 -12
- metadata +14 -1
|
@@ -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
|
|
@@ -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
|
#
|