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
|
@@ -47,7 +47,7 @@ module Gloo
|
|
|
47
47
|
if self.value_string?
|
|
48
48
|
first = self.value
|
|
49
49
|
self.set_array_value []
|
|
50
|
-
self.value << first unless first.empty?
|
|
50
|
+
self.value << first unless first.strip.empty?
|
|
51
51
|
elsif self.value_is_blank?
|
|
52
52
|
self.set_array_value []
|
|
53
53
|
end
|
|
@@ -62,6 +62,29 @@ module Gloo
|
|
|
62
62
|
return true
|
|
63
63
|
end
|
|
64
64
|
|
|
65
|
+
#
|
|
66
|
+
# Serialize this object's value for saving to a file.
|
|
67
|
+
# A script's value is an Array of command lines (once add_line
|
|
68
|
+
# has been called) or a single-line String. Render as indented
|
|
69
|
+
# body lines below the declaration, gloo's convention for
|
|
70
|
+
# scripts -- not a BEGIN/END block, which is reserved for a
|
|
71
|
+
# literal multi-line string value.
|
|
72
|
+
#
|
|
73
|
+
def serialize_value( indent )
|
|
74
|
+
lines = if value_is_array?
|
|
75
|
+
value
|
|
76
|
+
elsif value_string? && !value.strip.empty?
|
|
77
|
+
[ value ]
|
|
78
|
+
else
|
|
79
|
+
[]
|
|
80
|
+
end
|
|
81
|
+
return ' :' if lines.empty?
|
|
82
|
+
|
|
83
|
+
t = "\t" * ( indent + 1 )
|
|
84
|
+
body = lines.map { |line| "#{t}#{line}" }.join( "\n" )
|
|
85
|
+
return " :\n#{body}"
|
|
86
|
+
end
|
|
87
|
+
|
|
65
88
|
#
|
|
66
89
|
# Get the number of lines in this script.
|
|
67
90
|
#
|
|
@@ -105,7 +128,10 @@ module Gloo
|
|
|
105
128
|
{
|
|
106
129
|
:name => KEYWORD,
|
|
107
130
|
:shortcut => KEYWORD_SHORT,
|
|
108
|
-
:description => 'An executable script — a set of commands to
|
|
131
|
+
:description => 'An executable script — a set of commands to ' \
|
|
132
|
+
'be run. A command line may end with an inline `# ...` ' \
|
|
133
|
+
'comment; it is ignored when the line runs but kept when ' \
|
|
134
|
+
'the file is saved.',
|
|
109
135
|
:messages => [
|
|
110
136
|
'run — Run the script. The script can be run by telling the ' \
|
|
111
137
|
'object to run, or via the run verb.'
|
data/lib/gloo/objs/web/json.rb
CHANGED
|
@@ -213,7 +213,10 @@ module Gloo
|
|
|
213
213
|
{
|
|
214
214
|
:name => KEYWORD,
|
|
215
215
|
:shortcut => KEYWORD_SHORT,
|
|
216
|
-
:description => 'JSON data in a text string.'
|
|
216
|
+
:description => 'JSON data in a text string. Declare it ' \
|
|
217
|
+
'inline, in a BEGIN/END block, or assign it at run time: a ' \
|
|
218
|
+
'single-quoted literal keeps its double quotes, so ' \
|
|
219
|
+
'putting a JSON string into the object works.',
|
|
217
220
|
:messages => [
|
|
218
221
|
'get ({path}) — Get a value from the JSON data. Example: tell myjson to get (\'title\'). The parameter is the path in JSON to the value we want. The value is put into it.',
|
|
219
222
|
'set ({obj.path}) — Convert an object to an approximate JSON value. Example: tell myjson to set (my.object). The parameter is the path to the object used as the source. Note this is an approximate conversion — a gloo object can have both a simple value and be a container for child objects, which JSON can\'t represent the same way.',
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# Author:: Eric Crane (mailto:eric.crane@mac.com)
|
|
2
|
+
# Copyright:: Copyright (c) 2026 Eric Crane. All rights reserved.
|
|
3
|
+
#
|
|
4
|
+
# Buffers a run of consecutive whole-line comments while the loader
|
|
5
|
+
# decides whether they become a declaration's leading_doc or float as
|
|
6
|
+
# their own comment nodes.
|
|
7
|
+
#
|
|
8
|
+
|
|
9
|
+
module Gloo
|
|
10
|
+
module Persist
|
|
11
|
+
class CommentBuffer
|
|
12
|
+
|
|
13
|
+
#
|
|
14
|
+
# Set up an empty buffer.
|
|
15
|
+
#
|
|
16
|
+
def initialize
|
|
17
|
+
@pending = []
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
#
|
|
21
|
+
# Buffer one comment line. raw is the full source line (minus
|
|
22
|
+
# its trailing newline); tabs is its indentation level.
|
|
23
|
+
#
|
|
24
|
+
def push( raw, tabs )
|
|
25
|
+
@pending << { :raw => raw, :tabs => tabs }
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
#
|
|
29
|
+
# Detach the buffered run as floating comment nodes, appended in
|
|
30
|
+
# order to the given children array.
|
|
31
|
+
#
|
|
32
|
+
def flush_into( children )
|
|
33
|
+
return if @pending.empty?
|
|
34
|
+
|
|
35
|
+
@pending.each { |c| children << Source::CommentNode.new( c[ :raw ] ) }
|
|
36
|
+
@pending = []
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
#
|
|
40
|
+
# If the buffered run sits at the same indent as line_tabs, claim
|
|
41
|
+
# it as a leading_doc (raw, newline-joined) and clear the buffer.
|
|
42
|
+
# Otherwise flush it into children as floating comments first, so
|
|
43
|
+
# document order is kept, and return nil.
|
|
44
|
+
#
|
|
45
|
+
def take_leading_doc( line_tabs, children )
|
|
46
|
+
if !@pending.empty? && @pending.last[ :tabs ] == line_tabs
|
|
47
|
+
doc = @pending.map { |c| c[ :raw ] }.join( "\n" )
|
|
48
|
+
@pending = []
|
|
49
|
+
return doc
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
flush_into( children )
|
|
53
|
+
return nil
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# Author:: Eric Crane (mailto:eric.crane@mac.com)
|
|
2
|
+
# Copyright:: Copyright (c) 2026 Eric Crane. All rights reserved.
|
|
3
|
+
#
|
|
4
|
+
# Tracks what a single file declares as it loads: its top-level
|
|
5
|
+
# objects (for save resolution), and every object it created -- so a
|
|
6
|
+
# re-declaration of a name an earlier file already used can be
|
|
7
|
+
# flagged, since in that case the second value is silently dropped.
|
|
8
|
+
#
|
|
9
|
+
|
|
10
|
+
module Gloo
|
|
11
|
+
module Persist
|
|
12
|
+
class DeclarationLedger
|
|
13
|
+
|
|
14
|
+
attr_reader :roots
|
|
15
|
+
|
|
16
|
+
#
|
|
17
|
+
# Set up a ledger for the file at pn.
|
|
18
|
+
#
|
|
19
|
+
def initialize( engine, pn )
|
|
20
|
+
@engine = engine
|
|
21
|
+
@pn = pn
|
|
22
|
+
@roots = []
|
|
23
|
+
@mine = {}.compare_by_identity
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
#
|
|
27
|
+
# Record a top-level object this file created or reused (shorthand
|
|
28
|
+
# lines can re-introduce the same root, so dedupe).
|
|
29
|
+
#
|
|
30
|
+
def root( obj )
|
|
31
|
+
@roots << obj unless @roots.include?( obj )
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
#
|
|
35
|
+
# Record an object this file created, at any level.
|
|
36
|
+
#
|
|
37
|
+
def created( obj )
|
|
38
|
+
@mine[ obj ] = true
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
#
|
|
42
|
+
# `prior` already existed when this file declared `name` with
|
|
43
|
+
# `value`. If another file created it and the values differ, the
|
|
44
|
+
# new value is being ignored -- warn so it isn't a surprise. A
|
|
45
|
+
# duplicate name inside one file is left alone (a documented
|
|
46
|
+
# "first one wins").
|
|
47
|
+
#
|
|
48
|
+
def clash( prior, name, value )
|
|
49
|
+
return if @mine.key?( prior )
|
|
50
|
+
return if value.nil? || value.to_s.strip.empty?
|
|
51
|
+
return if prior.value.to_s == value.to_s
|
|
52
|
+
|
|
53
|
+
@engine.log.warn(
|
|
54
|
+
"#{@pn}: '#{name}' is already declared in another loaded file with a " \
|
|
55
|
+
"different value (#{prior.value.inspect} kept, #{value.inspect} ignored)."
|
|
56
|
+
)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -85,13 +85,26 @@ module Gloo
|
|
|
85
85
|
return nil
|
|
86
86
|
end
|
|
87
87
|
|
|
88
|
-
#
|
|
88
|
+
#
|
|
89
89
|
# Read in the contents of a single file.
|
|
90
|
-
#
|
|
90
|
+
#
|
|
91
91
|
def read( file )
|
|
92
92
|
return File.read( file )
|
|
93
93
|
end
|
|
94
94
|
|
|
95
|
+
#
|
|
96
|
+
# Resolve a path to save a new (not-yet-existing) file to, given
|
|
97
|
+
# a name or relative path -- same convention as expand (relative
|
|
98
|
+
# to the project root, .gloo appended if missing), but without
|
|
99
|
+
# requiring the file to already exist.
|
|
100
|
+
#
|
|
101
|
+
def resolve_save_path( name )
|
|
102
|
+
full_name = name.end_with?( file_ext ) ? name : "#{name}#{file_ext}"
|
|
103
|
+
return full_name if full_name.start_with?( '/' )
|
|
104
|
+
|
|
105
|
+
return File.join( @engine.settings.project_path, full_name )
|
|
106
|
+
end
|
|
107
|
+
|
|
95
108
|
#
|
|
96
109
|
# Write data to the file.
|
|
97
110
|
#
|
|
@@ -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,202 @@ 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
|
+
# First non-empty doc wins, same as "first value wins" for a
|
|
230
|
+
# name re-declared across files -- @last is the same object
|
|
231
|
+
# across re-declarations (the factory returns the existing
|
|
232
|
+
# one), so a later, comment-less re-declaration doesn't blank
|
|
233
|
+
# out an earlier file's doc. @last can be nil for an unknown
|
|
234
|
+
# type (factory.create logs a warning and returns nil).
|
|
235
|
+
@last.doc = node.doc if @last && @last.doc.to_s.strip.empty?
|
|
236
|
+
@indent_stack.node.children << node
|
|
237
|
+
@last_node = node
|
|
238
|
+
@obj ||= @ledger.roots.last || @last
|
|
239
|
+
|
|
240
|
+
# A whitespace-only inline value (trailing spaces after the ':'
|
|
241
|
+
# on a `script :` line) counts as no value -- start the body.
|
|
242
|
+
no_value = value.nil? || value.strip.empty?
|
|
243
|
+
@body.start( node, @last, @indent_stack.tabs ) if no_value && @last&.multiline_value?
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
#
|
|
247
|
+
# Split a declaration line into name/type/value, plus a style
|
|
248
|
+
# hash (:block_style, :raw_tail), folding in any BEGIN/END block
|
|
249
|
+
# value collected just before it. raw_tail is the exact source
|
|
250
|
+
# text that followed the type bracket on this line -- eg.
|
|
251
|
+
# " : BEGIN" or " :" -- kept for a save to reproduce the
|
|
252
|
+
# declaration's original spacing when unchanged.
|
|
253
|
+
#
|
|
254
|
+
def split_declaration( line )
|
|
255
|
+
splitter = LineSplitter.new( line, @indent_stack&.tabs || 0 )
|
|
256
|
+
name, type, value = splitter.split
|
|
257
|
+
style = { :raw_tail => splitter.raw_tail, :block_style => :inline }
|
|
258
|
+
return name, type, value, style if @block_value == ''
|
|
259
|
+
|
|
260
|
+
value = @block_value
|
|
261
|
+
@block_value = ''
|
|
262
|
+
style[ :block_style ] = :begin_end
|
|
263
|
+
style[ :raw_end_indent ] = @block_end_indent
|
|
264
|
+
return name, type, value, style
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
#
|
|
268
|
+
# Build the source node for one object declaration. raw_value is
|
|
269
|
+
# kept byte-exact with whatever was handed to the heap object's
|
|
270
|
+
# set_value (trailing newline included, for a BEGIN/END block) so
|
|
271
|
+
# a save can compare the two directly.
|
|
272
|
+
#
|
|
273
|
+
def build_obj_node( raw_indent, name, type, value, style )
|
|
274
|
+
node = Source::ObjNode.new( :name => name, :raw_type => type )
|
|
275
|
+
node.raw_indent = raw_indent
|
|
276
|
+
node.raw_tail = style[ :raw_tail ]
|
|
277
|
+
node.block_style = style[ :block_style ]
|
|
278
|
+
node.raw_value = value
|
|
279
|
+
node.raw_end_indent = style[ :raw_end_indent ] || ''
|
|
280
|
+
node.obj = @last
|
|
281
|
+
return node
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
#
|
|
285
|
+
# Is this line a comment or a blank line?
|
|
286
|
+
# If so we'll skip it.
|
|
287
|
+
#
|
|
288
|
+
def skip_line?( line )
|
|
289
|
+
line = line.strip
|
|
290
|
+
return true if line.empty?
|
|
291
|
+
return true if line[ 0 ] == '#'
|
|
200
292
|
|
|
201
|
-
|
|
293
|
+
return false
|
|
202
294
|
end
|
|
203
295
|
|
|
204
296
|
#
|
|
@@ -222,10 +314,24 @@ module Gloo
|
|
|
222
314
|
# Split the line into 3 parts.
|
|
223
315
|
#
|
|
224
316
|
def split_line( line )
|
|
225
|
-
o = LineSplitter.new( line, @tabs )
|
|
317
|
+
o = LineSplitter.new( line, @indent_stack&.tabs || 0 )
|
|
226
318
|
return o.split
|
|
227
319
|
end
|
|
228
320
|
|
|
321
|
+
#
|
|
322
|
+
# The line's leading whitespace, exactly as written.
|
|
323
|
+
#
|
|
324
|
+
def leading_ws( line )
|
|
325
|
+
return line[ /\A[ \t]*/ ]
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
#
|
|
329
|
+
# A line's raw text with its trailing newline removed.
|
|
330
|
+
#
|
|
331
|
+
def chomped( line )
|
|
332
|
+
return line.chomp( "\n" )
|
|
333
|
+
end
|
|
334
|
+
|
|
229
335
|
end
|
|
230
336
|
end
|
|
231
337
|
end
|