tickly 2.1.0 → 2.1.2

Sign up to get free protection for your applications and to get access to all the features.
data/Gemfile CHANGED
@@ -6,7 +6,6 @@ gem 'bychar', '~> 2'
6
6
  # Include everything needed to run rake, tests, features, etc.
7
7
  group :development do
8
8
  gem "rake"
9
- gem "shoulda", ">= 0"
10
9
  gem "rdoc", "~> 3.12"
11
10
  gem "jeweler", "~> 1.8.3"
12
11
  gem "ruby-prof"
data/README.rdoc CHANGED
@@ -1,7 +1,11 @@
1
1
  = tickly
2
2
 
3
- A highly simplistic TCL parser and evaluator (primarily designed for parsing Nuke scripts). It structures
4
- the passed Nuke scripts into a TCL AST and return it. You can use some cheap tricks to discard the nodes you are not interested in.
3
+ A highly simplistic TCL parser and evaluator (primarily designed for parsing Nuke scripts).
4
+ It transforms the passed Nuke scripts into a TCL AST.
5
+ It also supports some cheap tricks to discard the nodes you are not interested in, since Nuke
6
+ scripts easily grow into tens of megabytes.
7
+
8
+ The AST format is extremely simple (nested arrays).
5
9
 
6
10
  == Plain parsing
7
11
 
@@ -25,14 +29,18 @@ will always return an Array of expressions, even if you only fed it one expressi
25
29
  # Expressions in square brackets
26
30
  p.parse '{exec cmd [fileName]}' #=> [[:c, "exec", "cmd", [:b, "fileName"]]]
27
31
 
28
- The AST is represented by simple arrays. An array is a TCL expression. An array with the :c symbol at the beginning
29
- element is an expression in curly braces. An array with the :b symbol at the beginning represents an expression with
30
- string interpolations in it. If you are curious, :c stands for "curlies" and :b for "brackets". All the other array
31
- elements are guaranteed to be strings.
32
+ The AST is represented by simple arrays. Each TCL expression becomes an array. An array starting
33
+ with the :c symbol ("c" for "curlies") is a literal expression in curly braces ({}). An array with the
34
+ :b symbol at the beginning is an expression with string interpolations (square brackets).
35
+ All the other array elements are guaranteed to be strings.
36
+
37
+ String literals are expanded to string array elements.
38
+
39
+ p.parse( '"a string with \"quote"') #=> [['a string with "quote']]
32
40
 
33
- Multiple expressions separated by ; or a newline will be accumulated as multiple arrays.
41
+ Multiple expressions separated by semicolons or newlines will be accumulated as multiple arrays.
34
42
 
35
- Lots and lots of TCL features are not supported - remember that most Nuke scripts are machine-generated and they do not
43
+ Lots and lots of TCL features are probably not supported - remember that most Nuke scripts are machine-generated and they do not
36
44
  use most of the esoteric language features.
37
45
 
38
46
  == Evaulating nodes in Nuke scripts
@@ -42,6 +50,7 @@ are actially arguments for a node constructor written out in TCL. Consider this
42
50
  hypothetic SomeNode in your script:
43
51
 
44
52
  SomeNode {
53
+ name SomeNode4
45
54
  someknob 15
46
55
  anotherknob 3
47
56
  animation {curve x1 12 45 67}
@@ -49,14 +58,17 @@ hypothetic SomeNode in your script:
49
58
  y_pos -10
50
59
  }
51
60
 
52
- and so on. You can use a NodeProcessor to capture these node constructors right as they are being parsed.
61
+ and so on. You can use a +NodeProcessor+ to capture these node constructors right as they are being parsed.
62
+ The advantage of this workflow is that the processor will discard all the nodes you don't need, saving time
63
+ and memory.
53
64
 
54
- All the nodes you are not interested in will be discarded, which matters in terms of memory use.
65
+ To match nodes you create Ruby classes matching the node classes by name. It doesn't matter if your
66
+ custom node handler is inside a module since the processor will only use the last part of the name.
55
67
 
56
- To do it you need to create Ruby classes matching the node classes by name. For example, for that SomeNode
57
- of ours:
58
-
59
- class SomeNode
68
+ For example, to capture every +SomeNode+ in your script:
69
+
70
+ # Remember, only the last part of the class name matters
71
+ class MyAwesomeDirtyScript::SomeNode
60
72
  attr_reader :knobs
61
73
  def initialize(string_keyed_knobs_hash)
62
74
  @knobs = string_keyed_knobs_hash
@@ -67,8 +79,9 @@ of ours:
67
79
  e = Tickly::NodeProcessor.new
68
80
 
69
81
  # Add the class
70
- e.add_node_handler_class Blur
82
+ e.add_node_handler_class SomeNode
71
83
 
84
+ # Open the ginormous Nuke script
72
85
  file = File.open("/mnt/raid/nuke/scripts/HugeShot_123.nk")
73
86
 
74
87
  e.parse(file) do | every_some_node |
@@ -78,7 +91,8 @@ of ours:
78
91
  ...
79
92
  end
80
93
 
81
- If you are curious, this is how Tracksperanto parses various nodes containing tracking data:
94
+ Of course you can capture multiple node classes. This is how Tracksperanto parses various
95
+ nodes containing tracking data:
82
96
 
83
97
  parser = Tickly::NodeProcessor.new
84
98
  parser.add_node_handler_class(Tracker3)
@@ -32,7 +32,7 @@ module Tickly
32
32
  # end
33
33
  class Evaluator
34
34
  def initialize
35
- @node_handlers = []
35
+ @node_handlers = {}
36
36
  end
37
37
 
38
38
  # Add a Class object that can instantiate node handlers. The last part of the class name
@@ -40,7 +40,7 @@ module Tickly
40
40
  # For example, to capture Tracker3 nodes a name like this will do:
41
41
  # Whatever::YourModule::Better::Tracker3
42
42
  def add_node_handler_class(handler_class)
43
- @node_handlers << handler_class
43
+ @node_handlers[class_name_without_modules(handler_class)] = handler_class
44
44
  end
45
45
 
46
46
  # Evaluates a single Nuke TCL command, and if it is a node constructor
@@ -50,7 +50,7 @@ module Tickly
50
50
  # (it's more of a pattern matcher really)
51
51
  def evaluate(expr)
52
52
  if will_capture?(expr)
53
- handler_class = @node_handlers.find{|e| unconst_name(e) == expr[0]}
53
+ handler_class = @node_handlers[expr[0]]
54
54
  handler_arguments = expr[1]
55
55
  hash_of_args = {}
56
56
  # Use 1..-1 to skip the curly brace symbol
@@ -81,10 +81,10 @@ module Tickly
81
81
  end
82
82
 
83
83
  def has_handler?(expr)
84
- @node_handlers.map{|handler_class| unconst_name(handler_class) }.include?(expr[0])
84
+ @node_handlers.has_key? expr[0]
85
85
  end
86
86
 
87
- def unconst_name(some_module)
87
+ def class_name_without_modules(some_module)
88
88
  some_module.to_s.split('::').pop
89
89
  end
90
90
 
@@ -24,7 +24,6 @@ module Tickly
24
24
  # e.add_node_handler_class Blur
25
25
  # e.parse(File.open("/path/to/script.nk")) do | blur_node |
26
26
  # # do whatever you want to the node instance
27
- # end
28
27
  # end
29
28
  class NodeProcessor
30
29
  def initialize
@@ -78,4 +77,4 @@ module Tickly
78
77
  return nil
79
78
  end
80
79
  end
81
- end
80
+ end
data/lib/tickly/parser.rb CHANGED
@@ -5,6 +5,10 @@ module Tickly
5
5
  # Simplistic, incomplete and most likely incorrect TCL parser
6
6
  class Parser
7
7
 
8
+ # Gets raised on invalid input
9
+ class Error < RuntimeError
10
+ end
11
+
8
12
  # Parses a piece of TCL and returns it converted into internal expression
9
13
  # structures. A basic TCL expression is just an array of Strings. An expression
10
14
  # in curly braces will have the symbol :c tacked onto the beginning of the array.
@@ -19,10 +23,10 @@ module Tickly
19
23
  def parse(io_or_str)
20
24
  bare_io = io_or_str.respond_to?(:read) ? io_or_str : StringIO.new(io_or_str)
21
25
  # Wrap the IO in a Bychar buffer to read faster
22
- reader = Bychar.wrap(bare_io)
26
+ reader = R.new(Bychar.wrap(bare_io))
23
27
  # Use multiple_expressions = true so that the top-level parsed script is always an array
24
28
  # of expressions
25
- sub_parse(reader, stop_char = nil, stack_depth = 0, multiple_expressions = true)
29
+ parse_expr(reader, stop_char = nil, stack_depth = 0, multiple_expressions = true)
26
30
  end
27
31
 
28
32
  # Override this to remove any unneeded subexpressions.
@@ -37,13 +41,30 @@ module Tickly
37
41
 
38
42
  private
39
43
 
40
- LAST_CHAR = -1..-1 # If we were 1.9 only we could use -1
41
44
  TERMINATORS = ["\n", ";"]
42
45
  ESC = 92.chr # Backslash (\)
46
+ QUOTES = %w( " ' )
47
+
48
+ # TODO: this has to go into Bychar. We should not use exprs for flow control.
49
+ class R #:nodoc: :all
50
+ def initialize(bychar)
51
+ @bychar = bychar
52
+ end
53
+
54
+ def read_one_char
55
+ begin
56
+ c = @bychar.read_one_char!
57
+ rescue Bychar::EOF
58
+ nil
59
+ end
60
+ end
61
+ end
43
62
 
44
63
  # Package the expressions, stack and buffer.
45
- # We use a special flag to tell us whether we need multuple expressions
46
- # or not, if not we just discard them
64
+ # We use a special flag to tell us whether we need multuple expressions.
65
+ # If we do, the expressions will be returned. If not, just the stack.
66
+ # Also, anything that remains on the stack will be put on the expressions
67
+ # list if multiple_expressions is true.
47
68
  def wrap_up(expressions, stack, buf, stack_depth, multiple_expressions)
48
69
  stack << buf if (buf.length > 0)
49
70
  return stack unless multiple_expressions
@@ -53,21 +74,35 @@ module Tickly
53
74
  return expressions
54
75
  end
55
76
 
77
+ # If the passed buf contains any bytes, put them on the stack and
78
+ # empty the buffer
79
+ def consume_remaining_buffer(stack, buf)
80
+ return if buf.length == 0
81
+ stack << buf.dup
82
+ buf.replace('')
83
+ end
84
+
56
85
  # Parse from a passed IO object either until an unescaped stop_char is reached
57
86
  # or until the IO is exhausted. The last argument is the class used to
58
87
  # compose the subexpression being parsed. The subparser is reentrant and not
59
88
  # destructive for the object containing it.
60
- def sub_parse(io, stop_char = nil, stack_depth = 0, multiple_expressions = false)
89
+ def parse_expr(io, stop_char = nil, stack_depth = 0, multiple_expressions = false)
61
90
  # A standard stack is an expression that does not evaluate to a string
62
91
  expressions = []
63
92
  stack = []
64
93
  buf = ''
65
- last_char_was_linebreak = false
66
94
 
67
- no_eof do
68
- char = io.read_one_char!
95
+ loop do
96
+ char = io.read_one_char
69
97
 
70
- if char == stop_char # Bail out of a subexpr
98
+ # Ignore carriage returns
99
+ next if char == "\r"
100
+
101
+ if stop_char && char.nil?
102
+ raise Error, "IO ran out when parsing a subexpression (expected to end on #{stop_char.inspect})"
103
+ elsif char == stop_char # Bail out of a subexpr or bail out on nil
104
+ # TODO: default stop_char is nil, and this is also what gets returned from a depleted
105
+ # IO on IO#read(). We should do that in Bychar.
71
106
  # Handle any remaining subexpressions
72
107
  return wrap_up(expressions, stack, buf, stack_depth, multiple_expressions)
73
108
  elsif char == " " || char == "\n" # Space
@@ -75,68 +110,62 @@ module Tickly
75
110
  stack << buf
76
111
  buf = ''
77
112
  end
78
- if TERMINATORS.include?(char) && stack.any? && !last_char_was_linebreak # Introduce a stack separator! This is a new line
79
- stack << buf if buf.length > 0
80
- # Immediately run this expression through the filter
113
+ if TERMINATORS.include?(char) && stack.any? # Introduce a stack separator! This is a new line
114
+
115
+ # First get rid of the remaining buffer data
116
+ consume_remaining_buffer(stack, buf)
117
+
118
+ # Since we now finished an expression and it is on the stack,
119
+ # we can run this expression through the filter
81
120
  filtered_expr = compact_subexpr(stack, stack_depth + 1)
82
- stack = []
83
121
 
84
122
  # Only preserve the parsed expression if it's not nil
85
123
  expressions << filtered_expr unless filtered_expr.nil?
86
124
 
87
- last_char_was_linebreak = true
125
+ # Reset the stack for the next expression
126
+ stack = []
127
+
128
+ # Note that we will return multiple expressions instead of one
88
129
  multiple_expressions = true
89
- #puts "Next expression! #{expressions.inspect} #{stack.inspect} #{buf.inspect}"
90
- else
91
- last_char_was_linebreak = false
92
130
  end
93
131
  elsif char == '[' # Opens a new string expression
94
- stack << buf if (buf.length > 0)
95
- stack << [:b] + sub_parse(io, ']', stack_depth + 1)
132
+ consume_remaining_buffer(stack, buf)
133
+ stack << [:b] + parse_expr(io, ']', stack_depth + 1)
96
134
  elsif char == '{' # Opens a new literal expression
97
- stack << buf if (buf.length > 0)
98
- stack << [:c] + sub_parse(io, '}', stack_depth + 1)
99
- elsif char == '"'
100
- stack << buf if (buf.length > 0)
101
- stack << parse_str(io, '"')
102
- elsif char == "'"
103
- stack << buf if (buf.length > 0)
104
- stack << parse_str(io, "'")
135
+ consume_remaining_buffer(stack, buf)
136
+ stack << [:c] + parse_expr(io, '}', stack_depth + 1)
137
+ elsif QUOTES.include?(char) # String
138
+ consume_remaining_buffer(stack, buf)
139
+ stack << parse_str(io, char)
105
140
  else
106
141
  buf << char
107
142
  end
108
143
  end
109
144
 
110
- return wrap_up(expressions, stack, buf, stack_depth, multiple_expressions)
111
- end
112
-
113
- def chomp!(stack)
114
- stack.delete_at(-1) if stack.any? && stack[-1].nil?
115
- end
116
-
117
- def no_eof(&blk)
118
- begin
119
- loop(&blk)
120
- rescue Bychar::EOF
121
- end
145
+ raise Error, "Should never happen"
122
146
  end
123
147
 
124
- def parse_str(io, stop_char)
148
+ # Parse a string literal, in single or double quotes.
149
+ def parse_str(io, stop_quote)
125
150
  buf = ''
126
- no_eof do
127
- c = io.read_one_char!
128
- if c == stop_char && buf[LAST_CHAR] != ESC
129
- return buf
130
- elsif buf[LAST_CHAR] == ESC # Eat out the escape char
131
- buf = buf[0..-2] # Trim the escape character at the end of the buffer
151
+ loop do
152
+ c = io.read_one_char
153
+ if c.nil?
154
+ raise Error, "The IO ran out before the end of a literal string"
155
+ elsif buf.length > 0 && last_char(buf) == ESC # If this char was escaped
156
+ # Trim the escape character at the end of the buffer
157
+ buf = buf[0..-2]
132
158
  buf << c
159
+ elsif c == stop_quote
160
+ return buf
133
161
  else
134
162
  buf << c
135
163
  end
136
164
  end
137
-
138
- return buf
139
165
  end
140
166
 
167
+ def last_char(str)
168
+ RUBY_VERSION < '1.9' ? str[-1].chr : str[-1]
169
+ end
141
170
  end
142
171
  end
data/lib/tickly.rb CHANGED
@@ -4,22 +4,5 @@ require File.dirname(__FILE__) + "/tickly/curve"
4
4
  require File.dirname(__FILE__) + "/tickly/node_processor"
5
5
 
6
6
  module Tickly
7
- VERSION = '2.1.0'
8
-
9
- # Provides the methods for quickly emitting the expression arrays,
10
- # is used in tests
11
- module Emitter #:nodoc :all
12
- def le(*elems)
13
- [:c] + elems
14
- end
15
-
16
- def e(*elems)
17
- elems
18
- end
19
-
20
- def se(*elems)
21
- [:b] + elems
22
- end
23
- end
24
-
7
+ VERSION = '2.1.2'
25
8
  end
data/test/helper.rb CHANGED
@@ -8,11 +8,27 @@ rescue Bundler::BundlerError => e
8
8
  exit e.status_code
9
9
  end
10
10
  require 'test/unit'
11
- require 'shoulda'
12
11
 
13
12
  $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
14
13
  $LOAD_PATH.unshift(File.dirname(__FILE__))
15
14
  require 'tickly'
16
15
 
17
16
  class Test::Unit::TestCase
17
+ # Provides the methods for quickly emitting the expression arrays,
18
+ # is used in tests
19
+ module Emitter #:nodoc :all
20
+ def le(*elems)
21
+ e(*elems).unshift :c
22
+ end
23
+
24
+ def e(*elems)
25
+ elems
26
+ end
27
+
28
+ def se(*elems)
29
+ e(*elems).unshift :b
30
+ end
31
+ end
32
+
33
+ include Emitter
18
34
  end