tickly 2.1.0 → 2.1.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.
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