gloo 7.1.2 → 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/iterators.md +22 -5
- data/docs/language_objects.md +20 -15
- data/docs/language_scripting.md +6 -2
- data/docs/plugins.md +1 -1
- data/docs/testing.md +4 -7
- data/docs/verbs.md +23 -3
- data/lib/VERSION +1 -1
- data/lib/VERSION_NOTES +13 -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/exec/script.rb +12 -2
- data/lib/gloo/objs/basic/script.rb +5 -28
- data/lib/gloo/objs/ctrl/each.rb +4 -3
- data/lib/gloo/objs/ctrl/repeat.rb +4 -3
- 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/break.rb +99 -10
- 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/break.test.gloo +104 -11
- 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
|
@@ -37,15 +37,16 @@ module Gloo
|
|
|
37
37
|
end
|
|
38
38
|
|
|
39
39
|
#
|
|
40
|
-
# Run the do script once. Returns true if a break in it
|
|
41
|
-
# run, so the loop should stop.
|
|
40
|
+
# Run the do script once. Returns true if a break loop in it
|
|
41
|
+
# ended the run, so the loop should stop. A plain break ends only
|
|
42
|
+
# the do script, and the loop goes on.
|
|
42
43
|
#
|
|
43
44
|
def run_do
|
|
44
45
|
o = find_child DO
|
|
45
46
|
return false unless o
|
|
46
47
|
|
|
47
48
|
Gloo::Exec::Dispatch.message( @engine, 'run', o )
|
|
48
|
-
return o.respond_to?( :
|
|
49
|
+
return o.respond_to?( :broke_loop? ) && o.broke_loop?
|
|
49
50
|
end
|
|
50
51
|
|
|
51
52
|
#
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
#
|
|
4
4
|
# Tracks what a single file declares as it loads: its top-level
|
|
5
5
|
# objects (for save resolution), and every object it created -- so a
|
|
6
|
-
# re-declaration of a name an earlier file
|
|
7
|
-
#
|
|
6
|
+
# re-declaration of a name already used, in an earlier file or earlier
|
|
7
|
+
# in this one, can be flagged when its value is ignored.
|
|
8
8
|
#
|
|
9
9
|
|
|
10
10
|
module Gloo
|
|
@@ -39,19 +39,19 @@ module Gloo
|
|
|
39
39
|
|
|
40
40
|
#
|
|
41
41
|
# `prior` already existed when this file declared `name` with
|
|
42
|
-
# `value
|
|
43
|
-
#
|
|
44
|
-
#
|
|
45
|
-
#
|
|
46
|
-
# comes from the load.
|
|
42
|
+
# `value`: the line names the same object, and its value is
|
|
43
|
+
# ignored (the first declaration wins). When the ignored value is
|
|
44
|
+
# different, warn so it isn't a surprise -- whether the first
|
|
45
|
+
# declaration was in another file or earlier in this one. The
|
|
46
|
+
# warning's location (this file and line) comes from the load.
|
|
47
47
|
#
|
|
48
48
|
def clash( prior, name, value )
|
|
49
|
-
return if @mine.key?( prior )
|
|
50
49
|
return if value.nil? || value.to_s.strip.empty?
|
|
51
50
|
return if prior.value.to_s == value.to_s
|
|
52
51
|
|
|
52
|
+
where = @mine.key?( prior ) ? 'above' : 'in another loaded file'
|
|
53
53
|
@engine.warn(
|
|
54
|
-
"'#{name}' is already declared
|
|
54
|
+
"'#{name}' is already declared #{where} with a " \
|
|
55
55
|
"different value (#{prior.value.inspect} kept, #{value.inspect} ignored)."
|
|
56
56
|
)
|
|
57
57
|
end
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
# Author:: Eric Crane (mailto:eric.crane@mac.com)
|
|
2
|
+
# Copyright:: Copyright (c) 2026 Eric Crane. All rights reserved.
|
|
3
|
+
#
|
|
4
|
+
# Writes the text a save generates: a changed declaration (its head
|
|
5
|
+
# kept as written, only the value written again) or a declaration for
|
|
6
|
+
# a new object, in the file's style (see FileStyle).
|
|
7
|
+
#
|
|
8
|
+
# A value is written so it loads back the same: on the line, as a
|
|
9
|
+
# BEGIN/END block (exactly as it is -- a block's lines are the value),
|
|
10
|
+
# or as a script body. A value with no exact spelling is written as
|
|
11
|
+
# the closest one, with a warning.
|
|
12
|
+
#
|
|
13
|
+
|
|
14
|
+
module Gloo
|
|
15
|
+
module Persist
|
|
16
|
+
class DeclarationWriter
|
|
17
|
+
|
|
18
|
+
BEGIN_BLOCK = 'BEGIN'.freeze
|
|
19
|
+
END_BLOCK = 'END'.freeze
|
|
20
|
+
SEP = ' : '.freeze
|
|
21
|
+
NOT_EXACT = "Could not save '%s' exactly: %s.".freeze
|
|
22
|
+
NO_FINAL_NEWLINE = 'a block value always ends with a newline'.freeze
|
|
23
|
+
END_LINE = 'a line that is just END ends the block early'.freeze
|
|
24
|
+
NOT_UTF8 = 'it is not valid UTF-8'.freeze
|
|
25
|
+
|
|
26
|
+
#
|
|
27
|
+
# Set up a writer for a file with the given style.
|
|
28
|
+
#
|
|
29
|
+
def initialize( engine, style )
|
|
30
|
+
@engine = engine
|
|
31
|
+
@style = style
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
#
|
|
35
|
+
# The new text of a declaration whose object's value changed: its
|
|
36
|
+
# head as written, then the value.
|
|
37
|
+
#
|
|
38
|
+
def changed_text( node )
|
|
39
|
+
obj = node.obj
|
|
40
|
+
return body_text( node.head, node.raw_tail, obj, body_indent( node ), trailing_blanks( node ) ) if body?( obj, node.block_style == :body )
|
|
41
|
+
|
|
42
|
+
return value_text( node.head, separator( node ), obj, node.raw_indent, node.block_style == :begin_end )
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
#
|
|
46
|
+
# A source node for a new object (without its children), at the
|
|
47
|
+
# given indentation, written with the given name (its own, or a
|
|
48
|
+
# dotted path for a shorthand line).
|
|
49
|
+
#
|
|
50
|
+
def new_node( obj, indent, name = obj.name )
|
|
51
|
+
spelled = @style.type_for( obj )
|
|
52
|
+
head = spelled ? "#{indent}#{name} [#{spelled}]" : "#{indent}#{name}"
|
|
53
|
+
node = Source::ObjNode.new( :name => name, :raw_type => spelled || obj.type_display )
|
|
54
|
+
node.raw_indent = indent
|
|
55
|
+
node.head = head
|
|
56
|
+
node.obj = obj
|
|
57
|
+
node.raw = if body?( obj, false )
|
|
58
|
+
body_text( head, ' :', obj, "#{indent}#{@style.unit}", '' )
|
|
59
|
+
else
|
|
60
|
+
value_text( head, SEP, obj, indent, false )
|
|
61
|
+
end
|
|
62
|
+
node.block_style = block_style_of( node.raw )
|
|
63
|
+
node.raw_tail = node.raw.scrub.lines.first.chomp.delete_prefix( head )
|
|
64
|
+
return node
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
private
|
|
68
|
+
|
|
69
|
+
# ---------------------------------------------------------------------
|
|
70
|
+
# Script bodies
|
|
71
|
+
# ---------------------------------------------------------------------
|
|
72
|
+
|
|
73
|
+
#
|
|
74
|
+
# Is the value written as a script body: a multi-line value (a
|
|
75
|
+
# script) whose value is a list of lines, or that was a body?
|
|
76
|
+
#
|
|
77
|
+
def body?( obj, was_body )
|
|
78
|
+
return false unless obj.multiline_value?
|
|
79
|
+
|
|
80
|
+
return was_body || obj.value_is_array?
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
#
|
|
84
|
+
# A script body: the head and tail (eg. ' :'), then each command
|
|
85
|
+
# on its own line at the body's indentation, then any blank lines
|
|
86
|
+
# that followed the body as written.
|
|
87
|
+
#
|
|
88
|
+
def body_text( head, tail, obj, indent, trailing )
|
|
89
|
+
lines = script_lines( obj )
|
|
90
|
+
str = "#{head}#{tail.rstrip}\n"
|
|
91
|
+
lines.each { |line| str << "#{indent}#{line}\n" }
|
|
92
|
+
return "#{str}#{trailing}"
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
#
|
|
96
|
+
# A script's command lines, whatever form its value is in.
|
|
97
|
+
#
|
|
98
|
+
def script_lines( obj )
|
|
99
|
+
return obj.value if obj.value_is_array?
|
|
100
|
+
return [] if obj.value.to_s.strip.empty?
|
|
101
|
+
|
|
102
|
+
return [ obj.value.to_s.strip ]
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
#
|
|
106
|
+
# The indentation of a body as written: its first non-blank line's,
|
|
107
|
+
# or one step in from the declaration.
|
|
108
|
+
#
|
|
109
|
+
def body_indent( node )
|
|
110
|
+
line = node.raw.lines.drop( 1 ).find { |l| !l.strip.empty? }
|
|
111
|
+
return line[ /\A[ \t]*/ ] if line
|
|
112
|
+
|
|
113
|
+
return "#{node.raw_indent}#{@style.unit}"
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
#
|
|
117
|
+
# The blank lines at the end of a body as written, kept when the
|
|
118
|
+
# body is written again.
|
|
119
|
+
#
|
|
120
|
+
def trailing_blanks( node )
|
|
121
|
+
lines = node.raw.lines.drop( 1 )
|
|
122
|
+
blanks = []
|
|
123
|
+
blanks.unshift( lines.pop ) while lines.any? && lines.last.strip.empty?
|
|
124
|
+
return blanks.join
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# ---------------------------------------------------------------------
|
|
128
|
+
# Values on the line or in a BEGIN/END block
|
|
129
|
+
# ---------------------------------------------------------------------
|
|
130
|
+
|
|
131
|
+
#
|
|
132
|
+
# The declaration with its value: on the line if it can be written
|
|
133
|
+
# there, otherwise as a BEGIN/END block. A value that was a block
|
|
134
|
+
# stays one if it still ends with a newline.
|
|
135
|
+
#
|
|
136
|
+
def value_text( head, sep, obj, indent, was_block )
|
|
137
|
+
value = obj.value_display.to_s
|
|
138
|
+
warn_not_exact( obj, NOT_UTF8 ) unless value.valid_encoding?
|
|
139
|
+
return block_text( head, sep, obj, value, indent ) if block?( value, was_block )
|
|
140
|
+
return "#{head}#{sep.rstrip}\n" if value.empty?
|
|
141
|
+
|
|
142
|
+
return "#{head}#{sep}#{value}\n"
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
#
|
|
146
|
+
# Does the value need (or keep) a BEGIN/END block? A line break
|
|
147
|
+
# can't go on the line, and a value ending in a backslash or in
|
|
148
|
+
# BEGIN would be read as a continuation or a block start.
|
|
149
|
+
#
|
|
150
|
+
def block?( value, was_block )
|
|
151
|
+
return true if was_block && value.end_with?( "\n" )
|
|
152
|
+
|
|
153
|
+
clean = value.scrub
|
|
154
|
+
return clean.include?( "\n" ) || clean.end_with?( '\\' ) || clean.strip.end_with?( BEGIN_BLOCK )
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
#
|
|
158
|
+
# A BEGIN/END block: the value's lines exactly as they are, and END
|
|
159
|
+
# lined up with the declaration.
|
|
160
|
+
#
|
|
161
|
+
def block_text( head, sep, obj, value, indent )
|
|
162
|
+
content = value
|
|
163
|
+
unless content.end_with?( "\n" )
|
|
164
|
+
warn_not_exact( obj, NO_FINAL_NEWLINE )
|
|
165
|
+
content = "#{content}\n"
|
|
166
|
+
end
|
|
167
|
+
warn_not_exact( obj, END_LINE ) if content.scrub.lines.any? { |l| l.strip == END_BLOCK }
|
|
168
|
+
|
|
169
|
+
return "#{head}#{sep}#{BEGIN_BLOCK}\n#{content}#{indent}#{END_BLOCK}\n"
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
#
|
|
173
|
+
# What goes between a declaration's head and its value, as written:
|
|
174
|
+
# usually ' : '.
|
|
175
|
+
#
|
|
176
|
+
def separator( node )
|
|
177
|
+
tail = node.raw_tail.to_s
|
|
178
|
+
return tail.sub( /#{BEGIN_BLOCK}\s*\z/o, '' ) if node.block_style == :begin_end
|
|
179
|
+
|
|
180
|
+
value = node.raw_value.to_s
|
|
181
|
+
return tail[ 0, tail.length - value.length ] if !value.empty? && tail.end_with?( value )
|
|
182
|
+
|
|
183
|
+
trimmed = tail.rstrip
|
|
184
|
+
return trimmed.end_with?( ':' ) ? "#{trimmed} " : SEP
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
#
|
|
188
|
+
# The block style of a declaration written from scratch.
|
|
189
|
+
#
|
|
190
|
+
def block_style_of( text )
|
|
191
|
+
text = text.scrub
|
|
192
|
+
first = text.lines.first.to_s
|
|
193
|
+
return :begin_end if first.rstrip.end_with?( BEGIN_BLOCK ) && text.lines.size > 1
|
|
194
|
+
return :body if text.lines.size > 1
|
|
195
|
+
|
|
196
|
+
return :inline
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
#
|
|
200
|
+
# Warn that a value is saved as the closest thing gloo can write.
|
|
201
|
+
#
|
|
202
|
+
def warn_not_exact( obj, why )
|
|
203
|
+
@engine.warn format( NOT_EXACT, obj.pn, why )
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
end
|
|
@@ -30,28 +30,42 @@ module Gloo
|
|
|
30
30
|
#
|
|
31
31
|
# Get all the gloo files in the folder (partial path). The folder
|
|
32
32
|
# can end with the start of a file name, as a prefix (folder/a).
|
|
33
|
+
# A relative folder is looked for in each of the given folders in
|
|
34
|
+
# turn (see default_folders); the first that has matching files
|
|
35
|
+
# wins.
|
|
33
36
|
#
|
|
34
|
-
def get_all_files_in( folder )
|
|
35
|
-
|
|
36
|
-
|
|
37
|
+
def get_all_files_in( folder, folders = nil )
|
|
38
|
+
return Dir.glob( "#{absolute_folder( folder )}*.gloo" ) if absolute?( folder )
|
|
39
|
+
|
|
40
|
+
( folders || default_folders ).each do |base|
|
|
41
|
+
files = Dir.glob( "#{File.join( base, folder )}*.gloo" )
|
|
42
|
+
return files.map { |f| File.expand_path( f ) } unless files.empty?
|
|
43
|
+
end
|
|
44
|
+
return []
|
|
37
45
|
end
|
|
38
46
|
|
|
39
47
|
#
|
|
40
|
-
#
|
|
41
|
-
# given (~ expanded), anything else in the project folder, or if it
|
|
42
|
-
# isn't there, in the gloo home.
|
|
48
|
+
# An absolute or ~ folder, with ~ expanded.
|
|
43
49
|
#
|
|
44
|
-
def
|
|
50
|
+
def absolute_folder( folder )
|
|
45
51
|
return folder.sub( '~', Dir.home ) if folder.start_with?( '~' )
|
|
46
|
-
return folder if File.absolute_path?( folder )
|
|
47
52
|
|
|
48
|
-
|
|
49
|
-
|
|
53
|
+
return folder
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
#
|
|
57
|
+
# Is the name a full path, or one starting with ~?
|
|
58
|
+
#
|
|
59
|
+
def absolute?( name )
|
|
60
|
+
return name.start_with?( '~' ) || File.absolute_path?( name )
|
|
61
|
+
end
|
|
50
62
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
63
|
+
#
|
|
64
|
+
# Where a relative name is looked for when no folders are given:
|
|
65
|
+
# the project folder, then the gloo home.
|
|
66
|
+
#
|
|
67
|
+
def default_folders
|
|
68
|
+
return [ @engine.settings.project_path, @engine.settings.user_root ]
|
|
55
69
|
end
|
|
56
70
|
|
|
57
71
|
#
|
|
@@ -75,59 +89,76 @@ module Gloo
|
|
|
75
89
|
|
|
76
90
|
#
|
|
77
91
|
# Expand a single file path. The .gloo extension is optional: it's
|
|
78
|
-
# added when the name doesn't end with it
|
|
79
|
-
# and for a name in the project or gloo home.
|
|
92
|
+
# added when the name doesn't end with it.
|
|
80
93
|
#
|
|
81
|
-
|
|
94
|
+
# A full or ~ path is used as given. A relative name is looked for
|
|
95
|
+
# in each of the given folders in turn (the load verb's order, see
|
|
96
|
+
# PersistMan#search_folders). With no folders given (a file named
|
|
97
|
+
# on the command line), it's tried as given, relative to the
|
|
98
|
+
# working folder, and then in the project folder and gloo home.
|
|
99
|
+
#
|
|
100
|
+
def expand( name, folders = nil )
|
|
101
|
+
full_name = name.end_with?( file_ext ) ? name : "#{name}#{file_ext}"
|
|
102
|
+
return expand_in( full_name, folders ) if folders && !absolute?( name )
|
|
103
|
+
|
|
82
104
|
# Try the name as given: a full path, or relative to the working
|
|
83
105
|
# folder (eg. gloo hello_world.gloo).
|
|
84
106
|
ext_path = File.expand_path( name )
|
|
85
107
|
return [ ext_path ] if self.valid?( ext_path )
|
|
86
108
|
|
|
87
|
-
full_name = name.end_with?( file_ext ) ? name : "#{name}#{file_ext}"
|
|
88
|
-
|
|
89
109
|
# A full or ~ path without the extension.
|
|
90
|
-
if
|
|
110
|
+
if absolute?( name )
|
|
91
111
|
ext_path = File.expand_path( full_name )
|
|
92
112
|
return [ ext_path ] if self.valid?( ext_path )
|
|
93
113
|
end
|
|
94
114
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
return [ pn ] if self.valid?( pn )
|
|
98
|
-
|
|
99
|
-
# Try in user root
|
|
100
|
-
pn = File.join( @engine.settings.user_root, full_name )
|
|
101
|
-
return [ pn ] if self.valid?( pn )
|
|
115
|
+
return expand_in( full_name, default_folders )
|
|
116
|
+
end
|
|
102
117
|
|
|
118
|
+
#
|
|
119
|
+
# The file in the first of the folders that has it, as a one-item
|
|
120
|
+
# list; nil if none does.
|
|
121
|
+
#
|
|
122
|
+
def expand_in( full_name, folders )
|
|
123
|
+
folders.each do |base|
|
|
124
|
+
pn = File.expand_path( File.join( base, full_name ) )
|
|
125
|
+
return [ pn ] if self.valid?( pn )
|
|
126
|
+
end
|
|
103
127
|
return nil
|
|
104
128
|
end
|
|
105
129
|
|
|
106
130
|
#
|
|
107
|
-
# Read in the contents of a single file
|
|
131
|
+
# Read in the contents of a single file, as UTF-8 whatever the
|
|
132
|
+
# locale: a .gloo file is UTF-8 (under LANG=C, a plain read would
|
|
133
|
+
# take every non-ASCII character as an invalid byte). Bytes that
|
|
134
|
+
# aren't valid UTF-8 are kept as they are; the loader warns about
|
|
135
|
+
# them.
|
|
108
136
|
#
|
|
109
137
|
def read( file )
|
|
110
|
-
return File.
|
|
138
|
+
return File.binread( file ).force_encoding( Encoding::UTF_8 )
|
|
111
139
|
end
|
|
112
140
|
|
|
113
141
|
#
|
|
114
142
|
# Resolve a path to save a new (not-yet-existing) file to, given
|
|
115
|
-
# a name or relative path
|
|
116
|
-
#
|
|
117
|
-
#
|
|
143
|
+
# a name or relative path, with .gloo appended if missing. Unlike
|
|
144
|
+
# expand, the file doesn't have to exist. A full path is used as
|
|
145
|
+
# given and a ~ path is expanded. A relative name goes in the
|
|
146
|
+
# given folder (the first place a load would look, see
|
|
147
|
+
# PersistMan#save_folder), or the project folder if none is given.
|
|
118
148
|
#
|
|
119
|
-
def resolve_save_path( name )
|
|
149
|
+
def resolve_save_path( name, folder = nil )
|
|
120
150
|
full_name = name.end_with?( file_ext ) ? name : "#{name}#{file_ext}"
|
|
121
|
-
return full_name if
|
|
151
|
+
return File.expand_path( absolute_folder( full_name ) ) if absolute?( full_name )
|
|
122
152
|
|
|
123
|
-
return File.join( @engine.settings.project_path, full_name )
|
|
153
|
+
return File.expand_path( File.join( folder || @engine.settings.project_path, full_name ) )
|
|
124
154
|
end
|
|
125
155
|
|
|
126
|
-
#
|
|
127
|
-
# Write data to the file
|
|
128
|
-
#
|
|
156
|
+
#
|
|
157
|
+
# Write data to the file, byte for byte (no conversion for the
|
|
158
|
+
# locale).
|
|
159
|
+
#
|
|
129
160
|
def write( pn, data )
|
|
130
|
-
File.
|
|
161
|
+
File.binwrite( pn, data )
|
|
131
162
|
end
|
|
132
163
|
|
|
133
164
|
end
|