tracksperanto 1.1.1 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (94) hide show
  1. data/.DS_Store +0 -0
  2. data/History.txt +11 -0
  3. data/Manifest.txt +64 -20
  4. data/README.txt +82 -30
  5. data/Rakefile +1 -0
  6. data/bin/tracksperanto +21 -14
  7. data/lib/export/base.rb +16 -6
  8. data/lib/export/match_mover.rb +40 -0
  9. data/lib/export/nuke_script.rb +78 -0
  10. data/lib/export/pftrack.rb +14 -14
  11. data/lib/export/shake_text.rb +7 -8
  12. data/lib/export/syntheyes.rb +9 -5
  13. data/lib/import/base.rb +45 -12
  14. data/lib/import/flame_stabilizer.rb +17 -32
  15. data/lib/import/match_mover.rb +65 -0
  16. data/lib/import/nuke_script.rb +96 -0
  17. data/lib/import/pftrack.rb +9 -2
  18. data/lib/import/shake_grammar/catcher.rb +56 -0
  19. data/lib/import/shake_grammar/lexer.rb +95 -0
  20. data/lib/import/shake_script.rb +189 -103
  21. data/lib/import/shake_text.rb +5 -3
  22. data/lib/import/syntheyes.rb +9 -2
  23. data/lib/middleware/base.rb +6 -1
  24. data/lib/middleware/golden.rb +7 -0
  25. data/lib/middleware/reformat.rb +10 -3
  26. data/lib/middleware/scaler.rb +14 -4
  27. data/lib/middleware/shift.rb +10 -0
  28. data/lib/middleware/slipper.rb +3 -0
  29. data/lib/pipeline/base.rb +111 -37
  30. data/lib/tracksperanto/block_init.rb +7 -0
  31. data/lib/tracksperanto/casts.rb +31 -0
  32. data/lib/tracksperanto/format_detector.rb +35 -0
  33. data/lib/tracksperanto/keyframe.rb +31 -0
  34. data/lib/tracksperanto/safety.rb +20 -0
  35. data/lib/tracksperanto/tracker.rb +38 -0
  36. data/lib/tracksperanto/zip_tuples.rb +20 -0
  37. data/lib/tracksperanto.rb +13 -100
  38. data/test/.DS_Store +0 -0
  39. data/test/export/.DS_Store +0 -0
  40. data/test/export/README_EXPORT_TESTS.txt +15 -0
  41. data/test/export/samples/ref_NukeScript.nk +25 -0
  42. data/test/export/samples/ref_NukeScript.nk.autosave +77 -0
  43. data/test/export/samples/ref_PFTrack.2dt +50 -0
  44. data/test/export/samples/ref_ShakeText.txt +48 -0
  45. data/test/export/samples/ref_Syntheyes.txt +42 -0
  46. data/test/export/samples/ref_matchmover.rz2 +52 -0
  47. data/test/export/test_match_mover_export.rb +16 -0
  48. data/test/export/test_mux.rb +23 -0
  49. data/test/export/test_nuke_export.rb +22 -0
  50. data/test/export/test_pftrack_export.rb +19 -0
  51. data/test/export/test_shake_export.rb +15 -0
  52. data/test/export/test_syntheyes_export.rb +15 -0
  53. data/test/helper.rb +85 -2
  54. data/test/import/.DS_Store +0 -0
  55. data/test/{samples → import/samples}/.DS_Store +0 -0
  56. data/test/{samples → import/samples}/flyover2DP_syntheyes.txt +0 -0
  57. data/test/import/samples/four_tracks_in_one_matchmove.shk +323 -0
  58. data/test/import/samples/four_tracks_in_one_stabilizer.shk +321 -0
  59. data/test/{samples → import/samples}/fromCombustion_fromMidClip_wSnap.stabilizer +0 -0
  60. data/test/{samples → import/samples}/hugeFlameSetup.stabilizer +0 -0
  61. data/test/import/samples/kipPointsMatchmover.rz2 +523 -0
  62. data/test/{samples → import/samples}/megaTrack.action.3dtrack.stabilizer +0 -0
  63. data/test/{samples → import/samples}/one_shake_tracker.txt +0 -0
  64. data/test/{samples → import/samples}/one_shake_tracker_from_first.txt +0 -0
  65. data/test/import/samples/one_tracker_with_break.nk +71 -0
  66. data/test/import/samples/one_tracker_with_break_in_grp.nk +91 -0
  67. data/test/{samples → import/samples}/shake_tracker_nodes.shk +0 -0
  68. data/test/{samples → import/samples}/shake_tracker_nodes_to_syntheyes.txt +0 -0
  69. data/test/{samples → import/samples}/sourcefile_pftrack.2dt +0 -0
  70. data/test/{samples → import/samples}/three_tracks_in_one_stabilizer.shk +0 -0
  71. data/test/{samples → import/samples}/two_shake_trackers.txt +0 -0
  72. data/test/{samples → import/samples}/two_tracks_in_one_tracker.shk +0 -0
  73. data/test/{test_flame_import.rb → import/test_flame_import.rb} +15 -9
  74. data/test/import/test_match_mover_import.rb +44 -0
  75. data/test/import/test_nuke_import.rb +63 -0
  76. data/test/{test_pftrack_import.rb → import/test_pftrack_import.rb} +10 -4
  77. data/test/import/test_shake_catcher.rb +72 -0
  78. data/test/import/test_shake_lexer.rb +95 -0
  79. data/test/import/test_shake_script_import.rb +75 -0
  80. data/test/{test_shake_text_import.rb → import/test_shake_text_import.rb} +10 -3
  81. data/test/{test_syntheyes_import.rb → import/test_syntheyes_import.rb} +8 -3
  82. data/test/middleware/test_golden_middleware.rb +32 -0
  83. data/test/middleware/test_reformat_middleware.rb +35 -0
  84. data/test/middleware/test_scaler_middleware.rb +51 -0
  85. data/test/middleware/test_shift_middleware.rb +26 -0
  86. data/test/middleware/test_slip_middleware.rb +31 -0
  87. data/test/pipeline/test_pipeline_base.rb +14 -0
  88. data/test/test_format_detector.rb +39 -0
  89. data/test/test_tracker.rb +48 -0
  90. data/tracksperanto.gemspec +9 -7
  91. metadata +101 -30
  92. data/lib/middleware/close.rb +0 -6
  93. data/test/test_shake_export.rb +0 -58
  94. data/test/test_shake_script_import.rb +0 -50
data/lib/import/base.rb CHANGED
@@ -1,34 +1,67 @@
1
- require 'stringio'
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
- # Some importers need it
11
- cast_to_int :width
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 original image.
14
- # Some importers need it
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
- # Safety on readers
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
- # Call this method to tell what you are doing. This gets propagated to the caller
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
- # Should return an array of Tracksperanto::Tracker objects
31
- def parse(track_file_content)
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
- class Kf
6
- include ::Tracksperanto::Casts
7
- include ::Tracksperanto::BlockInit
8
- cast_to_int :frame
9
- cast_to_float :value
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 Kf.new(:frame => frame, :value => value)
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(stabilizer_setup_content)
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
- trackers = scavenge_trackers_from_channels(channels)
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
- Here's how a Flame channel looks like
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 = zip_channels(shift_x, shift_y)
173
- track_tuples = zip_channels(track_x, track_y)
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
@@ -1,7 +1,14 @@
1
1
  class Tracksperanto::Import::PFTrack < Tracksperanto::Import::Base
2
- def parse(file_content)
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