tracksperanto 1.1.1 → 1.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.DS_Store +0 -0
- data/History.txt +11 -0
- data/Manifest.txt +64 -20
- data/README.txt +82 -30
- data/Rakefile +1 -0
- data/bin/tracksperanto +21 -14
- data/lib/export/base.rb +16 -6
- data/lib/export/match_mover.rb +40 -0
- data/lib/export/nuke_script.rb +78 -0
- data/lib/export/pftrack.rb +14 -14
- data/lib/export/shake_text.rb +7 -8
- data/lib/export/syntheyes.rb +9 -5
- data/lib/import/base.rb +45 -12
- data/lib/import/flame_stabilizer.rb +17 -32
- data/lib/import/match_mover.rb +65 -0
- data/lib/import/nuke_script.rb +96 -0
- data/lib/import/pftrack.rb +9 -2
- data/lib/import/shake_grammar/catcher.rb +56 -0
- data/lib/import/shake_grammar/lexer.rb +95 -0
- data/lib/import/shake_script.rb +189 -103
- data/lib/import/shake_text.rb +5 -3
- data/lib/import/syntheyes.rb +9 -2
- data/lib/middleware/base.rb +6 -1
- data/lib/middleware/golden.rb +7 -0
- data/lib/middleware/reformat.rb +10 -3
- data/lib/middleware/scaler.rb +14 -4
- data/lib/middleware/shift.rb +10 -0
- data/lib/middleware/slipper.rb +3 -0
- data/lib/pipeline/base.rb +111 -37
- data/lib/tracksperanto/block_init.rb +7 -0
- data/lib/tracksperanto/casts.rb +31 -0
- data/lib/tracksperanto/format_detector.rb +35 -0
- data/lib/tracksperanto/keyframe.rb +31 -0
- data/lib/tracksperanto/safety.rb +20 -0
- data/lib/tracksperanto/tracker.rb +38 -0
- data/lib/tracksperanto/zip_tuples.rb +20 -0
- data/lib/tracksperanto.rb +13 -100
- data/test/.DS_Store +0 -0
- data/test/export/.DS_Store +0 -0
- data/test/export/README_EXPORT_TESTS.txt +15 -0
- data/test/export/samples/ref_NukeScript.nk +25 -0
- data/test/export/samples/ref_NukeScript.nk.autosave +77 -0
- data/test/export/samples/ref_PFTrack.2dt +50 -0
- data/test/export/samples/ref_ShakeText.txt +48 -0
- data/test/export/samples/ref_Syntheyes.txt +42 -0
- data/test/export/samples/ref_matchmover.rz2 +52 -0
- data/test/export/test_match_mover_export.rb +16 -0
- data/test/export/test_mux.rb +23 -0
- data/test/export/test_nuke_export.rb +22 -0
- data/test/export/test_pftrack_export.rb +19 -0
- data/test/export/test_shake_export.rb +15 -0
- data/test/export/test_syntheyes_export.rb +15 -0
- data/test/helper.rb +85 -2
- data/test/import/.DS_Store +0 -0
- data/test/{samples → import/samples}/.DS_Store +0 -0
- data/test/{samples → import/samples}/flyover2DP_syntheyes.txt +0 -0
- data/test/import/samples/four_tracks_in_one_matchmove.shk +323 -0
- data/test/import/samples/four_tracks_in_one_stabilizer.shk +321 -0
- data/test/{samples → import/samples}/fromCombustion_fromMidClip_wSnap.stabilizer +0 -0
- data/test/{samples → import/samples}/hugeFlameSetup.stabilizer +0 -0
- data/test/import/samples/kipPointsMatchmover.rz2 +523 -0
- data/test/{samples → import/samples}/megaTrack.action.3dtrack.stabilizer +0 -0
- data/test/{samples → import/samples}/one_shake_tracker.txt +0 -0
- data/test/{samples → import/samples}/one_shake_tracker_from_first.txt +0 -0
- data/test/import/samples/one_tracker_with_break.nk +71 -0
- data/test/import/samples/one_tracker_with_break_in_grp.nk +91 -0
- data/test/{samples → import/samples}/shake_tracker_nodes.shk +0 -0
- data/test/{samples → import/samples}/shake_tracker_nodes_to_syntheyes.txt +0 -0
- data/test/{samples → import/samples}/sourcefile_pftrack.2dt +0 -0
- data/test/{samples → import/samples}/three_tracks_in_one_stabilizer.shk +0 -0
- data/test/{samples → import/samples}/two_shake_trackers.txt +0 -0
- data/test/{samples → import/samples}/two_tracks_in_one_tracker.shk +0 -0
- data/test/{test_flame_import.rb → import/test_flame_import.rb} +15 -9
- data/test/import/test_match_mover_import.rb +44 -0
- data/test/import/test_nuke_import.rb +63 -0
- data/test/{test_pftrack_import.rb → import/test_pftrack_import.rb} +10 -4
- data/test/import/test_shake_catcher.rb +72 -0
- data/test/import/test_shake_lexer.rb +95 -0
- data/test/import/test_shake_script_import.rb +75 -0
- data/test/{test_shake_text_import.rb → import/test_shake_text_import.rb} +10 -3
- data/test/{test_syntheyes_import.rb → import/test_syntheyes_import.rb} +8 -3
- data/test/middleware/test_golden_middleware.rb +32 -0
- data/test/middleware/test_reformat_middleware.rb +35 -0
- data/test/middleware/test_scaler_middleware.rb +51 -0
- data/test/middleware/test_shift_middleware.rb +26 -0
- data/test/middleware/test_slip_middleware.rb +31 -0
- data/test/pipeline/test_pipeline_base.rb +14 -0
- data/test/test_format_detector.rb +39 -0
- data/test/test_tracker.rb +48 -0
- data/tracksperanto.gemspec +9 -7
- metadata +101 -30
- data/lib/middleware/close.rb +0 -6
- data/test/test_shake_export.rb +0 -58
- data/test/test_shake_script_import.rb +0 -50
data/lib/import/base.rb
CHANGED
@@ -1,34 +1,67 @@
|
|
1
|
-
|
2
|
-
|
1
|
+
# The base class for all the import modules. By default, when you inherit from this class the inherited class will be included
|
2
|
+
# in the list of supported Tracksperanto importers. The API that an importer should present is very basic, and consists only of a few methods.
|
3
|
+
# The main method is parse(io) which should return an array of Tracker objects.
|
3
4
|
class Tracksperanto::Import::Base
|
4
5
|
include Tracksperanto::Safety
|
5
6
|
include Tracksperanto::Casts
|
7
|
+
include Tracksperanto::BlockInit
|
8
|
+
include Tracksperanto::ZipTuples
|
6
9
|
|
10
|
+
# Tracksperanto will assign a proc that reports the status of the import to the caller.
|
11
|
+
# This block is automatically used by report_progress IF the proc is assigned. Should
|
12
|
+
# the proc be nil, the report_progress method will just pass (so you don't need to check for nil
|
13
|
+
# yourself)
|
7
14
|
attr_accessor :progress_block
|
8
15
|
|
9
|
-
# The original width of the tracked image
|
10
|
-
#
|
11
|
-
|
16
|
+
# The original width of the tracked image.
|
17
|
+
# If you need to know the width for your specific format and cannot autodetect it,
|
18
|
+
# Trakcksperanto will assign the passed width and height to the importer object before running
|
19
|
+
# the import. If not, you can replace the assigned values with your own. At the end of the import
|
20
|
+
# procedure, Tracksperanto will read the values from you again and will use the read values
|
21
|
+
# for determining the original comp size. +width+ and +height+ MUST return integer values after
|
22
|
+
# the import completes
|
23
|
+
attr_accessor :width
|
12
24
|
|
13
|
-
# The original height of the
|
14
|
-
|
15
|
-
cast_to_int :height
|
25
|
+
# The original height of the comp, same conventions as for width apply
|
26
|
+
attr_accessor :height
|
16
27
|
|
17
|
-
#
|
28
|
+
# These reader methods will raise when the values are nil
|
29
|
+
cast_to_int :width, :height
|
18
30
|
safe_reader :width, :height
|
19
31
|
|
32
|
+
# Used to register your importer in the list of supported formats
|
20
33
|
def self.inherited(by)
|
21
34
|
Tracksperanto.importers << by
|
22
35
|
super
|
23
36
|
end
|
24
37
|
|
25
|
-
#
|
38
|
+
# Return an extension WITH DOT if this format has a typical extension that
|
39
|
+
# you can detect
|
40
|
+
def self.distinct_file_ext
|
41
|
+
nil
|
42
|
+
end
|
43
|
+
|
44
|
+
# Should return a human-readable (read: properly capitalized and with spaces) name of the
|
45
|
+
# import format
|
46
|
+
def self.human_name
|
47
|
+
"Abstract import format"
|
48
|
+
end
|
49
|
+
|
50
|
+
# Return true from this method if your importer can deduce the comp size from the passed file
|
51
|
+
def self.autodetects_size?
|
52
|
+
false
|
53
|
+
end
|
54
|
+
|
55
|
+
# Call this method to tell what you are doing. This gets propagated to the caller automatically, or
|
56
|
+
# gets ignored if the caller did not request any progress reports
|
26
57
|
def report_progress(message)
|
27
58
|
@progress_block.call(message) if @progress_block
|
28
59
|
end
|
29
60
|
|
30
|
-
#
|
31
|
-
|
61
|
+
# The main method of the parser. Will receive an IO handle to the file being imported, and should
|
62
|
+
# return an array of Tracksperanto::Tracker objects containing keyframes. If you have a problem
|
63
|
+
# doing an import, raise from here.
|
64
|
+
def parse(track_file_io)
|
32
65
|
[]
|
33
66
|
end
|
34
67
|
end
|
@@ -1,12 +1,17 @@
|
|
1
|
-
require 'stringio'
|
2
1
|
|
3
2
|
class Tracksperanto::Import::FlameStabilizer < Tracksperanto::Import::Base
|
4
3
|
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
4
|
+
# Flame setups contain clear size indications
|
5
|
+
def self.autodetects_size?
|
6
|
+
true
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.distinct_file_ext
|
10
|
+
".stabilizer"
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.human_name
|
14
|
+
"Flame .stabilizer file"
|
10
15
|
end
|
11
16
|
|
12
17
|
T = ::Tracksperanto::Tracker
|
@@ -56,7 +61,7 @@ class Tracksperanto::Import::FlameStabilizer < Tracksperanto::Import::Base
|
|
56
61
|
frame = $1.to_i
|
57
62
|
elsif line =~ value_matcher
|
58
63
|
value = $1.to_f
|
59
|
-
return
|
64
|
+
return [frame,value]
|
60
65
|
end
|
61
66
|
end
|
62
67
|
|
@@ -64,17 +69,14 @@ class Tracksperanto::Import::FlameStabilizer < Tracksperanto::Import::Base
|
|
64
69
|
end
|
65
70
|
end
|
66
71
|
|
67
|
-
def parse(
|
68
|
-
|
69
|
-
io = StringIO.new(stabilizer_setup_content)
|
70
|
-
|
72
|
+
def parse(io)
|
71
73
|
self.width, self.height = extract_width_and_height_from_stream(io)
|
72
74
|
channels = extract_channels_from_stream(io)
|
73
75
|
|
74
76
|
raise "The setup contained no channels that we could process" if channels.empty?
|
75
77
|
raise "A channel was nil" if channels.find{|e| e.nil? }
|
76
78
|
|
77
|
-
|
79
|
+
scavenge_trackers_from_channels(channels)
|
78
80
|
end
|
79
81
|
|
80
82
|
private
|
@@ -97,7 +99,7 @@ class Tracksperanto::Import::FlameStabilizer < Tracksperanto::Import::Base
|
|
97
99
|
|
98
100
|
end
|
99
101
|
=begin
|
100
|
-
|
102
|
+
Here's how a Flame channel looks like
|
101
103
|
The Size will not be present if there are no keyframes
|
102
104
|
|
103
105
|
Channel tracker1/ref/x
|
@@ -132,7 +134,6 @@ Channel tracker1/ref/x
|
|
132
134
|
Colour 50 50 50
|
133
135
|
End
|
134
136
|
=end
|
135
|
-
|
136
137
|
def extract_channels_from_stream(io)
|
137
138
|
channels = []
|
138
139
|
channel_matcher = /Channel (.+)\n/
|
@@ -169,8 +170,8 @@ Channel tracker1/ref/x
|
|
169
170
|
shift_x = channels.find{|e| e.name == "#{t.name}/shift/x" }
|
170
171
|
shift_y = channels.find{|e| e.name == "#{t.name}/shift/y" }
|
171
172
|
|
172
|
-
shift_tuples =
|
173
|
-
track_tuples =
|
173
|
+
shift_tuples = zip_curve_tuples(shift_x, shift_y)
|
174
|
+
track_tuples = zip_curve_tuples(track_x, track_y)
|
174
175
|
|
175
176
|
base_x, base_y = begin
|
176
177
|
find_base_x_and_y(track_tuples, shift_tuples)
|
@@ -205,20 +206,4 @@ Channel tracker1/ref/x
|
|
205
206
|
raise UseBase
|
206
207
|
end
|
207
208
|
end
|
208
|
-
|
209
|
-
# Zip two channel objects to tuples of [frame, valuex, valuey]
|
210
|
-
# skipping keyframes that do not match in the two
|
211
|
-
def zip_channels(a, b)
|
212
|
-
tuples = []
|
213
|
-
|
214
|
-
a.each do | keyframe |
|
215
|
-
tuples[keyframe.frame] = [keyframe.frame, keyframe.value]
|
216
|
-
end
|
217
|
-
|
218
|
-
b.each do | keyframe |
|
219
|
-
tuples[keyframe.frame] = (tuples[keyframe.frame] << keyframe.value) if tuples[keyframe.frame]
|
220
|
-
end
|
221
|
-
|
222
|
-
tuples.compact
|
223
|
-
end
|
224
209
|
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
class Tracksperanto::Import::MatchMover < Tracksperanto::Import::Base
|
2
|
+
|
3
|
+
def self.autodetects_size?
|
4
|
+
true
|
5
|
+
end
|
6
|
+
|
7
|
+
def self.human_name
|
8
|
+
"MatchMover .rz2 file"
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.distinct_file_ext
|
12
|
+
".rz2"
|
13
|
+
end
|
14
|
+
|
15
|
+
def parse(io)
|
16
|
+
trackers = []
|
17
|
+
detect_format(io)
|
18
|
+
extract_trackers(io)
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def detect_format(io)
|
24
|
+
lines = (0..2).map{ io.gets }
|
25
|
+
last_line = lines[-1]
|
26
|
+
w, h, _ = last_line.scan(/(\d+)/).flatten
|
27
|
+
@width, @height = w.to_i, h.to_i
|
28
|
+
end
|
29
|
+
|
30
|
+
def extract_trackers(io)
|
31
|
+
tracks = []
|
32
|
+
while(line = io.gets) do
|
33
|
+
tracks << extract_track(line, io) if line =~ /^pointTrack/
|
34
|
+
end
|
35
|
+
tracks
|
36
|
+
end
|
37
|
+
|
38
|
+
def extract_track(start_line, io)
|
39
|
+
tracker_name = start_line.scan(/\"([^\"]+)\"/).to_s
|
40
|
+
t = Tracksperanto::Tracker.new(:name => tracker_name)
|
41
|
+
while(line = io.gets) do
|
42
|
+
return t if line =~ /\}/
|
43
|
+
t.keyframes.push(extract_key(line.strip)) if line =~ /^(\s+?)(\d)/
|
44
|
+
end
|
45
|
+
raise "Track didn't close"
|
46
|
+
end
|
47
|
+
|
48
|
+
LINE_PATTERN = /(\d+)(\s+)([\-\d\.]+)(\s+)([\-\d\.]+)(\s+)(.+)/
|
49
|
+
|
50
|
+
def extract_key(line)
|
51
|
+
frame, x, y, residual, rest = line.scan(LINE_PATTERN).flatten.reject{|e| e.strip.empty? }
|
52
|
+
Tracksperanto::Keyframe.new(
|
53
|
+
:frame => (frame.to_i() - 1),
|
54
|
+
:abs_x => x,
|
55
|
+
:abs_y => y,
|
56
|
+
:residual => extract_residual(residual)
|
57
|
+
)
|
58
|
+
end
|
59
|
+
|
60
|
+
def extract_residual(residual_segment)
|
61
|
+
# Parse to the first opening brace and pick the residual from there
|
62
|
+
float_pat = /([\-\d\.]+)/
|
63
|
+
1 - residual_segment.scan(float_pat).flatten.shift.to_f
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,96 @@
|
|
1
|
+
require 'delegate'
|
2
|
+
|
3
|
+
class Tracksperanto::Import::NukeScript < Tracksperanto::Import::Base
|
4
|
+
|
5
|
+
def self.human_name
|
6
|
+
"Nuke .nk script file"
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.distinct_file_ext
|
10
|
+
".nk"
|
11
|
+
end
|
12
|
+
|
13
|
+
# Nuke files are extensively indented and indentation is significant.
|
14
|
+
# We use this to always strip the lines we process since we capture before
|
15
|
+
# indentation becomes crucial
|
16
|
+
class IOC < DelegateClass(IO)
|
17
|
+
def initialize(h)
|
18
|
+
__setobj__(h)
|
19
|
+
end
|
20
|
+
|
21
|
+
def gets_and_strip
|
22
|
+
s = __getobj__.gets
|
23
|
+
s ? s.strip : nil
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
|
28
|
+
def parse(io)
|
29
|
+
scan_for_tracker3_nodes(IOC.new(io))
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
TRACKER_3_PATTERN = /^Tracker3 \{/
|
35
|
+
TRACK_PATTERN = /^track(\d) \{/
|
36
|
+
NODENAME = /^name ([^\n]+)/
|
37
|
+
|
38
|
+
def scan_for_tracker3_nodes(io)
|
39
|
+
tracks = []
|
40
|
+
while line = io.gets_and_strip
|
41
|
+
tracks += scan_tracker_node(io) if line =~ TRACKER_3_PATTERN
|
42
|
+
end
|
43
|
+
tracks
|
44
|
+
end
|
45
|
+
|
46
|
+
def scan_tracker_node(io)
|
47
|
+
tracks_in_tracker = []
|
48
|
+
while line = io.gets_and_strip
|
49
|
+
if line =~ TRACK_PATTERN
|
50
|
+
tuples = scan_track(line)
|
51
|
+
tk = Tracksperanto::Tracker.new(
|
52
|
+
:keyframes => tuples.map do | (f, x, y) |
|
53
|
+
Tracksperanto::Keyframe.new(:frame => f -1, :abs_x => x, :abs_y => y)
|
54
|
+
end
|
55
|
+
)
|
56
|
+
tracks_in_tracker.push(tk)
|
57
|
+
elsif line =~ NODENAME
|
58
|
+
tracks_in_tracker.each_with_index do | t, i |
|
59
|
+
t.name = "#{$1}_track#{i+1}"
|
60
|
+
end
|
61
|
+
return tracks_in_tracker
|
62
|
+
end
|
63
|
+
end
|
64
|
+
raise "Tracker node went all the way to end of stream"
|
65
|
+
end
|
66
|
+
|
67
|
+
def scan_track(line_with_curve)
|
68
|
+
x_curve, y_curve = line_with_curve.split(/\}/).map{ | one_curve| parse_curve(one_curve) }
|
69
|
+
zip_curve_tuples(x_curve, y_curve)
|
70
|
+
end
|
71
|
+
|
72
|
+
# Scan a curve to a number of triplets
|
73
|
+
def parse_curve(curve_text)
|
74
|
+
# Replace the closing curly brace with a curly brace with space so that it gets caught by split
|
75
|
+
atoms, tuples = curve_text.gsub(/\}/, ' }').split, []
|
76
|
+
# Nuke saves curves very efficiently. x(keyframe_number) means that an uninterrupted sequence of values will start,
|
77
|
+
# after which values follow. When the curve is interrupted in some way a new x(keyframe_number) will signifu that we
|
78
|
+
# skip to that specified keyframe and the curve continues from there
|
79
|
+
section_start = /^x(\d+)$/
|
80
|
+
keyframe = /^([-\d\.]+)$/
|
81
|
+
|
82
|
+
last_processed_keyframe = 1
|
83
|
+
while atom = atoms.shift
|
84
|
+
if atom =~ section_start
|
85
|
+
last_processed_keyframe = $1.to_i
|
86
|
+
elsif atom =~ keyframe
|
87
|
+
tuples << [last_processed_keyframe, $1.to_f]
|
88
|
+
last_processed_keyframe += 1
|
89
|
+
elsif atom == '}'
|
90
|
+
return tuples
|
91
|
+
end
|
92
|
+
end
|
93
|
+
tuples
|
94
|
+
end
|
95
|
+
|
96
|
+
end
|
data/lib/import/pftrack.rb
CHANGED
@@ -1,7 +1,14 @@
|
|
1
1
|
class Tracksperanto::Import::PFTrack < Tracksperanto::Import::Base
|
2
|
-
def
|
2
|
+
def self.human_name
|
3
|
+
"PFTrack .2dt file"
|
4
|
+
end
|
5
|
+
|
6
|
+
def self.distinct_file_ext
|
7
|
+
".2dt"
|
8
|
+
end
|
9
|
+
|
10
|
+
def parse(io)
|
3
11
|
trackers = []
|
4
|
-
io = StringIO.new(file_content)
|
5
12
|
until io.eof?
|
6
13
|
line = io.gets
|
7
14
|
next unless line
|
@@ -0,0 +1,56 @@
|
|
1
|
+
module Tracksperanto::ShakeGrammar
|
2
|
+
# Will replay funcalls through to methods if such methods exist in the public insntance
|
3
|
+
class Catcher < Lexer
|
4
|
+
|
5
|
+
def push(atom_array)
|
6
|
+
atom_name = atom_array[0]
|
7
|
+
if atom_name == :funcall
|
8
|
+
func, funcargs = atom_array[1], atom_array[2..-1]
|
9
|
+
meth_for_shake_func = func.downcase
|
10
|
+
if can_handle_meth?(meth_for_shake_func)
|
11
|
+
super([:retval, exec_funcall(meth_for_shake_func, funcargs)])
|
12
|
+
else
|
13
|
+
# This is a funcall we cannot perform, replace the return result of the funcall
|
14
|
+
# with a token to signify that some unknown function's result would have been here
|
15
|
+
super(:unknown_func)
|
16
|
+
end
|
17
|
+
elsif atom_name == :comment
|
18
|
+
# Skip comments
|
19
|
+
else
|
20
|
+
super
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def get_variable_name
|
27
|
+
@stack[-2][1]
|
28
|
+
end
|
29
|
+
|
30
|
+
def can_handle_meth?(m)
|
31
|
+
self.class.public_instance_methods(false).include?(m)
|
32
|
+
end
|
33
|
+
|
34
|
+
def exec_funcall(methname, args)
|
35
|
+
ruby_args = unwrap_atoms_in_args(args)
|
36
|
+
send(methname, *ruby_args)
|
37
|
+
end
|
38
|
+
|
39
|
+
def unwrap_atoms_in_args(args)
|
40
|
+
args.map do | arg |
|
41
|
+
(arg.is_a?(Array) && arg[0].is_a?(Symbol)) ? unwrap_atom(arg) : arg
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def unwrap_atom(atom)
|
46
|
+
kind = atom.shift
|
47
|
+
case kind
|
48
|
+
when :retval, :atom_i, :atom_c, :atom_f
|
49
|
+
atom.shift
|
50
|
+
else
|
51
|
+
:unknown
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
module Tracksperanto::ShakeGrammar
|
2
|
+
# Since Shake uses a C-like language for it's scripts we rig up a very sloppy
|
3
|
+
# but concise C-like lexer to cope
|
4
|
+
class Lexer
|
5
|
+
|
6
|
+
attr_reader :stack
|
7
|
+
|
8
|
+
def initialize(with_io)
|
9
|
+
@io, @stack, @buf = with_io, [], ''
|
10
|
+
parse until (@io.eof? || @stop)
|
11
|
+
in_comment? ? consume_comment("\n") : consume_atom!
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
def in_comment?
|
17
|
+
(@buf.strip =~ /^\/\//) ? true : false
|
18
|
+
end
|
19
|
+
|
20
|
+
def consume_comment(c)
|
21
|
+
if c == "\n" # Comment
|
22
|
+
push [:comment, @buf.gsub(/(\s+?)\/\/{1}/, '')]
|
23
|
+
@buf = ''
|
24
|
+
else
|
25
|
+
@buf << c
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def parse
|
30
|
+
c = @io.read(1)
|
31
|
+
return consume_comment(c) if in_comment?
|
32
|
+
|
33
|
+
if !@buf.empty? && (c == "(") # Funcall
|
34
|
+
push([:funcall, @buf.strip] + self.class.new(@io).stack)
|
35
|
+
@buf = ''
|
36
|
+
elsif c == "[" # Array, booring
|
37
|
+
push([:arr] + self.class.new(@io).stack)
|
38
|
+
elsif (c == "]" || c == ")")
|
39
|
+
# Funcall end, and when it happens assume we are called as
|
40
|
+
# a subexpression.
|
41
|
+
consume_atom!
|
42
|
+
@stop = true
|
43
|
+
elsif (c == ",")
|
44
|
+
consume_atom!
|
45
|
+
elsif (c == ";" || c == "\n")
|
46
|
+
# Skip these - the subexpression already is expanded anyway
|
47
|
+
elsif (c == "=")
|
48
|
+
push [:var, @buf.strip]
|
49
|
+
push [:eq]
|
50
|
+
@buf = ''
|
51
|
+
else
|
52
|
+
@buf << c
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
INT_ATOM = /^(\d+)$/
|
57
|
+
FLOAT_ATOM = /^([\-\d\.]+)$/
|
58
|
+
STR_ATOM = /^\"/
|
59
|
+
AT_ATOM = /^([\-\d\.]+)@([\-\d\.]+)$/
|
60
|
+
AT_CONSUMED = /^@(\d+)/
|
61
|
+
VAR_ASSIGN = /^([\w_]+)(\s+?)\=(\s+?)(.+)/
|
62
|
+
|
63
|
+
# Grab the minimum atomic value
|
64
|
+
def consume_atom!
|
65
|
+
at, @buf = @buf.strip, ''
|
66
|
+
return if at.empty?
|
67
|
+
|
68
|
+
the_atom = case at
|
69
|
+
when INT_ATOM
|
70
|
+
[:atom_i, at.to_i]
|
71
|
+
when STR_ATOM
|
72
|
+
[:atom_c, unquote_s(at)]
|
73
|
+
when FLOAT_ATOM
|
74
|
+
[:atom_f, at.to_f]
|
75
|
+
when AT_ATOM
|
76
|
+
v, f = at.strip.split("@")
|
77
|
+
[[:atom_f, v.to_f], [:atom_at_i, f.to_i]]
|
78
|
+
when AT_CONSUMED
|
79
|
+
[:atom_at_i, $1.to_i]
|
80
|
+
else
|
81
|
+
[:atom, at]
|
82
|
+
end
|
83
|
+
|
84
|
+
push(the_atom)
|
85
|
+
end
|
86
|
+
|
87
|
+
def unquote_s(string)
|
88
|
+
string.strip.gsub(/^\"/, '').gsub(/\"$/, '').gsub(/\\\"/, '"')
|
89
|
+
end
|
90
|
+
|
91
|
+
def push(atom_array)
|
92
|
+
@stack << atom_array
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|