puer 0.0.1 → 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- data/VERSION +1 -1
- data/bin/puer +58 -0
- data/lib/puer.rb +6 -1
- data/lib/puer/config.rb +24 -0
- data/lib/puer/converters.rb +36 -0
- data/lib/puer/nodes.rb +56 -0
- data/lib/puer/session.rb +129 -0
- data/lib/puer/xibtoti.rb +33 -0
- metadata +38 -33
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.0.
|
1
|
+
0.0.2
|
data/bin/puer
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
# -*- coding: utf-8 -*-
|
3
3
|
|
4
|
+
require 'rubygems'
|
4
5
|
require 'rubygems' unless defined?(Gem)
|
5
6
|
require 'thread'
|
6
7
|
|
@@ -14,3 +15,60 @@ unless $LOAD_PATH.include?(lib_dir)
|
|
14
15
|
end
|
15
16
|
|
16
17
|
require 'puer'
|
18
|
+
require 'optparse'
|
19
|
+
|
20
|
+
OptionParser.new do |opts|
|
21
|
+
opts.banner =<<END
|
22
|
+
Usage: puer [options] [filename]
|
23
|
+
Usage: puer all # convert all xib files to js under current directory
|
24
|
+
END
|
25
|
+
|
26
|
+
opts.on("-w", "--[no-]warnings", "Show warnings") do |w|
|
27
|
+
@show_warnings = w
|
28
|
+
end
|
29
|
+
|
30
|
+
opts.on("-o", "--output-file name", "Specify output file") do |o|
|
31
|
+
@output_file = o
|
32
|
+
end
|
33
|
+
|
34
|
+
opts.on("-c", "--config-file name", "Specify config file") do |o|
|
35
|
+
@config_file = o
|
36
|
+
end
|
37
|
+
|
38
|
+
end.parse!
|
39
|
+
|
40
|
+
if ARGV.size == 1
|
41
|
+
|
42
|
+
case ARGV[0]
|
43
|
+
when "all"
|
44
|
+
Dir.glob(File.join('**','*.xib')).each do |s|
|
45
|
+
puts "#{s} is converted to #{File.basename(s, '.*')}.js "
|
46
|
+
system "puer #{s} -o #{File.basename(s, '.*')}.js"
|
47
|
+
exit
|
48
|
+
end
|
49
|
+
else
|
50
|
+
input_file = ARGV.first
|
51
|
+
|
52
|
+
session = Session.new @config_file || File.join("#{File.dirname(__FILE__)}/../lib/puer", 'config.rb')
|
53
|
+
session.parse_file input_file
|
54
|
+
if session.has_errors?
|
55
|
+
puts "Aborted!"
|
56
|
+
puts session.full_log [:error]
|
57
|
+
else
|
58
|
+
severities = []
|
59
|
+
severities.unshift :warning if @show_warnings
|
60
|
+
log = session.full_log severities
|
61
|
+
script = js_comments_for(log) + js_for(session.out)
|
62
|
+
if @output_file
|
63
|
+
File.open(@output_file, 'w') do |file|
|
64
|
+
file.write script
|
65
|
+
end
|
66
|
+
puts log
|
67
|
+
else
|
68
|
+
puts script
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
else
|
73
|
+
puts "For help, type: puer -h"
|
74
|
+
end
|
data/lib/puer.rb
CHANGED
data/lib/puer/config.rb
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
ignore_properties 'contentStretch', 'simulatedStatusBarMetrics', 'simulatedOrientationMetrics'
|
2
|
+
ignore_classes 'IBProxyObject'
|
3
|
+
|
4
|
+
# To get another creation call than standard
|
5
|
+
# Ti.UI.create#{name}
|
6
|
+
# give a array as value: 'IBUIWindow' => ['Window', 'myWindowCall']
|
7
|
+
classes 'IBUIWindow' => 'Window',
|
8
|
+
'IBUIView' => 'View',
|
9
|
+
'IBUILabel' => 'Label',
|
10
|
+
'IBUIButton' => 'Button'
|
11
|
+
|
12
|
+
# Available types:
|
13
|
+
# val(:output)
|
14
|
+
# bool(:output) # Where '0' gives {:output => false} and '1' gives {:output => true}
|
15
|
+
# lookup(:output, {'yes' => true, 'no' => false})
|
16
|
+
# color(:output)
|
17
|
+
# font(:output)
|
18
|
+
# vextor(:x, :y) # Where '{1, 2}' => {:x => 1, :y => 2}
|
19
|
+
properties 'backgroundColor' => color(:backgroundColor),
|
20
|
+
'font' => font(:font),
|
21
|
+
'frameOrigin' => vector(:top, :bottom),
|
22
|
+
'frameSize' => vector(:height, :width),
|
23
|
+
'text' => val(:text),
|
24
|
+
'textColor' => color(:color)
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# Converters converts data in the xib files property bag to the NodeInfos property bag.
|
2
|
+
# The output key it will have is stored here. The converter can be created with a conversion block
|
3
|
+
# Converter.new :output {|v| Convert value v here...}
|
4
|
+
|
5
|
+
class Converter
|
6
|
+
def initialize(output, &conversion)
|
7
|
+
@output = output
|
8
|
+
@conversion = conversion || proc {|v| v}
|
9
|
+
end
|
10
|
+
|
11
|
+
def props_for(value)
|
12
|
+
converted_value = @conversion.call(value)
|
13
|
+
if converted_value
|
14
|
+
{ @output => converted_value }
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
# MultiConverter is used for cases when a property in the xhib file's property bag needs multiple
|
20
|
+
# properties in JS
|
21
|
+
# Example: {'frameOrigin' => "{123, 456}"} should give {:top => 123, :bottom => 456}
|
22
|
+
# Done by: MultiConverter.new([:top, :bottom], /\{(\d+), (\d+)\}/) {|v| v.to_i}
|
23
|
+
|
24
|
+
class MultiConverter < Converter
|
25
|
+
def initialize(outputs, regex, &conversion)
|
26
|
+
@outputs = outputs
|
27
|
+
@regex = regex
|
28
|
+
@conversion = conversion || proc {|v| v}
|
29
|
+
end
|
30
|
+
|
31
|
+
def props_for(value)
|
32
|
+
if match = @regex.match(value)
|
33
|
+
Hash[@outputs.zip(match.captures.map(&@conversion))]
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
data/lib/puer/nodes.rb
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
# NodeInfo contains the information from the xib, both hierarchy and properties
|
2
|
+
|
3
|
+
class NodeInfo
|
4
|
+
def initialize(name, node_id, node_class, subviews)
|
5
|
+
@name = name
|
6
|
+
@node_id = node_id
|
7
|
+
@node_class = node_class
|
8
|
+
@subviews = subviews
|
9
|
+
@properties = {}
|
10
|
+
end
|
11
|
+
|
12
|
+
attr_reader :properties, :name, :node_class, :subviews
|
13
|
+
|
14
|
+
def self.enumerate(name)
|
15
|
+
"#{name}#{(@@name_counters ||= Hash.new {0})[name]+=1}"
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.for(hierarchy, data, session)
|
19
|
+
id = hierarchy['object-id'].to_s
|
20
|
+
info = data[id]
|
21
|
+
node_class = session.class_info_for info['class']
|
22
|
+
if node_class
|
23
|
+
name = hierarchy['name'] || enumerate(node_class.name.downcase)
|
24
|
+
subviews = (hierarchy['children'] || []).map {|child_hierarchy| NodeInfo.for(child_hierarchy, data, session)}.compact
|
25
|
+
node = NodeInfo.new name, id, node_class, subviews
|
26
|
+
info.each do |prop, value|
|
27
|
+
if converter = session.converter_for(prop)
|
28
|
+
props = converter.props_for(value)
|
29
|
+
if props
|
30
|
+
node.properties.merge! props
|
31
|
+
else
|
32
|
+
session.log(:error, "Could not convert #{prop}: #{value}")
|
33
|
+
end
|
34
|
+
else
|
35
|
+
session.log(:warning, "Skipped property for #{info['class']}: #{prop}") unless session.ignore_property? prop
|
36
|
+
end
|
37
|
+
end
|
38
|
+
node
|
39
|
+
else
|
40
|
+
session.log(:warning, "Skipped class #{info['class']}") unless session.ignore_class? info['class']
|
41
|
+
end
|
42
|
+
node
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# ClassInfo contains all known information on the class of the node info (Windows, Views... etc)
|
47
|
+
|
48
|
+
class ClassInfo
|
49
|
+
def initialize(name, creation_call="Ti.UI.create#{name}")
|
50
|
+
@name = name
|
51
|
+
@creation_call = creation_call
|
52
|
+
end
|
53
|
+
|
54
|
+
attr_reader :name, :creation_call
|
55
|
+
end
|
56
|
+
|
data/lib/puer/session.rb
ADDED
@@ -0,0 +1,129 @@
|
|
1
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
2
|
+
require 'nodes'
|
3
|
+
require 'converters'
|
4
|
+
require 'rubygems'
|
5
|
+
require 'plist'
|
6
|
+
|
7
|
+
# Stores all configurable info on how to translate the xib to JS
|
8
|
+
# The session is created from a config file.
|
9
|
+
|
10
|
+
class Session
|
11
|
+
def initialize(path)
|
12
|
+
@ignore_properties = []
|
13
|
+
@ignore_classes = []
|
14
|
+
@classes = {}
|
15
|
+
@properties = {}
|
16
|
+
@log = []
|
17
|
+
File.open path do |file|
|
18
|
+
eval file.read
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
attr_reader :out
|
23
|
+
|
24
|
+
def parse_file(file)
|
25
|
+
data = Plist::parse_xml( %x[ibtool #{file} --hierarchy --objects --connections] )
|
26
|
+
@out = data['com.apple.ibtool.document.hierarchy'].map {|hierarchy| NodeInfo.for hierarchy, data['com.apple.ibtool.document.objects'], self}.compact
|
27
|
+
data['com.apple.ibtool.document.connections'].each do |connection|
|
28
|
+
# TODO
|
29
|
+
end
|
30
|
+
@out
|
31
|
+
end
|
32
|
+
|
33
|
+
def full_log(severities=[])
|
34
|
+
excluded = @log.map(&:first).uniq - severities
|
35
|
+
(excluded.empty? ? '' : "There were log entries of severity: #{excluded.join ', '}\n") +
|
36
|
+
@log.map do |severity, message|
|
37
|
+
"[#{severity}] #{message}" if severities.include? severity
|
38
|
+
end.compact.join("\n")
|
39
|
+
end
|
40
|
+
|
41
|
+
def has_errors?
|
42
|
+
@log.any? {|severity, message| severity == :error}
|
43
|
+
end
|
44
|
+
|
45
|
+
def log(severity, message)
|
46
|
+
@log.push [severity, message]
|
47
|
+
end
|
48
|
+
|
49
|
+
def ignore_class?(class_name)
|
50
|
+
@ignore_classes.include? class_name
|
51
|
+
end
|
52
|
+
|
53
|
+
def ignore_property?(property_name)
|
54
|
+
@ignore_properties.include? property_name
|
55
|
+
end
|
56
|
+
|
57
|
+
def converter_for(property_name)
|
58
|
+
@properties[property_name]
|
59
|
+
end
|
60
|
+
|
61
|
+
def class_info_for(class_name)
|
62
|
+
@classes[class_name]
|
63
|
+
end
|
64
|
+
|
65
|
+
def classes(class_hash)
|
66
|
+
class_hash.each do |key, value|
|
67
|
+
@classes[key] = ClassInfo.new(*value)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def ignore_properties(*names)
|
72
|
+
@ignore_properties += names
|
73
|
+
end
|
74
|
+
|
75
|
+
def ignore_classes(*names)
|
76
|
+
@ignore_classes += names
|
77
|
+
end
|
78
|
+
|
79
|
+
def properties(properties_hash)
|
80
|
+
@properties.merge! properties_hash
|
81
|
+
end
|
82
|
+
|
83
|
+
# Color helper methods
|
84
|
+
|
85
|
+
def hex(v)
|
86
|
+
"%02x" % (v.to_f*255).round
|
87
|
+
end
|
88
|
+
|
89
|
+
def rgba(r, g, b, a)
|
90
|
+
"##{hex(r)}#{hex(g)}#{hex(b)}#{hex(a)}"
|
91
|
+
end
|
92
|
+
|
93
|
+
# Methods for converter creation in config file
|
94
|
+
|
95
|
+
def val(name)
|
96
|
+
Converter.new(name)
|
97
|
+
end
|
98
|
+
|
99
|
+
def bool(name)
|
100
|
+
Converter.new(name) {|v| v==1}
|
101
|
+
end
|
102
|
+
|
103
|
+
def lookup(name, hash)
|
104
|
+
Converter.new(name) {|v| hash[v]}
|
105
|
+
end
|
106
|
+
|
107
|
+
def color(name)
|
108
|
+
Converter.new(name) do |v|
|
109
|
+
case v
|
110
|
+
when /NS(?:Calibrated|Device)RGBColorSpace (\d+(?:\.\d+)?) (\d+(?:\.\d+)?) (\d+(?:\.\d+)?) (\d+(?:\.\d+)?)/
|
111
|
+
rgba($1, $2, $3, $4)
|
112
|
+
when /NS(?:Calibrated|Device)WhiteColorSpace (\d+(?:\.\d+)?) (\d+(?:\.\d+)?)/
|
113
|
+
rgba($1, $1, $1, $2)
|
114
|
+
when /NSCustomColorSpace Generic Gray colorspace (\d+(?:\.\d+)?) (\d+(?:\.\d+)?)/
|
115
|
+
rgba($1, $1, $1, $2)
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
def font(name)
|
121
|
+
Converter.new(name) do |v|
|
122
|
+
{:fontFamily => v['Family'], :fontSize => v['Size']}
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
def vector(x, y)
|
127
|
+
MultiConverter.new([x, y], /\{(\d+), (\d+)\}/) {|v| v.to_i}
|
128
|
+
end
|
129
|
+
end
|
data/lib/puer/xibtoti.rb
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
2
|
+
require 'session'
|
3
|
+
|
4
|
+
def inline_js_for(data)
|
5
|
+
|
6
|
+
case data
|
7
|
+
when Hash
|
8
|
+
'{' + data.map {|k,v| "#{k}:#{inline_js_for(v)}"}.join(',') + '}'
|
9
|
+
when String
|
10
|
+
"'#{data}'"
|
11
|
+
else
|
12
|
+
data.to_s
|
13
|
+
end
|
14
|
+
|
15
|
+
end
|
16
|
+
|
17
|
+
def creation_call(name, class_name, info)
|
18
|
+
"#{name} = #{class_name}({\n" +
|
19
|
+
info.keys.sort.map {|key| "\t#{key}:#{inline_js_for(info[key])}"}.join(",\n") + "\n});"
|
20
|
+
end
|
21
|
+
|
22
|
+
def js_sections_for(node)
|
23
|
+
[creation_call(node.name, node.node_class.creation_call, node.properties)] +
|
24
|
+
node.subviews.map {|child| [js_sections_for(child), "#{node.name}.add(#{child.name});"]}.flatten
|
25
|
+
end
|
26
|
+
|
27
|
+
def js_for(nodes)
|
28
|
+
nodes.map {|node| js_sections_for(node)}.flatten.join("\n\n")
|
29
|
+
end
|
30
|
+
|
31
|
+
def js_comments_for text
|
32
|
+
"" #text.map {|line| line.chomp.empty? ? line : "// #{line}"}.join + "\n"
|
33
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: puer
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.2
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2012-
|
12
|
+
date: 2012-10-16 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: thor
|
@@ -75,6 +75,22 @@ dependencies:
|
|
75
75
|
- - ! '>='
|
76
76
|
- !ruby/object:Gem::Version
|
77
77
|
version: '0'
|
78
|
+
- !ruby/object:Gem::Dependency
|
79
|
+
name: plist
|
80
|
+
requirement: !ruby/object:Gem::Requirement
|
81
|
+
none: false
|
82
|
+
requirements:
|
83
|
+
- - ! '>='
|
84
|
+
- !ruby/object:Gem::Version
|
85
|
+
version: '0'
|
86
|
+
type: :runtime
|
87
|
+
prerelease: false
|
88
|
+
version_requirements: !ruby/object:Gem::Requirement
|
89
|
+
none: false
|
90
|
+
requirements:
|
91
|
+
- - ! '>='
|
92
|
+
- !ruby/object:Gem::Version
|
93
|
+
version: '0'
|
78
94
|
- !ruby/object:Gem::Dependency
|
79
95
|
name: rdoc
|
80
96
|
requirement: !ruby/object:Gem::Requirement
|
@@ -220,45 +236,29 @@ dependencies:
|
|
220
236
|
- !ruby/object:Gem::Version
|
221
237
|
version: 2.3.1
|
222
238
|
- !ruby/object:Gem::Dependency
|
223
|
-
name:
|
224
|
-
requirement: !ruby/object:Gem::Requirement
|
225
|
-
none: false
|
226
|
-
requirements:
|
227
|
-
- - ! '>='
|
228
|
-
- !ruby/object:Gem::Version
|
229
|
-
version: 0.9.1
|
230
|
-
type: :development
|
231
|
-
prerelease: false
|
232
|
-
version_requirements: !ruby/object:Gem::Requirement
|
233
|
-
none: false
|
234
|
-
requirements:
|
235
|
-
- - ! '>='
|
236
|
-
- !ruby/object:Gem::Version
|
237
|
-
version: 0.9.1
|
238
|
-
- !ruby/object:Gem::Dependency
|
239
|
-
name: system_timer
|
239
|
+
name: activesupport
|
240
240
|
requirement: !ruby/object:Gem::Requirement
|
241
241
|
none: false
|
242
242
|
requirements:
|
243
243
|
- - ! '>='
|
244
244
|
- !ruby/object:Gem::Version
|
245
|
-
version:
|
246
|
-
type: :
|
245
|
+
version: 3.2.8
|
246
|
+
type: :runtime
|
247
247
|
prerelease: false
|
248
248
|
version_requirements: !ruby/object:Gem::Requirement
|
249
249
|
none: false
|
250
250
|
requirements:
|
251
251
|
- - ! '>='
|
252
252
|
- !ruby/object:Gem::Version
|
253
|
-
version:
|
253
|
+
version: 3.2.8
|
254
254
|
- !ruby/object:Gem::Dependency
|
255
|
-
name:
|
255
|
+
name: grit
|
256
256
|
requirement: !ruby/object:Gem::Requirement
|
257
257
|
none: false
|
258
258
|
requirements:
|
259
259
|
- - ! '>='
|
260
260
|
- !ruby/object:Gem::Version
|
261
|
-
version:
|
261
|
+
version: '0'
|
262
262
|
type: :runtime
|
263
263
|
prerelease: false
|
264
264
|
version_requirements: !ruby/object:Gem::Requirement
|
@@ -266,9 +266,9 @@ dependencies:
|
|
266
266
|
requirements:
|
267
267
|
- - ! '>='
|
268
268
|
- !ruby/object:Gem::Version
|
269
|
-
version:
|
269
|
+
version: '0'
|
270
270
|
- !ruby/object:Gem::Dependency
|
271
|
-
name:
|
271
|
+
name: i18n
|
272
272
|
requirement: !ruby/object:Gem::Requirement
|
273
273
|
none: false
|
274
274
|
requirements:
|
@@ -284,7 +284,7 @@ dependencies:
|
|
284
284
|
- !ruby/object:Gem::Version
|
285
285
|
version: '0'
|
286
286
|
- !ruby/object:Gem::Dependency
|
287
|
-
name:
|
287
|
+
name: hirb
|
288
288
|
requirement: !ruby/object:Gem::Requirement
|
289
289
|
none: false
|
290
290
|
requirements:
|
@@ -300,7 +300,7 @@ dependencies:
|
|
300
300
|
- !ruby/object:Gem::Version
|
301
301
|
version: '0'
|
302
302
|
- !ruby/object:Gem::Dependency
|
303
|
-
name:
|
303
|
+
name: cli-colorize
|
304
304
|
requirement: !ruby/object:Gem::Requirement
|
305
305
|
none: false
|
306
306
|
requirements:
|
@@ -316,7 +316,7 @@ dependencies:
|
|
316
316
|
- !ruby/object:Gem::Version
|
317
317
|
version: '0'
|
318
318
|
- !ruby/object:Gem::Dependency
|
319
|
-
name:
|
319
|
+
name: rdoc
|
320
320
|
requirement: !ruby/object:Gem::Requirement
|
321
321
|
none: false
|
322
322
|
requirements:
|
@@ -332,7 +332,7 @@ dependencies:
|
|
332
332
|
- !ruby/object:Gem::Version
|
333
333
|
version: '0'
|
334
334
|
- !ruby/object:Gem::Dependency
|
335
|
-
name:
|
335
|
+
name: yajl-ruby
|
336
336
|
requirement: !ruby/object:Gem::Requirement
|
337
337
|
none: false
|
338
338
|
requirements:
|
@@ -348,7 +348,7 @@ dependencies:
|
|
348
348
|
- !ruby/object:Gem::Version
|
349
349
|
version: '0'
|
350
350
|
- !ruby/object:Gem::Dependency
|
351
|
-
name:
|
351
|
+
name: plist
|
352
352
|
requirement: !ruby/object:Gem::Requirement
|
353
353
|
none: false
|
354
354
|
requirements:
|
@@ -377,6 +377,11 @@ files:
|
|
377
377
|
- README.md
|
378
378
|
- VERSION
|
379
379
|
- lib/puer.rb
|
380
|
+
- lib/puer/config.rb
|
381
|
+
- lib/puer/converters.rb
|
382
|
+
- lib/puer/nodes.rb
|
383
|
+
- lib/puer/session.rb
|
384
|
+
- lib/puer/xibtoti.rb
|
380
385
|
- !binary |-
|
381
386
|
YmluL3B1ZXI=
|
382
387
|
homepage: http://github.com/eiffelqiu/puer
|
@@ -394,7 +399,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
394
399
|
version: '0'
|
395
400
|
segments:
|
396
401
|
- 0
|
397
|
-
hash:
|
402
|
+
hash: -841221982100314824
|
398
403
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
399
404
|
none: false
|
400
405
|
requirements:
|
@@ -403,7 +408,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
403
408
|
version: '0'
|
404
409
|
requirements: []
|
405
410
|
rubyforge_project:
|
406
|
-
rubygems_version: 1.8.
|
411
|
+
rubygems_version: 1.8.24
|
407
412
|
signing_key:
|
408
413
|
specification_version: 3
|
409
414
|
summary: Titanium Starter Project Generate Tools
|