quik 0.3.0 → 1.0.0

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.
@@ -1,147 +1,148 @@
1
- # encoding: utf-8
2
-
3
- ### todo: move to textutils for (re)use - why? why not??
4
- ### todo add some references to alternative color gems
5
- ##
6
- ## e.g. https://github.com/fazibear/colorize
7
- ## http://github.com/ssoroka/ansi
8
- ## http://flori.github.com/term-ansicolor/
9
- ## http://github.com/sickill/rainbow
10
-
11
-
12
- ## move to core_ext.rb - why? why not??
13
-
14
- ###
15
- ## fix: use css style
16
- ## e.g. color( :red )
17
- ## color( :blue )
18
- ## background_color( :red )
19
- ## font_style( :bold ) -- why? why not??
20
-
21
- class String
22
-
23
- def self.use_colors?
24
- @@use_color ||= true
25
- ## todo/fix: check for windows on load (set to false;otherwise true)
26
- @@use_color
27
- end
28
-
29
- def self.use_colors=(value)
30
- @@use_color = value
31
- end
32
-
33
-
34
- def red() colorize(31); end
35
- def green() colorize(32); end
36
- def yellow() colorize(33); end ## note: more brown-ish (if not used w/ bold/bright mode) ??
37
- def blue() colorize(34); end
38
- def magenta() colorize(35); end ## pink ??
39
- def cyan() colorize(36); end ## light blue ??
40
-
41
- def bold() colorize(1); end ## just use 0 clear for now; instead of BOLD_OFF (22) code; use bright as an alias??
42
-
43
- private
44
- def colorize(code)
45
- if self.class.use_colors?
46
- "\e[#{code}m#{self}\e[0m"
47
- else
48
- self
49
- end
50
- end
51
-
52
-
53
- ## todo - add modes e.g. bold/underscore/etc.
54
- =begin
55
- class String
56
- def black; "\e[30m#{self}\e[0m" end
57
- def red; "\e[31m#{self}\e[0m" end
58
- def green; "\e[32m#{self}\e[0m" end
59
- def brown; "\e[33m#{self}\e[0m" end
60
- def blue; "\e[34m#{self}\e[0m" end
61
- def magenta; "\e[35m#{self}\e[0m" end
62
- def cyan; "\e[36m#{self}\e[0m" end
63
- def gray; "\e[37m#{self}\e[0m" end
64
-
65
- def on_black; "\e[40m#{self}\e[0m" end
66
- def on_red; "\e[41m#{self}\e[0m" end
67
- def on_green; "\e[42m#{self}\e[0m" end
68
- def on_brown; "\e[43m#{self}\e[0m" end
69
- def on_blue; "\e[44m#{self}\e[0m" end
70
- def on_magenta; "\e[45m#{self}\e[0m" end
71
- def on_cyan; "\e[46m#{self}\e[0m" end
72
- def on_gray; "\e[47m#{self}\e[0m" end
73
-
74
- def bold; "\e[1m#{self}\e[21m" end
75
- def italic; "\e[3m#{self}\e[23m" end
76
- def underline; "\e[4m#{self}\e[24m" end
77
- def blink; "\e[5m#{self}\e[25m" end
78
- def reverse_color; "\e[7m#{self}\e[27m" end
79
-
80
- use Ansi constants - why? why not? e.g.:
81
- //foreground color
82
- public static final String BLACK_TEXT() { return "\033[30m";}
83
- public static final String RED_TEXT() { return "\033[31m";}
84
- public static final String GREEN_TEXT() { return "\033[32m";}
85
- public static final String BROWN_TEXT() { return "\033[33m";}
86
- public static final String BLUE_TEXT() { return "\033[34m";}
87
- public static final String MAGENTA_TEXT() { return "\033[35m";}
88
- public static final String CYAN_TEXT() { return "\033[36m";}
89
- public static final String GRAY_TEXT() { return "\033[37m";}
90
-
91
- //background color
92
- public static final String BLACK_BACK() { return "\033[40m";}
93
- public static final String RED_BACK() { return "\033[41m";}
94
- public static final String GREEN_BACK() { return "\033[42m";}
95
- public static final String BROWN_BACK() { return "\033[43m";}
96
- public static final String BLUE_BACK() { return "\033[44m";}
97
- public static final String MAGENTA_BACK() { return "\033[45m";}
98
- public static final String CYAN_BACK() { return "\033[46m";}
99
- public static final String WHITE_BACK() { return "\033[47m";}
100
-
101
- //ANSI control chars
102
- public static final String RESET_COLORS() { return "\033[0m";}
103
- public static final String BOLD_ON() { return "\033[1m";}
104
- public static final String BLINK_ON() { return "\033[5m";}
105
- public static final String REVERSE_ON() { return "\033[7m";}
106
- public static final String BOLD_OFF() { return "\033[22m";}
107
- public static final String BLINK_OFF() { return "\033[25m";}
108
- public static final String REVERSE_OFF() { return "\033[27m";}
109
- end
110
-
111
- Code Effect
112
- 0 Turn off all attributes
113
- 1 Set bright mode
114
- 4 Set underline mode
115
- 5 Set blink mode
116
- 7 Exchange foreground and background colors
117
- 8 Hide text (foreground color would be the same as background)
118
- 30 Black text
119
- 31 Red text
120
- 32 Green text
121
- 33 Yellow text
122
- 34 Blue text
123
- 35 Magenta text
124
- 36 Cyan text
125
- 37 White text
126
- 39 Default text color
127
- 40 Black background
128
- 41 Red background
129
- 42 Green background
130
- 43 Yellow background
131
- 44 Blue background
132
- 45 Magenta background
133
- 46 Cyan background
134
- 47 White background
135
- 49 Default background color
136
-
137
- note:
138
- puts "\e[31m" # set format (red foreground)
139
- puts "\e[0" # clear format
140
- puts "green-#{"red".red}-green".green # will be green-red-normal, because of \e[0
141
- e.g. for now colors can NOT get nested
142
- plus if you bold/italic/etc. use it before the color e.g.
143
- bold.red etc.
144
- =end
145
-
146
-
147
- end # class String
1
+ # encoding: utf-8
2
+
3
+ ### todo: move to textutils for (re)use - why? why not??
4
+ ### todo add some references to alternative color gems
5
+ ##
6
+ ## e.g. https://github.com/fazibear/colorize
7
+ ## http://github.com/ssoroka/ansi
8
+ ## http://flori.github.com/term-ansicolor/
9
+ ## http://github.com/sickill/rainbow
10
+ ## https://github.com/janlelis/paint
11
+
12
+
13
+ ## move to core_ext.rb - why? why not??
14
+
15
+ ###
16
+ ## fix: use css style
17
+ ## e.g. color( :red )
18
+ ## color( :blue )
19
+ ## background_color( :red )
20
+ ## font_style( :bold ) -- why? why not??
21
+
22
+ class String
23
+
24
+ def self.use_colors?
25
+ @@use_color ||= true
26
+ ## todo/fix: check for windows on load (set to false;otherwise true)
27
+ @@use_color
28
+ end
29
+
30
+ def self.use_colors=(value)
31
+ @@use_color = value
32
+ end
33
+
34
+
35
+ def red() colorize(31); end
36
+ def green() colorize(32); end
37
+ def yellow() colorize(33); end ## note: more brown-ish (if not used w/ bold/bright mode) ??
38
+ def blue() colorize(34); end
39
+ def magenta() colorize(35); end ## pink ??
40
+ def cyan() colorize(36); end ## light blue ??
41
+
42
+ def bold() colorize(1); end ## just use 0 clear for now; instead of BOLD_OFF (22) code; use bright as an alias??
43
+
44
+ private
45
+ def colorize(code)
46
+ if self.class.use_colors?
47
+ "\e[#{code}m#{self}\e[0m"
48
+ else
49
+ self
50
+ end
51
+ end
52
+
53
+
54
+ ## todo - add modes e.g. bold/underscore/etc.
55
+ =begin
56
+ class String
57
+ def black; "\e[30m#{self}\e[0m" end
58
+ def red; "\e[31m#{self}\e[0m" end
59
+ def green; "\e[32m#{self}\e[0m" end
60
+ def brown; "\e[33m#{self}\e[0m" end
61
+ def blue; "\e[34m#{self}\e[0m" end
62
+ def magenta; "\e[35m#{self}\e[0m" end
63
+ def cyan; "\e[36m#{self}\e[0m" end
64
+ def gray; "\e[37m#{self}\e[0m" end
65
+
66
+ def on_black; "\e[40m#{self}\e[0m" end
67
+ def on_red; "\e[41m#{self}\e[0m" end
68
+ def on_green; "\e[42m#{self}\e[0m" end
69
+ def on_brown; "\e[43m#{self}\e[0m" end
70
+ def on_blue; "\e[44m#{self}\e[0m" end
71
+ def on_magenta; "\e[45m#{self}\e[0m" end
72
+ def on_cyan; "\e[46m#{self}\e[0m" end
73
+ def on_gray; "\e[47m#{self}\e[0m" end
74
+
75
+ def bold; "\e[1m#{self}\e[21m" end
76
+ def italic; "\e[3m#{self}\e[23m" end
77
+ def underline; "\e[4m#{self}\e[24m" end
78
+ def blink; "\e[5m#{self}\e[25m" end
79
+ def reverse_color; "\e[7m#{self}\e[27m" end
80
+
81
+ use Ansi constants - why? why not? e.g.:
82
+ //foreground color
83
+ public static final String BLACK_TEXT() { return "\033[30m";}
84
+ public static final String RED_TEXT() { return "\033[31m";}
85
+ public static final String GREEN_TEXT() { return "\033[32m";}
86
+ public static final String BROWN_TEXT() { return "\033[33m";}
87
+ public static final String BLUE_TEXT() { return "\033[34m";}
88
+ public static final String MAGENTA_TEXT() { return "\033[35m";}
89
+ public static final String CYAN_TEXT() { return "\033[36m";}
90
+ public static final String GRAY_TEXT() { return "\033[37m";}
91
+
92
+ //background color
93
+ public static final String BLACK_BACK() { return "\033[40m";}
94
+ public static final String RED_BACK() { return "\033[41m";}
95
+ public static final String GREEN_BACK() { return "\033[42m";}
96
+ public static final String BROWN_BACK() { return "\033[43m";}
97
+ public static final String BLUE_BACK() { return "\033[44m";}
98
+ public static final String MAGENTA_BACK() { return "\033[45m";}
99
+ public static final String CYAN_BACK() { return "\033[46m";}
100
+ public static final String WHITE_BACK() { return "\033[47m";}
101
+
102
+ //ANSI control chars
103
+ public static final String RESET_COLORS() { return "\033[0m";}
104
+ public static final String BOLD_ON() { return "\033[1m";}
105
+ public static final String BLINK_ON() { return "\033[5m";}
106
+ public static final String REVERSE_ON() { return "\033[7m";}
107
+ public static final String BOLD_OFF() { return "\033[22m";}
108
+ public static final String BLINK_OFF() { return "\033[25m";}
109
+ public static final String REVERSE_OFF() { return "\033[27m";}
110
+ end
111
+
112
+ Code Effect
113
+ 0 Turn off all attributes
114
+ 1 Set bright mode
115
+ 4 Set underline mode
116
+ 5 Set blink mode
117
+ 7 Exchange foreground and background colors
118
+ 8 Hide text (foreground color would be the same as background)
119
+ 30 Black text
120
+ 31 Red text
121
+ 32 Green text
122
+ 33 Yellow text
123
+ 34 Blue text
124
+ 35 Magenta text
125
+ 36 Cyan text
126
+ 37 White text
127
+ 39 Default text color
128
+ 40 Black background
129
+ 41 Red background
130
+ 42 Green background
131
+ 43 Yellow background
132
+ 44 Blue background
133
+ 45 Magenta background
134
+ 46 Cyan background
135
+ 47 White background
136
+ 49 Default background color
137
+
138
+ note:
139
+ puts "\e[31m" # set format (red foreground)
140
+ puts "\e[0" # clear format
141
+ puts "green-#{"red".red}-green".green # will be green-red-normal, because of \e[0
142
+ e.g. for now colors can NOT get nested
143
+ plus if you bold/italic/etc. use it before the color e.g.
144
+ bold.red etc.
145
+ =end
146
+
147
+
148
+ end # class String
@@ -1,60 +1,60 @@
1
- #encoding: utf-8
2
-
3
- module Quik
4
-
5
- ##
6
- # used for config block
7
- # lets you access props (even nested) that don't yet exist
8
- # and all props get stored in a hash
9
- #
10
- # e.g
11
- # c = OpenConfig.new
12
- # c.title = 'title'
13
- # c.author.name = 'name'
14
-
15
- # c.quik.last_updated = Time.now
16
- # c.quik.title = 'title'
17
- # c.quik.name = 'name'
18
- # c.quik.theme = 'theme'
19
-
20
- class OpenConfig
21
-
22
- def initialize
23
- @h = {}
24
- end
25
-
26
- def to_h
27
- h = {}
28
- @h.each do |k,v|
29
- if v.is_a? OpenConfig
30
- h[ k ] = v.to_h
31
- else
32
- h[ k ] = v ## just pass along as is
33
- end
34
- end
35
- h
36
- end
37
-
38
- def method_missing( m, *args, &block)
39
- if m.to_s =~ /^(.*)=$/ ## setter
40
- puts "config lookup (setter) >#{m}< #{m.class.name}, #{args.inspect}"
41
- key = m[0..-2].to_s ## cut off trailing =
42
- @h[ key ] = args[0].to_s # note: assume first arg is value for setter
43
- # note: for now all values are strings (always use to_s)
44
- else ## assume getter
45
- ## fix: add check for args?? must be 0 for getters??
46
- ## use else super to delegate non-getters??
47
- puts "config lookup (getter) >#{m}< #{m.class.name}"
48
- key = m.to_s
49
- value = @h[ key ]
50
- if value.nil?
51
- puts " config add (nested) hash"
52
- value = @h[ key ] = OpenConfig.new
53
- end
54
- value
55
- end
56
- end # method_missing
57
-
58
- end # class OpenConfig
59
-
60
- end # module Quik
1
+ #encoding: utf-8
2
+
3
+ module Quik
4
+
5
+ ##
6
+ # used for config block
7
+ # lets you access props (even nested) that don't yet exist
8
+ # and all props get stored in a hash
9
+ #
10
+ # e.g
11
+ # c = OpenConfig.new
12
+ # c.title = 'title'
13
+ # c.author.name = 'name'
14
+
15
+ # c.quik.last_updated = Time.now
16
+ # c.quik.title = 'title'
17
+ # c.quik.name = 'name'
18
+ # c.quik.theme = 'theme'
19
+
20
+ class OpenConfig
21
+
22
+ def initialize
23
+ @h = {}
24
+ end
25
+
26
+ def to_h
27
+ h = {}
28
+ @h.each do |k,v|
29
+ if v.is_a? OpenConfig
30
+ h[ k ] = v.to_h
31
+ else
32
+ h[ k ] = v ## just pass along as is
33
+ end
34
+ end
35
+ h
36
+ end
37
+
38
+ def method_missing( m, *args, &block)
39
+ if m.to_s =~ /^(.*)=$/ ## setter
40
+ puts "config lookup (setter) >#{m}< #{m.class.name}, #{args.inspect}"
41
+ key = m[0..-2].to_s ## cut off trailing =
42
+ @h[ key ] = args[0].to_s # note: assume first arg is value for setter
43
+ # note: for now all values are strings (always use to_s)
44
+ else ## assume getter
45
+ ## fix: add check for args?? must be 0 for getters??
46
+ ## use else super to delegate non-getters??
47
+ puts "config lookup (getter) >#{m}< #{m.class.name}"
48
+ key = m.to_s
49
+ value = @h[ key ]
50
+ if value.nil?
51
+ puts " config add (nested) hash"
52
+ value = @h[ key ] = OpenConfig.new
53
+ end
54
+ value
55
+ end
56
+ end # method_missing
57
+
58
+ end # class OpenConfig
59
+
60
+ end # module Quik
@@ -1,208 +1,207 @@
1
- # encoding: utf-8
2
-
3
- module Quik
4
-
5
- class Merger
6
-
7
-
8
-
9
- def find_files( root_dir )
10
- tree = []
11
-
12
- files = Dir.entries( root_dir )
13
- files = files.sort
14
- puts "#{root_dir}:"
15
- pp files
16
-
17
- files.each do |file|
18
- if File.directory?( "#{root_dir}/#{file}" )
19
- ## note: skip directory if it starts with dot e.g. . or .. or .git etc.
20
- if file.start_with?( '.' )
21
- puts "skipping directory >#{file}< (#{root_dir})"
22
- next
23
- end
24
- subtree = find_files( "#{root_dir}/#{file}" )
25
- tree << [ file, subtree ]
26
- else
27
- ## just a "regular" file
28
- tree << file
29
- end
30
- end
31
- tree
32
- end
33
-
34
-
35
- def merge_filenames( root_dir, hash, opts={} ) ## flags e.g. noop, verbose, etc. see FileUtils
36
- tree = find_files( root_dir )
37
- pp tree
38
-
39
- flags = { ## always use verbose mode for now
40
- verbose: true
41
- }
42
-
43
- ## todo/check: move opts={} to initialize e.g. Merger.new( opts={}) why? why not??
44
-
45
- ## check for noop e.g. test mode/dry run
46
- flags[ :noop ] = true if opts[:test] || opts[:dry_run] || opts[:noop]
47
-
48
- puts "walk tree:"
49
- merge_filenames_worker_step1( root_dir, hash, tree, flags )
50
- merge_filenames_worker_step2( root_dir, hash, tree, flags )
51
- end
52
-
53
- def merge_filenames_worker_step1( root_dir, hash, tree, flags )
54
- ## note: step 1 move all files
55
- ## step 2 move all dirs
56
-
57
- tree.each do |node|
58
- if node.is_a? Array ## assume it's a directory
59
- merge_filenames_worker_step1( "#{root_dir}/#{node[0]}", hash, node[1], flags )
60
- else ## assume it's a file
61
- old_name = node
62
- new_name = merge_path( old_name, hash )
63
- if old_name == new_name
64
- puts " keep file #{node} (#{root_dir})"
65
- else
66
- puts " *** move file #{old_name} => #{new_name} (#{root_dir})"
67
- src_path = "#{root_dir}/#{old_name}"
68
- dest_path = "#{root_dir}/#{new_name}"
69
- ## note: make sure subpath exists (e.g. replacement might include new (sub)dirs too)
70
- FileUtils.mkdir_p( File.dirname( dest_path ), flags ) unless Dir.exist?( File.dirname( dest_path ))
71
- FileUtils.mv( src_path, dest_path, flags )
72
- end
73
- end
74
- end
75
- end
76
-
77
- def merge_filenames_worker_step2( root_dir, hash, tree, flags )
78
- ## note: step 1 move all files
79
- ## step 2 move all dirs
80
-
81
- tree.each do |node|
82
- if node.is_a? Array ## assume it's a directory
83
- merge_filenames_worker_step2( "#{root_dir}/#{node[0]}", hash, node[1], flags )
84
- old_name = node[0]
85
- new_name = merge_path( old_name, hash )
86
- if old_name == new_name
87
- puts " keep dir #{node[0]} (#{root_dir})"
88
- else
89
- puts " *** move dir #{old_name} => #{new_name} (#{root_dir})"
90
- src_path = "#{root_dir}/#{old_name}"
91
- dest_path = "#{root_dir}/#{new_name}"
92
- ## note: make sure subpath exists (e.g. replacement might include new (sub)dirs too)
93
- FileUtils.mkdir_p( File.dirname( dest_path ), flags ) unless Dir.exist?( File.dirname( dest_path ))
94
- FileUtils.mv( src_path, dest_path, flags )
95
- end
96
- end
97
- end
98
- end
99
-
100
-
101
-
102
- def merge_files( root_dir, hash, opts={} )
103
- ## note: rescan files (after renames)
104
- tree = find_files( root_dir )
105
- pp tree
106
-
107
- puts "walk tree:"
108
- merge_files_worker( root_dir, '', hash, tree, opts )
109
- end
110
-
111
- def merge_files_worker( root_dir, relative_dir, hash, tree, opts )
112
- tree.each do |node|
113
- if node.is_a? Array ## assume it's a directory
114
- if relative_dir.empty? # e.g. just add w/o leading slash e.g. 'lib' and not '/lib'
115
- new_relative_dir = node[0].dup # note: create a new string; just in case
116
- else
117
- new_relative_dir = "#{relative_dir}/#{node[0]}"
118
- end
119
- merge_files_worker( root_dir, new_relative_dir, hash, node[1], opts )
120
- else ## assume it's a file
121
- if relative_dir.empty?
122
- relative_path = node
123
- else
124
- relative_path = "#{relative_dir}/#{node}"
125
- end
126
-
127
- src_path = "#{root_dir}/#{relative_path}"
128
-
129
- ## note: for now assume always assume text files/utf8
130
- ## fix: change to File.read_utf8 ??
131
- old_text = File.read( src_path )
132
- new_text = merge_text( old_text, hash )
133
-
134
- if opts[:o]
135
- dest_root = opts[:o]
136
- dest_path = "#{dest_root}/#{relative_path}"
137
- ## make sure dest_path exists
138
- dest_dir = File.dirname( dest_path )
139
- FileUtils.mkdir_p( dest_dir ) unless Dir.exist?( dest_dir )
140
- else
141
- dest_root = root_dir
142
- dest_path = "#{dest_root}/#{relative_path}"
143
- end
144
-
145
- if old_text == new_text
146
- if opts[:o] ## for testing copy file 1:1
147
- puts " copy file 1:1 #{node} (#{relative_dir}) in (#{dest_root})"
148
- FileUtils.cp( src_path, dest_path, verbose: true )
149
- else
150
- puts " skip file #{node} (#{relative_dir})"
151
- end
152
- else
153
- puts " *** update file #{node} (#{relative_dir}) in (#{dest_root})"
154
- File.open( dest_path, 'w' ) do |f|
155
- f.write new_text
156
- end
157
- end
158
- end
159
- end
160
- end
161
-
162
-
163
-
164
- def merge_path( path, hash )
165
- ## e.g. allow
166
- ## __filename__ or
167
- ## $filename$ for now
168
- ## note: allow underline too e.g $file_name$ etc.
169
- path.gsub( /(__|\$)([a-z_]+)\1/i ) do |_|
170
- key = $2.to_s
171
- value = hash[ key ]
172
- puts " [path] replacing #{key} w/ >#{value}< in (#{path})"
173
- value
174
- end
175
- end
176
-
177
- def merge_text( text, hash )
178
- ## e.g. allow
179
- ## $filename$ for now only in text
180
- ## note: must include leading and trailing word boundry (/B)
181
- ## e.g. hello$test$ will not match only "free-standing $test
182
- ## or in quote e.g. "$test$"
183
- ## e.g. no letters or digits allowed before or after $ to match
184
- ## note: allow underline too e.g. $test_klass$ etc.
185
-
186
- ## pp text
187
-
188
- text.gsub( /\B\$([a-z_]+)\$\B/i ) do |_|
189
- key = $1.to_s
190
- value = hash[ key ]
191
- puts " [text] replacing #{key} w/ >#{value}<"
192
- value
193
- end
194
- end
195
-
196
-
197
-
198
- def merge( root_dir, hash, opts={} )
199
- puts " merge #{root_dir}, #{hash.inspect}"
200
-
201
- merge_filenames( root_dir, hash, opts )
202
- merge_files( root_dir, hash, opts )
203
- end
204
-
205
- end # class Merger
206
-
207
- end # module Quik
208
-
1
+ # encoding: utf-8
2
+
3
+ module Quik
4
+
5
+ class Merger
6
+
7
+
8
+
9
+ def find_files( root_dir )
10
+ tree = []
11
+
12
+ files = Dir.entries( root_dir )
13
+ files = files.sort
14
+ puts "#{root_dir}:"
15
+ pp files
16
+
17
+ files.each do |file|
18
+ if File.directory?( "#{root_dir}/#{file}" )
19
+ ## note: skip directory if it starts with dot e.g. . or .. or .git etc.
20
+ if file.start_with?( '.' )
21
+ puts "skipping directory >#{file}< (#{root_dir})"
22
+ next
23
+ end
24
+ subtree = find_files( "#{root_dir}/#{file}" )
25
+ tree << [ file, subtree ]
26
+ else
27
+ ## just a "regular" file
28
+ tree << file
29
+ end
30
+ end
31
+ tree
32
+ end
33
+
34
+
35
+ def merge_filenames( root_dir, hash, opts={} ) ## flags e.g. noop, verbose, etc. see FileUtils
36
+ tree = find_files( root_dir )
37
+ pp tree
38
+
39
+ flags = { ## always use verbose mode for now
40
+ verbose: true
41
+ }
42
+
43
+ ## todo/check: move opts={} to initialize e.g. Merger.new( opts={}) why? why not??
44
+
45
+ ## check for noop e.g. test mode/dry run
46
+ flags[ :noop ] = true if opts[:test] || opts[:dry_run] || opts[:noop]
47
+
48
+ puts "walk tree:"
49
+ merge_filenames_worker_step1( root_dir, hash, tree, flags )
50
+ merge_filenames_worker_step2( root_dir, hash, tree, flags )
51
+ end
52
+
53
+ def merge_filenames_worker_step1( root_dir, hash, tree, flags )
54
+ ## note: step 1 move all files
55
+ ## step 2 move all dirs
56
+
57
+ tree.each do |node|
58
+ if node.is_a? Array ## assume it's a directory
59
+ merge_filenames_worker_step1( "#{root_dir}/#{node[0]}", hash, node[1], flags )
60
+ else ## assume it's a file
61
+ old_name = node
62
+ new_name = merge_path( old_name, hash )
63
+ if old_name == new_name
64
+ puts " keep file #{node} (#{root_dir})"
65
+ else
66
+ puts " *** move file #{old_name} => #{new_name} (#{root_dir})"
67
+ src_path = "#{root_dir}/#{old_name}"
68
+ dest_path = "#{root_dir}/#{new_name}"
69
+ ## note: make sure subpath exists (e.g. replacement might include new (sub)dirs too)
70
+ FileUtils.mkdir_p( File.dirname( dest_path ), flags ) unless Dir.exist?( File.dirname( dest_path ))
71
+ FileUtils.mv( src_path, dest_path, flags )
72
+ end
73
+ end
74
+ end
75
+ end
76
+
77
+ def merge_filenames_worker_step2( root_dir, hash, tree, flags )
78
+ ## note: step 1 move all files
79
+ ## step 2 move all dirs
80
+
81
+ tree.each do |node|
82
+ if node.is_a? Array ## assume it's a directory
83
+ merge_filenames_worker_step2( "#{root_dir}/#{node[0]}", hash, node[1], flags )
84
+ old_name = node[0]
85
+ new_name = merge_path( old_name, hash )
86
+ if old_name == new_name
87
+ puts " keep dir #{node[0]} (#{root_dir})"
88
+ else
89
+ puts " *** move dir #{old_name} => #{new_name} (#{root_dir})"
90
+ src_path = "#{root_dir}/#{old_name}"
91
+ dest_path = "#{root_dir}/#{new_name}"
92
+ ## note: make sure subpath exists (e.g. replacement might include new (sub)dirs too)
93
+ FileUtils.mkdir_p( File.dirname( dest_path ), flags ) unless Dir.exist?( File.dirname( dest_path ))
94
+ FileUtils.mv( src_path, dest_path, flags )
95
+ end
96
+ end
97
+ end
98
+ end
99
+
100
+
101
+
102
+ def merge_files( root_dir, hash, opts={} )
103
+ ## note: rescan files (after renames)
104
+ tree = find_files( root_dir )
105
+ pp tree
106
+
107
+ puts "walk tree:"
108
+ merge_files_worker( root_dir, '', hash, tree, opts )
109
+ end
110
+
111
+ def merge_files_worker( root_dir, relative_dir, hash, tree, opts )
112
+ tree.each do |node|
113
+ if node.is_a? Array ## assume it's a directory
114
+ if relative_dir.empty? # e.g. just add w/o leading slash e.g. 'lib' and not '/lib'
115
+ new_relative_dir = node[0].dup # note: create a new string; just in case
116
+ else
117
+ new_relative_dir = "#{relative_dir}/#{node[0]}"
118
+ end
119
+ merge_files_worker( root_dir, new_relative_dir, hash, node[1], opts )
120
+ else ## assume it's a file
121
+ if relative_dir.empty?
122
+ relative_path = node
123
+ else
124
+ relative_path = "#{relative_dir}/#{node}"
125
+ end
126
+
127
+ src_path = "#{root_dir}/#{relative_path}"
128
+
129
+ ## note: for now assume always assume text files/utf8
130
+ old_text = File.open( src_path, 'r:utf-8' ) { |f| f.read }
131
+ new_text = merge_text( old_text, hash )
132
+
133
+ if opts[:o]
134
+ dest_root = opts[:o]
135
+ dest_path = "#{dest_root}/#{relative_path}"
136
+ ## make sure dest_path exists
137
+ dest_dir = File.dirname( dest_path )
138
+ FileUtils.mkdir_p( dest_dir ) unless Dir.exist?( dest_dir )
139
+ else
140
+ dest_root = root_dir
141
+ dest_path = "#{dest_root}/#{relative_path}"
142
+ end
143
+
144
+ if old_text == new_text
145
+ if opts[:o] ## for testing copy file 1:1
146
+ puts " copy file 1:1 #{node} (#{relative_dir}) in (#{dest_root})"
147
+ FileUtils.cp( src_path, dest_path, verbose: true )
148
+ else
149
+ puts " skip file #{node} (#{relative_dir})"
150
+ end
151
+ else
152
+ puts " *** update file #{node} (#{relative_dir}) in (#{dest_root})"
153
+ File.open( dest_path, 'w:utf-8' ) do |f|
154
+ f.write( new_text )
155
+ end
156
+ end
157
+ end
158
+ end
159
+ end
160
+
161
+
162
+
163
+ def merge_path( path, hash )
164
+ ## e.g. allow
165
+ ## __filename__ or
166
+ ## $filename$ for now
167
+ ## note: allow underline too e.g $file_name$ etc.
168
+ path.gsub( /(__|\$)([a-z_]+)\1/i ) do |_|
169
+ key = $2.to_s
170
+ value = hash[ key ]
171
+ puts " [path] replacing #{key} w/ >#{value}< in (#{path})"
172
+ value
173
+ end
174
+ end
175
+
176
+ def merge_text( text, hash )
177
+ ## e.g. allow
178
+ ## $filename$ for now only in text
179
+ ## note: must include leading and trailing word boundry (/B)
180
+ ## e.g. hello$test$ will not match only "free-standing $test
181
+ ## or in quote e.g. "$test$"
182
+ ## e.g. no letters or digits allowed before or after $ to match
183
+ ## note: allow underline too e.g. $test_klass$ etc.
184
+
185
+ ## pp text
186
+
187
+ text.gsub( /\B\$([a-z_]+)\$\B/i ) do |_|
188
+ key = $1.to_s
189
+ value = hash[ key ]
190
+ puts " [text] replacing #{key} w/ >#{value}<"
191
+ value
192
+ end
193
+ end
194
+
195
+
196
+
197
+ def merge( root_dir, hash, opts={} )
198
+ puts " merge #{root_dir}, #{hash.inspect}"
199
+
200
+ merge_filenames( root_dir, hash, opts )
201
+ merge_files( root_dir, hash, opts )
202
+ end
203
+
204
+ end # class Merger
205
+
206
+ end # module Quik
207
+