gloo 7.1.3 → 7.2
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/Rakefile +23 -0
- data/docs/application.md +47 -2
- data/docs/development.md +10 -0
- data/docs/language_objects.md +20 -15
- data/docs/language_scripting.md +6 -2
- data/docs/testing.md +4 -7
- data/docs/verbs.md +23 -3
- data/lib/VERSION +1 -1
- data/lib/VERSION_NOTES +8 -0
- data/lib/dependencies.rb +4 -1
- data/lib/gloo/app/engine.rb +18 -6
- data/lib/gloo/core/obj.rb +0 -23
- data/lib/gloo/objs/basic/script.rb +0 -23
- data/lib/gloo/persist/declaration_ledger.rb +9 -9
- data/lib/gloo/persist/declaration_writer.rb +208 -0
- data/lib/gloo/persist/disc_mech.rb +70 -39
- data/lib/gloo/persist/file_loader.rb +125 -66
- data/lib/gloo/persist/file_saver.rb +130 -198
- data/lib/gloo/persist/file_storage.rb +15 -4
- data/lib/gloo/persist/file_style.rb +89 -0
- data/lib/gloo/persist/line_splitter.rb +16 -1
- data/lib/gloo/persist/persist_man.rb +112 -30
- data/lib/gloo/persist/save_index.rb +211 -0
- data/lib/gloo/persist/script_body_collector.rb +16 -21
- data/lib/gloo/persist/source/blank_node.rb +10 -3
- data/lib/gloo/persist/source/comment_node.rb +10 -2
- data/lib/gloo/persist/source/directive_node.rb +10 -2
- data/lib/gloo/persist/source/obj_node.rb +174 -9
- data/lib/gloo/persist/source/source_doc.rb +54 -0
- data/lib/gloo/persist/subtree_extractor.rb +9 -37
- data/lib/gloo/persist/trivia_buffer.rb +7 -7
- data/lib/gloo/verbs/load.rb +27 -21
- data/lib/gloo/verbs/move.rb +42 -3
- data/lib/gloo/verbs/save.rb +26 -4
- data/test.gloo/app/exit_status.test.gloo +34 -0
- data/test.gloo/app/fixtures/exit_after_error.gloo +9 -0
- data/test.gloo/lang/declaration.test.gloo +36 -0
- data/test.gloo/lang/fixtures/mixed_indent.gloo +5 -0
- data/test.gloo/lang/naming.test.gloo +44 -6
- data/test.gloo/verbs/fixtures/load_beside.gloo +6 -0
- data/test.gloo/verbs/fixtures/lossless_text.gloo +7 -0
- data/test.gloo/verbs/fixtures/lossless_text_saved.gloo +9 -0
- data/test.gloo/verbs/load.test.gloo +18 -0
- data/test.gloo/verbs/move.test.gloo +39 -0
- data/test.gloo/verbs/save.test.gloo +239 -0
- metadata +10 -1
|
@@ -4,16 +4,20 @@
|
|
|
4
4
|
# Helper class used to save an object to a file.
|
|
5
5
|
#
|
|
6
6
|
# Given a Source::SourceDoc (captured at load time), this is a
|
|
7
|
-
# rewriter, not a regenerator
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
7
|
+
# rewriter, not a regenerator. Comments, blank lines and directives are
|
|
8
|
+
# written back exactly as read. A declaration whose object is still in
|
|
9
|
+
# its place is written back exactly too, unless it holds the object's
|
|
10
|
+
# value and the value changed: then its head is kept and the value is
|
|
11
|
+
# written again. A declaration whose object was deleted or moved away
|
|
12
|
+
# is dropped. A new object, or one moved in, is written after the last
|
|
13
|
+
# child of its parent's first declaration, in the file's style.
|
|
14
|
+
#
|
|
15
|
+
# The save builds a new source document for what it writes, so the
|
|
16
|
+
# file can be saved again (and checked for changes) the same way.
|
|
13
17
|
#
|
|
14
18
|
# With no SourceDoc at all (the object was never loaded from a file),
|
|
15
|
-
#
|
|
16
|
-
#
|
|
19
|
+
# the file is written from the heap, so saving a brand-new in-memory
|
|
20
|
+
# object still works.
|
|
17
21
|
#
|
|
18
22
|
|
|
19
23
|
module Gloo
|
|
@@ -22,17 +26,10 @@ module Gloo
|
|
|
22
26
|
|
|
23
27
|
#
|
|
24
28
|
# Set up a file saver. source_doc is optional -- when given, the
|
|
25
|
-
# save rewrites it
|
|
26
|
-
#
|
|
27
|
-
#
|
|
28
|
-
#
|
|
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.
|
|
29
|
+
# save rewrites it; otherwise the file is written from the heap.
|
|
30
|
+
# batch, when given, carries the SaveIndex shared by every file in
|
|
31
|
+
# one save (PersistMan#save_batch builds it); without one, the
|
|
32
|
+
# saver indexes every loaded file itself.
|
|
36
33
|
#
|
|
37
34
|
def initialize( engine, pn, obj, source_doc = nil, batch = nil )
|
|
38
35
|
@engine = engine
|
|
@@ -40,256 +37,191 @@ module Gloo
|
|
|
40
37
|
@pn = pn
|
|
41
38
|
@obj = obj
|
|
42
39
|
@source_doc = source_doc
|
|
43
|
-
@
|
|
44
|
-
@claimed = batch && batch[ :claimed ]
|
|
40
|
+
@index = batch && batch[ :index ]
|
|
45
41
|
end
|
|
46
42
|
|
|
47
43
|
#
|
|
48
|
-
# Save the object to the file.
|
|
44
|
+
# Save the object to the file. Returns the source document for
|
|
45
|
+
# what was written.
|
|
49
46
|
#
|
|
50
47
|
def save
|
|
51
|
-
|
|
52
|
-
@mech.write( @pn,
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
#
|
|
56
|
-
# Get string of tabs for indentation.
|
|
57
|
-
#
|
|
58
|
-
def tabs( indent = 0 )
|
|
59
|
-
return "\t" * indent
|
|
48
|
+
doc = render
|
|
49
|
+
@mech.write( @pn, doc.text )
|
|
50
|
+
return doc
|
|
60
51
|
end
|
|
61
52
|
|
|
62
53
|
#
|
|
63
|
-
# Would saving now actually change anything on disk?
|
|
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.)
|
|
54
|
+
# Would saving now actually change anything on disk?
|
|
68
55
|
#
|
|
69
56
|
def dirty?
|
|
70
57
|
return false unless @source_doc
|
|
71
58
|
|
|
72
|
-
return
|
|
59
|
+
return render.text != @source_doc.text
|
|
73
60
|
end
|
|
74
61
|
|
|
75
62
|
#
|
|
76
|
-
#
|
|
77
|
-
#
|
|
78
|
-
#
|
|
79
|
-
# load (which has no source node of its own to rewrite).
|
|
63
|
+
# The text for an object and everything under it, written from
|
|
64
|
+
# scratch in the default style (indent is the depth of its
|
|
65
|
+
# declaration).
|
|
80
66
|
#
|
|
81
67
|
def get_obj( obj, indent = 0 )
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
end
|
|
87
|
-
return str
|
|
68
|
+
@index = SaveIndex.new( @engine, [] )
|
|
69
|
+
@writer = DeclarationWriter.new( @engine, FileStyle.new( @engine ) )
|
|
70
|
+
@unit = FileStyle::DEFAULT_UNIT
|
|
71
|
+
return unplaced( obj, FileStyle::DEFAULT_UNIT * indent ).text
|
|
88
72
|
end
|
|
89
73
|
|
|
90
|
-
private
|
|
91
|
-
|
|
92
74
|
#
|
|
93
|
-
#
|
|
94
|
-
#
|
|
75
|
+
# A source node for an object and everything under it, written
|
|
76
|
+
# from scratch in the default style, at the top of a file, with
|
|
77
|
+
# the given name (a dotted path when it's written into a file of
|
|
78
|
+
# its own).
|
|
95
79
|
#
|
|
96
|
-
def
|
|
97
|
-
|
|
80
|
+
def new_node( obj, name = obj.name )
|
|
81
|
+
@index = SaveIndex.new( @engine, [] )
|
|
82
|
+
@writer = DeclarationWriter.new( @engine, FileStyle.new( @engine ) )
|
|
83
|
+
@unit = FileStyle::DEFAULT_UNIT
|
|
84
|
+
@index.claim( obj )
|
|
85
|
+
return unplaced( obj, '', name )
|
|
98
86
|
end
|
|
99
87
|
|
|
100
88
|
#
|
|
101
|
-
#
|
|
102
|
-
# on disk -- its object gone from live_parent's children (an
|
|
103
|
-
# untracked deletion counts too), or its value changed?
|
|
89
|
+
# The source document for what a save would write.
|
|
104
90
|
#
|
|
105
|
-
def
|
|
106
|
-
|
|
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 )
|
|
91
|
+
def render
|
|
92
|
+
return regenerate unless @source_doc
|
|
112
93
|
|
|
113
|
-
|
|
114
|
-
|
|
94
|
+
@index ||= SaveIndex.new( @engine, @engine.persist_man.maps )
|
|
95
|
+
style = FileStyle.new( @engine, @source_doc )
|
|
96
|
+
@writer = DeclarationWriter.new( @engine, style )
|
|
97
|
+
@unit = style.unit
|
|
98
|
+
doc = Source::SourceDoc.new
|
|
99
|
+
doc.no_final_newline = @source_doc.no_final_newline
|
|
100
|
+
render_nodes( @source_doc.children, doc.children )
|
|
101
|
+
return doc
|
|
115
102
|
end
|
|
116
103
|
|
|
117
|
-
|
|
118
|
-
# Rewrite (Source::SourceDoc-driven)
|
|
119
|
-
# ---------------------------------------------------------------------
|
|
104
|
+
private
|
|
120
105
|
|
|
121
106
|
#
|
|
122
|
-
#
|
|
123
|
-
#
|
|
124
|
-
# appended.
|
|
107
|
+
# A document for a file written from scratch: the object and
|
|
108
|
+
# everything under it.
|
|
125
109
|
#
|
|
126
|
-
def
|
|
127
|
-
|
|
110
|
+
def regenerate
|
|
111
|
+
doc = Source::SourceDoc.new
|
|
112
|
+
doc.children << new_node( @obj )
|
|
113
|
+
return doc
|
|
128
114
|
end
|
|
129
115
|
|
|
130
116
|
#
|
|
131
|
-
#
|
|
132
|
-
#
|
|
133
|
-
#
|
|
134
|
-
#
|
|
135
|
-
# with no matching source node is new and gets rendered fresh
|
|
136
|
-
# (unless another file owns it -- see #owned).
|
|
117
|
+
# Write one level of source nodes into out: comments, blank lines
|
|
118
|
+
# and directives as they are; each standing declaration (with
|
|
119
|
+
# anything new under it); and after a dotted declaration, any new
|
|
120
|
+
# children of the containers it names.
|
|
137
121
|
#
|
|
138
|
-
def
|
|
139
|
-
str = ''
|
|
140
|
-
seen = []
|
|
122
|
+
def render_nodes( nodes, out )
|
|
141
123
|
nodes.each do |node|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
next
|
|
145
|
-
|
|
146
|
-
seen << top
|
|
147
|
-
str << render_obj_node( node, indent )
|
|
148
|
-
else
|
|
149
|
-
str << render_trivia( node )
|
|
124
|
+
unless node.is_a?( Source::ObjNode )
|
|
125
|
+
out << node
|
|
126
|
+
next
|
|
150
127
|
end
|
|
151
|
-
|
|
128
|
+
next unless @index.standing?( node )
|
|
152
129
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
130
|
+
out << render_obj_node( node )
|
|
131
|
+
@index.shorthand_targets( node ).each do |parent, prefix|
|
|
132
|
+
@index.unplaced_children( parent ).each do |child|
|
|
133
|
+
out << unplaced( child, node.raw_indent, "#{prefix}.#{child.name}" )
|
|
134
|
+
end
|
|
135
|
+
end
|
|
157
136
|
end
|
|
158
|
-
return str
|
|
159
137
|
end
|
|
160
138
|
|
|
161
139
|
#
|
|
162
|
-
#
|
|
163
|
-
#
|
|
164
|
-
#
|
|
165
|
-
# written here and claimed.
|
|
140
|
+
# One standing declaration: written as it was unless it holds a
|
|
141
|
+
# value that changed; its children the same way; then, if this is
|
|
142
|
+
# where its object's new children go, those.
|
|
166
143
|
#
|
|
167
|
-
def
|
|
168
|
-
|
|
169
|
-
|
|
144
|
+
def render_obj_node( node )
|
|
145
|
+
copy = node.detached( own_text( node ) )
|
|
146
|
+
render_nodes( node.children, copy.children )
|
|
147
|
+
return copy unless node.obj && writes_children_of?( node )
|
|
170
148
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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 ]
|
|
149
|
+
indent = child_indent( copy )
|
|
150
|
+
at = after_last_declaration( copy.children )
|
|
151
|
+
@index.unplaced_children( node.obj ).each do |child|
|
|
152
|
+
copy.children.insert( at, unplaced( child, indent ) )
|
|
153
|
+
at += 1
|
|
188
154
|
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"
|
|
155
|
+
return copy
|
|
207
156
|
end
|
|
208
157
|
|
|
209
158
|
#
|
|
210
|
-
#
|
|
211
|
-
#
|
|
212
|
-
#
|
|
213
|
-
# on whether it changed, and its children are rewritten the same
|
|
214
|
-
# way, one level deeper.
|
|
159
|
+
# Where a new child goes in a list of nodes: after the last
|
|
160
|
+
# declaration, ahead of any comments and blank lines that follow
|
|
161
|
+
# it (they lead into what comes next).
|
|
215
162
|
#
|
|
216
|
-
def
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
str << render_children( node.children, node.obj, indent + 1 )
|
|
220
|
-
return str
|
|
163
|
+
def after_last_declaration( nodes )
|
|
164
|
+
last = nodes.rindex { |n| n.is_a?( Source::ObjNode ) }
|
|
165
|
+
return last ? last + 1 : 0
|
|
221
166
|
end
|
|
222
167
|
|
|
223
168
|
#
|
|
224
|
-
# The text
|
|
225
|
-
#
|
|
169
|
+
# The declaration's own text: as read (with any head gloo had to
|
|
170
|
+
# guess at written as understood), or written again if it holds
|
|
171
|
+
# its object's value and the value changed.
|
|
226
172
|
#
|
|
227
|
-
def
|
|
228
|
-
|
|
173
|
+
def own_text( node )
|
|
174
|
+
fixed = node.repaired
|
|
175
|
+
return fixed.raw unless node.obj && @index.holder?( node ) && node.changed?
|
|
229
176
|
|
|
230
|
-
return
|
|
177
|
+
return @writer.changed_text( fixed )
|
|
231
178
|
end
|
|
232
179
|
|
|
233
180
|
#
|
|
234
|
-
#
|
|
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.
|
|
181
|
+
# Is this declaration where its object's new children are written?
|
|
238
182
|
#
|
|
239
|
-
def
|
|
240
|
-
|
|
241
|
-
|
|
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
|
|
183
|
+
def writes_children_of?( node )
|
|
184
|
+
fs, target, prefix = @index.writer_for( node.obj )
|
|
185
|
+
return fs&.source_doc.equal?( @source_doc ) && target.equal?( node ) && prefix.nil?
|
|
248
186
|
end
|
|
249
187
|
|
|
250
|
-
# ---------------------------------------------------------------------
|
|
251
|
-
# Has the value actually changed since load?
|
|
252
|
-
# ---------------------------------------------------------------------
|
|
253
|
-
|
|
254
188
|
#
|
|
255
|
-
#
|
|
256
|
-
#
|
|
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.
|
|
189
|
+
# The indentation for a new child of a declaration: its last
|
|
190
|
+
# child's, or one step in from it.
|
|
260
191
|
#
|
|
261
|
-
def
|
|
262
|
-
|
|
263
|
-
return
|
|
264
|
-
|
|
265
|
-
trial = node.obj.class.new( @engine )
|
|
266
|
-
# Only a comparison: any bad-value warning was given on load.
|
|
267
|
-
@engine.without_warnings { trial.set_value( node.raw_value ) }
|
|
268
|
-
return trial.value == node.obj.value
|
|
269
|
-
rescue
|
|
270
|
-
return false
|
|
192
|
+
def child_indent( node )
|
|
193
|
+
last = node.children.reverse.find { |c| c.is_a?( Source::ObjNode ) }
|
|
194
|
+
return last ? last.raw_indent : "#{node.raw_indent}#{@unit}"
|
|
271
195
|
end
|
|
272
196
|
|
|
273
197
|
#
|
|
274
|
-
# A
|
|
275
|
-
#
|
|
276
|
-
#
|
|
198
|
+
# A node for an object no standing declaration places, and
|
|
199
|
+
# everything under it: a moved object keeps its first declaration
|
|
200
|
+
# (and doc) at the new indentation; a new one is written from
|
|
201
|
+
# scratch. name is how it's written (a dotted path for a shorthand
|
|
202
|
+
# line).
|
|
277
203
|
#
|
|
278
|
-
def
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
204
|
+
def unplaced( obj, indent, name = obj.name )
|
|
205
|
+
node = moved_node( obj, indent, name ) || @writer.new_node( obj, indent, name )
|
|
206
|
+
obj.children.each do |child|
|
|
207
|
+
next if @index.placed?( child ) || !@index.claim( child )
|
|
208
|
+
|
|
209
|
+
node.children << unplaced( child, "#{indent}#{@unit}" )
|
|
210
|
+
end
|
|
211
|
+
return node
|
|
282
212
|
end
|
|
283
213
|
|
|
284
214
|
#
|
|
285
|
-
# The
|
|
286
|
-
# currently in.
|
|
215
|
+
# The first declaration of a moved object, at its new place.
|
|
287
216
|
#
|
|
288
|
-
def
|
|
289
|
-
|
|
290
|
-
return
|
|
217
|
+
def moved_node( obj, indent, name )
|
|
218
|
+
_fs, first = @index.entries_for( obj ).first
|
|
219
|
+
return nil unless first
|
|
291
220
|
|
|
292
|
-
|
|
221
|
+
node = first.detached( first.changed? ? @writer.changed_text( first ) : first.raw )
|
|
222
|
+
node.rename( name ) unless name == node.name
|
|
223
|
+
node.reindent( indent )
|
|
224
|
+
return node
|
|
293
225
|
end
|
|
294
226
|
|
|
295
227
|
end
|
|
@@ -28,13 +28,24 @@ module Gloo
|
|
|
28
28
|
end
|
|
29
29
|
|
|
30
30
|
#
|
|
31
|
-
# Save the object to the file. batch, when given,
|
|
32
|
-
#
|
|
33
|
-
# FileSaver.
|
|
31
|
+
# Save the object to the file. batch, when given, carries the
|
|
32
|
+
# SaveIndex shared by a save of several files -- see
|
|
33
|
+
# PersistMan#save_batch and FileSaver. Returns the source document
|
|
34
|
+
# for what was written.
|
|
34
35
|
#
|
|
35
36
|
def save( batch = nil )
|
|
36
37
|
fs = FileSaver.new( @engine, @pn, @obj, @source_doc, batch )
|
|
37
|
-
fs.save
|
|
38
|
+
return fs.save
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
#
|
|
42
|
+
# The file was just saved: keep the source document for what was
|
|
43
|
+
# written, with each declaration remembering its object's place and
|
|
44
|
+
# value as saved.
|
|
45
|
+
#
|
|
46
|
+
def saved( doc )
|
|
47
|
+
@source_doc = doc
|
|
48
|
+
doc.obj_nodes.each( &:remember )
|
|
38
49
|
end
|
|
39
50
|
|
|
40
51
|
#
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# Author:: Eric Crane (mailto:eric.crane@mac.com)
|
|
2
|
+
# Copyright:: Copyright (c) 2026 Eric Crane. All rights reserved.
|
|
3
|
+
#
|
|
4
|
+
# How a file is written, learned from its source: the indentation it
|
|
5
|
+
# steps in by, and how it spells each type. A save writes new text the
|
|
6
|
+
# same way, so a 2-space file full of [int] and [can] gets 2 spaces,
|
|
7
|
+
# [int] and [can] back. A new file, or one with nothing to learn from,
|
|
8
|
+
# gets 2 spaces and each type's own name.
|
|
9
|
+
#
|
|
10
|
+
|
|
11
|
+
module Gloo
|
|
12
|
+
module Persist
|
|
13
|
+
class FileStyle
|
|
14
|
+
|
|
15
|
+
DEFAULT_UNIT = ' '.freeze
|
|
16
|
+
|
|
17
|
+
attr_reader :unit
|
|
18
|
+
|
|
19
|
+
#
|
|
20
|
+
# Learn the style of a source document (nil for a new file).
|
|
21
|
+
#
|
|
22
|
+
def initialize( engine, source_doc = nil )
|
|
23
|
+
@engine = engine
|
|
24
|
+
@unit = DEFAULT_UNIT
|
|
25
|
+
@spellings = {}
|
|
26
|
+
learn( source_doc ) if source_doc
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
#
|
|
30
|
+
# How to write an object's type: the spelling the file uses most
|
|
31
|
+
# for that type, or the type's own name. nil means the file writes
|
|
32
|
+
# it with no brackets at all (an untyped 'name : value').
|
|
33
|
+
#
|
|
34
|
+
def type_for( obj )
|
|
35
|
+
return @spellings.fetch( obj.class ) { obj.type_display }
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
#
|
|
41
|
+
# Read the unit and the type spellings from the document.
|
|
42
|
+
#
|
|
43
|
+
def learn( source_doc )
|
|
44
|
+
learn_unit( source_doc.children )
|
|
45
|
+
counts = Hash.new { |h, k| h[ k ] = Hash.new( 0 ) }
|
|
46
|
+
source_doc.obj_nodes.each do |node|
|
|
47
|
+
type = type_class( node )
|
|
48
|
+
counts[ type ][ spelling( node ) ] += 1 if type
|
|
49
|
+
end
|
|
50
|
+
counts.each { |type, spelled| @spellings[ type ] = spelled.max_by { |_, n| n }.first }
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
#
|
|
54
|
+
# The step in from a top-level declaration to its first child.
|
|
55
|
+
#
|
|
56
|
+
def learn_unit( nodes )
|
|
57
|
+
nodes.each do |node|
|
|
58
|
+
next unless node.is_a?( Source::ObjNode )
|
|
59
|
+
|
|
60
|
+
child = node.children.find { |c| c.is_a?( Source::ObjNode ) }
|
|
61
|
+
next unless child
|
|
62
|
+
|
|
63
|
+
step = child.raw_indent.delete_prefix( node.raw_indent )
|
|
64
|
+
@unit = step unless step.empty?
|
|
65
|
+
return
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
#
|
|
70
|
+
# The object type class a declaration was written with (nil for a
|
|
71
|
+
# type gloo doesn't know, which tells us nothing).
|
|
72
|
+
#
|
|
73
|
+
def type_class( node )
|
|
74
|
+
return @engine.dictionary.find_obj( node.raw_type.to_s )
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
#
|
|
78
|
+
# How the declaration spells its type: the text in its brackets,
|
|
79
|
+
# or nil when it has none.
|
|
80
|
+
#
|
|
81
|
+
def spelling( node )
|
|
82
|
+
return nil unless node.head.include?( '[' )
|
|
83
|
+
|
|
84
|
+
return node.raw_type
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
@@ -11,6 +11,8 @@ module Gloo
|
|
|
11
11
|
|
|
12
12
|
BEGIN_BLOCK = 'BEGIN'.freeze
|
|
13
13
|
END_BLOCK = 'END'.freeze
|
|
14
|
+
# A type in brackets with whitespace inside them: '[ int ]', '[int ]'.
|
|
15
|
+
SPACED_TYPE = /\A\[[ \t]*([^\s\]]+)[ \t]*\]/.freeze
|
|
14
16
|
|
|
15
17
|
attr_reader :obj, :raw_tail, :missing_bracket
|
|
16
18
|
|
|
@@ -77,7 +79,7 @@ module Gloo
|
|
|
77
79
|
# count.
|
|
78
80
|
#
|
|
79
81
|
def detect_type
|
|
80
|
-
@line = @line[ @idx + 1..-1 ]
|
|
82
|
+
@line = close_up_spaced_type( @line[ @idx + 1..-1 ] )
|
|
81
83
|
@idx = @line.index( ' ' )
|
|
82
84
|
|
|
83
85
|
if @line[ 0 ] == ':'
|
|
@@ -95,6 +97,19 @@ module Gloo
|
|
|
95
97
|
@raw_tail = close ? @line[ close + 1..-1 ] : ''
|
|
96
98
|
end
|
|
97
99
|
|
|
100
|
+
#
|
|
101
|
+
# Spaces just inside the brackets ('[ int ]', '[string ]') don't
|
|
102
|
+
# change the type: with one type word and only whitespace around
|
|
103
|
+
# it, read the brackets as if written closed up ('[int]'). The
|
|
104
|
+
# rest of the line is untouched, so raw_tail is exactly as written.
|
|
105
|
+
#
|
|
106
|
+
def close_up_spaced_type( line )
|
|
107
|
+
match = SPACED_TYPE.match( line )
|
|
108
|
+
return line unless match
|
|
109
|
+
|
|
110
|
+
return "[#{match[ 1 ]}]#{match.post_match}"
|
|
111
|
+
end
|
|
112
|
+
|
|
98
113
|
#
|
|
99
114
|
# The type word starts with '['. The type ends at the ']' in that
|
|
100
115
|
# word (which may be followed directly by the ':', as in
|