svg_preprocess.rb (12480B)
1 #!/usr/bin/env ruby 2 3 require 'css_parser' 4 require 'listen' 5 require 'nokogiri' 6 require 'optparse' 7 8 options = { 9 reprocess: false, 10 listen: false, 11 module_prefixes: [], 12 debug: false 13 } 14 option_parser = OptionParser.new do |opts| 15 opts.banner = "Usage: #{$0} [options] [res-src/MODULE-src.svg]" 16 opts.on('--reprocess', 'Process everything immediately, if no file specfied') do 17 options[:reprocess] = true 18 end 19 opts.on('--listen', 'Listen for changes, if no file specfied; may be used with --reprocess') do 20 options[:listen] = true 21 end 22 opts.on('--filter=[module prefix]', 'Limit --reprocess or --listen to modules matching prefix; may be used multiply') do |v| 23 options[:module_prefixes] << v if v 24 end 25 opts.on('--debug', 'Print directory and file status information and exit') do 26 options[:debug] = true 27 end 28 opts.on_tail('-h', '--help', 'Show this message') do 29 puts opts 30 exit 31 end 32 end 33 begin 34 option_parser.parse! 35 rescue => e 36 STDERR.puts e.to_s 37 STDERR.puts "\n" 38 STDERR.puts option_parser.help 39 exit 1 40 end 41 42 def parse_xml(s) 43 Nokogiri::XML(s) do |config| 44 config.norecover.strict 45 end 46 end 47 48 def read_xml(fn) 49 parse_xml(File.open(fn)) 50 end 51 52 $src_dir = nil 53 $pp_dir = nil 54 def load_directories() 55 unless $src_dir 56 $src_dir = File.absolute_path(File.join(File.dirname($0), '..', 'res-src')) 57 unless Dir.exist?($src_dir) 58 STDERR.puts "Source directory doesn't exist: #{$src_dir}" 59 exit 1 60 end 61 end 62 63 unless $pp_dir 64 $pp_dir = File.absolute_path(File.join(File.dirname($0), '..', 'res-pp')) 65 unless Dir.exist?($pp_dir) 66 STDERR.puts "Preprocess directory doesn't exist: #{$pp_dir}" 67 exit 1 68 end 69 end 70 end 71 72 def main_styles_name() 73 load_directories() 74 File.join($src_dir, 'styles.css') 75 end 76 77 $styles_loaded = false 78 $main_styles = nil 79 $skins = {} 80 def load_styles(force = false) 81 if force 82 $styles_loaded = false 83 $main_styles = nil 84 $skins = {} 85 end 86 87 unless $styles_loaded 88 $styles_loaded = true 89 90 fn = main_styles_name() 91 if File.readable?(fn) 92 $main_styles = File.read(fn) 93 end 94 95 load_directories() 96 Dir.glob(File.join($src_dir, 'skin-*.css')).each do |fn| 97 m = File.basename(fn).match(/^skin-(.*)\.css/) 98 $skins[m[1]] = [fn, File.read(fn)] 99 end 100 end 101 end 102 103 def defs_name() 104 load_directories() 105 File.join($src_dir, 'defs.svg') 106 end 107 108 $defs_loaded = false 109 $defs = {} 110 def load_defs(force = false) 111 if force 112 $defs_loaded = false 113 $defs = {} 114 end 115 116 unless $defs_loaded 117 $defs_loaded = true 118 119 fn = defs_name() 120 if File.readable?(fn) 121 doc = read_xml(fn) 122 doc.css('defs symbol').each do |n| 123 id = n['id'].to_s 124 unless id && !id.empty? 125 STDERR.puts "Symbol in #{fn} with missing ID: #{n.to_s}" 126 exit 1 127 end 128 $defs[id] = n 129 end 130 end 131 end 132 end 133 134 def widget_from_filename(fn) 135 File.basename(fn).sub(/^(.*)-src\.svg$/, '\1') 136 end 137 138 $widget_names_loaded = false 139 $widget_names = [] 140 def load_widget_names(force = false) 141 if force 142 $widget_names_loaded = false 143 $widget_names = [] 144 end 145 146 unless $widget_names_loaded 147 $widget_names_loaded = true 148 149 load_directories() 150 $widget_names = Dir.glob(File.join($src_dir, '*-src.svg')).map do |fn| 151 widget_from_filename(fn) 152 end.sort 153 end 154 end 155 156 def debug() 157 puts 158 159 load_directories() 160 puts "Source directory: #{$src_dir}" 161 puts "Preprocess directory: #{$pp_dir}" 162 puts 163 164 load_styles() 165 puts "Main stylesheet (#{main_styles_name()}): #{$main_styles ? "#{$main_styles.size} bytes" : 'missing'}" 166 if $skins.empty? 167 puts "Skins: none" 168 else 169 $skins.keys.sort.each do |skin_name| 170 skin = $skins[skin_name] 171 puts " - #{skin_name} (#{skin[0]}): #{skin[1].size} bytes" 172 end 173 end 174 puts 175 176 load_defs() 177 if $defs.empty? 178 puts "Defs (#{defs_name()}): missing" 179 else 180 puts "Defs (#{defs_name()}):" 181 $defs.keys.sort.each do |def_id| 182 puts " - #{def_id}" 183 end 184 end 185 puts 186 187 load_widget_names() 188 puts "Modules:" 189 $widget_names.each do |name| 190 puts " - #{name}" 191 end 192 puts 193 end 194 195 def process_def(fn, doc, n, local_defs) 196 id = n.attribute('href').to_s 197 if id 198 id.sub!(/^#/, '') 199 d = local_defs[id] 200 d = $defs[id] unless d 201 if d 202 nn = d.dup 203 nn.node_name = 'svg' 204 nn.delete('viewBox') 205 nn.delete('id') 206 207 g = Nokogiri::XML::Node.new('g', doc) 208 g.add_child(nn) 209 n.replace(g) 210 211 n.each do |k, v| 212 case k 213 when 'id' 214 if !v.to_s.empty? 215 nn['id'] = v 216 end 217 when 'transform' 218 if !v.to_s.empty? 219 g['transform'] = v 220 end 221 when /^var-(\w+)$/ 222 g[k] = v 223 end 224 end 225 226 nn.css('def').each do |dn| 227 process_def(fn, doc, dn, local_defs) 228 end 229 else 230 puts "WARN: no def defined for def ID '#{id}' in #{fn}" 231 n.remove 232 end 233 else 234 puts "WARN: def without ID in #{fn}: #{n.to_s}" 235 n.remove 236 end 237 end 238 239 def variable_value(k, vars, defaults) 240 v = nil 241 vars.reverse_each do |vs| 242 if vs.key?(k) 243 v = vs[k] 244 break 245 end 246 end 247 248 if v.nil? 249 if defaults.key?(k) 250 v = defaults[k] 251 else 252 v = 0.0 253 end 254 end 255 256 v 257 end 258 259 def eval_expressions(s, vars, defaults) 260 s.gsub!(/\$\{(\w+)\}/) do 261 variable_value($1, vars, defaults) 262 end 263 264 s.gsub!(/\$(\w+)/) do 265 variable_value($1, vars, defaults) 266 end 267 268 if s =~ /[\+\-\*\/]/ 269 s.gsub!(/\-?\d+(\.\d+)?([\+\-\*\%\/]+\d+(\.\d+)?)*/) do |m| 270 eval(m) 271 end 272 end 273 274 s 275 end 276 277 def process_variables(n, vars) 278 n.each do |k, v| 279 if k =~ /^(var|default)-(\w+)$/ 280 n[k] = eval_expressions(v.to_s, vars, {}) 281 end 282 end 283 284 vs = {} 285 n.each do |k, v| 286 if k =~ /^var-(\w+)$/ 287 vs[$1] = v 288 n.delete(k) 289 end 290 end 291 vars.push(vs) 292 293 default_vars = {} 294 n.each do |k, v| 295 if k =~ /^default-(\w+)$/ 296 default_vars[$1] = v 297 n.delete(k) 298 end 299 end 300 301 n.each do |k, v| 302 n[k] = eval_expressions(v.to_s, vars, default_vars) 303 end 304 305 case n.node_name 306 when 'text' 307 n.traverse do |t| 308 if t.text? 309 t.content = eval_expressions(t.content, vars, default_vars) 310 end 311 end 312 end 313 314 n.elements.each do |nn| 315 process_variables(nn, vars) 316 end 317 318 vars.pop 319 end 320 321 def write_output(name, doc, styles) 322 doc.css('style').each do |n| 323 n.content = styles 324 end 325 326 doc.css('localstyle').each do |n| 327 n.node_name = 'style' 328 styles += n.content 329 end 330 331 # hack to inline the path stroke on each path; rendering through Inkscape doesn't handle styles on paths correctly. 332 parser = CssParser::Parser.new 333 parser.load_string!(styles) 334 335 stroke = '#333' 336 if parser.find_by_selector('path').last =~ /stroke:\s+(#\w+);/ 337 stroke = $1 338 end 339 340 input_stroke = '#333' 341 if parser.find_by_selector('path.input-label').last =~ /stroke:\s+(#\w+);/ 342 input_stroke = $1 343 end 344 345 output_stroke = '#333' 346 if parser.find_by_selector('path.output-label').last =~ /stroke:\s+(#\w+);/ 347 output_stroke = $1 348 end 349 350 doc.css('path').each do |n| 351 n['stroke'] = stroke 352 end 353 doc.css('path[@class="input-label"]').each do |n| 354 n['stroke'] = input_stroke 355 end 356 doc.css('path[@class="output-label"]').each do |n| 357 n['stroke'] = output_stroke 358 end 359 # end hack 360 361 fn = File.join($pp_dir, "#{name}-pp.svg") 362 File.write(fn, doc.to_xml) 363 puts "Wrote #{fn}" 364 end 365 366 def process(name) 367 load_directories() 368 load_styles() 369 load_defs() 370 371 fn = File.join($src_dir, File.basename(name)) 372 unless File.readable?(fn) 373 STDERR.puts "No such file: #{fn}" 374 exit 1 375 end 376 377 doc = read_xml(fn) 378 noskin = false 379 globals = {} 380 381 root = doc.at_css(':root') 382 if root.node_name == 'module' 383 hp = 3 384 if root['hp'] && !root['hp'].to_s.empty? 385 hp = root['hp'].to_s.to_i 386 end 387 root.delete('hp') 388 if root['noskin'] && root['noskin'].to_s == 'true' 389 noskin = true 390 end 391 root.delete('noskin') 392 globals['hp'] = hp 393 globals['width'] = hp * 15.0 394 globals['height'] = 380.0 395 396 root.node_name = 'svg' 397 root['xmlns'] = 'http://www.w3.org/2000/svg' 398 root['xmlns:xlink'] = 'http://www.w3.org/1999/xlink' 399 root['version'] = '1.1' 400 root['width'] = globals['width'] 401 root['height'] = globals['height'] 402 root['viewBox'] = "0 0 #{root['width']} #{root['height']}" 403 404 doc = parse_xml(doc.to_xml) 405 elsif root.node_name == 'widget' 406 if root['noskin'] && root['noskin'].to_s == 'true' 407 noskin = true 408 end 409 root.delete('noskin') 410 411 width = 30.0 412 if root['width'] && !root['width'].to_s.empty? 413 width = root['width'].to_f 414 end 415 height = 30.0 416 if root['height'] && !root['height'].to_s.empty? 417 height = root['height'].to_f 418 end 419 globals['width'] = width 420 globals['height'] = height 421 422 root.node_name = 'svg' 423 root['xmlns'] = 'http://www.w3.org/2000/svg' 424 root['xmlns:xlink'] = 'http://www.w3.org/1999/xlink' 425 root['version'] = '1.1' 426 root['width'] = globals['width'] 427 root['height'] = globals['height'] 428 root['viewBox'] = "0 0 #{root['width']} #{root['height']}" 429 430 doc = parse_xml(doc.to_xml) 431 end 432 433 local_defs = {} 434 doc.css('defs symbol').each do |n| 435 id = n['id'].to_s 436 unless id && !id.empty? 437 STDERR.puts "Symbol in #{fn} with missing ID: #{n.to_s}" 438 exit 1 439 end 440 local_defs[id] = n 441 end 442 443 doc.css('defs import').each do |n| 444 id = n.attribute('id').to_s 445 if id 446 d = $defs[id] 447 if d 448 n.replace(d) 449 else 450 puts "WARN: no def for import ID '#{id}' in #{fn}" 451 n.remove 452 end 453 else 454 puts "WARN: import without ID in #{fn}: #{n.to_s}" 455 n.remove 456 end 457 end 458 459 doc.css('def').each do |n| 460 process_def(fn, doc, n, local_defs) 461 end 462 463 doc.xpath('//comment()').each(&:remove) 464 465 vars = [globals] 466 process_variables(doc.at_css(':root'), vars) 467 468 doc.css('g').each do |n| 469 n.replace(n.children) if n.keys.empty? 470 end 471 doc.css('svg').each do |n| 472 n.replace(n.children) if n.keys.empty? 473 end 474 475 name = widget_from_filename(fn) 476 write_output(name, doc, $main_styles) 477 unless noskin 478 $skins.each do |skin_name, skin| 479 write_output("#{name}-#{skin_name}", doc, "#{$main_styles}\n\n#{skin[1]}") 480 end 481 end 482 end 483 484 def reprocess(prefixes) 485 load_directories() 486 load_widget_names() 487 488 name_re = if prefixes.empty? 489 /./ 490 else 491 /^(#{prefixes.join('|')})/ 492 end 493 $widget_names.each do |name| 494 if name =~ name_re 495 process("#{name}-src.svg") 496 end 497 end 498 end 499 500 def listen(prefixes) 501 load_directories() 502 503 puts "Listening for changes in #{$src_dir}..." 504 unless prefixes.empty? 505 puts "(filering on #{prefixes.join(', ')})" 506 end 507 508 name_re = if prefixes.empty? 509 /-src.svg$/ 510 else 511 /^(#{prefixes.join('|')}).*-src.svg$/ 512 end 513 514 listener = Listen.to($src_dir) do |modified, added, removed| 515 modified.each do |fn| 516 begin 517 bn = File.basename(fn) 518 if bn =~ name_re 519 process(fn) 520 elsif bn == 'styles.css' || bn =~ /^skin-.*\.css$/ 521 load_styles(true) 522 reprocess(prefixes) 523 elsif bn == 'defs.svg' 524 load_defs(true) 525 reprocess(prefixes) 526 end 527 rescue => e 528 STDERR.puts "Error processing #{fn}:\n#{e}" 529 end 530 end 531 532 added.each do |fn| 533 begin 534 bn = File.basename(fn) 535 if bn =~ /-src\.svg$/ 536 load_widget_names(true) 537 end 538 539 if bn =~ name_re 540 process(fn) 541 end 542 rescue => e 543 STDERR.puts "Error processing #{fn}:\n#{e}" 544 end 545 end 546 547 removed.each do |fn| 548 begin 549 bn = File.basename(fn) 550 if bn =~ /-src\.svg$/ 551 load_widget_names(true) 552 end 553 554 if bn =~ name_re 555 fs = [bn.sub(/-src.svg/, '-pp.svg')] 556 $skins.keys.each do |skin_name| 557 fs << bn.sub(/-src.svg/, "-#{skin_name}-pp.svg") 558 end 559 fs.each do |f| 560 begin 561 f = File.join($pp_dir, f) 562 File.unlink(f) 563 rescue Errno::ENOENT => e 564 end 565 end 566 end 567 rescue => e 568 STDERR.puts "Error processing #{fn}:\n#{e}" 569 end 570 end 571 end 572 573 listener.start 574 begin 575 sleep 576 rescue Interrupt => e 577 end 578 end 579 580 if options[:debug] 581 debug() 582 elsif ARGV.size >= 1 583 process(ARGV[0]) 584 elsif options[:reprocess] || options[:listen] 585 reprocess(options[:module_prefixes]) if options[:reprocess] 586 listen(options[:module_prefixes]) if options[:listen] 587 else 588 STDERR.puts option_parser.help 589 exit 1 590 end